Skip to main content

ploit_malper/
cve.rs

1use std::collections::BTreeSet;
2
3use regex::Regex;
4use serde_json::Value;
5
6use crate::models::ScanRecord;
7
8/// Matches CVE ids in their canonical form. The trailing sequence can be 4
9/// digits (legacy) or up to 7 digits (2014+ scheme); the boundary guards
10/// against matching inside a longer alphanumeric token.
11fn cve_regex() -> Regex {
12    Regex::new(r"(?i)\bCVE-\d{4}-\d{4,7}\b").expect("valid CVE regex")
13}
14
15/// Extract every CVE id present in a piece of free-form text, including ids
16/// embedded inside URLs (e.g. `...cvename.cgi?name=CVE-2003-1418`).
17pub fn extract_cves(text: &str) -> Vec<String> {
18    let mut seen = BTreeSet::new();
19    for capture in cve_regex().captures_iter(text) {
20        if let Some(found) = capture.get(0) {
21            seen.insert(found.as_str().to_uppercase());
22        }
23    }
24    seen.into_iter().collect()
25}
26
27/// Search a parsed JSON value (recursively) for CVE ids in string fields.
28pub fn extract_cves_from_json(value: &Value) -> Vec<String> {
29    let mut all = Vec::new();
30    collect_strings(value, &mut all);
31    let joined = all.join("\n");
32    extract_cves(&joined)
33}
34
35fn collect_strings(value: &Value, out: &mut Vec<String>) {
36    match value {
37        Value::String(text) => out.push(text.clone()),
38        Value::Array(items) => {
39            for item in items {
40                collect_strings(item, out);
41            }
42        }
43        Value::Object(map) => {
44            for field in map.values() {
45                collect_strings(field, out);
46            }
47        }
48        _ => {}
49    }
50}
51
52/// Populate `record.cves` from every available field (title, detail,
53/// reference, raw). The legacy single `cve` field is kept as the first id.
54pub fn extract_into_record(record: &mut ScanRecord) {
55    let mut discovered = Vec::new();
56    discovered.extend(extract_cves(&record.title));
57    discovered.extend(extract_cves(&record.detail));
58    discovered.extend(extract_cves(&record.reference));
59    if let Some(cve) = record.cve.as_deref() {
60        discovered.extend(extract_cves(cve));
61    }
62    discovered.extend(extract_cves_from_json(&record.raw));
63
64    let mut seen = BTreeSet::new();
65    record.cves = discovered
66        .into_iter()
67        .filter(|id| seen.insert(id.clone()))
68        .collect();
69
70    if record.cve.is_none() {
71        record.cve = record.cves.first().cloned();
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use serde_json::json;
79
80    #[test]
81    fn extracts_from_free_text() {
82        let cves = extract_cves(
83            "See: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2003-1418 and CVE-2021-44228",
84        );
85        assert_eq!(
86            cves,
87            vec!["CVE-2003-1418".to_string(), "CVE-2021-44228".to_string()]
88        );
89    }
90
91    #[test]
92    fn handles_newer_and_legacy_id_lengths() {
93        assert_eq!(extract_cves("CVE-2014-1234"), vec!["CVE-2014-1234"]);
94        assert_eq!(extract_cves("CVE-2024-1234567"), vec!["CVE-2024-1234567"]);
95    }
96
97    #[test]
98    fn rejects_bad_ids_and_case_insensitive() {
99        assert!(extract_cves("CVE-99-12").is_empty());
100        assert!(extract_cves("CVE-2021-44").is_empty());
101        assert_eq!(extract_cves("cve-2003-1418"), vec!["CVE-2003-1418"]);
102    }
103
104    #[test]
105    fn scans_json_recursively() {
106        let value = json!({
107            "nested": {"data": "ref CVE-2022-22965"},
108            "list": ["CVE-2021-41773"]
109        });
110        let mut cves = extract_cves_from_json(&value);
111        cves.sort();
112        assert_eq!(
113            cves,
114            vec!["CVE-2021-41773".to_string(), "CVE-2022-22965".to_string()]
115        );
116    }
117
118    #[test]
119    fn populates_record_cves() {
120        let mut record = ScanRecord {
121            title: "Apache CVE-2021-41773 RCE".into(),
122            detail: "reference CVE-2021-44228".into(),
123            ..Default::default()
124        };
125        extract_into_record(&mut record);
126        assert_eq!(
127            record.cves,
128            vec!["CVE-2021-41773".to_string(), "CVE-2021-44228".to_string()]
129        );
130        assert_eq!(record.cve.as_deref(), Some("CVE-2021-41773"));
131    }
132}