1use std::collections::BTreeSet;
2
3use regex::Regex;
4use serde_json::Value;
5
6use crate::models::ScanRecord;
7
8fn cve_regex() -> Regex {
12 Regex::new(r"(?i)\bCVE-\d{4}-\d{4,7}\b").expect("valid CVE regex")
13}
14
15pub 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
27pub 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
52pub 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}