nabla_cli/binary/
check_vulnerabilities.rs

1use 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
36// Lazily-loaded CVE database so we incur the cost only once per process.
37static 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    // Try to load from cache first
52    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    // If cache doesn't exist or is invalid, download from NVD
63    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    // For now, use a simplified approach - download recent CVEs only
69    // In production, you might want to download the full database or use incremental updates
70    let response = ureq::get(CVE_JSON_URL)
71        .query("resultsPerPage", "2000") // Limit to most recent 2000 CVEs to keep size manageable
72        .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    // Cache the downloaded data
79    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    // Handle both old format (CVE_Items) and new NVD API 2.0 format (vulnerabilities)
91    let items = if let Some(items) = v.get("CVE_Items").and_then(|x| x.as_array()) {
92        // Old format
93        items
94    } else if let Some(items) = v.get("vulnerabilities").and_then(|x| x.as_array()) {
95        // New NVD API 2.0 format
96        items
97    } else {
98        return Ok(entries);
99    };
100
101    for item in items {
102        // Handle both old and new formats
103        let (id, description) = if let Some(cve) = item.get("cve") {
104            // New format
105            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            // Old format fallback
117            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        // Collect CPEs
138        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            // Recurse into children nodes if present
170            if let Some(children) = map.get("children") {
171                collect_cpes(children, out);
172            }
173        }
174        _ => {}
175    }
176}
177
178/// Scan a `BinaryAnalysis` for potential vulnerabilities by matching linked libraries and import names
179/// against the locally cached NVD CVE database.
180pub 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    // Add CPE candidates from metadata
189    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}