nabla_cli/binary/
check_vulnerabilities.rs

1use 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
24// Lazily-loaded CVE database so we incur the cost only once per process.
25static 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            // Collect cpe23Uris (may be nested)
65            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            // Recurse into children nodes if present
98            if let Some(children) = map.get("children") {
99                collect_cpes(children, out);
100            }
101        }
102        _ => {}
103    }
104}
105
106/// Scan a `BinaryAnalysis` for potential vulnerabilities by matching linked libraries and import names
107/// against the locally cached NVD CVE database.
108pub 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    // Add CPE candidates from metadata
117    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}