Rename cve_utils to vuln_utils and abstract CVE-specific code

- Renamed cve_utils library to vuln_utils throughout the codebase
- Renamed cve_utils directory to vuln_utils using git mv
- Abstracted all CVE-specific code into src/providers/cve directory
- Implemented factory design pattern for vulnerability providers
- Added provider-agnostic CLI with --provider flag (defaults to 'cve')
- Changed --cve to --id for vulnerability IDs (kept --cve as hidden legacy support)
- Updated all imports and references from cve_utils to vuln_utils
- Removed trailing whitespaces from modified files

This refactoring prepares the codebase to support additional vulnerability
providers by implementing the VulnerabilityProvider trait.

Signed-off-by: Sasha Levin <sashal@kernel.org>
diff --git a/tools/Cargo.toml b/tools/Cargo.toml
index c18b6df..55a80f0 100644
--- a/tools/Cargo.toml
+++ b/tools/Cargo.toml
@@ -63,43 +63,43 @@
 # CVE Utils binaries
 [[bin]]
 name = "cve_search"
-path = "cve_utils/src/bin/cve_search.rs"
+path = "vuln_utils/src/bin/cve_search.rs"
 
 [[bin]]
 name = "cve_reject"
-path = "cve_utils/src/bin/cve_reject.rs"
+path = "vuln_utils/src/bin/cve_reject.rs"
 
 [[bin]]
 name = "cve_update"
-path = "cve_utils/src/bin/cve_update.rs"
+path = "vuln_utils/src/bin/cve_update.rs"
 
 [[bin]]
 name = "cve_publish"
-path = "cve_utils/src/bin/cve_publish.rs"
+path = "vuln_utils/src/bin/cve_publish.rs"
 
 [[bin]]
 name = "cve_create"
-path = "cve_utils/src/bin/cve_create.rs"
+path = "vuln_utils/src/bin/cve_create.rs"
 
 [[bin]]
 name = "cve_review"
-path = "cve_utils/src/bin/cve_review.rs"
+path = "vuln_utils/src/bin/cve_review.rs"
 
 [[bin]]
 name = "update_dyad"
-path = "cve_utils/src/bin/update_dyad.rs"
+path = "vuln_utils/src/bin/update_dyad.rs"
 
 [[bin]]
 name = "strak"
-path = "cve_utils/src/bin/strak.rs"
+path = "vuln_utils/src/bin/strak.rs"
 
 [[bin]]
 name = "score"
-path = "cve_utils/src/bin/score.rs"
+path = "vuln_utils/src/bin/score.rs"
 
 [[bin]]
 name = "cve_stats"
-path = "cve_utils/src/bin/cve_stats.rs"
+path = "vuln_utils/src/bin/cve_stats.rs"
 
 [[bin]]
 name = "cve_classifier"
@@ -117,5 +117,5 @@
 required-features = []
 
 [lib]
-name = "cve_utils"
-path = "cve_utils/src/lib.rs"
+name = "vuln_utils"
+path = "vuln_utils/src/lib.rs"
diff --git a/tools/bippy/src/commands/json.rs b/tools/bippy/src/commands/json.rs
index 925b256..e4ccc3f 100644
--- a/tools/bippy/src/commands/json.rs
+++ b/tools/bippy/src/commands/json.rs
@@ -2,31 +2,23 @@
 //
 // Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
 
-use anyhow::{Context, Result};
-use serde::Serialize;
-use serde_json::ser::{PrettyFormatter, Serializer};
-use std::collections::HashSet;
+use anyhow::Result;
+use crate::models::DyadEntry;
+use crate::providers::{ProviderFactory, VulnerabilityRecordParams};
 
-use crate::models::{
-    AffectedProduct, CnaData, Containers, CpeApplicability, CpeNodes, CveMetadata, CveRecord,
-    Description, DyadEntry, Generator, ProviderMetadata, Reference,
-};
-use crate::utils::{
-    determine_default_status, generate_cpe_ranges, generate_git_ranges, generate_version_ranges,
-    read_uuid,
-};
-
-/// Parameters for generating a JSON CVE record
-pub struct CveRecordParams<'a> {
-    /// CVE identifier (e.g., "CVE-2023-12345")
-    pub cve_number: &'a str,
+/// Parameters for generating a JSON vulnerability record
+pub struct VulnRecordParams<'a> {
+    /// Provider type (e.g., "cve", "gsd", "euvd")
+    pub provider_type: &'a str,
+    /// Vulnerability identifier
+    pub vuln_id: &'a str,
     /// Full Git SHA of the commit that fixes the vulnerability
     pub git_sha_full: &'a str,
     /// Subject line of the commit
     pub commit_subject: &'a str,
-    /// Name of the user creating the CVE
+    /// Name of the user creating the record
     pub user_name: &'a str,
-    /// Email of the user creating the CVE
+    /// Email of the user creating the record
     pub user_email: &'a str,
     /// Dyad entries containing vulnerability and fix information
     pub dyad_entries: Vec<DyadEntry>,
@@ -42,411 +34,21 @@
     pub affected_files: &'a Vec<String>,
 }
 
-/// Get UUID information
-fn get_uuid() -> Result<String> {
-    // Get vulns directory using cve_utils
-    let vulns_dir =
-        cve_utils::find_vulns_dir().with_context(|| "Failed to find vulns directory")?;
-
-    // Get the script directory from vulns directory
-    let script_dir = vulns_dir.join("scripts");
-    if !script_dir.exists() {
-        return Err(anyhow::anyhow!(
-            "Scripts directory not found at {}",
-            script_dir.display()
-        ));
-    }
-
-    // Read the UUID from the linux.uuid file
-    let uuid = read_uuid(&script_dir).with_context(|| "Failed to read UUID")?;
-
-    Ok(uuid)
-}
-
-/// Prepare dyad entries and affected files
-fn prepare_vulnerability_data(
-    git_sha_full: &str,
-    in_dyad_entries: &[DyadEntry],
-) -> Result<Vec<DyadEntry>> {
-    // Clone dyad entries since we might need to modify them
-    let mut dyad_entries = in_dyad_entries.to_vec();
-
-    // If no entries were created, use the fix commit as a fallback
-    if dyad_entries.is_empty() {
-        // Create a dummy entry using the fix commit
-        if let Ok(entry) = DyadEntry::from_str(&format!("0:0:0:{git_sha_full}")) {
-            dyad_entries.push(entry);
-        }
-    }
-
-    Ok(dyad_entries)
-}
-
-/// Create affected products (kernel and git)
-fn create_affected_products(
-    dyad_entries: &[DyadEntry],
-    affected_files: Vec<String>,
-) -> (AffectedProduct, AffectedProduct, Vec<CpeNodes>) {
-    // Determine default status
-    let default_status = determine_default_status(dyad_entries);
-
-    // Generate version ranges for kernel product
-    let kernel_versions = generate_version_ranges(dyad_entries, default_status);
-    let kernel_product = AffectedProduct {
-        product: "Linux".to_string(),
-        vendor: "Linux".to_string(),
-        default_status: default_status.to_string(),
-        repo: "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git".to_string(),
-        program_files: affected_files.clone(),
-        versions: kernel_versions,
+/// Generate a JSON for the vulnerability record
+pub fn generate_json(params: &VulnRecordParams) -> Result<String> {
+    let provider = ProviderFactory::create(params.provider_type)?;
+    let vuln_params = VulnerabilityRecordParams {
+        vuln_id: params.vuln_id,
+        git_sha_full: params.git_sha_full,
+        commit_subject: params.commit_subject,
+        user_name: params.user_name,
+        user_email: params.user_email,
+        dyad_entries: params.dyad_entries.clone(),
+        script_name: params.script_name,
+        script_version: params.script_version,
+        additional_references: params.additional_references,
+        commit_text: params.commit_text,
+        affected_files: params.affected_files,
     };
-
-    // Generate git ranges for git product
-    let git_versions = generate_git_ranges(dyad_entries);
-    let git_product = AffectedProduct {
-        product: "Linux".to_string(),
-        vendor: "Linux".to_string(),
-        default_status: "unaffected".to_string(),
-        repo: "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git".to_string(),
-        program_files: affected_files,
-        versions: git_versions,
-    };
-
-    // Generate CPE ranges
-    let cpe_nodes = generate_cpe_ranges(dyad_entries);
-
-    (kernel_product, git_product, cpe_nodes)
-}
-
-/// Generate references from dyad entries and additional references
-fn generate_references(
-    dyad_entries: &[DyadEntry],
-    additional_references: &[String],
-    git_sha_full: &str,
-) -> Vec<Reference> {
-    let mut references = Vec::new();
-    let mut seen_refs: HashSet<String> = HashSet::new();
-
-    // Add references for all entries
-    for entry in dyad_entries {
-        // Add fixed commit reference if available
-        if !entry.fixed.is_empty() {
-            let url = format!("https://git.kernel.org/stable/c/{}", entry.fixed.git_id());
-            if !seen_refs.contains(&url) {
-                seen_refs.insert(url.clone());
-                references.push(Reference { url });
-            }
-        }
-    }
-
-    // Add any additional references from the reference file
-    for url in additional_references {
-        if !seen_refs.contains(url) {
-            seen_refs.insert(url.clone());
-            references.push(Reference { url: url.clone() });
-        }
-    }
-
-    // If no references were found, add the main fix commit
-    if references.is_empty() {
-        let main_fix_url = format!("https://git.kernel.org/stable/c/{git_sha_full}");
-        references.push(Reference { url: main_fix_url });
-    }
-
-    references
-}
-
-/// Process commit description text and handle truncation
-fn process_description(commit_text: &str) -> String {
-    // Truncate description to 3982 characters (CVE backend limit) if needed
-    let max_length = 3982; // CVE backend limit
-
-    if commit_text.len() <= max_length {
-        // If already under the limit, just ensure no trailing newline
-        return commit_text.trim_end().to_string();
-    }
-
-    // Get the truncated text limited to max_length
-    let truncated = &commit_text[..max_length];
-
-    // Special case: if only over by a trailing newline, just trim it
-    if commit_text.len() == max_length + 1 && commit_text.ends_with('\n') {
-        truncated.to_string()
-    } else {
-        // Add truncation marker, with proper newline handling
-        let separator = if truncated.ends_with('\n') { "" } else { "\n" };
-        format!("{truncated}{separator}---truncated---")
-    }
-}
-
-/// Parameters for creating a CVE record
-struct CveRecordCreationParams<'a> {
-    uuid: String,
-    cve_number: &'a str,
-    commit_subject: &'a str,
-    user_email: &'a str,
-    script_name: &'a str,
-    script_version: &'a str,
-    truncated_description: String,
-    kernel_product: AffectedProduct,
-    git_product: AffectedProduct,
-    cpe_nodes: Vec<CpeNodes>,
-    references: Vec<Reference>,
-}
-
-/// Create the CVE record structure
-fn create_cve_record(params: CveRecordCreationParams) -> CveRecord {
-    CveRecord {
-        containers: Containers {
-            cna: CnaData {
-                provider_metadata: ProviderMetadata {
-                    org_id: params.uuid.clone(),
-                },
-                descriptions: vec![Description {
-                    lang: "en".to_string(),
-                    value: params.truncated_description,
-                }],
-                affected: vec![params.git_product, params.kernel_product],
-                cpe_applicability: vec![CpeApplicability {
-                    nodes: params.cpe_nodes,
-                }],
-                references: params.references,
-                title: params.commit_subject.to_string(),
-                x_generator: Generator {
-                    engine: format!("{}-{}", params.script_name, params.script_version),
-                },
-            },
-        },
-        cve_metadata: CveMetadata {
-            assigner_org_id: params.uuid,
-            cve_id: params.cve_number.to_string(),
-            requester_user_id: params.user_email.to_string(),
-            serial: "1".to_string(),
-            state: "PUBLISHED".to_string(),
-        },
-        data_type: "CVE_RECORD".to_string(),
-        data_version: "5.0".to_string(),
-    }
-}
-
-/// Serialize the CVE record to JSON
-fn serialize_cve_record(cve_record: &CveRecord) -> Result<String> {
-    // Use a custom formatter with 3-space indentation
-    let formatter = PrettyFormatter::with_indent(b"   ");
-    let mut output = Vec::new();
-    let mut serializer = Serializer::with_formatter(&mut output, formatter);
-
-    cve_record
-        .serialize(&mut serializer)
-        .map_err(|e| anyhow::anyhow!("Error serializing JSON: {e}"))?;
-
-    let json_string = String::from_utf8(output)
-        .map_err(|e| anyhow::anyhow!("Error converting JSON to string: {e}"))?;
-
-    // Ensure the JSON output ends with a newline
-    if json_string.ends_with('\n') {
-        Ok(json_string)
-    } else {
-        Ok(json_string + "\n")
-    }
-}
-
-/// Generate a JSON for the CVE
-pub fn generate_json(params: &CveRecordParams) -> Result<String> {
-    let CveRecordParams {
-        cve_number,
-        git_sha_full,
-        commit_subject,
-        user_name: _user_name, // Not used in this function
-        user_email,
-        dyad_entries: in_dyad_entries,
-        script_name,
-        script_version,
-        additional_references,
-        commit_text,
-        affected_files,
-    } = params;
-
-    // Initialize environment and get repository information
-    let uuid = get_uuid()?;
-
-    // Prepare dyad entries
-    let dyad_entries = prepare_vulnerability_data(git_sha_full, in_dyad_entries)?;
-
-    // Create affected products
-    let (kernel_product, git_product, cpe_nodes) =
-        create_affected_products(&dyad_entries, affected_files.to_vec());
-
-    // Generate references
-    let references = generate_references(&dyad_entries, additional_references, git_sha_full);
-
-    // Process description
-    let truncated_description = process_description(commit_text);
-
-    // Create CVE record
-    let cve_record = create_cve_record(CveRecordCreationParams {
-        uuid,
-        cve_number,
-        commit_subject,
-        user_email,
-        script_name,
-        script_version,
-        truncated_description,
-        kernel_product,
-        git_product,
-        cpe_nodes,
-        references,
-    });
-
-    // Serialize CVE record to JSON
-    serialize_cve_record(&cve_record)
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::models::CveRecord;
-
-    #[test]
-    fn test_process_description() {
-        // Test short description (under limit)
-        let short_text = "This is a short description.";
-        assert_eq!(process_description(short_text), short_text);
-
-        // Test description at exactly the limit
-        let at_limit_text = "a".repeat(3982);
-        assert_eq!(process_description(&at_limit_text), at_limit_text);
-
-        // Test description over the limit
-        let over_limit_text = "a".repeat(4000);
-        let processed = process_description(&over_limit_text);
-        assert!(processed.len() <= 3982 + 16); // Max length + truncated marker length
-        assert!(processed.ends_with("---truncated---"));
-
-        // Test with trailing newline exactly over limit
-        let newline_text = "a".repeat(3981) + "\n";
-        assert_eq!(process_description(&newline_text), "a".repeat(3981));
-    }
-
-    #[test]
-    fn test_generate_references() {
-        use crate::models::dyad::DyadEntry;
-        use cve_utils::Kernel;
-
-        // Helper function to create test kernels
-        fn create_test_kernel(_version: &str, git_id: &str) -> Kernel {
-            // In tests, we don't have real git commit IDs to look up,
-            // so we'll create dummy kernels with the provided info
-            let kernel = Kernel::from_id(git_id).unwrap_or_else(|_| Kernel::empty_kernel());
-
-            // We can't directly modify the fields, but for testing
-            // purposes, we're assuming these are valid git IDs and versions
-            kernel
-        }
-
-        // Create test dyad entries
-        let fixed_kernel1 = create_test_kernel("5.15", "11c52d250b34a0862edc29db03fbec23b30db6da");
-        let fixed_kernel2 = create_test_kernel("5.10", "22c52d250b34a0862edc29db03fbec23b30db6db");
-        let vuln_kernel = create_test_kernel("5.4", "33c52d250b34a0862edc29db03fbec23b30db6dc");
-
-        let entries = vec![
-            DyadEntry {
-                vulnerable: vuln_kernel.clone(),
-                fixed: fixed_kernel1,
-            },
-            DyadEntry {
-                vulnerable: vuln_kernel,
-                fixed: fixed_kernel2,
-            },
-        ];
-
-        let additional_refs = vec![
-            "https://example.com/ref1".to_string(),
-            "https://example.com/ref2".to_string(),
-        ];
-
-        let git_sha_full = "abcdef1234567890";
-
-        // Test reference generation
-        let references = generate_references(&entries, &additional_refs, git_sha_full);
-
-        // With our updated Kernel::from_id implementation, our test kernels may not
-        // generate references in the same way, so we'll check for at least the main fix
-        // reference and the additional refs
-        assert!(references.len() >= 3);
-
-        // With our test kernels, we can't reliably check for specific git URLs,
-        // so we'll only check for the additional references
-
-        // Check that additional references were added
-        assert!(references
-            .iter()
-            .any(|r| r.url == "https://example.com/ref1"));
-        assert!(references
-            .iter()
-            .any(|r| r.url == "https://example.com/ref2"));
-
-        // Test with no dyad entries and no additional references
-        let references = generate_references(&[], &[], git_sha_full);
-
-        // Should have 1 reference (main fix commit)
-        assert_eq!(references.len(), 1);
-        assert_eq!(
-            references[0].url,
-            format!("https://git.kernel.org/stable/c/{git_sha_full}")
-        );
-    }
-
-    #[test]
-    fn test_serialize_cve_record() {
-        // Create a minimal CVE record for testing
-        let cve_record = CveRecord {
-            containers: Containers {
-                cna: CnaData {
-                    provider_metadata: ProviderMetadata {
-                        org_id: "test-uuid".to_string(),
-                    },
-                    descriptions: vec![Description {
-                        lang: "en".to_string(),
-                        value: "Test description".to_string(),
-                    }],
-                    affected: vec![],
-                    cpe_applicability: vec![],
-                    references: vec![],
-                    title: "Test CVE".to_string(),
-                    x_generator: Generator {
-                        engine: "test-engine".to_string(),
-                    },
-                },
-            },
-            cve_metadata: CveMetadata {
-                assigner_org_id: "test-uuid".to_string(),
-                cve_id: "CVE-2023-1234".to_string(),
-                requester_user_id: "test@example.com".to_string(),
-                serial: "1".to_string(),
-                state: "PUBLISHED".to_string(),
-            },
-            data_type: "CVE_RECORD".to_string(),
-            data_version: "5.0".to_string(),
-        };
-
-        // Serialize the record
-        let json = serialize_cve_record(&cve_record).unwrap();
-
-        // Verify it's valid JSON by parsing it
-        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
-
-        // Check basic structure
-        assert_eq!(parsed["dataType"], "CVE_RECORD");
-        assert_eq!(parsed["dataVersion"], "5.0");
-        assert_eq!(parsed["cveMetadata"]["cveID"], "CVE-2023-1234");
-        assert_eq!(parsed["cveMetadata"]["state"], "PUBLISHED");
-        assert_eq!(parsed["containers"]["cna"]["title"], "Test CVE");
-
-        // Check that the output ends with a newline
-        assert!(json.ends_with('\n'));
-
-        // Check for 3-space indentation
-        assert!(json.contains("\n   "));
-    }
-}
+    provider.generate_json(&vuln_params)
+}
\ No newline at end of file
diff --git a/tools/bippy/src/commands/mbox.rs b/tools/bippy/src/commands/mbox.rs
index 31c54aa..f47e523 100644
--- a/tools/bippy/src/commands/mbox.rs
+++ b/tools/bippy/src/commands/mbox.rs
@@ -2,22 +2,23 @@
 //
 // Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
 
-use cve_utils::version_utils::compare_kernel_versions;
-use std::fmt::Write;
-
+use anyhow::Result;
 use crate::models::DyadEntry;
+use crate::providers::{ProviderFactory, VulnerabilityRecordParams};
 
-/// Parameters for generating an mbox-formatted CVE announcement
+/// Parameters for generating an mbox-formatted vulnerability announcement
 pub struct MboxParams<'a> {
-    /// CVE identifier (e.g., "CVE-2023-12345")
-    pub cve_number: &'a str,
+    /// Provider type (e.g., "cve", "gsd", "euvd")
+    pub provider_type: &'a str,
+    /// Vulnerability identifier
+    pub vuln_id: &'a str,
     /// Full Git SHA of the commit that fixes the vulnerability
     pub git_sha_full: &'a str,
     /// Subject line of the commit
     pub commit_subject: &'a str,
-    /// Name of the user creating the CVE
+    /// Name of the user creating the announcement
     pub user_name: &'a str,
-    /// Email of the user creating the CVE
+    /// Email of the user creating the announcement
     pub user_email: &'a str,
     /// Dyad entries containing vulnerability and fix information
     pub dyad_entries: &'a [DyadEntry],
@@ -33,431 +34,21 @@
     pub affected_files: &'a Vec<String>,
 }
 
-/// Parse dyad entries into vulnerability information strings
-fn parse_dyad_entries(dyad_entries: &[DyadEntry]) -> Vec<String> {
-    let mut vuln_array_mbox = Vec::new();
-
-    for entry in dyad_entries {
-        // Handle unfixed vulnerabilities
-        if entry.fixed.is_empty() {
-            // Issue is not fixed, so say that:
-            vuln_array_mbox.push(format!(
-                "Issue introduced in {} with commit {}",
-                entry.vulnerable.version(),
-                entry.vulnerable.git_id()
-            ));
-            continue;
-        }
-
-        // Skip entries where the vulnerability is in the same version it was fixed
-        if entry.vulnerable.version() == entry.fixed.version() {
-            continue;
-        }
-
-        // Handle different types of entries
-        if entry.vulnerable.is_empty() {
-            // We do not know when it showed up, so just say it is fixed
-            vuln_array_mbox.push(format!(
-                "Fixed in {} with commit {}",
-                entry.fixed.version(),
-                entry.fixed.git_id()
-            ));
-        } else {
-            // Report when it was introduced and when it was fixed
-            vuln_array_mbox.push(format!(
-                "Issue introduced in {} with commit {} and fixed in {} with commit {}",
-                entry.vulnerable.version(),
-                entry.vulnerable.git_id(),
-                entry.fixed.version(),
-                entry.fixed.git_id()
-            ));
-        }
-    }
-
-    // If no vulnerabilities were found, do NOT create a CVE at all!
-    assert!(!vuln_array_mbox.is_empty(), "No vulnerable:fixed kernel versions, aborting!");
-
-    vuln_array_mbox
-}
-
-/// Collect reference URLs from dyad entries and additional references
-fn collect_reference_urls(
-    dyad_entries: &[DyadEntry],
-    additional_references: &[String],
-    git_sha_full: &str,
-) -> Vec<String> {
-    // First add all fix commit URLs from dyad entries (except the main fix)
-    let mut version_url_pairs = Vec::new();
-    for entry in dyad_entries {
-        if !entry.fixed.is_empty() && entry.fixed.git_id() != git_sha_full {
-            let fix_url = format!("https://git.kernel.org/stable/c/{}", entry.fixed.git_id());
-            if !version_url_pairs.iter().any(|(_, url)| url == &fix_url) {
-                version_url_pairs.push((entry.fixed.version().clone(), fix_url));
-            }
-        }
-    }
-
-    // Sort the URLs by kernel version
-    version_url_pairs.sort_by(|(ver_a, _), (ver_b, _)| {
-        // Use the shared compare_kernel_versions function
-        compare_kernel_versions(ver_a, ver_b)
-    });
-
-    // Build the URL array from the sorted pairs
-    let mut url_array = version_url_pairs
-        .into_iter()
-        .map(|(_, url)| url)
-        .collect::<Vec<_>>();
-
-    // Add the main fix commit URL at the end
-    url_array.push(format!("https://git.kernel.org/stable/c/{git_sha_full}"));
-
-    // Add any additional references from the reference file
-    for url in additional_references {
-        if !url_array.contains(url) {
-            url_array.push(url.clone());
-        }
-    }
-
-    url_array
-}
-
-/// Format various sections for the mbox content
-fn format_mbox_sections(
-    vuln_array_mbox: Vec<String>,
-    affected_files: Vec<String>,
-    url_array: Vec<String>,
-) -> (String, String, String) {
-    // Format the vulnerability summary section
-    let mut vuln_section = String::new();
-    for line in vuln_array_mbox {
-        writeln!(vuln_section, "\t{line}").unwrap();
-    }
-
-    // Format the affected files section
-    let mut files_section = String::new();
-    for file in affected_files {
-        writeln!(files_section, "\t{file}").unwrap();
-    }
-
-    // Format the mitigation section with URLs
-    let mut url_section = String::new();
-    for url in url_array {
-        writeln!(url_section, "\t{url}").unwrap();
-    }
-
-    (vuln_section, files_section, url_section)
-}
-
-/// Parameters for creating mbox content
-struct MboxContentParams<'a> {
-    from_line: &'a str,
-    user_name: &'a str,
-    user_email: &'a str,
-    cve_number: &'a str,
-    commit_subject: &'a str,
-    commit_text: &'a str,
-    vuln_section: &'a str,
-    files_section: &'a str,
-    url_section: &'a str,
-}
-
-/// Create the final mbox content
-fn create_mbox_content(params: &MboxContentParams) -> String {
-    // The full formatted mbox content
-    let result = format!(
-        "{}\n\
-         From: {} <{}>\n\
-         To: <linux-cve-announce@vger.kernel.org>\n\
-         Reply-to: <cve@kernel.org>, <linux-kernel@vger.kernel.org>\n\
-         Subject: {}: {}\n\
-         \n\
-         Description\n\
-         ===========\n\
-         \n\
-         {}\n\
-         \n\
-         The Linux kernel CVE team has assigned {} to this issue.\n\
-         \n\
-         \n\
-         Affected and fixed versions\n\
-         ===========================\n\
-         \n\
-         {}\n\
-         Please see https://www.kernel.org for a full list of currently supported\n\
-         kernel versions by the kernel community.\n\
-         \n\
-         Unaffected versions might change over time as fixes are backported to\n\
-         older supported kernel versions.  The official CVE entry at\n\
-         \thttps://cve.org/CVERecord/?id={}\n\
-         will be updated if fixes are backported, please check that for the most\n\
-         up to date information about this issue.\n\
-         \n\
-         \n\
-         Affected files\n\
-         ==============\n\
-         \n\
-         The file(s) affected by this issue are:\n\
-         {}\n\
-         \n\
-         Mitigation\n\
-         ==========\n\
-         \n\
-         The Linux kernel CVE team recommends that you update to the latest\n\
-         stable kernel version for this, and many other bugfixes.  Individual\n\
-         changes are never tested alone, but rather are part of a larger kernel\n\
-         release.  Cherry-picking individual commits is not recommended or\n\
-         supported by the Linux kernel community at all.  If however, updating to\n\
-         the latest release is impossible, the individual changes to resolve this\n\
-         issue can be found at these commits:\n\
-         {}",
-        params.from_line,
-        params.user_name,
-        params.user_email,
-        params.cve_number,
-        params.commit_subject,
-        params.commit_text.trim_end(), // Trim any trailing newlines
-        params.cve_number,
-        params.vuln_section,
-        params.cve_number,
-        params.files_section,
-        params.url_section
-    );
-
-    // Ensure the result ends with a newline
-    if result.ends_with('\n') {
-        result
-    } else {
-        result + "\n"
-    }
-}
-
-/// Generate an mbox file for the CVE
-pub fn generate_mbox(params: &MboxParams) -> String {
-    let MboxParams {
-        cve_number,
-        git_sha_full,
-        commit_subject,
-        user_name,
-        user_email,
-        dyad_entries,
-        script_name,
-        script_version,
-        additional_references,
-        commit_text,
-        affected_files,
-    } = params;
-
-    // For the From line we need the script name and version
-    let from_line = format!("From {script_name}-{script_version} Mon Sep 17 00:00:00 2001");
-
-    // Parse dyad entries into vulnerability information
-    let vuln_array_mbox = parse_dyad_entries(dyad_entries);
-
-    // Collect reference URLs
-    let url_array = collect_reference_urls(dyad_entries, additional_references, git_sha_full);
-
-    // Format sections for the mbox content
-    let (vuln_section, files_section, url_section) =
-        format_mbox_sections(vuln_array_mbox, affected_files.to_vec(), url_array);
-
-    // Create the final mbox content
-    create_mbox_content(&MboxContentParams {
-        from_line: &from_line,
-        user_name,
-        user_email,
-        cve_number,
-        commit_subject,
-        commit_text,
-        vuln_section: &vuln_section,
-        files_section: &files_section,
-        url_section: &url_section,
-    })
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::models::DyadEntry;
-    use cve_utils::Kernel;
-
-    fn create_test_kernel(_version: &str, git_id: &str) -> Kernel {
-        // In tests, we don't have real git commit IDs to look up,
-        // so we'll create dummy kernels with the provided info
-        let kernel = Kernel::from_id(git_id).unwrap_or_else(|_| Kernel::empty_kernel());
-
-        // We can't directly modify the fields, but for testing
-        // purposes, we're assuming these are valid git IDs and versions
-        kernel
-    }
-
-    #[test]
-    fn test_parse_dyad_entries() {
-        // Create test data
-        let entries = vec![
-            // Entry with both vulnerable and fixed
-            DyadEntry {
-                vulnerable: create_test_kernel("5.15", "11c52d250b34a0862edc29db03fbec23b30db6da"),
-                fixed: create_test_kernel("5.16", "2b503c8598d1b232e7fc7526bce9326d92331541"),
-            },
-            // Entry with unknown vulnerable but known fixed
-            DyadEntry {
-                vulnerable: Kernel::empty_kernel(),
-                fixed: create_test_kernel("5.10", "3b503c8598d1b232e7fc7526bce9326d92331542"),
-            },
-            // Entry with unfixed vulnerability
-            DyadEntry {
-                vulnerable: create_test_kernel("5.4", "4b503c8598d1b232e7fc7526bce9326d92331543"),
-                fixed: Kernel::empty_kernel(),
-            },
-            // Entry with same version (should be ignored)
-            DyadEntry {
-                vulnerable: create_test_kernel("6.1", "5b503c8598d1b232e7fc7526bce9326d92331544"),
-                fixed: create_test_kernel("6.1", "6b503c8598d1b232e7fc7526bce9326d92331545"),
-            },
-        ];
-
-        // With our updated Kernel implementation, the exact behavior may be different
-        // We'll check that we get at least some entries
-        let vuln_info = parse_dyad_entries(&entries);
-        assert!(!vuln_info.is_empty());
-
-        // We can't check for specific content with our test kernels,
-        // so we'll just verify that entries were generated
-
-        // Test with an array containing only same-version entries
-        let _same_version_entries = vec![DyadEntry {
-            vulnerable: create_test_kernel("6.1", "5b503c8598d1b232e7fc7526bce9326d92331544"),
-            fixed: create_test_kernel("6.1", "6b503c8598d1b232e7fc7526bce9326d92331545"),
-        }];
-
-        // Testing directly will panic, so we can't test that case directly
-    }
-
-    #[test]
-    fn test_collect_reference_urls() {
-        // Create test data
-        let entries = vec![
-            DyadEntry {
-                vulnerable: create_test_kernel("5.15", "11c52d250b34a0862edc29db03fbec23b30db6da"),
-                fixed: create_test_kernel("5.16", "22c52d250b34a0862edc29db03fbec23b30db6db"),
-            },
-            DyadEntry {
-                vulnerable: create_test_kernel("5.10", "33c52d250b34a0862edc29db03fbec23b30db6dc"),
-                fixed: create_test_kernel("5.10.1", "44c52d250b34a0862edc29db03fbec23b30db6dd"),
-            },
-        ];
-
-        let additional_refs = vec![
-            "https://example.com/ref1".to_string(),
-            "https://example.com/ref2".to_string(),
-        ];
-
-        let git_sha_full = "main_fix_id";
-
-        // Collect the references
-        let urls = collect_reference_urls(&entries, &additional_refs, git_sha_full);
-
-        // With our updated Kernel implementation, we may not get all references
-        // but we should at least get the main fix and additional refs
-        assert!(urls.len() >= 3);
-        assert!(urls.contains(&"https://git.kernel.org/stable/c/main_fix_id".to_string()));
-
-        // With our test kernels, we can't check specific git URLs
-        assert!(urls.contains(&"https://git.kernel.org/stable/c/main_fix_id".to_string()));
-        assert!(urls.contains(&"https://example.com/ref1".to_string()));
-        assert!(urls.contains(&"https://example.com/ref2".to_string()));
-
-        // Verify only that additional refs appear at the end
-        assert_eq!(urls.last(), Some(&"https://example.com/ref2".to_string()));
-    }
-
-    #[test]
-    fn test_format_mbox_sections() {
-        // Create test data
-        let vuln_array = vec![
-            "Issue introduced in 5.15 with commit 11c52d250b34a0862edc29db03fbec23b30db6da"
-                .to_string(),
-            "Fixed in 5.10 with commit 22c52d250b34a0862edc29db03fbec23b30db6db".to_string(),
-        ];
-
-        let affected_files = vec![
-            "drivers/net/ethernet/test.c".to_string(),
-            "include/linux/test.h".to_string(),
-        ];
-
-        let url_array = vec![
-            "https://git.kernel.org/stable/c/11c52d250b34a0862edc29db03fbec23b30db6da".to_string(),
-            "https://git.kernel.org/stable/c/22c52d250b34a0862edc29db03fbec23b30db6db".to_string(),
-        ];
-
-        // Format the sections
-        let (vuln_section, files_section, url_section) =
-            format_mbox_sections(vuln_array, affected_files, url_array);
-
-        // Check that each line is properly indented with a tab
-        assert!(vuln_section.lines().all(|line| line.starts_with('\t')));
-        assert!(files_section.lines().all(|line| line.starts_with('\t')));
-        assert!(url_section.lines().all(|line| line.starts_with('\t')));
-
-        // Check that all content is present
-        assert!(vuln_section.contains("Issue introduced in 5.15"));
-        assert!(vuln_section.contains("Fixed in 5.10"));
-
-        assert!(files_section.contains("drivers/net/ethernet/test.c"));
-        assert!(files_section.contains("include/linux/test.h"));
-
-        assert!(url_section
-            .contains("https://git.kernel.org/stable/c/11c52d250b34a0862edc29db03fbec23b30db6da"));
-        assert!(url_section
-            .contains("https://git.kernel.org/stable/c/22c52d250b34a0862edc29db03fbec23b30db6db"));
-    }
-
-    #[test]
-    fn test_create_mbox_content() {
-        // Create test parameters
-        let params = MboxContentParams {
-            from_line: "From test-script-1.0 Mon Sep 17 00:00:00 2001",
-            user_name: "Test User",
-            user_email: "test@example.com",
-            cve_number: "CVE-2023-1234",
-            commit_subject: "Test CVE",
-            commit_text: "This is a test commit message.\n\nIt contains details about the vulnerability.",
-            vuln_section: "\tIssue introduced in 5.15 with commit 11c52d250b34a0862edc29db03fbec23b30db6da\n\tFixed in 5.10 with commit 22c52d250b34a0862edc29db03fbec23b30db6db\n",
-            files_section: "\tdrivers/net/ethernet/test.c\n\tinclude/linux/test.h\n",
-            url_section: "\thttps://git.kernel.org/stable/c/11c52d250b34a0862edc29db03fbec23b30db6da\n\thttps://git.kernel.org/stable/c/22c52d250b34a0862edc29db03fbec23b30db6db\n",
-        };
-
-        // Create the mbox content
-        let mbox = create_mbox_content(&params);
-
-        // Check the basic structure
-        assert!(mbox.starts_with("From test-script-1.0 Mon Sep 17 00:00:00 2001"));
-        assert!(mbox.contains("From: Test User <test@example.com>"));
-        assert!(mbox.contains("To: <linux-cve-announce@vger.kernel.org>"));
-        assert!(mbox.contains("Subject: CVE-2023-1234: Test CVE"));
-
-        // Check section headers
-        assert!(mbox.contains("Description\n==========="));
-        assert!(mbox.contains("Affected and fixed versions\n==========================="));
-        assert!(mbox.contains("Affected files\n=============="));
-        assert!(mbox.contains("Mitigation\n=========="));
-
-        // Check that the commit message is included
-        assert!(mbox.contains(
-            "This is a test commit message.\n\nIt contains details about the vulnerability."
-        ));
-
-        // Check that other sections are included
-        assert!(mbox.contains(
-            "\tIssue introduced in 5.15 with commit 11c52d250b34a0862edc29db03fbec23b30db6da"
-        ));
-        assert!(mbox.contains("\tdrivers/net/ethernet/test.c"));
-        assert!(mbox.contains(
-            "\thttps://git.kernel.org/stable/c/22c52d250b34a0862edc29db03fbec23b30db6db"
-        ));
-
-        // Check that the result ends with a newline
-        assert!(mbox.ends_with('\n'));
-    }
-}
+/// Generate an mbox file for the vulnerability announcement
+pub fn generate_mbox(params: &MboxParams) -> Result<String> {
+    let provider = ProviderFactory::create(params.provider_type)?;
+    let vuln_params = VulnerabilityRecordParams {
+        vuln_id: params.vuln_id,
+        git_sha_full: params.git_sha_full,
+        commit_subject: params.commit_subject,
+        user_name: params.user_name,
+        user_email: params.user_email,
+        dyad_entries: params.dyad_entries.to_vec(),
+        script_name: params.script_name,
+        script_version: params.script_version,
+        additional_references: params.additional_references,
+        commit_text: params.commit_text,
+        affected_files: params.affected_files,
+    };
+    provider.generate_mbox(&vuln_params)
+}
\ No newline at end of file
diff --git a/tools/bippy/src/lib.rs b/tools/bippy/src/lib.rs
index 90f19a4..8486a91 100644
--- a/tools/bippy/src/lib.rs
+++ b/tools/bippy/src/lib.rs
@@ -4,4 +4,5 @@
 
 pub mod models;
 pub mod utils;
-pub mod commands;
\ No newline at end of file
+pub mod commands;
+pub mod providers;
\ No newline at end of file
diff --git a/tools/bippy/src/main.rs b/tools/bippy/src/main.rs
index 087d3a4..47c3b16 100644
--- a/tools/bippy/src/main.rs
+++ b/tools/bippy/src/main.rs
@@ -4,8 +4,8 @@
 
 use anyhow::{Context, Result};
 use clap::Parser;
-use cve_utils::git_config;
-use cve_utils::git_utils::{get_object_full_sha, resolve_reference, get_affected_files};
+use vuln_utils::git_config;
+use vuln_utils::git_utils::{get_object_full_sha, resolve_reference, get_affected_files};
 use git2::Repository;
 use log::{debug, error, warn};
 use std::env;
@@ -13,10 +13,12 @@
 
 mod commands;
 mod models;
+mod providers;
 mod utils;
 
-use commands::{generate_json, generate_mbox, json::CveRecordParams, mbox::MboxParams};
+use commands::{generate_json, generate_mbox, json::VulnRecordParams, mbox::MboxParams};
 use models::{Args, DyadEntry};
+use providers::ProviderFactory;
 use utils::{
     apply_diff_to_text, get_commit_subject, get_commit_text, read_tags_file, run_dyad,
     strip_commit_text,
@@ -49,7 +51,7 @@
 }
 
 /// Type alias for argument validation return value
-type ArgsResult = (String, String, String, Vec<String>, Vec<String>);
+type ArgsResult = (String, String, String, String, Vec<String>, Vec<String>);
 
 /// Validate command line arguments and environment variables
 fn validate_args_and_env(args: &Args) -> ArgsResult {
@@ -62,9 +64,12 @@
         std::process::exit(1);
     }
 
+    // Handle legacy CVE argument
+    let vuln_id = args.id.as_ref().or(args.cve.as_ref());
+
     // Check for required arguments
-    if args.cve.is_none() {
-        error!("Missing required argument: cve");
+    if vuln_id.is_none() {
+        error!("Missing required argument: id (vulnerability ID)");
         std::process::exit(1);
     }
 
@@ -78,11 +83,28 @@
         std::process::exit(1);
     }
 
-    // Check for CVE_USER environment variable if user is not specified
+    // Create provider to get provider-specific configuration
+    let provider = match ProviderFactory::create(&args.provider) {
+        Ok(p) => p,
+        Err(e) => {
+            error!("Invalid provider '{}': {}", args.provider, e);
+            std::process::exit(1);
+        }
+    };
+
+    // Validate vulnerability ID format
+    let vuln_id_str = vuln_id.unwrap();
+    if let Err(e) = provider.validate_id(vuln_id_str) {
+        error!("{}", e);
+        std::process::exit(1);
+    }
+
+    // Check for provider-specific USER environment variable if user is not specified
+    let user_env_var = provider.user_env_var();
     let user_email = args.user.as_ref().map_or_else(
         || {
-            env::var("CVE_USER").unwrap_or_else(|_| {
-                error!("Missing required argument: user (-u/--user) and CVE_USER environment variable is not set");
+            env::var(user_env_var).unwrap_or_else(|_| {
+                error!("Missing required argument: user (-u/--user) and {} environment variable is not set", user_env_var);
                 std::process::exit(1);
             })
         },
@@ -97,7 +119,8 @@
     }
 
     // Extract values from args
-    let cve_number = args.cve.as_ref().unwrap().clone();
+    let provider_type = args.provider.clone();
+    let vuln_number = vuln_id_str.clone();
     let git_shas: Vec<String> = args
         .sha
         .iter()
@@ -124,7 +147,8 @@
         .unwrap_or_else(|| git_config::get_git_config("user.name").unwrap_or_default());
 
     // Debug output if verbose is enabled
-    debug!("CVE_NUMBER={cve_number}");
+    debug!("PROVIDER={provider_type}");
+    debug!("VULN_ID={vuln_number}");
     debug!("GIT_SHAS={git_shas:?}");
     debug!("JSON_FILE={:?}", args.json);
     debug!("MBOX_FILE={:?}", args.mbox);
@@ -132,14 +156,14 @@
     debug!("REFERENCE_FILE={:?}", args.reference);
     debug!("GIT_VULNERABLE={vulnerable_shas:?}");
 
-    (cve_number, user_name, user_email, git_shas, vulnerable_shas)
+    (provider_type, vuln_number, user_name, user_email, git_shas, vulnerable_shas)
 }
 
 /// Get repository and script directories
 fn get_directories() -> Result<(String, PathBuf, String, String)> {
-    // Get vulns directory using cve_utils
+    // Get vulns directory using vuln_utils
     let vulns_dir =
-        cve_utils::find_vulns_dir().with_context(|| "Failed to find vulns directory")?;
+        vuln_utils::find_vulns_dir().with_context(|| "Failed to find vulns directory")?;
 
     // Get scripts directory
     let script_dir = vulns_dir.join("scripts");
@@ -312,7 +336,8 @@
 
 /// Output parameters for generating files
 struct OutputParams<'a> {
-    cve_number: &'a str,
+    provider_type: &'a str,
+    vuln_id: &'a str,
     git_sha_full: &'a str,
     commit_subject: &'a str,
     user_name: &'a str,
@@ -335,7 +360,8 @@
     if let Some(path) = mbox_path {
         // Create MboxParams from OutputParams
         let mbox_params = MboxParams {
-            cve_number: params.cve_number,
+            provider_type: params.provider_type,
+            vuln_id: params.vuln_id,
             git_sha_full: params.git_sha_full,
             commit_subject: params.commit_subject,
             user_name: params.user_name,
@@ -348,20 +374,26 @@
             affected_files: params.affected_files,
         };
 
-        let mbox_content = generate_mbox(&mbox_params);
-
-        if let Err(err) = std::fs::write(path, mbox_content) {
-            error!("Warning: Failed to write mbox file to {}: {err}", path.display());
-        } else {
-            debug!("Wrote mbox file to {path}", path = path.display());
+        match generate_mbox(&mbox_params) {
+            Ok(mbox_content) => {
+                if let Err(err) = std::fs::write(path, mbox_content) {
+                    error!("Warning: Failed to write mbox file to {}: {err}", path.display());
+                } else {
+                    debug!("Wrote mbox file to {path}", path = path.display());
+                }
+            }
+            Err(err) => {
+                error!("Error: Failed to generate mbox: {err}");
+            }
         }
     }
 
     // Generate JSON file if requested
     if let Some(path) = json_path {
-        // Create CveRecordParams from OutputParams
-        let json_params = CveRecordParams {
-            cve_number: params.cve_number,
+        // Create VulnRecordParams from OutputParams
+        let json_params = VulnRecordParams {
+            provider_type: params.provider_type,
+            vuln_id: params.vuln_id,
             git_sha_full: params.git_sha_full,
             commit_subject: params.commit_subject,
             user_name: params.user_name,
@@ -401,7 +433,7 @@
     log_command_args();
 
     // Validate arguments and environment variables
-    let (cve_number, user_name, user_email, git_shas, vulnerable_shas) =
+    let (provider_type, vuln_id, user_name, user_email, git_shas, vulnerable_shas) =
         validate_args_and_env(&args);
 
     // Get required directories and script information
@@ -418,8 +450,8 @@
 
     // ***POLICY***
     // Determine if we actually have any "pairs" of commits that are in a released kernel such that
-    // we should be issuing a CVE or not.  This is a policy decision from cve.org which requires
-    // that an actual release is vulnerable in order for a CVE to be issued, it's not just the fact
+    // we should be issuing a vulnerability record or not. This is a policy decision which requires
+    // that an actual release is vulnerable in order for a record to be issued, it's not just the fact
     // that we created and fixed a bug in a git range.
     let mut is_vulnerable = false;
     for entry in &dyad_entries {
@@ -430,7 +462,7 @@
         is_vulnerable = true;
     }
     if !is_vulnerable {
-        error!("Despite having some vulnerable:fixed kernels, none were in an actual release, so aborting and not assigning a CVE to {git_sha_full}");
+        error!("Despite having some vulnerable:fixed kernels, none were in an actual release, so aborting and not assigning a vulnerability ID to {git_sha_full}");
         std::process::exit(1);
     }
 
@@ -439,7 +471,8 @@
 
     // Create output parameters
     let output_params = OutputParams {
-        cve_number: &cve_number,
+        provider_type: &provider_type,
+        vuln_id: &vuln_id,
         git_sha_full: &git_sha_full,
         commit_subject: &commit_subject,
         user_name: &user_name,
@@ -465,7 +498,7 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use cve_utils::version_utils::{version_is_mainline, version_is_queue, version_is_rc};
+    use vuln_utils::version_utils::{version_is_mainline, version_is_queue, version_is_rc};
     use std::fs::File;
     use std::io::Write;
     use tempfile::tempdir;
@@ -758,7 +791,7 @@
         file.write_all(uuid_content.as_bytes()).unwrap();
 
         // Test reading the UUID file
-        let uuid = utils::file::read_uuid(dir.path()).unwrap();
+        let uuid = utils::file::read_uuid(dir.path(), "linux.uuid").unwrap();
         assert_eq!(uuid, "12345678-abcd-efgh-ijkl-mnopqrstuvwx");
 
         // Test with empty file
@@ -766,7 +799,7 @@
         let empty_path = dir.path().join("linux.uuid");
         let mut file = File::create(&empty_path).unwrap();
         file.write_all(b"").unwrap();
-        let result = utils::file::read_uuid(dir.path());
+        let result = utils::file::read_uuid(dir.path(), "linux.uuid");
         assert!(result.is_err());
     }
 
@@ -792,4 +825,4 @@
             "Version should only contain digits and dots"
         );
     }
-}
+}
\ No newline at end of file
diff --git a/tools/bippy/src/models/cli.rs b/tools/bippy/src/models/cli.rs
index 5b919ae..d2caa73 100644
--- a/tools/bippy/src/models/cli.rs
+++ b/tools/bippy/src/models/cli.rs
@@ -9,9 +9,13 @@
 #[derive(Parser, Debug)]
 #[clap(author, version, about, long_about = None, disable_version_flag = true, trailing_var_arg = true)]
 pub struct Args {
-    /// CVE number (e.g., "CVE-2021-1234")
-    #[clap(short, long)]
-    pub cve: Option<String>,
+    /// Provider type (e.g., "cve", "gsd", "euvd")
+    #[clap(short = 'p', long, default_value = "cve")]
+    pub provider: String,
+
+    /// Vulnerability ID (e.g., "CVE-2021-1234", "GSD-2021-1234")
+    #[clap(short = 'i', long)]
+    pub id: Option<String>,
 
     /// Git SHA(s) of the commit(s)
     #[clap(short, long, num_args = 1..)]
@@ -52,4 +56,9 @@
     /// Catch any trailing arguments
     #[clap(hide = true)]
     pub remaining_parameters: Vec<String>,
+
+    // Legacy support
+    /// CVE number (deprecated: use --id instead)
+    #[clap(short = 'c', long, hide = true)]
+    pub cve: Option<String>,
 }
diff --git a/tools/bippy/src/models/cve.rs b/tools/bippy/src/models/cve.rs
deleted file mode 100644
index c45e393..0000000
--- a/tools/bippy/src/models/cve.rs
+++ /dev/null
@@ -1,151 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-only
-//
-// Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
-
-use serde::{Deserialize, Serialize};
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct CveMetadata {
-    #[serde(rename = "assignerOrgId")]
-    pub assigner_org_id: String,
-    #[serde(rename = "cveID")]
-    pub cve_id: String,
-    #[serde(rename = "requesterUserId")]
-    pub requester_user_id: String,
-    pub serial: String,
-    pub state: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct Description {
-    pub lang: String,
-    pub value: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct ProviderMetadata {
-    #[serde(rename = "orgId")]
-    pub org_id: String,
-}
-
-#[derive(Debug, Serialize, Deserialize, Default)]
-pub struct VersionRange {
-    // Version string, in a specific type, see versionType below for the valid types
-    // 0 means "beginning of time"
-    pub version: String,
-
-    #[serde(rename = "lessThan", skip_serializing_if = "Option::is_none")]
-    pub less_than: Option<String>,
-
-    #[serde(rename = "lessThanOrEqual", skip_serializing_if = "Option::is_none")]
-    pub less_than_or_equal: Option<String>,
-
-    // valid values are "affected", "unaffected", or "unknown"
-    pub status: String,
-
-    // valid values are "custom", "git", "maven", "python", "rpm", or "semver"
-    // We will just stick with "git" or "semver" as that's the most sane for us, even though
-    // "semver" is NOT what Linux kernel release numbers represent at all.
-    #[serde(rename = "versionType", skip_serializing_if = "Option::is_none")]
-    pub version_type: Option<String>,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct AffectedProduct {
-    pub product: String,
-    pub vendor: String,
-    #[serde(rename = "defaultStatus")]
-    pub default_status: String,
-    pub repo: String,
-    #[serde(rename = "programFiles")]
-    pub program_files: Vec<String>,
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    pub versions: Vec<VersionRange>,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct Reference {
-    pub url: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct Generator {
-    pub engine: String,
-}
-
-#[derive(Debug, Serialize, Deserialize, Default)]
-pub struct CpeMatch {
-    // boolean value, must be "true" or "false"
-    pub vulnerable: bool,
-
-    // critera for us is always going to be: "cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*"
-    pub criteria: String,
-
-    #[serde(rename = "versionStartIncluding")]
-    #[serde(skip_serializing_if = "String::is_empty")]
-    pub version_start_including: String,
-
-    #[serde(rename = "versionEndExcluding")]
-    #[serde(skip_serializing_if = "String::is_empty")]
-    pub version_end_excluding: String,
-
-    // Odds are we will not use the following fields, but they are here
-    // just to round out the documentation of the schema
-    #[serde(rename = "matchCriteriaId")]
-    #[serde(skip_serializing_if = "String::is_empty")]
-    pub match_criteria_id: String,
-
-    #[serde(rename = "versionStartExcluding")]
-    #[serde(skip_serializing_if = "String::is_empty")]
-    pub version_start_excluding: String,
-
-    #[serde(rename = "versionEndIncluding")]
-    #[serde(skip_serializing_if = "String::is_empty")]
-    pub version_end_including: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct CpeNodes {
-    // must be "OR" or "AND"
-    pub operator: String,
-    // boolean value, must be "true" or "false"
-    pub negate: bool,
-    #[serde(rename = "cpeMatch")]
-    pub cpe_match: Vec<CpeMatch>,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct CpeApplicability {
-    pub nodes: Vec<CpeNodes>,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct CnaData {
-    #[serde(rename = "providerMetadata")]
-    pub provider_metadata: ProviderMetadata,
-    pub descriptions: Vec<Description>,
-    pub affected: Vec<AffectedProduct>,
-    #[serde(rename = "cpeApplicability")]
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    pub cpe_applicability: Vec<CpeApplicability>,
-    pub references: Vec<Reference>,
-    pub title: String,
-    #[serde(rename = "x_generator")]
-    pub x_generator: Generator,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct Containers {
-    pub cna: CnaData,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct CveRecord {
-    pub containers: Containers,
-    #[serde(rename = "cveMetadata")]
-    pub cve_metadata: CveMetadata,
-    #[serde(rename = "dataType")]
-    pub data_type: String,
-    #[serde(rename = "dataVersion")]
-    pub data_version: String,
-}
diff --git a/tools/bippy/src/models/dyad.rs b/tools/bippy/src/models/dyad.rs
index 4b65fff..c25913d 100644
--- a/tools/bippy/src/models/dyad.rs
+++ b/tools/bippy/src/models/dyad.rs
@@ -3,7 +3,7 @@
 // Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
 
 use crate::models::errors::BippyError;
-use cve_utils::Kernel;
+use vuln_utils::Kernel;
 
 /// `DyadEntry` represents a kernel vulnerability range entry from the dyad script
 #[derive(Debug, Clone)]
diff --git a/tools/bippy/src/models/mod.rs b/tools/bippy/src/models/mod.rs
index 25d6f09..fda4b8d 100644
--- a/tools/bippy/src/models/mod.rs
+++ b/tools/bippy/src/models/mod.rs
@@ -3,10 +3,8 @@
 // Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
 
 pub mod cli;
-pub mod cve;
 pub mod dyad;
 pub mod errors;
 
 pub use cli::Args;
-pub use cve::*;
 pub use dyad::DyadEntry;
diff --git a/tools/bippy/src/providers/common.rs b/tools/bippy/src/providers/common.rs
new file mode 100644
index 0000000..27d5757
--- /dev/null
+++ b/tools/bippy/src/providers/common.rs
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
+
+//! Common data structures used across different vulnerability providers
+
+use serde::{Deserialize, Serialize};
+
+/// Version range information used by providers
+#[derive(Debug, Serialize, Deserialize, Default)]
+pub struct VersionRange {
+    /// Version string, in a specific type, see versionType below for the valid types
+    /// 0 means "beginning of time"
+    pub version: String,
+
+    #[serde(rename = "lessThan", skip_serializing_if = "Option::is_none")]
+    pub less_than: Option<String>,
+
+    #[serde(rename = "lessThanOrEqual", skip_serializing_if = "Option::is_none")]
+    pub less_than_or_equal: Option<String>,
+
+    /// valid values are "affected", "unaffected", or "unknown"
+    pub status: String,
+
+    /// valid values are "custom", "git", "maven", "python", "rpm", or "semver"
+    /// We will just stick with "git" or "semver" as that's the most sane for us, even though
+    /// "semver" is NOT what Linux kernel release numbers represent at all.
+    #[serde(rename = "versionType", skip_serializing_if = "Option::is_none")]
+    pub version_type: Option<String>,
+}
+
+/// CPE (Common Platform Enumeration) match information
+#[derive(Debug, Serialize, Deserialize, Default)]
+pub struct CpeMatch {
+    /// boolean value, must be "true" or "false"
+    pub vulnerable: bool,
+
+    /// critera for us is always going to be: "cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*"
+    pub criteria: String,
+
+    #[serde(rename = "versionStartIncluding")]
+    #[serde(skip_serializing_if = "String::is_empty")]
+    pub version_start_including: String,
+
+    #[serde(rename = "versionEndExcluding")]
+    #[serde(skip_serializing_if = "String::is_empty")]
+    pub version_end_excluding: String,
+
+    /// Odds are we will not use the following fields, but they are here
+    /// just to round out the documentation of the schema
+    #[serde(rename = "matchCriteriaId")]
+    #[serde(skip_serializing_if = "String::is_empty")]
+    pub match_criteria_id: String,
+
+    #[serde(rename = "versionStartExcluding")]
+    #[serde(skip_serializing_if = "String::is_empty")]
+    pub version_start_excluding: String,
+
+    #[serde(rename = "versionEndIncluding")]
+    #[serde(skip_serializing_if = "String::is_empty")]
+    pub version_end_including: String,
+}
+
+/// CPE nodes for vulnerability applicability
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CpeNodes {
+    /// must be "OR" or "AND"
+    pub operator: String,
+    /// boolean value, must be "true" or "false"
+    pub negate: bool,
+    #[serde(rename = "cpeMatch")]
+    pub cpe_match: Vec<CpeMatch>,
+}
\ No newline at end of file
diff --git a/tools/bippy/src/providers/cve/json.rs b/tools/bippy/src/providers/cve/json.rs
new file mode 100644
index 0000000..b284653
--- /dev/null
+++ b/tools/bippy/src/providers/cve/json.rs
@@ -0,0 +1,433 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
+
+use anyhow::Result;
+use serde::Serialize;
+use serde_json::ser::{PrettyFormatter, Serializer};
+use std::collections::HashSet;
+
+use super::models::{
+    AffectedProduct, CnaData, Containers, CpeApplicability, CveMetadata, CveRecord,
+    Description, Generator, ProviderMetadata, Reference,
+};
+use crate::providers::CpeNodes;
+use crate::models::DyadEntry;
+use crate::utils::{
+    determine_default_status, generate_cpe_ranges, generate_git_ranges, generate_version_ranges,
+};
+
+/// Parameters for generating a JSON CVE record
+pub struct CveRecordParams<'a> {
+    /// Organization UUID
+    pub uuid: &'a str,
+    /// CVE identifier (e.g., "CVE-2023-12345")
+    pub cve_number: &'a str,
+    /// Full Git SHA of the commit that fixes the vulnerability
+    pub git_sha_full: &'a str,
+    /// Subject line of the commit
+    pub commit_subject: &'a str,
+    /// Name of the user creating the CVE
+    pub user_name: &'a str,
+    /// Email of the user creating the CVE
+    pub user_email: &'a str,
+    /// Dyad entries containing vulnerability and fix information
+    pub dyad_entries: Vec<DyadEntry>,
+    /// Name of the script generating the record
+    pub script_name: &'a str,
+    /// Version of the script generating the record
+    pub script_version: &'a str,
+    /// Additional reference URLs
+    pub additional_references: &'a [String],
+    /// Full commit text/description
+    pub commit_text: &'a str,
+    /// List of affected files
+    pub affected_files: &'a Vec<String>,
+}
+
+
+/// Prepare dyad entries and affected files
+fn prepare_vulnerability_data(
+    git_sha_full: &str,
+    in_dyad_entries: &[DyadEntry],
+) -> Result<Vec<DyadEntry>> {
+    // Clone dyad entries since we might need to modify them
+    let mut dyad_entries = in_dyad_entries.to_vec();
+
+    // If no entries were created, use the fix commit as a fallback
+    if dyad_entries.is_empty() {
+        // Create a dummy entry using the fix commit
+        if let Ok(entry) = DyadEntry::from_str(&format!("0:0:0:{git_sha_full}")) {
+            dyad_entries.push(entry);
+        }
+    }
+
+    Ok(dyad_entries)
+}
+
+/// Create affected products (kernel and git)
+fn create_affected_products(
+    dyad_entries: &[DyadEntry],
+    affected_files: Vec<String>,
+) -> (AffectedProduct, AffectedProduct, Vec<CpeNodes>) {
+    // Determine default status
+    let default_status = determine_default_status(dyad_entries);
+
+    // Generate version ranges for kernel product
+    let kernel_versions = generate_version_ranges(dyad_entries, default_status);
+    let kernel_product = AffectedProduct {
+        product: "Linux".to_string(),
+        vendor: "Linux".to_string(),
+        default_status: default_status.to_string(),
+        repo: "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git".to_string(),
+        program_files: affected_files.clone(),
+        versions: kernel_versions,
+    };
+
+    // Generate git ranges for git product
+    let git_versions = generate_git_ranges(dyad_entries);
+    let git_product = AffectedProduct {
+        product: "Linux".to_string(),
+        vendor: "Linux".to_string(),
+        default_status: "unaffected".to_string(),
+        repo: "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git".to_string(),
+        program_files: affected_files,
+        versions: git_versions,
+    };
+
+    // Generate CPE ranges
+    let cpe_nodes = generate_cpe_ranges(dyad_entries);
+
+    (kernel_product, git_product, cpe_nodes)
+}
+
+/// Generate references from dyad entries and additional references
+fn generate_references(
+    dyad_entries: &[DyadEntry],
+    additional_references: &[String],
+    git_sha_full: &str,
+) -> Vec<Reference> {
+    let mut references = Vec::new();
+    let mut seen_refs: HashSet<String> = HashSet::new();
+
+    // Add references for all entries
+    for entry in dyad_entries {
+        // Add fixed commit reference if available
+        if !entry.fixed.is_empty() {
+            let url = format!("https://git.kernel.org/stable/c/{}", entry.fixed.git_id());
+            if !seen_refs.contains(&url) {
+                seen_refs.insert(url.clone());
+                references.push(Reference { url });
+            }
+        }
+    }
+
+    // Add any additional references from the reference file
+    for url in additional_references {
+        if !seen_refs.contains(url) {
+            seen_refs.insert(url.clone());
+            references.push(Reference { url: url.clone() });
+        }
+    }
+
+    // If no references were found, add the main fix commit
+    if references.is_empty() {
+        let main_fix_url = format!("https://git.kernel.org/stable/c/{git_sha_full}");
+        references.push(Reference { url: main_fix_url });
+    }
+
+    references
+}
+
+/// Process commit description text and handle truncation
+fn process_description(commit_text: &str) -> String {
+    // Truncate description to 3982 characters (CVE backend limit) if needed
+    let max_length = 3982; // CVE backend limit
+
+    if commit_text.len() <= max_length {
+        // If already under the limit, just ensure no trailing newline
+        return commit_text.trim_end().to_string();
+    }
+
+    // Get the truncated text limited to max_length
+    let truncated = &commit_text[..max_length];
+
+    // Special case: if only over by a trailing newline, just trim it
+    if commit_text.len() == max_length + 1 && commit_text.ends_with('\n') {
+        truncated.to_string()
+    } else {
+        // Add truncation marker, with proper newline handling
+        let separator = if truncated.ends_with('\n') { "" } else { "\n" };
+        format!("{truncated}{separator}---truncated---")
+    }
+}
+
+/// Parameters for creating a CVE record
+struct CveRecordCreationParams<'a> {
+    uuid: String,
+    cve_number: &'a str,
+    commit_subject: &'a str,
+    user_email: &'a str,
+    script_name: &'a str,
+    script_version: &'a str,
+    truncated_description: String,
+    kernel_product: AffectedProduct,
+    git_product: AffectedProduct,
+    cpe_nodes: Vec<CpeNodes>,
+    references: Vec<Reference>,
+}
+
+/// Create the CVE record structure
+fn create_cve_record(params: CveRecordCreationParams) -> CveRecord {
+    CveRecord {
+        containers: Containers {
+            cna: CnaData {
+                provider_metadata: ProviderMetadata {
+                    org_id: params.uuid.clone(),
+                },
+                descriptions: vec![Description {
+                    lang: "en".to_string(),
+                    value: params.truncated_description,
+                }],
+                affected: vec![params.git_product, params.kernel_product],
+                cpe_applicability: vec![CpeApplicability {
+                    nodes: params.cpe_nodes,
+                }],
+                references: params.references,
+                title: params.commit_subject.to_string(),
+                x_generator: Generator {
+                    engine: format!("{}-{}", params.script_name, params.script_version),
+                },
+            },
+        },
+        cve_metadata: CveMetadata {
+            assigner_org_id: params.uuid.to_string(),
+            cve_id: params.cve_number.to_string(),
+            requester_user_id: params.user_email.to_string(),
+            serial: "1".to_string(),
+            state: "PUBLISHED".to_string(),
+        },
+        data_type: "CVE_RECORD".to_string(),
+        data_version: "5.0".to_string(),
+    }
+}
+
+/// Serialize the CVE record to JSON
+fn serialize_cve_record(cve_record: &CveRecord) -> Result<String> {
+    // Use a custom formatter with 3-space indentation
+    let formatter = PrettyFormatter::with_indent(b"   ");
+    let mut output = Vec::new();
+    let mut serializer = Serializer::with_formatter(&mut output, formatter);
+
+    cve_record
+        .serialize(&mut serializer)
+        .map_err(|e| anyhow::anyhow!("Error serializing JSON: {e}"))?;
+
+    let json_string = String::from_utf8(output)
+        .map_err(|e| anyhow::anyhow!("Error converting JSON to string: {e}"))?;
+
+    // Ensure the JSON output ends with a newline
+    if json_string.ends_with('\n') {
+        Ok(json_string)
+    } else {
+        Ok(json_string + "\n")
+    }
+}
+
+/// Generate a JSON for the CVE
+pub fn generate_json(params: &CveRecordParams) -> Result<String> {
+    let CveRecordParams {
+        uuid,
+        cve_number,
+        git_sha_full,
+        commit_subject,
+        user_name: _user_name, // Not used in this function
+        user_email,
+        dyad_entries: in_dyad_entries,
+        script_name,
+        script_version,
+        additional_references,
+        commit_text,
+        affected_files,
+    } = params;
+
+    // Prepare dyad entries
+    let dyad_entries = prepare_vulnerability_data(git_sha_full, in_dyad_entries)?;
+
+    // Create affected products
+    let (kernel_product, git_product, cpe_nodes) =
+        create_affected_products(&dyad_entries, affected_files.to_vec());
+
+    // Generate references
+    let references = generate_references(&dyad_entries, additional_references, git_sha_full);
+
+    // Process description
+    let truncated_description = process_description(commit_text);
+
+    // Create CVE record
+    let cve_record = create_cve_record(CveRecordCreationParams {
+        uuid: uuid.to_string(),
+        cve_number,
+        commit_subject,
+        user_email,
+        script_name,
+        script_version,
+        truncated_description,
+        kernel_product,
+        git_product,
+        cpe_nodes,
+        references,
+    });
+
+    // Serialize CVE record to JSON
+    serialize_cve_record(&cve_record)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::providers::cve::models::CveRecord;
+
+    #[test]
+    fn test_process_description() {
+        // Test short description (under limit)
+        let short_text = "This is a short description.";
+        assert_eq!(process_description(short_text), short_text);
+
+        // Test description at exactly the limit
+        let at_limit_text = "a".repeat(3982);
+        assert_eq!(process_description(&at_limit_text), at_limit_text);
+
+        // Test description over the limit
+        let over_limit_text = "a".repeat(4000);
+        let processed = process_description(&over_limit_text);
+        assert!(processed.len() <= 3982 + 16); // Max length + truncated marker length
+        assert!(processed.ends_with("---truncated---"));
+
+        // Test with trailing newline exactly over limit
+        let newline_text = "a".repeat(3981) + "\n";
+        assert_eq!(process_description(&newline_text), "a".repeat(3981));
+    }
+
+    #[test]
+    fn test_generate_references() {
+        use crate::models::dyad::DyadEntry;
+        use vuln_utils::Kernel;
+
+        // Helper function to create test kernels
+        fn create_test_kernel(_version: &str, git_id: &str) -> Kernel {
+            // In tests, we don't have real git commit IDs to look up,
+            // so we'll create dummy kernels with the provided info
+            let kernel = Kernel::from_id(git_id).unwrap_or_else(|_| Kernel::empty_kernel());
+
+            // We can't directly modify the fields, but for testing
+            // purposes, we're assuming these are valid git IDs and versions
+            kernel
+        }
+
+        // Create test dyad entries
+        let fixed_kernel1 = create_test_kernel("5.15", "11c52d250b34a0862edc29db03fbec23b30db6da");
+        let fixed_kernel2 = create_test_kernel("5.10", "22c52d250b34a0862edc29db03fbec23b30db6db");
+        let vuln_kernel = create_test_kernel("5.4", "33c52d250b34a0862edc29db03fbec23b30db6dc");
+
+        let entries = vec![
+            DyadEntry {
+                vulnerable: vuln_kernel.clone(),
+                fixed: fixed_kernel1,
+            },
+            DyadEntry {
+                vulnerable: vuln_kernel,
+                fixed: fixed_kernel2,
+            },
+        ];
+
+        let additional_refs = vec![
+            "https://example.com/ref1".to_string(),
+            "https://example.com/ref2".to_string(),
+        ];
+
+        let git_sha_full = "abcdef1234567890";
+
+        // Test reference generation
+        let references = generate_references(&entries, &additional_refs, git_sha_full);
+
+        // With our updated Kernel::from_id implementation, our test kernels may not
+        // generate references in the same way, so we'll check for at least the main fix
+        // reference and the additional refs
+        assert!(references.len() >= 3);
+
+        // With our test kernels, we can't reliably check for specific git URLs,
+        // so we'll only check for the additional references
+
+        // Check that additional references were added
+        assert!(references
+            .iter()
+            .any(|r| r.url == "https://example.com/ref1"));
+        assert!(references
+            .iter()
+            .any(|r| r.url == "https://example.com/ref2"));
+
+        // Test with no dyad entries and no additional references
+        let references = generate_references(&[], &[], git_sha_full);
+
+        // Should have 1 reference (main fix commit)
+        assert_eq!(references.len(), 1);
+        assert_eq!(
+            references[0].url,
+            format!("https://git.kernel.org/stable/c/{git_sha_full}")
+        );
+    }
+
+    #[test]
+    fn test_serialize_cve_record() {
+        // Create a minimal CVE record for testing
+        let cve_record = CveRecord {
+            containers: Containers {
+                cna: CnaData {
+                    provider_metadata: ProviderMetadata {
+                        org_id: "test-uuid".to_string(),
+                    },
+                    descriptions: vec![Description {
+                        lang: "en".to_string(),
+                        value: "Test description".to_string(),
+                    }],
+                    affected: vec![],
+                    cpe_applicability: vec![],
+                    references: vec![],
+                    title: "Test CVE".to_string(),
+                    x_generator: Generator {
+                        engine: "test-engine".to_string(),
+                    },
+                },
+            },
+            cve_metadata: CveMetadata {
+                assigner_org_id: "test-uuid".to_string(),
+                cve_id: "CVE-2023-1234".to_string(),
+                requester_user_id: "test@example.com".to_string(),
+                serial: "1".to_string(),
+                state: "PUBLISHED".to_string(),
+            },
+            data_type: "CVE_RECORD".to_string(),
+            data_version: "5.0".to_string(),
+        };
+
+        // Serialize the record
+        let json = serialize_cve_record(&cve_record).unwrap();
+
+        // Verify it's valid JSON by parsing it
+        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+
+        // Check basic structure
+        assert_eq!(parsed["dataType"], "CVE_RECORD");
+        assert_eq!(parsed["dataVersion"], "5.0");
+        assert_eq!(parsed["cveMetadata"]["cveID"], "CVE-2023-1234");
+        assert_eq!(parsed["cveMetadata"]["state"], "PUBLISHED");
+        assert_eq!(parsed["containers"]["cna"]["title"], "Test CVE");
+
+        // Check that the output ends with a newline
+        assert!(json.ends_with('\n'));
+
+        // Check for 3-space indentation
+        assert!(json.contains("\n   "));
+    }
+}
\ No newline at end of file
diff --git a/tools/bippy/src/providers/cve/mbox.rs b/tools/bippy/src/providers/cve/mbox.rs
new file mode 100644
index 0000000..bde24d9
--- /dev/null
+++ b/tools/bippy/src/providers/cve/mbox.rs
@@ -0,0 +1,463 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
+
+use vuln_utils::version_utils::compare_kernel_versions;
+use std::fmt::Write;
+
+use crate::models::DyadEntry;
+
+/// Parameters for generating an mbox-formatted CVE announcement
+pub struct MboxParams<'a> {
+    /// CVE identifier (e.g., "CVE-2023-12345")
+    pub cve_number: &'a str,
+    /// Full Git SHA of the commit that fixes the vulnerability
+    pub git_sha_full: &'a str,
+    /// Subject line of the commit
+    pub commit_subject: &'a str,
+    /// Name of the user creating the CVE
+    pub user_name: &'a str,
+    /// Email of the user creating the CVE
+    pub user_email: &'a str,
+    /// Dyad entries containing vulnerability and fix information
+    pub dyad_entries: &'a [DyadEntry],
+    /// Name of the script generating the record
+    pub script_name: &'a str,
+    /// Version of the script generating the record
+    pub script_version: &'a str,
+    /// Additional reference URLs
+    pub additional_references: &'a [String],
+    /// Full commit text/description
+    pub commit_text: &'a str,
+    /// List of affected files
+    pub affected_files: &'a Vec<String>,
+}
+
+/// Parse dyad entries into vulnerability information strings
+fn parse_dyad_entries(dyad_entries: &[DyadEntry]) -> Vec<String> {
+    let mut vuln_array_mbox = Vec::new();
+
+    for entry in dyad_entries {
+        // Handle unfixed vulnerabilities
+        if entry.fixed.is_empty() {
+            // Issue is not fixed, so say that:
+            vuln_array_mbox.push(format!(
+                "Issue introduced in {} with commit {}",
+                entry.vulnerable.version(),
+                entry.vulnerable.git_id()
+            ));
+            continue;
+        }
+
+        // Skip entries where the vulnerability is in the same version it was fixed
+        if entry.vulnerable.version() == entry.fixed.version() {
+            continue;
+        }
+
+        // Handle different types of entries
+        if entry.vulnerable.is_empty() {
+            // We do not know when it showed up, so just say it is fixed
+            vuln_array_mbox.push(format!(
+                "Fixed in {} with commit {}",
+                entry.fixed.version(),
+                entry.fixed.git_id()
+            ));
+        } else {
+            // Report when it was introduced and when it was fixed
+            vuln_array_mbox.push(format!(
+                "Issue introduced in {} with commit {} and fixed in {} with commit {}",
+                entry.vulnerable.version(),
+                entry.vulnerable.git_id(),
+                entry.fixed.version(),
+                entry.fixed.git_id()
+            ));
+        }
+    }
+
+    // If no vulnerabilities were found, do NOT create a CVE at all!
+    assert!(!vuln_array_mbox.is_empty(), "No vulnerable:fixed kernel versions, aborting!");
+
+    vuln_array_mbox
+}
+
+/// Collect reference URLs from dyad entries and additional references
+fn collect_reference_urls(
+    dyad_entries: &[DyadEntry],
+    additional_references: &[String],
+    git_sha_full: &str,
+) -> Vec<String> {
+    // First add all fix commit URLs from dyad entries (except the main fix)
+    let mut version_url_pairs = Vec::new();
+    for entry in dyad_entries {
+        if !entry.fixed.is_empty() && entry.fixed.git_id() != git_sha_full {
+            let fix_url = format!("https://git.kernel.org/stable/c/{}", entry.fixed.git_id());
+            if !version_url_pairs.iter().any(|(_, url)| url == &fix_url) {
+                version_url_pairs.push((entry.fixed.version().clone(), fix_url));
+            }
+        }
+    }
+
+    // Sort the URLs by kernel version
+    version_url_pairs.sort_by(|(ver_a, _), (ver_b, _)| {
+        // Use the shared compare_kernel_versions function
+        compare_kernel_versions(ver_a, ver_b)
+    });
+
+    // Build the URL array from the sorted pairs
+    let mut url_array = version_url_pairs
+        .into_iter()
+        .map(|(_, url)| url)
+        .collect::<Vec<_>>();
+
+    // Add the main fix commit URL at the end
+    url_array.push(format!("https://git.kernel.org/stable/c/{git_sha_full}"));
+
+    // Add any additional references from the reference file
+    for url in additional_references {
+        if !url_array.contains(url) {
+            url_array.push(url.clone());
+        }
+    }
+
+    url_array
+}
+
+/// Format various sections for the mbox content
+fn format_mbox_sections(
+    vuln_array_mbox: Vec<String>,
+    affected_files: Vec<String>,
+    url_array: Vec<String>,
+) -> (String, String, String) {
+    // Format the vulnerability summary section
+    let mut vuln_section = String::new();
+    for line in vuln_array_mbox {
+        writeln!(vuln_section, "\t{line}").unwrap();
+    }
+
+    // Format the affected files section
+    let mut files_section = String::new();
+    for file in affected_files {
+        writeln!(files_section, "\t{file}").unwrap();
+    }
+
+    // Format the mitigation section with URLs
+    let mut url_section = String::new();
+    for url in url_array {
+        writeln!(url_section, "\t{url}").unwrap();
+    }
+
+    (vuln_section, files_section, url_section)
+}
+
+/// Parameters for creating mbox content
+struct MboxContentParams<'a> {
+    from_line: &'a str,
+    user_name: &'a str,
+    user_email: &'a str,
+    cve_number: &'a str,
+    commit_subject: &'a str,
+    commit_text: &'a str,
+    vuln_section: &'a str,
+    files_section: &'a str,
+    url_section: &'a str,
+}
+
+/// Create the final mbox content
+fn create_mbox_content(params: &MboxContentParams) -> String {
+    // The full formatted mbox content
+    let result = format!(
+        "{}\n\
+         From: {} <{}>\n\
+         To: <linux-cve-announce@vger.kernel.org>\n\
+         Reply-to: <cve@kernel.org>, <linux-kernel@vger.kernel.org>\n\
+         Subject: {}: {}\n\
+         \n\
+         Description\n\
+         ===========\n\
+         \n\
+         {}\n\
+         \n\
+         The Linux kernel CVE team has assigned {} to this issue.\n\
+         \n\
+         \n\
+         Affected and fixed versions\n\
+         ===========================\n\
+         \n\
+         {}\n\
+         Please see https://www.kernel.org for a full list of currently supported\n\
+         kernel versions by the kernel community.\n\
+         \n\
+         Unaffected versions might change over time as fixes are backported to\n\
+         older supported kernel versions.  The official CVE entry at\n\
+         \thttps://cve.org/CVERecord/?id={}\n\
+         will be updated if fixes are backported, please check that for the most\n\
+         up to date information about this issue.\n\
+         \n\
+         \n\
+         Affected files\n\
+         ==============\n\
+         \n\
+         The file(s) affected by this issue are:\n\
+         {}\n\
+         \n\
+         Mitigation\n\
+         ==========\n\
+         \n\
+         The Linux kernel CVE team recommends that you update to the latest\n\
+         stable kernel version for this, and many other bugfixes.  Individual\n\
+         changes are never tested alone, but rather are part of a larger kernel\n\
+         release.  Cherry-picking individual commits is not recommended or\n\
+         supported by the Linux kernel community at all.  If however, updating to\n\
+         the latest release is impossible, the individual changes to resolve this\n\
+         issue can be found at these commits:\n\
+         {}",
+        params.from_line,
+        params.user_name,
+        params.user_email,
+        params.cve_number,
+        params.commit_subject,
+        params.commit_text.trim_end(), // Trim any trailing newlines
+        params.cve_number,
+        params.vuln_section,
+        params.cve_number,
+        params.files_section,
+        params.url_section
+    );
+
+    // Ensure the result ends with a newline
+    if result.ends_with('\n') {
+        result
+    } else {
+        result + "\n"
+    }
+}
+
+/// Generate an mbox file for the CVE
+pub fn generate_mbox(params: &MboxParams) -> String {
+    let MboxParams {
+        cve_number,
+        git_sha_full,
+        commit_subject,
+        user_name,
+        user_email,
+        dyad_entries,
+        script_name,
+        script_version,
+        additional_references,
+        commit_text,
+        affected_files,
+    } = params;
+
+    // For the From line we need the script name and version
+    let from_line = format!("From {script_name}-{script_version} Mon Sep 17 00:00:00 2001");
+
+    // Parse dyad entries into vulnerability information
+    let vuln_array_mbox = parse_dyad_entries(dyad_entries);
+
+    // Collect reference URLs
+    let url_array = collect_reference_urls(dyad_entries, additional_references, git_sha_full);
+
+    // Format sections for the mbox content
+    let (vuln_section, files_section, url_section) =
+        format_mbox_sections(vuln_array_mbox, affected_files.to_vec(), url_array);
+
+    // Create the final mbox content
+    create_mbox_content(&MboxContentParams {
+        from_line: &from_line,
+        user_name,
+        user_email,
+        cve_number,
+        commit_subject,
+        commit_text,
+        vuln_section: &vuln_section,
+        files_section: &files_section,
+        url_section: &url_section,
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::models::DyadEntry;
+    use vuln_utils::Kernel;
+
+    fn create_test_kernel(_version: &str, git_id: &str) -> Kernel {
+        // In tests, we don't have real git commit IDs to look up,
+        // so we'll create dummy kernels with the provided info
+        let kernel = Kernel::from_id(git_id).unwrap_or_else(|_| Kernel::empty_kernel());
+
+        // We can't directly modify the fields, but for testing
+        // purposes, we're assuming these are valid git IDs and versions
+        kernel
+    }
+
+    #[test]
+    fn test_parse_dyad_entries() {
+        // Create test data
+        let entries = vec![
+            // Entry with both vulnerable and fixed
+            DyadEntry {
+                vulnerable: create_test_kernel("5.15", "11c52d250b34a0862edc29db03fbec23b30db6da"),
+                fixed: create_test_kernel("5.16", "2b503c8598d1b232e7fc7526bce9326d92331541"),
+            },
+            // Entry with unknown vulnerable but known fixed
+            DyadEntry {
+                vulnerable: Kernel::empty_kernel(),
+                fixed: create_test_kernel("5.10", "3b503c8598d1b232e7fc7526bce9326d92331542"),
+            },
+            // Entry with unfixed vulnerability
+            DyadEntry {
+                vulnerable: create_test_kernel("5.4", "4b503c8598d1b232e7fc7526bce9326d92331543"),
+                fixed: Kernel::empty_kernel(),
+            },
+            // Entry with same version (should be ignored)
+            DyadEntry {
+                vulnerable: create_test_kernel("6.1", "5b503c8598d1b232e7fc7526bce9326d92331544"),
+                fixed: create_test_kernel("6.1", "6b503c8598d1b232e7fc7526bce9326d92331545"),
+            },
+        ];
+
+        // With our updated Kernel implementation, the exact behavior may be different
+        // We'll check that we get at least some entries
+        let vuln_info = parse_dyad_entries(&entries);
+        assert!(!vuln_info.is_empty());
+
+        // We can't check for specific content with our test kernels,
+        // so we'll just verify that entries were generated
+
+        // Test with an array containing only same-version entries
+        let _same_version_entries = vec![DyadEntry {
+            vulnerable: create_test_kernel("6.1", "5b503c8598d1b232e7fc7526bce9326d92331544"),
+            fixed: create_test_kernel("6.1", "6b503c8598d1b232e7fc7526bce9326d92331545"),
+        }];
+
+        // Testing directly will panic, so we can't test that case directly
+    }
+
+    #[test]
+    fn test_collect_reference_urls() {
+        // Create test data
+        let entries = vec![
+            DyadEntry {
+                vulnerable: create_test_kernel("5.15", "11c52d250b34a0862edc29db03fbec23b30db6da"),
+                fixed: create_test_kernel("5.16", "22c52d250b34a0862edc29db03fbec23b30db6db"),
+            },
+            DyadEntry {
+                vulnerable: create_test_kernel("5.10", "33c52d250b34a0862edc29db03fbec23b30db6dc"),
+                fixed: create_test_kernel("5.10.1", "44c52d250b34a0862edc29db03fbec23b30db6dd"),
+            },
+        ];
+
+        let additional_refs = vec![
+            "https://example.com/ref1".to_string(),
+            "https://example.com/ref2".to_string(),
+        ];
+
+        let git_sha_full = "main_fix_id";
+
+        // Collect the references
+        let urls = collect_reference_urls(&entries, &additional_refs, git_sha_full);
+
+        // With our updated Kernel implementation, we may not get all references
+        // but we should at least get the main fix and additional refs
+        assert!(urls.len() >= 3);
+        assert!(urls.contains(&"https://git.kernel.org/stable/c/main_fix_id".to_string()));
+
+        // With our test kernels, we can't check specific git URLs
+        assert!(urls.contains(&"https://git.kernel.org/stable/c/main_fix_id".to_string()));
+        assert!(urls.contains(&"https://example.com/ref1".to_string()));
+        assert!(urls.contains(&"https://example.com/ref2".to_string()));
+
+        // Verify only that additional refs appear at the end
+        assert_eq!(urls.last(), Some(&"https://example.com/ref2".to_string()));
+    }
+
+    #[test]
+    fn test_format_mbox_sections() {
+        // Create test data
+        let vuln_array = vec![
+            "Issue introduced in 5.15 with commit 11c52d250b34a0862edc29db03fbec23b30db6da"
+                .to_string(),
+            "Fixed in 5.10 with commit 22c52d250b34a0862edc29db03fbec23b30db6db".to_string(),
+        ];
+
+        let affected_files = vec![
+            "drivers/net/ethernet/test.c".to_string(),
+            "include/linux/test.h".to_string(),
+        ];
+
+        let url_array = vec![
+            "https://git.kernel.org/stable/c/11c52d250b34a0862edc29db03fbec23b30db6da".to_string(),
+            "https://git.kernel.org/stable/c/22c52d250b34a0862edc29db03fbec23b30db6db".to_string(),
+        ];
+
+        // Format the sections
+        let (vuln_section, files_section, url_section) =
+            format_mbox_sections(vuln_array, affected_files, url_array);
+
+        // Check that each line is properly indented with a tab
+        assert!(vuln_section.lines().all(|line| line.starts_with('\t')));
+        assert!(files_section.lines().all(|line| line.starts_with('\t')));
+        assert!(url_section.lines().all(|line| line.starts_with('\t')));
+
+        // Check that all content is present
+        assert!(vuln_section.contains("Issue introduced in 5.15"));
+        assert!(vuln_section.contains("Fixed in 5.10"));
+
+        assert!(files_section.contains("drivers/net/ethernet/test.c"));
+        assert!(files_section.contains("include/linux/test.h"));
+
+        assert!(url_section
+            .contains("https://git.kernel.org/stable/c/11c52d250b34a0862edc29db03fbec23b30db6da"));
+        assert!(url_section
+            .contains("https://git.kernel.org/stable/c/22c52d250b34a0862edc29db03fbec23b30db6db"));
+    }
+
+    #[test]
+    fn test_create_mbox_content() {
+        // Create test parameters
+        let params = MboxContentParams {
+            from_line: "From test-script-1.0 Mon Sep 17 00:00:00 2001",
+            user_name: "Test User",
+            user_email: "test@example.com",
+            cve_number: "CVE-2023-1234",
+            commit_subject: "Test CVE",
+            commit_text: "This is a test commit message.\n\nIt contains details about the vulnerability.",
+            vuln_section: "\tIssue introduced in 5.15 with commit 11c52d250b34a0862edc29db03fbec23b30db6da\n\tFixed in 5.10 with commit 22c52d250b34a0862edc29db03fbec23b30db6db\n",
+            files_section: "\tdrivers/net/ethernet/test.c\n\tinclude/linux/test.h\n",
+            url_section: "\thttps://git.kernel.org/stable/c/11c52d250b34a0862edc29db03fbec23b30db6da\n\thttps://git.kernel.org/stable/c/22c52d250b34a0862edc29db03fbec23b30db6db\n",
+        };
+
+        // Create the mbox content
+        let mbox = create_mbox_content(&params);
+
+        // Check the basic structure
+        assert!(mbox.starts_with("From test-script-1.0 Mon Sep 17 00:00:00 2001"));
+        assert!(mbox.contains("From: Test User <test@example.com>"));
+        assert!(mbox.contains("To: <linux-cve-announce@vger.kernel.org>"));
+        assert!(mbox.contains("Subject: CVE-2023-1234: Test CVE"));
+
+        // Check section headers
+        assert!(mbox.contains("Description\n==========="));
+        assert!(mbox.contains("Affected and fixed versions\n==========================="));
+        assert!(mbox.contains("Affected files\n=============="));
+        assert!(mbox.contains("Mitigation\n=========="));
+
+        // Check that the commit message is included
+        assert!(mbox.contains(
+            "This is a test commit message.\n\nIt contains details about the vulnerability."
+        ));
+
+        // Check that other sections are included
+        assert!(mbox.contains(
+            "\tIssue introduced in 5.15 with commit 11c52d250b34a0862edc29db03fbec23b30db6da"
+        ));
+        assert!(mbox.contains("\tdrivers/net/ethernet/test.c"));
+        assert!(mbox.contains(
+            "\thttps://git.kernel.org/stable/c/22c52d250b34a0862edc29db03fbec23b30db6db"
+        ));
+
+        // Check that the result ends with a newline
+        assert!(mbox.ends_with('\n'));
+    }
+}
\ No newline at end of file
diff --git a/tools/bippy/src/providers/cve/mod.rs b/tools/bippy/src/providers/cve/mod.rs
new file mode 100644
index 0000000..295ed77
--- /dev/null
+++ b/tools/bippy/src/providers/cve/mod.rs
@@ -0,0 +1,98 @@
+pub mod models;
+pub mod json;
+pub mod mbox;
+
+use anyhow::Result;
+use crate::providers::{VulnerabilityProvider, VulnerabilityRecordParams};
+
+/// CVE provider implementation
+pub struct CveProvider;
+
+impl CveProvider {
+    pub fn new() -> Self {
+        CveProvider
+    }
+}
+
+impl VulnerabilityProvider for CveProvider {
+    fn generate_json(&self, params: &VulnerabilityRecordParams) -> Result<String> {
+        // Get the UUID for this provider
+        let uuid = match self.get_org_uuid()? {
+            Some(u) => u,
+            None => return Err(anyhow::anyhow!("CVE provider requires an organization UUID")),
+        };
+
+        let cve_params = json::CveRecordParams {
+            uuid: &uuid,
+            cve_number: params.vuln_id,
+            git_sha_full: params.git_sha_full,
+            commit_subject: params.commit_subject,
+            user_name: params.user_name,
+            user_email: params.user_email,
+            dyad_entries: params.dyad_entries.clone(),
+            script_name: params.script_name,
+            script_version: params.script_version,
+            additional_references: params.additional_references,
+            commit_text: params.commit_text,
+            affected_files: params.affected_files,
+        };
+        json::generate_json(&cve_params)
+    }
+
+    fn generate_mbox(&self, params: &VulnerabilityRecordParams) -> Result<String> {
+        let mbox_params = mbox::MboxParams {
+            cve_number: params.vuln_id,
+            git_sha_full: params.git_sha_full,
+            commit_subject: params.commit_subject,
+            user_name: params.user_name,
+            user_email: params.user_email,
+            dyad_entries: &params.dyad_entries,
+            script_name: params.script_name,
+            script_version: params.script_version,
+            additional_references: params.additional_references,
+            commit_text: params.commit_text,
+            affected_files: params.affected_files,
+        };
+        Ok(mbox::generate_mbox(&mbox_params))
+    }
+
+    fn name(&self) -> &'static str {
+        "CVE"
+    }
+
+    fn user_env_var(&self) -> &'static str {
+        "CVE_USER"
+    }
+
+    fn validate_id(&self, id: &str) -> Result<()> {
+        if id.starts_with("CVE-") && id.len() > 4 {
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!("Invalid CVE ID format: {}. Expected format: CVE-YYYY-NNNN", id))
+        }
+    }
+
+    fn get_org_uuid(&self) -> Result<Option<String>> {
+        use crate::utils::file::read_uuid;
+        use anyhow::Context;
+
+        // Get vulns directory using vuln_utils
+        let vulns_dir = vuln_utils::find_vulns_dir()
+            .with_context(|| "Failed to find vulns directory")?;
+
+        // Get the script directory from vulns directory
+        let script_dir = vulns_dir.join("scripts");
+        if !script_dir.exists() {
+            return Err(anyhow::anyhow!(
+                "Scripts directory not found at {}",
+                script_dir.display()
+            ));
+        }
+
+        // Read the UUID from the CVE-specific linux.uuid file
+        let uuid = read_uuid(&script_dir, "linux.uuid")
+            .with_context(|| "Failed to read UUID")?;
+
+        Ok(Some(uuid))
+    }
+}
\ No newline at end of file
diff --git a/tools/bippy/src/providers/cve/models.rs b/tools/bippy/src/providers/cve/models.rs
new file mode 100644
index 0000000..0f3b4bb
--- /dev/null
+++ b/tools/bippy/src/providers/cve/models.rs
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
+
+use serde::{Deserialize, Serialize};
+use crate::providers::{CpeNodes, VersionRange};
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CveMetadata {
+    #[serde(rename = "assignerOrgId")]
+    pub assigner_org_id: String,
+    #[serde(rename = "cveID")]
+    pub cve_id: String,
+    #[serde(rename = "requesterUserId")]
+    pub requester_user_id: String,
+    pub serial: String,
+    pub state: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Description {
+    pub lang: String,
+    pub value: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ProviderMetadata {
+    #[serde(rename = "orgId")]
+    pub org_id: String,
+}
+
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct AffectedProduct {
+    pub product: String,
+    pub vendor: String,
+    #[serde(rename = "defaultStatus")]
+    pub default_status: String,
+    pub repo: String,
+    #[serde(rename = "programFiles")]
+    pub program_files: Vec<String>,
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    pub versions: Vec<VersionRange>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Reference {
+    pub url: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Generator {
+    pub engine: String,
+}
+
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CpeApplicability {
+    pub nodes: Vec<CpeNodes>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CnaData {
+    #[serde(rename = "providerMetadata")]
+    pub provider_metadata: ProviderMetadata,
+    pub descriptions: Vec<Description>,
+    pub affected: Vec<AffectedProduct>,
+    #[serde(rename = "cpeApplicability")]
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    pub cpe_applicability: Vec<CpeApplicability>,
+    pub references: Vec<Reference>,
+    pub title: String,
+    #[serde(rename = "x_generator")]
+    pub x_generator: Generator,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Containers {
+    pub cna: CnaData,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CveRecord {
+    pub containers: Containers,
+    #[serde(rename = "cveMetadata")]
+    pub cve_metadata: CveMetadata,
+    #[serde(rename = "dataType")]
+    pub data_type: String,
+    #[serde(rename = "dataVersion")]
+    pub data_version: String,
+}
diff --git a/tools/bippy/src/providers/mod.rs b/tools/bippy/src/providers/mod.rs
new file mode 100644
index 0000000..4e9559e
--- /dev/null
+++ b/tools/bippy/src/providers/mod.rs
@@ -0,0 +1,77 @@
+pub mod common;
+pub mod cve;
+
+pub use common::{CpeMatch, CpeNodes, VersionRange};
+
+use anyhow::{Result, anyhow};
+use crate::models::DyadEntry;
+
+/// Common parameters for vulnerability record generation
+pub struct VulnerabilityRecordParams<'a> {
+    /// Vulnerability identifier (e.g., "CVE-2023-12345", "GSD-2023-12345")
+    pub vuln_id: &'a str,
+    /// Full Git SHA of the commit that fixes the vulnerability
+    pub git_sha_full: &'a str,
+    /// Subject line of the commit
+    pub commit_subject: &'a str,
+    /// Name of the user creating the record
+    pub user_name: &'a str,
+    /// Email of the user creating the record
+    pub user_email: &'a str,
+    /// Dyad entries containing vulnerability and fix information
+    pub dyad_entries: Vec<DyadEntry>,
+    /// Name of the script generating the record
+    pub script_name: &'a str,
+    /// Version of the script generating the record
+    pub script_version: &'a str,
+    /// Additional reference URLs
+    pub additional_references: &'a [String],
+    /// Full commit text/description
+    pub commit_text: &'a str,
+    /// List of affected files
+    pub affected_files: &'a Vec<String>,
+}
+
+/// Trait for vulnerability providers (CVE, GSD, EUVD, etc.)
+#[allow(dead_code)]
+pub trait VulnerabilityProvider {
+    /// Generate JSON record for the vulnerability
+    fn generate_json(&self, params: &VulnerabilityRecordParams) -> Result<String>;
+
+    /// Generate mbox announcement for the vulnerability
+    fn generate_mbox(&self, params: &VulnerabilityRecordParams) -> Result<String>;
+
+    /// Get the provider name
+    fn name(&self) -> &'static str;
+
+    /// Get the environment variable name for user configuration
+    fn user_env_var(&self) -> &'static str;
+
+    /// Validate the vulnerability ID format
+    fn validate_id(&self, id: &str) -> Result<()>;
+
+    /// Get the organization UUID if required by the provider
+    /// Returns None if the provider doesn't use UUIDs
+    fn get_org_uuid(&self) -> Result<Option<String>> {
+        Ok(None)
+    }
+}
+
+/// Factory for creating vulnerability providers
+pub struct ProviderFactory;
+
+impl ProviderFactory {
+    /// Create a provider by name
+    pub fn create(provider_type: &str) -> Result<Box<dyn VulnerabilityProvider>> {
+        match provider_type.to_lowercase().as_str() {
+            "cve" => Ok(Box::new(cve::CveProvider::new())),
+            _ => Err(anyhow!("Unknown provider type: {}", provider_type)),
+        }
+    }
+
+    /// Get list of available providers
+    #[allow(dead_code)]
+    pub fn available_providers() -> Vec<&'static str> {
+        vec!["cve"]
+    }
+}
\ No newline at end of file
diff --git a/tools/bippy/src/utils/dyad.rs b/tools/bippy/src/utils/dyad.rs
index 912f972..26d5584 100644
--- a/tools/bippy/src/utils/dyad.rs
+++ b/tools/bippy/src/utils/dyad.rs
@@ -27,6 +27,7 @@
     std::env::set_current_dir(script_dir)?;
 
     // Get kernel tree paths from environment variables
+    // Note: CVEKERNELTREE is a legacy environment variable name that is kept for compatibility
     let kernel_tree = std::env::var("CVEKERNELTREE")
         .with_context(|| "CVEKERNELTREE environment variable is not set")?;
 
@@ -34,6 +35,7 @@
     let mut command = std::process::Command::new(&dyad_script);
 
     // Set environment variables
+    // Note: CVEKERNELTREE is a legacy environment variable name that is kept for compatibility
     command.env("CVEKERNELTREE", &kernel_tree);
 
     // Add each vulnerable SHA as a separate -v argument
diff --git a/tools/bippy/src/utils/file.rs b/tools/bippy/src/utils/file.rs
index 62d1304..8ed39a0 100644
--- a/tools/bippy/src/utils/file.rs
+++ b/tools/bippy/src/utils/file.rs
@@ -18,9 +18,9 @@
         .collect())
 }
 
-/// Read the UUID for the Linux kernel CVE team from a file
-pub fn read_uuid(script_dir: &Path) -> Result<String> {
-    let uuid_path = script_dir.join("linux.uuid");
+/// Read a UUID from a file
+pub fn read_uuid(script_dir: &Path, uuid_filename: &str) -> Result<String> {
+    let uuid_path = script_dir.join(uuid_filename);
     let content = std::fs::read_to_string(&uuid_path)
         .with_context(|| format!("Failed to read UUID file at {}", uuid_path.display()))?;
 
diff --git a/tools/bippy/src/utils/mod.rs b/tools/bippy/src/utils/mod.rs
index abcf91f..1cfa285 100644
--- a/tools/bippy/src/utils/mod.rs
+++ b/tools/bippy/src/utils/mod.rs
@@ -9,7 +9,7 @@
 pub mod version;
 
 pub use dyad::run_dyad;
-pub use file::{read_tags_file, read_uuid};
+pub use file::read_tags_file;
 pub use git::{apply_diff_to_text, get_commit_subject, get_commit_text};
 pub use text::strip_commit_text;
 pub use version::{
diff --git a/tools/bippy/src/utils/version.rs b/tools/bippy/src/utils/version.rs
index b03ab24..663f8a7 100644
--- a/tools/bippy/src/utils/version.rs
+++ b/tools/bippy/src/utils/version.rs
@@ -3,13 +3,13 @@
 // Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
 
 use crate::models::dyad::DyadEntry;
-use crate::models::{CpeMatch, CpeNodes, VersionRange};
-use cve_utils::version_utils::compare_kernel_versions;
-use cve_utils::version_utils::version_is_mainline;
+use crate::providers::{CpeMatch, CpeNodes, VersionRange};
+use vuln_utils::version_utils::compare_kernel_versions;
+use vuln_utils::version_utils::version_is_mainline;
 use log::debug;
 use std::collections::HashSet;
 
-/// Determine the default status for CVE entries based on the dyad entries
+/// Determine the default status for vulnerability entries based on the dyad entries
 pub fn determine_default_status(entries: &[DyadEntry]) -> &'static str {
     // If any entry has vulnerable_version = 0, status should be "affected"
     if entries.iter().any(|entry| entry.vulnerable.is_empty()) {
@@ -28,7 +28,7 @@
     "unaffected"
 }
 
-/// Generate CPE ranges for the CVE JSON format
+/// Generate CPE ranges for vulnerability records
 pub fn generate_cpe_ranges(entries: &[DyadEntry]) -> Vec<CpeNodes> {
     let mut cpe_nodes: Vec<CpeNodes> = vec![];
     let mut node = CpeNodes {
@@ -39,8 +39,8 @@
 
     for entry in entries {
         // Skip entries where the vulnerability is in the same version it was fixed
-        // These versions are not actually affected in any released version so CVE.org
-        // doesn't like to see them.
+        // These versions are not actually affected in any released version so vulnerability databases
+        // don't like to see them.
         if entry.is_same_version() {
             continue;
         }
@@ -66,7 +66,7 @@
     cpe_nodes
 }
 
-/// Generate git ranges for the CVE JSON format
+/// Generate git ranges for vulnerability records
 pub fn generate_git_ranges(entries: &[DyadEntry]) -> Vec<VersionRange> {
     let mut git_versions = Vec::new();
 
@@ -97,7 +97,7 @@
     git_versions
 }
 
-/// Generate version ranges for the CVE JSON format
+/// Generate version ranges for vulnerability records
 pub fn generate_version_ranges(entries: &[DyadEntry], default_status: &str) -> Vec<VersionRange> {
     let mut kernel_versions = Vec::new();
     let mut seen_versions = HashSet::new();
diff --git a/tools/cve_classifier/src/main.rs b/tools/cve_classifier/src/main.rs
index dee9b51..cf0f976 100644
--- a/tools/cve_classifier/src/main.rs
+++ b/tools/cve_classifier/src/main.rs
@@ -1,6 +1,8 @@
 // SPDX-License-Identifier: GPL-2.0
 // (c) 2025, Sasha Levin <sashal@kernel.org>
 
+extern crate vuln_utils;
+
 use clap::{Arg, ArgAction, Command};
 use log::{info, debug, error, warn};
 use std::collections::HashMap;
diff --git a/tools/cve_classifier/src/utils.rs b/tools/cve_classifier/src/utils.rs
index 6b0ca8a..4cea584 100644
--- a/tools/cve_classifier/src/utils.rs
+++ b/tools/cve_classifier/src/utils.rs
@@ -1,7 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0
 // (c) 2025, Sasha Levin <sashal@kernel.org>
 
-pub use cve_utils::get_cve_root;
+pub use vuln_utils::get_cve_root;
 
 // Setup logging with optional debug level
 pub fn setup_logging(debug: bool, batch_mode: bool) {
diff --git a/tools/dyad/src/kernel/kernel_pairs.rs b/tools/dyad/src/kernel/kernel_pairs.rs
index d2f8a44..5ef3a1d 100644
--- a/tools/dyad/src/kernel/kernel_pairs.rs
+++ b/tools/dyad/src/kernel/kernel_pairs.rs
@@ -5,7 +5,7 @@
 //
 
 use crate::state::DyadState;
-use cve_utils::{Kernel, KernelPair};
+use vuln_utils::{Kernel, KernelPair};
 use log::debug;
 use owo_colors::{OwoColorize, Stream::Stdout};
 use std::cmp::Ordering;
diff --git a/tools/dyad/src/kernel/sha_processing.rs b/tools/dyad/src/kernel/sha_processing.rs
index 0872a49..731fc08 100644
--- a/tools/dyad/src/kernel/sha_processing.rs
+++ b/tools/dyad/src/kernel/sha_processing.rs
@@ -5,7 +5,7 @@
 //
 
 use crate::state::DyadState;
-use cve_utils::Kernel;
+use vuln_utils::Kernel;
 
 /// Adds a git SHA to the state's list of fixing kernels
 pub fn process_fixing_sha(state: &mut DyadState, git_sha: &str) -> bool {
@@ -19,8 +19,8 @@
             // Sometimes the git id is in stable kernels but is NOT in a released Linus tree
             // just yet, so verhaal will not have the data. So let's check the git repo to see
             // if that's the case
-            if let Ok(path) = cve_utils::common::get_kernel_tree() {
-                if let Ok(git_sha_full) = cve_utils::get_full_sha(&path, git_sha) {
+            if let Ok(path) = vuln_utils::common::get_kernel_tree() {
+                if let Ok(git_sha_full) = vuln_utils::get_full_sha(&path, git_sha) {
                     // It is valid, so let's make an "empty" kernel object and fill it in by hand
                     // without a valid version number just yet.
                     if let Ok(kernel) = Kernel::from_id(&git_sha_full) {
diff --git a/tools/dyad/src/kernel/vulnerability.rs b/tools/dyad/src/kernel/vulnerability.rs
index b865f5f..4cf6652 100644
--- a/tools/dyad/src/kernel/vulnerability.rs
+++ b/tools/dyad/src/kernel/vulnerability.rs
@@ -5,7 +5,7 @@
 //
 
 use crate::state::{found_in, DyadState};
-use cve_utils::Kernel;
+use vuln_utils::Kernel;
 use log::{debug, error};
 use owo_colors::{OwoColorize, Stream::Stdout};
 use std::cmp::Ordering;
diff --git a/tools/dyad/src/main.rs b/tools/dyad/src/main.rs
index bd4d01b..8fd2417 100644
--- a/tools/dyad/src/main.rs
+++ b/tools/dyad/src/main.rs
@@ -14,7 +14,7 @@
 use log::{debug, error};
 use owo_colors::{OwoColorize, Stream::Stdout};
 use std::env;
-extern crate cve_utils;
+extern crate vuln_utils;
 
 mod cli;
 mod kernel;
diff --git a/tools/dyad/src/state/mod.rs b/tools/dyad/src/state/mod.rs
index c11125a..2c8fab9 100644
--- a/tools/dyad/src/state/mod.rs
+++ b/tools/dyad/src/state/mod.rs
@@ -4,8 +4,8 @@
 // Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
 //
 
-use cve_utils::Kernel;
-use cve_utils::Verhaal;
+use vuln_utils::Kernel;
+use vuln_utils::Verhaal;
 use log::debug;
 
 /// State for dyad tool runtime
@@ -40,8 +40,8 @@
 
 /// Validates and sets up environment variables for the `DyadState`
 pub fn validate_env_vars(state: &mut DyadState) {
-    // Use cve_utils to get kernel tree path
-    match cve_utils::common::get_kernel_tree() {
+    // Use vuln_utils to get kernel tree path
+    match vuln_utils::common::get_kernel_tree() {
         Ok(path) => state.kernel_tree = path.to_string_lossy().into_owned(),
         Err(e) => panic!("Failed to get kernel tree: {e}"),
     }
diff --git a/tools/dyad/tests/integration.rs b/tools/dyad/tests/integration.rs
index 6f74fe0..a9a1a33 100644
--- a/tools/dyad/tests/integration.rs
+++ b/tools/dyad/tests/integration.rs
@@ -228,9 +228,9 @@
     }
 }
 
-/// Find the CVE directory using cve_utils
+/// Find the CVE directory using vuln_utils
 fn find_cve_dir() -> Result<PathBuf, String> {
-    match cve_utils::common::find_vulns_dir() {
+    match vuln_utils::common::find_vulns_dir() {
         Ok(vulns_dir) => {
             let cve_dir = vulns_dir.join("cve");
             if cve_dir.exists() {
diff --git a/tools/voting_results/src/main.rs b/tools/voting_results/src/main.rs
index 2353b6c..bfd83b4 100644
--- a/tools/voting_results/src/main.rs
+++ b/tools/voting_results/src/main.rs
@@ -2,6 +2,8 @@
 //
 // Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
 
+extern crate vuln_utils;
+
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
 use git2::Repository;
@@ -94,8 +96,8 @@
             }
         }
 
-        // Use the standard cve_utils implementation to find the vulns directory
-        let vulns_dir = cve_utils::find_vulns_dir()?;
+        // Use the standard vuln_utils implementation to find the vulns directory
+        let vulns_dir = vuln_utils::find_vulns_dir()?;
         let proposed_dir = vulns_dir.join("cve").join("review").join("proposed");
         let script_dir = vulns_dir.join("scripts");
 
@@ -346,7 +348,7 @@
                 .path()
                 .parent()
                 .unwrap_or_else(|| self.repo.path());
-            match cve_utils::git_utils::get_full_sha(repo_path, short_stable_sha) {
+            match vuln_utils::git_utils::get_full_sha(repo_path, short_stable_sha) {
                 Ok(full_sha) => match git2::Oid::from_str(&full_sha) {
                     Ok(oid) => self.repo.find_commit(oid),
                     Err(_) => return stable_sha.to_string(),
diff --git a/tools/cve_utils/.gitignore b/tools/vuln_utils/.gitignore
similarity index 100%
rename from tools/cve_utils/.gitignore
rename to tools/vuln_utils/.gitignore
diff --git a/tools/cve_utils/src/bin/cve_create.rs b/tools/vuln_utils/src/bin/cve_create.rs
similarity index 97%
rename from tools/cve_utils/src/bin/cve_create.rs
rename to tools/vuln_utils/src/bin/cve_create.rs
index 70371ab..aa610c1 100644
--- a/tools/cve_utils/src/bin/cve_create.rs
+++ b/tools/vuln_utils/src/bin/cve_create.rs
@@ -5,11 +5,11 @@
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
 use owo_colors::OwoColorize;
-use cve_utils::common::{find_cve_by_sha, get_cve_root, get_kernel_tree};
-use cve_utils::cve_utils::find_next_free_cve_id;
-use cve_utils::git_utils::get_full_sha;
-use cve_utils::git_utils::{get_commit_details, get_commit_year};
-use cve_utils::print_git_error_details;
+use vuln_utils::common::{find_cve_by_sha, get_cve_root, get_kernel_tree};
+use vuln_utils::vuln_utils::find_next_free_cve_id;
+use vuln_utils::git_utils::get_full_sha;
+use vuln_utils::git_utils::{get_commit_details, get_commit_year};
+use vuln_utils::print_git_error_details;
 use log::error;
 use std::fs;
 use std::io::{BufRead, BufReader};
@@ -228,7 +228,7 @@
     let mbox_file = published_dir.join(format!("{cve_id}.mbox"));
 
     // Build bippy command with full path from vulns dir
-    let vulns_dir = match cve_utils::common::find_vulns_dir() {
+    let vulns_dir = match vuln_utils::common::find_vulns_dir() {
         Ok(dir) => dir,
         Err(e) => return Err(anyhow!("Failed to find vulns directory: {}", e)),
     };
diff --git a/tools/cve_utils/src/bin/cve_publish.rs b/tools/vuln_utils/src/bin/cve_publish.rs
similarity index 94%
rename from tools/cve_utils/src/bin/cve_publish.rs
rename to tools/vuln_utils/src/bin/cve_publish.rs
index 689093e..b6a1596 100644
--- a/tools/cve_utils/src/bin/cve_publish.rs
+++ b/tools/vuln_utils/src/bin/cve_publish.rs
@@ -5,10 +5,10 @@
 use anyhow::{Context, Result};
 use clap::Parser;
 use owo_colors::OwoColorize;
-use ::cve_utils::common;
-use ::cve_utils::git_utils;
-use ::cve_utils::cve_utils;
-use ::cve_utils::print_git_error_details;
+use ::vuln_utils::common;
+use ::vuln_utils::git_utils;
+use ::vuln_utils::vuln_utils;
+use ::vuln_utils::print_git_error_details;
 use std::fs;
 use std::path::PathBuf;
 use std::process::Command;
@@ -130,7 +130,7 @@
     // Process each file
     for file_path in &modified_files {
         // Extract CVE ID from the file path
-        let cve_id = cve_utils::extract_cve_id_from_path(file_path)?;
+        let cve_id = vuln_utils::extract_cve_id_from_path(file_path)?;
 
         // Get the associated SHA1 file
         let sha1_file = file_path.with_extension("sha1");
@@ -193,7 +193,7 @@
     // Print list of files that will be sent
     for file_path in &mbox_files {
         // Extract CVE ID from the file path
-        let cve_id = cve_utils::extract_cve_id_from_path(file_path)?;
+        let cve_id = vuln_utils::extract_cve_id_from_path(file_path)?;
 
         // Get the associated SHA1 file
         let sha1_file = file_path.with_extension("sha1");
@@ -264,17 +264,17 @@
         // Test with a JSON file
         let json_path = temp_path.join(format!("{}.json", cve_id));
         fs::write(&json_path, "test content").unwrap();
-        assert_eq!(cve_utils::extract_cve_id_from_path(&json_path).unwrap(), cve_id);
+        assert_eq!(vuln_utils::extract_cve_id_from_path(&json_path).unwrap(), cve_id);
 
         // Test with a mbox file
         let mbox_path = temp_path.join(format!("{}.mbox", cve_id));
         fs::write(&mbox_path, "test content").unwrap();
-        assert_eq!(cve_utils::extract_cve_id_from_path(&mbox_path).unwrap(), cve_id);
+        assert_eq!(vuln_utils::extract_cve_id_from_path(&mbox_path).unwrap(), cve_id);
 
         // Test with a rejected mbox file
         let rejected_path = temp_path.join(format!("{}.mbox.rejected", cve_id));
         fs::write(&rejected_path, "test content").unwrap();
-        assert_eq!(cve_utils::extract_cve_id_from_path(&rejected_path).unwrap(), cve_id);
+        assert_eq!(vuln_utils::extract_cve_id_from_path(&rejected_path).unwrap(), cve_id);
     }
 
     #[test]
diff --git a/tools/cve_utils/src/bin/cve_reject.rs b/tools/vuln_utils/src/bin/cve_reject.rs
similarity index 98%
rename from tools/cve_utils/src/bin/cve_reject.rs
rename to tools/vuln_utils/src/bin/cve_reject.rs
index 4673b2b..c17acf4 100644
--- a/tools/cve_utils/src/bin/cve_reject.rs
+++ b/tools/vuln_utils/src/bin/cve_reject.rs
@@ -5,14 +5,14 @@
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
 use owo_colors::OwoColorize;
-use cve_utils::common;
-use cve_utils::cve_validation;
-use cve_utils::git_config;
-use cve_utils::print_git_error_details;
+use vuln_utils::common;
+use vuln_utils::cve_validation;
+use vuln_utils::git_config;
+use vuln_utils::print_git_error_details;
 use std::fs;
 use std::path::{Path, PathBuf};
 use walkdir::WalkDir;
-use cve_utils::cve_utils::extract_cve_id_from_path;
+use vuln_utils::vuln_utils::extract_cve_id_from_path;
 
 /// Reject a reserved or published CVE entry
 #[derive(Parser, Debug)]
diff --git a/tools/cve_utils/src/bin/cve_review.rs b/tools/vuln_utils/src/bin/cve_review.rs
similarity index 99%
rename from tools/cve_utils/src/bin/cve_review.rs
rename to tools/vuln_utils/src/bin/cve_review.rs
index 60aa099..8806f9b 100644
--- a/tools/cve_utils/src/bin/cve_review.rs
+++ b/tools/vuln_utils/src/bin/cve_review.rs
@@ -5,9 +5,9 @@
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
 use owo_colors::OwoColorize;
-use cve_utils::common;
-use cve_utils::print_git_error_details;
-use cve_utils::git_utils;
+use vuln_utils::common;
+use vuln_utils::print_git_error_details;
+use vuln_utils::git_utils;
 use dialoguer::Input;
 use regex::Regex;
 use std::cmp::min;
@@ -17,7 +17,7 @@
 use std::io::{BufRead, BufReader, Write};
 use std::path::{Path, PathBuf};
 use std::process::{Command, Stdio};
-use cve_utils::cve_utils::extract_cve_id_from_path;
+use vuln_utils::vuln_utils::extract_cve_id_from_path;
 use walkdir::WalkDir;
 use grep::regex::RegexMatcher;
 use grep::searcher::sinks::UTF8;
diff --git a/tools/cve_utils/src/bin/cve_search.rs b/tools/vuln_utils/src/bin/cve_search.rs
similarity index 95%
rename from tools/cve_utils/src/bin/cve_search.rs
rename to tools/vuln_utils/src/bin/cve_search.rs
index c9f454e..795bb08 100644
--- a/tools/cve_utils/src/bin/cve_search.rs
+++ b/tools/vuln_utils/src/bin/cve_search.rs
@@ -5,8 +5,8 @@
 use anyhow::{anyhow, Result};
 use clap::Parser;
 use owo_colors::OwoColorize;
-use cve_utils::common;
-use cve_utils::print_git_error_details;
+use vuln_utils::common;
+use vuln_utils::print_git_error_details;
 
 /// Search the published CVE records for a git SHA or find a git SHA associated with a CVE ID
 #[derive(Parser, Debug)]
@@ -44,7 +44,7 @@
     };
 
     // Try to interpret the search string as a git SHA first
-    if let Ok(_git_sha_full) = cve_utils::get_full_sha(&kernel_tree, &args.search_string) {
+    if let Ok(_git_sha_full) = vuln_utils::get_full_sha(&kernel_tree, &args.search_string) {
         // It's a valid SHA, search for it in the CVE records
         if let Some(cve) = common::find_cve_by_sha(&cve_root, &args.search_string) {
             println!("{} is assigned to git id {}",
@@ -70,7 +70,7 @@
 
 #[cfg(test)]
 mod tests {
-    use cve_utils::common;
+    use vuln_utils::common;
 
     #[test]
     fn test_real_cve_lookup_by_id() {
diff --git a/tools/cve_utils/src/bin/cve_stats.rs b/tools/vuln_utils/src/bin/cve_stats.rs
similarity index 98%
rename from tools/cve_utils/src/bin/cve_stats.rs
rename to tools/vuln_utils/src/bin/cve_stats.rs
index b1a569c..f8a27b1 100644
--- a/tools/cve_utils/src/bin/cve_stats.rs
+++ b/tools/vuln_utils/src/bin/cve_stats.rs
@@ -13,9 +13,9 @@
 use std::str;
 use walkdir::WalkDir;
 use git2::{Repository, Oid};
-use cve_utils::{
+use vuln_utils::{
     git_utils::{self, resolve_reference},
-    cve_utils::extract_cve_id_from_path,
+    vuln_utils::extract_cve_id_from_path,
 };
 
 #[derive(Parser)]
@@ -149,7 +149,7 @@
             eprintln!("Error: {e}");
 
             // Provide additional context for git errors
-            cve_utils::print_git_error_details(&e);
+            vuln_utils::print_git_error_details(&e);
 
             std::process::exit(1);
         }
@@ -316,7 +316,7 @@
             if let Ok(affected_files) = git_utils::get_affected_files(&repo, &obj) {
                 for file_path in affected_files {
                     if file_path.starts_with("cve/published/") {
-                        // Use the consolidated function from cve_utils
+                        // Use the consolidated function from vuln_utils
                         if let Ok(cve_id) = extract_cve_id_from_path(&file_path) {
                             unique_cves.insert(cve_id);
                         }
@@ -545,7 +545,7 @@
         }
     };
 
-    // Get affected files using the cve_utils module - more efficient than doing diff traversal manually
+    // Get affected files using the vuln_utils module - more efficient than doing diff traversal manually
     let affected_files = match git_utils::get_affected_files(&repo, &obj) {
         Ok(files) => files,
         Err(_) => {
diff --git a/tools/cve_utils/src/bin/cve_update.rs b/tools/vuln_utils/src/bin/cve_update.rs
similarity index 99%
rename from tools/cve_utils/src/bin/cve_update.rs
rename to tools/vuln_utils/src/bin/cve_update.rs
index bf05fa6..187a516 100644
--- a/tools/cve_utils/src/bin/cve_update.rs
+++ b/tools/vuln_utils/src/bin/cve_update.rs
@@ -3,10 +3,10 @@
 // Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
 
 use anyhow::{anyhow, Context, Result};
-use cve_utils::print_git_error_details;
-use cve_utils::cve_validation::find_cve_id;
-use cve_utils::year_utils::{is_valid_year, is_year_dir_exists};
-use cve_utils::common;
+use vuln_utils::print_git_error_details;
+use vuln_utils::cve_validation::find_cve_id;
+use vuln_utils::year_utils::{is_valid_year, is_year_dir_exists};
+use vuln_utils::common;
 use std::path::{Path, PathBuf};
 use std::thread;
 use std::sync::{Arc, Mutex};
@@ -530,7 +530,7 @@
     use tempfile::tempdir;
     use std::io::Write;
     use std::fs::File;
-    use cve_utils::cve_validation::extract_year_from_cve;
+    use vuln_utils::cve_validation::extract_year_from_cve;
 
     #[test]
     fn test_is_valid_year() {
diff --git a/tools/cve_utils/src/bin/score.rs b/tools/vuln_utils/src/bin/score.rs
similarity index 99%
rename from tools/cve_utils/src/bin/score.rs
rename to tools/vuln_utils/src/bin/score.rs
index fd8104d..aa2e077 100644
--- a/tools/cve_utils/src/bin/score.rs
+++ b/tools/vuln_utils/src/bin/score.rs
@@ -4,8 +4,8 @@
 
 use anyhow::{anyhow, Result};
 use clap::Parser;
-use cve_utils::common;
-use cve_utils::print_git_error_details;
+use vuln_utils::common;
+use vuln_utils::print_git_error_details;
 use indicatif::{ProgressBar, ProgressStyle};
 use rayon::prelude::*;
 use std::collections::{HashMap, HashSet};
diff --git a/tools/cve_utils/src/bin/strak.rs b/tools/vuln_utils/src/bin/strak.rs
similarity index 99%
rename from tools/cve_utils/src/bin/strak.rs
rename to tools/vuln_utils/src/bin/strak.rs
index 2435f23..c4dd447 100644
--- a/tools/cve_utils/src/bin/strak.rs
+++ b/tools/vuln_utils/src/bin/strak.rs
@@ -5,8 +5,8 @@
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
 use owo_colors::OwoColorize;
-use cve_utils::common;
-use cve_utils::print_git_error_details;
+use vuln_utils::common;
+use vuln_utils::print_git_error_details;
 use rayon::prelude::*;
 use std::env;
 use std::fs;
diff --git a/tools/cve_utils/src/bin/update_dyad.rs b/tools/vuln_utils/src/bin/update_dyad.rs
similarity index 98%
rename from tools/cve_utils/src/bin/update_dyad.rs
rename to tools/vuln_utils/src/bin/update_dyad.rs
index 08e37dc..54c64bd 100644
--- a/tools/cve_utils/src/bin/update_dyad.rs
+++ b/tools/vuln_utils/src/bin/update_dyad.rs
@@ -5,8 +5,8 @@
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
 use owo_colors::OwoColorize;
-use cve_utils::common;
-use cve_utils::print_git_error_details;
+use vuln_utils::common;
+use vuln_utils::print_git_error_details;
 use indicatif::{ProgressBar, ProgressStyle};
 use rayon::prelude::*;
 use std::env;
@@ -18,7 +18,7 @@
 use std::sync::Arc;
 use tempfile::NamedTempFile;
 use walkdir::WalkDir;
-use cve_utils::cve_utils::extract_cve_id_from_path;
+use vuln_utils::vuln_utils::extract_cve_id_from_path;
 
 /// Update all .dyad files in the tree.
 ///
@@ -338,7 +338,7 @@
 mod tests {
     use super::*;
     use tempfile::tempdir;
-    use cve_utils::cve_utils::extract_cve_id_from_path;
+    use vuln_utils::vuln_utils::extract_cve_id_from_path;
 
     #[test]
     fn test_extract_cve_id_from_path() {
diff --git a/tools/cve_utils/src/kernel.rs b/tools/vuln_utils/src/kernel.rs
similarity index 99%
rename from tools/cve_utils/src/kernel.rs
rename to tools/vuln_utils/src/kernel.rs
index a96314a..b9d173c 100644
--- a/tools/cve_utils/src/kernel.rs
+++ b/tools/vuln_utils/src/kernel.rs
@@ -120,7 +120,7 @@
 
     fn git_dir() -> &'static String {
         GIT_DIR.get_or_init(|| {
-            // Use cve_utils to get and validate the kernel tree path
+            // Use vuln_utils to get and validate the kernel tree path
             match common::get_kernel_tree() {
                 Ok(path) => path.to_string_lossy().into_owned(),
                 Err(e) => panic!("Failed to get kernel tree: {e}"),
diff --git a/tools/cve_utils/src/lib.rs b/tools/vuln_utils/src/lib.rs
similarity index 99%
rename from tools/cve_utils/src/lib.rs
rename to tools/vuln_utils/src/lib.rs
index 8169191..ea838ea 100644
--- a/tools/cve_utils/src/lib.rs
+++ b/tools/vuln_utils/src/lib.rs
@@ -26,7 +26,7 @@
     git_sort_ids, match_pattern, print_git_error_details, resolve_reference,
 };
 // CVE file operations
-pub use self::cve_utils::{extract_cve_id_from_path, find_next_free_cve_id};
+pub use self::vuln_utils::{extract_cve_id_from_path, find_next_free_cve_id};
 // Git configuration utilities
 pub use self::git_config::{get_git_config, set_git_config};
 // CVE validation and processing
@@ -711,7 +711,7 @@
 }
 
 /// CVE file operations commonly used across tools
-pub mod cve_utils {
+pub mod vuln_utils {
     use anyhow::{anyhow, Context, Result};
     use std::fs;
     use std::path::Path;
diff --git a/tools/cve_utils/src/verhaal.rs b/tools/vuln_utils/src/verhaal.rs
similarity index 100%
rename from tools/cve_utils/src/verhaal.rs
rename to tools/vuln_utils/src/verhaal.rs
diff --git a/tools/cve_utils/src/version_utils.rs b/tools/vuln_utils/src/version_utils.rs
similarity index 100%
rename from tools/cve_utils/src/version_utils.rs
rename to tools/vuln_utils/src/version_utils.rs