nabla_cli/binary/
check_vulnerabilities.rs1use anyhow::Result;
2use home::home_dir;
3use once_cell::sync::Lazy;
4use serde::Serialize;
5use serde_json::Value;
6use std::{fs::File, io::BufReader, path::PathBuf};
7
8use super::BinaryAnalysis;
9
10const CVE_JSON_URL: &str = "https://services.nvd.nist.gov/rest/json/cves/2.0";
11
12fn get_cve_cache_path() -> Result<PathBuf> {
13 let home = home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
14 let nabla_dir = home.join(".nabla");
15
16 if !nabla_dir.exists() {
17 std::fs::create_dir_all(&nabla_dir)?;
18 }
19
20 Ok(nabla_dir.join("cve_cache.json"))
21}
22
23#[derive(Debug, Clone, Serialize)]
24pub struct VulnerabilityMatch {
25 pub cve_id: String,
26 pub description: String,
27 pub matched_keyword: String,
28}
29
30pub struct CveEntry {
31 id: String,
32 description: String,
33 cpes: Vec<String>,
34}
35
36static CVE_DB: Lazy<Vec<CveEntry>> = Lazy::new(|| match load_cve_db() {
38 Ok(db) => {
39 tracing::info!("Loaded {} CVE records", db.len());
40 db
41 }
42 Err(e) => {
43 tracing::error!("Failed to load CVE DB: {}", e);
44 Vec::new()
45 }
46});
47
48pub fn load_cve_db() -> Result<Vec<CveEntry>> {
49 let cache_path = get_cve_cache_path()?;
50
51 if cache_path.exists() {
53 if let Ok(file) = File::open(&cache_path) {
54 let reader = BufReader::new(file);
55 if let Ok(v) = serde_json::from_reader::<_, Value>(reader) {
56 tracing::info!("Loading CVE database from cache: {}", cache_path.display());
57 return parse_cve_json(v);
58 }
59 }
60 }
61
62 tracing::info!("Downloading CVE database from NVD (this may take a moment)...");
64 download_and_cache_cve_db(cache_path)
65}
66
67fn download_and_cache_cve_db(cache_path: PathBuf) -> Result<Vec<CveEntry>> {
68 let response = ureq::get(CVE_JSON_URL)
71 .query("resultsPerPage", "2000") .call()
73 .map_err(|e| anyhow::anyhow!("Failed to download CVE data: {}", e))?;
74
75 let v: Value = response.into_json()
76 .map_err(|e| anyhow::anyhow!("Failed to parse CVE JSON: {}", e))?;
77
78 if let Ok(file) = std::fs::File::create(&cache_path) {
80 let _ = serde_json::to_writer(file, &v);
81 tracing::info!("Cached CVE database to: {}", cache_path.display());
82 }
83
84 parse_cve_json(v)
85}
86
87fn parse_cve_json(v: Value) -> Result<Vec<CveEntry>> {
88 let mut entries = Vec::new();
89
90 let items = if let Some(items) = v.get("CVE_Items").and_then(|x| x.as_array()) {
92 items
94 } else if let Some(items) = v.get("vulnerabilities").and_then(|x| x.as_array()) {
95 items
97 } else {
98 return Ok(entries);
99 };
100
101 for item in items {
102 let (id, description) = if let Some(cve) = item.get("cve") {
104 let id = cve.get("id").and_then(|i| i.as_str()).unwrap_or("").to_string();
106 let description = cve
107 .get("descriptions")
108 .and_then(|arr| arr.as_array())
109 .and_then(|arr| arr.iter().find(|d| d.get("lang").and_then(|l| l.as_str()) == Some("en")))
110 .and_then(|d| d.get("value"))
111 .and_then(|v| v.as_str())
112 .unwrap_or("")
113 .to_string();
114 (id, description)
115 } else {
116 let id = item
118 .get("cve")
119 .and_then(|c| c.get("CVE_data_meta"))
120 .and_then(|m| m.get("ID"))
121 .and_then(|i| i.as_str())
122 .unwrap_or("")
123 .to_string();
124 let description = item
125 .get("cve")
126 .and_then(|c| c.get("description"))
127 .and_then(|d| d.get("description_data"))
128 .and_then(|arr| arr.as_array())
129 .and_then(|arr| arr.first())
130 .and_then(|d| d.get("value"))
131 .and_then(|v| v.as_str())
132 .unwrap_or("")
133 .to_string();
134 (id, description)
135 };
136
137 let mut cpes = Vec::new();
139 if let Some(configs) = item.get("configurations").and_then(|c| c.get("nodes")) {
140 collect_cpes(configs, &mut cpes);
141 }
142
143 entries.push(CveEntry {
144 id,
145 description,
146 cpes,
147 });
148 }
149 Ok(entries)
150}
151
152pub fn collect_cpes(value: &Value, out: &mut Vec<String>) {
153 match value {
154 Value::Array(arr) => {
155 for v in arr {
156 collect_cpes(v, out);
157 }
158 }
159 Value::Object(map) => {
160 if let Some(cpe_matches) = map.get("cpe_match") {
161 if let Some(arr) = cpe_matches.as_array() {
162 for cm in arr {
163 if let Some(uri) = cm.get("cpe23Uri").and_then(|u| u.as_str()) {
164 out.push(uri.to_lowercase());
165 }
166 }
167 }
168 }
169 if let Some(children) = map.get("children") {
171 collect_cpes(children, out);
172 }
173 }
174 _ => {}
175 }
176}
177
178pub fn scan_binary_vulnerabilities(analysis: &BinaryAnalysis) -> Vec<VulnerabilityMatch> {
181 let mut keywords: Vec<String> = analysis
182 .linked_libraries
183 .iter()
184 .chain(analysis.imports.iter())
185 .map(|s| s.to_lowercase())
186 .collect();
187
188 if let Some(cpe_candidates) = analysis
190 .metadata
191 .get("cpe_candidates")
192 .and_then(|c| c.as_array())
193 {
194 keywords.extend(
195 cpe_candidates
196 .iter()
197 .filter_map(|c| c.as_str().map(|s| s.to_string())),
198 );
199 }
200
201 let mut matches = Vec::new();
202
203 for entry in CVE_DB.iter() {
204 for kw in &keywords {
205 if kw.is_empty() {
206 continue;
207 }
208 if entry.description.to_lowercase().contains(kw)
209 || entry.cpes.iter().any(|c| c.contains(kw))
210 {
211 matches.push(VulnerabilityMatch {
212 cve_id: entry.id.clone(),
213 description: entry.description.clone(),
214 matched_keyword: kw.clone(),
215 });
216 break;
217 }
218 }
219 }
220
221 matches
222}