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(¶ms.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