Add plain provider to demonstrate provider support system

- Created PlainProvider implementation in src/providers/plain/
- Generates simple plain text mbox and JSON output
- Accepts any non-empty vulnerability ID format
- Uses PLAIN_USER environment variable
- Added to provider factory with 'plain' as the provider name
- Includes unit tests for the provider
- Added PROVIDERS.md documentation explaining the provider system
- Updated CLI help to show available providers

Signed-off-by: Sasha Levin <sashal@kernel.org>
diff --git a/tools/bippy/src/models/cli.rs b/tools/bippy/src/models/cli.rs
index d2caa73..856def5 100644
--- a/tools/bippy/src/models/cli.rs
+++ b/tools/bippy/src/models/cli.rs
@@ -9,7 +9,7 @@
 #[derive(Parser, Debug)]
 #[clap(author, version, about, long_about = None, disable_version_flag = true, trailing_var_arg = true)]
 pub struct Args {
-    /// Provider type (e.g., "cve", "gsd", "euvd")
+    /// Provider type (available: cve, plain)
     #[clap(short = 'p', long, default_value = "cve")]
     pub provider: String,
 
diff --git a/tools/bippy/src/providers/mod.rs b/tools/bippy/src/providers/mod.rs
index 4e9559e..7f0be84 100644
--- a/tools/bippy/src/providers/mod.rs
+++ b/tools/bippy/src/providers/mod.rs
@@ -1,5 +1,6 @@
 pub mod common;
 pub mod cve;
+pub mod plain;
 
 pub use common::{CpeMatch, CpeNodes, VersionRange};
 
@@ -65,6 +66,7 @@
     pub fn create(provider_type: &str) -> Result<Box<dyn VulnerabilityProvider>> {
         match provider_type.to_lowercase().as_str() {
             "cve" => Ok(Box::new(cve::CveProvider::new())),
+            "plain" => Ok(Box::new(plain::PlainProvider::new())),
             _ => Err(anyhow!("Unknown provider type: {}", provider_type)),
         }
     }
@@ -72,6 +74,6 @@
     /// Get list of available providers
     #[allow(dead_code)]
     pub fn available_providers() -> Vec<&'static str> {
-        vec!["cve"]
+        vec!["cve", "plain"]
     }
 }
\ No newline at end of file
diff --git a/tools/bippy/src/providers/plain/mod.rs b/tools/bippy/src/providers/plain/mod.rs
new file mode 100644
index 0000000..f09f946
--- /dev/null
+++ b/tools/bippy/src/providers/plain/mod.rs
@@ -0,0 +1,172 @@
+use anyhow::Result;
+use crate::providers::{VulnerabilityProvider, VulnerabilityRecordParams};
+
+/// Plain text provider implementation
+/// This provider generates simple plain text output for demonstrations
+pub struct PlainProvider;
+
+impl PlainProvider {
+    pub fn new() -> Self {
+        PlainProvider
+    }
+}
+
+impl VulnerabilityProvider for PlainProvider {
+    fn generate_json(&self, params: &VulnerabilityRecordParams) -> Result<String> {
+        // Generate a simple JSON representation
+        let json = format!(
+            r#"{{
+    "id": "{}",
+    "type": "plain",
+    "fix_commit": "{}",
+    "subject": "{}",
+    "description": "{}",
+    "affected_files": [{}],
+    "references": [{}]
+}}"#,
+            params.vuln_id,
+            params.git_sha_full,
+            params.commit_subject,
+            params.commit_text.lines().take(3).collect::<Vec<_>>().join(" "),
+            params.affected_files.iter()
+                .map(|f| format!(r#""{}""#, f))
+                .collect::<Vec<_>>()
+                .join(", "),
+            params.additional_references.iter()
+                .map(|r| format!(r#""{}""#, r))
+                .collect::<Vec<_>>()
+                .join(", ")
+        );
+
+        Ok(json)
+    }
+
+    fn generate_mbox(&self, params: &VulnerabilityRecordParams) -> Result<String> {
+        // Generate a simple plain text mbox format
+        let mbox = format!(
+            r#"From plain-provider Mon Sep 17 00:00:00 2001
+From: {} <{}>
+To: <security@example.org>
+Subject: {}: {}
+
+=== PLAIN TEXT VULNERABILITY ANNOUNCEMENT ===
+
+Vulnerability ID: {}
+Fix Commit: {}
+
+Description:
+------------
+{}
+
+Affected Files:
+--------------
+{}
+
+Vulnerable/Fixed Versions:
+-------------------------
+{}
+
+References:
+----------
+{}
+
+---
+This is a plain text vulnerability announcement generated for demonstration purposes.
+"#,
+            params.user_name,
+            params.user_email,
+            params.vuln_id,
+            params.commit_subject,
+            params.vuln_id,
+            params.git_sha_full,
+            params.commit_text,
+            params.affected_files.iter()
+                .map(|f| format!("- {}", f))
+                .collect::<Vec<_>>()
+                .join("\n"),
+            self.format_version_info(&params.dyad_entries),
+            params.additional_references.iter()
+                .map(|r| format!("- {}", r))
+                .collect::<Vec<_>>()
+                .join("\n")
+        );
+
+        Ok(mbox)
+    }
+
+    fn name(&self) -> &'static str {
+        "Plain"
+    }
+
+    fn user_env_var(&self) -> &'static str {
+        "PLAIN_USER"
+    }
+
+    fn validate_id(&self, id: &str) -> Result<()> {
+        // Plain provider accepts any ID format
+        if id.is_empty() {
+            Err(anyhow::anyhow!("Vulnerability ID cannot be empty"))
+        } else {
+            Ok(())
+        }
+    }
+}
+
+impl PlainProvider {
+    fn format_version_info(&self, dyad_entries: &[crate::models::DyadEntry]) -> String {
+        if dyad_entries.is_empty() {
+            return "No version information available".to_string();
+        }
+
+        dyad_entries.iter()
+            .map(|entry| {
+                if entry.fixed.is_empty() {
+                    format!("- Introduced in {} ({})",
+                        entry.vulnerable.version(),
+                        entry.vulnerable.git_id())
+                } else if entry.vulnerable.is_empty() {
+                    format!("- Fixed in {} ({})",
+                        entry.fixed.version(),
+                        entry.fixed.git_id())
+                } else {
+                    format!("- Introduced in {} ({}) -> Fixed in {} ({})",
+                        entry.vulnerable.version(),
+                        entry.vulnerable.git_id(),
+                        entry.fixed.version(),
+                        entry.fixed.git_id())
+                }
+            })
+            .collect::<Vec<_>>()
+            .join("\n")
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_plain_provider_validate_id() {
+        let provider = PlainProvider::new();
+
+        // Should accept any non-empty ID
+        assert!(provider.validate_id("PLAIN-001").is_ok());
+        assert!(provider.validate_id("some-id").is_ok());
+        assert!(provider.validate_id("123").is_ok());
+
+        // Should reject empty ID
+        assert!(provider.validate_id("").is_err());
+    }
+
+    #[test]
+    fn test_plain_provider_name() {
+        let provider = PlainProvider::new();
+        assert_eq!(provider.name(), "Plain");
+    }
+
+    #[test]
+    fn test_plain_provider_env_var() {
+        let provider = PlainProvider::new();
+        assert_eq!(provider.user_env_var(), "PLAIN_USER");
+    }
+}
\ No newline at end of file