nabla_cli/binary/
check_vulnerabilities.rs1use anyhow::Result;
2use once_cell::sync::Lazy;
3use serde::Serialize;
4use serde_json::Value;
5use std::{fs::File, io::BufReader, path::Path};
6
7use super::BinaryAnalysis;
8
9const CVE_JSON_PATH: &str = "public/nvdcve-1.1-2025.json";
10
11#[derive(Debug, Clone, Serialize)]
12pub struct VulnerabilityMatch {
13 pub cve_id: String,
14 pub description: String,
15 pub matched_keyword: String,
16}
17
18pub struct CveEntry {
19 id: String,
20 description: String,
21 cpes: Vec<String>,
22}
23
24static CVE_DB: Lazy<Vec<CveEntry>> = Lazy::new(|| match load_cve_db() {
26 Ok(db) => {
27 tracing::info!("Loaded {} CVE records", db.len());
28 db
29 }
30 Err(e) => {
31 tracing::error!("Failed to load CVE DB: {}", e);
32 Vec::new()
33 }
34});
35
36pub fn load_cve_db() -> Result<Vec<CveEntry>> {
37 let path = Path::new(CVE_JSON_PATH);
38 let file = File::open(path)?;
39 let reader = BufReader::new(file);
40 let v: Value = serde_json::from_reader(reader)?;
41
42 let mut entries = Vec::new();
43 if let Some(items) = v.get("CVE_Items").and_then(|x| x.as_array()) {
44 for item in items {
45 let id = item
46 .get("cve")
47 .and_then(|c| c.get("CVE_data_meta"))
48 .and_then(|m| m.get("ID"))
49 .and_then(|i| i.as_str())
50 .unwrap_or("")
51 .to_string();
52
53 let description = item
54 .get("cve")
55 .and_then(|c| c.get("description"))
56 .and_then(|d| d.get("description_data"))
57 .and_then(|arr| arr.as_array())
58 .and_then(|arr| arr.first())
59 .and_then(|d| d.get("value"))
60 .and_then(|v| v.as_str())
61 .unwrap_or("")
62 .to_string();
63
64 let mut cpes = Vec::new();
66 if let Some(configs) = item.get("configurations").and_then(|c| c.get("nodes")) {
67 collect_cpes(configs, &mut cpes);
68 }
69
70 entries.push(CveEntry {
71 id,
72 description,
73 cpes,
74 });
75 }
76 }
77 Ok(entries)
78}
79
80pub fn collect_cpes(value: &Value, out: &mut Vec<String>) {
81 match value {
82 Value::Array(arr) => {
83 for v in arr {
84 collect_cpes(v, out);
85 }
86 }
87 Value::Object(map) => {
88 if let Some(cpe_matches) = map.get("cpe_match") {
89 if let Some(arr) = cpe_matches.as_array() {
90 for cm in arr {
91 if let Some(uri) = cm.get("cpe23Uri").and_then(|u| u.as_str()) {
92 out.push(uri.to_lowercase());
93 }
94 }
95 }
96 }
97 if let Some(children) = map.get("children") {
99 collect_cpes(children, out);
100 }
101 }
102 _ => {}
103 }
104}
105
106pub fn scan_binary_vulnerabilities(analysis: &BinaryAnalysis) -> Vec<VulnerabilityMatch> {
109 let mut keywords: Vec<String> = analysis
110 .linked_libraries
111 .iter()
112 .chain(analysis.imports.iter())
113 .map(|s| s.to_lowercase())
114 .collect();
115
116 if let Some(cpe_candidates) = analysis
118 .metadata
119 .get("cpe_candidates")
120 .and_then(|c| c.as_array())
121 {
122 keywords.extend(
123 cpe_candidates
124 .iter()
125 .filter_map(|c| c.as_str().map(|s| s.to_string())),
126 );
127 }
128
129 let mut matches = Vec::new();
130
131 for entry in CVE_DB.iter() {
132 for kw in &keywords {
133 if kw.is_empty() {
134 continue;
135 }
136 if entry.description.to_lowercase().contains(kw)
137 || entry.cpes.iter().any(|c| c.contains(kw))
138 {
139 matches.push(VulnerabilityMatch {
140 cve_id: entry.id.clone(),
141 description: entry.description.clone(),
142 matched_keyword: kw.clone(),
143 });
144 break;
145 }
146 }
147 }
148
149 matches
150}