Skip to main content

web_analyzer/
nmap_zero_day.rs

1use reqwest::Client;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::time::{Duration, Instant};
5use tokio::process::Command;
6
7// ── Data Structures ─────────────────────────────────────────────────────────
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PortInfo {
11    pub port: u16,
12    pub state: String,
13    pub service: String,
14    pub version: String,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub product: Option<String>,
17    #[serde(skip_serializing_if = "Vec::is_empty")]
18    pub cpe: Vec<String>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct VulnerabilityInfo {
23    pub source: String,
24    pub vuln_type: String,
25    pub id: String,
26    pub description: String,
27    pub severity: SeverityInfo,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct SeverityInfo {
32    pub level: String,
33    pub score: f64,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct DnsInfo {
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub ipv4: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub ipv6: Option<String>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct NmapScanResult {
46    pub domain: String,
47    pub ip: String,
48    pub scan_time_secs: f64,
49    pub dns_info: DnsInfo,
50    pub open_ports: Vec<PortInfo>,
51    pub vulnerabilities: Vec<VulnerabilityInfo>,
52}
53
54// ── Security sources ────────────────────────────────────────────────────────
55
56const NVD_API: &str = "https://services.nvd.nist.gov/rest/json/cves/2.0";
57
58// ── Main scan function ──────────────────────────────────────────────────────
59
60pub async fn run_nmap_scan(
61    domain: &str,
62    progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
63) -> Result<NmapScanResult, Box<dyn std::error::Error + Send + Sync>> {
64    let start = Instant::now();
65
66    // ── DNS Resolution ──────────────────────────────────────────────────
67    if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Nmap Zero-Day".into(), percentage: 5.0, message: "Resolving target IP for direct scanning...".into(), status: "Info".into() }).await; }
68    let mut ipv4: Option<String> = None;
69    let mut ipv6: Option<String> = None;
70
71    if let Ok(addrs) = tokio::net::lookup_host(format!("{}:80", domain)).await {
72        for addr in addrs {
73            match addr.ip() {
74                std::net::IpAddr::V4(ip) if ipv4.is_none() => ipv4 = Some(ip.to_string()),
75                std::net::IpAddr::V6(ip) if ipv6.is_none() => ipv6 = Some(ip.to_string()),
76                _ => {}
77            }
78        }
79    }
80
81    let ip = ipv4.clone().unwrap_or_else(|| domain.to_string());
82
83    // ── Nmap Port Scan ──────────────────────────────────────────────────
84    if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Nmap Zero-Day".into(), percentage: 15.0, message: "Executing Nmap fast scan on top 1000 ports...".into(), status: "Info".into() }).await; }
85    let output = Command::new("nmap")
86        .args([
87            "-sV",
88            "-Pn",
89            "-A",
90            "-T5",
91            "--top-ports",
92            "1000",
93            "-oG",
94            "-",
95            &ip,
96        ])
97        .output()
98        .await?;
99
100    let stdout = String::from_utf8_lossy(&output.stdout);
101    let mut open_ports: Vec<PortInfo> = Vec::new();
102
103    if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Nmap Zero-Day".into(), percentage: 50.0, message: "Parsing Nmap schema mapping...".into(), status: "Info".into() }).await; }
104
105    // Parse grepable output: Host: x.x.x.x () Ports: 22/open/tcp//ssh//OpenSSH 8.9/, ...
106    for line in stdout.lines() {
107        if !line.contains("Ports:") {
108            continue;
109        }
110        if let Some(ports_section) = line.split("Ports: ").nth(1) {
111            for port_entry in ports_section.split(',') {
112                let parts: Vec<&str> = port_entry.trim().split('/').collect();
113                if parts.len() >= 5 && parts[1].trim() == "open" {
114                    let port: u16 = parts[0].trim().parse().unwrap_or(0);
115                    let service = parts[4].trim().to_string();
116                    let product = if parts.len() > 6 && !parts[6].trim().is_empty() {
117                        Some(parts[6].trim().to_string())
118                    } else {
119                        None
120                    };
121                    let version = if parts.len() > 6 {
122                        let p = parts[6].trim();
123                        let v = if parts.len() > 7 { parts[7].trim() } else { "" };
124                        format!("{} {}", p, v).trim().to_string()
125                    } else {
126                        String::new()
127                    };
128
129                    // Extract CPE from nmap XML output (if available in grepable)
130                    let cpe = Vec::new(); // CPE extraction requires XML output mode
131
132                    open_ports.push(PortInfo {
133                        port,
134                        state: "open".into(),
135                        service,
136                        version,
137                        product,
138                        cpe,
139                    });
140                }
141            }
142        }
143    }
144
145    // ── Vulnerability Lookup (NVD CVE) ──────────────────────────────────
146    if let Some(t) = &progress_tx { let _ = t.send(crate::ScanProgress { module: "Nmap Zero-Day".into(), percentage: 60.0, message: "Initializing NVD Extractor...".into(), status: "Info".into() }).await; }
147    let vulnerabilities = fetch_vulnerabilities(&open_ports, &progress_tx).await;
148
149    let scan_time = start.elapsed().as_secs_f64();
150
151    Ok(NmapScanResult {
152        domain: domain.to_string(),
153        ip,
154        scan_time_secs: scan_time,
155        dns_info: DnsInfo { ipv4, ipv6 },
156        open_ports,
157        vulnerabilities,
158    })
159}
160
161// ── CVE/Vulnerability Lookup ────────────────────────────────────────────────
162
163async fn fetch_vulnerabilities(
164    ports: &[PortInfo],
165    progress_tx: &Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
166) -> Vec<VulnerabilityInfo> {
167    let client = Client::builder()
168        .timeout(Duration::from_secs(20))
169        .build()
170        .unwrap_or_else(|_| Client::new());
171
172    let mut all_vulns = Vec::new();
173
174    for (i, port) in ports.iter().enumerate() {
175        if let Some(t) = progress_tx { let _ = t.send(crate::ScanProgress { module: "Nmap Zero-Day".into(), percentage: 60.0 + (40.0 * (i as f32 / ports.len().max(1) as f32)), message: format!("Matching CVEs for port {} ({})", port.port, port.service), status: "Info".into() }).await; }
176        // Build keyword from service + version/product
177        let keywords: Vec<&str> = [
178            port.service.as_str(),
179            port.product.as_deref().unwrap_or(""),
180            port.version.as_str(),
181        ]
182        .into_iter()
183        .filter(|s| !s.is_empty())
184        .collect();
185
186        if keywords.is_empty() {
187            continue;
188        }
189        let keyword = keywords.join(" ");
190
191        // ── NVD CVE Query ───────────────────────────────────────────
192        let nvd_vulns = query_nvd(&client, &keyword).await;
193        all_vulns.extend(nvd_vulns);
194
195        // ── Exploit-DB Query ────────────────────────────────────────
196        let exploit_vulns = query_exploit_db(&client, &keyword).await;
197        all_vulns.extend(exploit_vulns);
198    }
199
200    all_vulns
201}
202
203async fn query_nvd(client: &Client, keyword: &str) -> Vec<VulnerabilityInfo> {
204    let mut results = Vec::new();
205
206    let encoded = urlencoding::encode(keyword);
207    let url = format!("{}?keywordSearch={}&resultsPerPage=10", NVD_API, encoded);
208    let resp = match client.get(&url).send().await {
209        Ok(r) if r.status().is_success() => r,
210        _ => return results,
211    };
212
213    let body: Value = match resp.json().await {
214        Ok(v) => v,
215        Err(_) => return results,
216    };
217
218    if let Some(vulns) = body.get("vulnerabilities").and_then(|v| v.as_array()) {
219        for item in vulns {
220            let cve = match item.get("cve") {
221                Some(c) => c,
222                None => continue,
223            };
224            let id = cve
225                .get("id")
226                .and_then(|v| v.as_str())
227                .unwrap_or("N/A")
228                .to_string();
229            let description = cve
230                .get("descriptions")
231                .and_then(|d| d.as_array())
232                .and_then(|arr| arr.first())
233                .and_then(|d| d.get("value"))
234                .and_then(|v| v.as_str())
235                .unwrap_or("No description available")
236                .to_string();
237
238            let severity = calculate_severity(cve);
239
240            results.push(VulnerabilityInfo {
241                source: "NVD".into(),
242                vuln_type: "CVE".into(),
243                id,
244                description,
245                severity,
246            });
247        }
248    }
249
250    results
251}
252
253async fn query_exploit_db(client: &Client, keyword: &str) -> Vec<VulnerabilityInfo> {
254    let mut results = Vec::new();
255
256    let encoded = urlencoding::encode(keyword);
257    let url = format!("https://www.exploit-db.com/search?q={}", encoded);
258    if let Ok(resp) = client
259        .get(&url)
260        .header("User-Agent", "Mozilla/5.0")
261        .send()
262        .await
263    {
264        if resp.status().is_success() {
265            results.push(VulnerabilityInfo {
266                source: "Exploit-DB".into(),
267                vuln_type: "Exploit".into(),
268                id: "N/A".into(),
269                description: format!("Potential exploit for {}", keyword),
270                severity: SeverityInfo {
271                    level: "Unknown".into(),
272                    score: 0.0,
273                },
274            });
275        }
276    }
277
278    results
279}
280
281// ── Severity Calculation (CVSS v3.1) ────────────────────────────────────────
282
283fn calculate_severity(cve: &Value) -> SeverityInfo {
284    let base_score = cve
285        .get("metrics")
286        .and_then(|m| m.get("cvssMetricV31"))
287        .and_then(|v| v.as_array())
288        .and_then(|arr| arr.first())
289        .and_then(|m| m.get("cvssData"))
290        .and_then(|d| d.get("baseScore"))
291        .and_then(|s| s.as_f64())
292        .unwrap_or(0.0);
293
294    let level = if base_score >= 9.0 {
295        "Critical"
296    } else if base_score >= 7.0 {
297        "High"
298    } else if base_score >= 4.0 {
299        "Medium"
300    } else if base_score > 0.0 {
301        "Low"
302    } else {
303        "Unknown"
304    };
305
306    SeverityInfo {
307        level: level.into(),
308        score: base_score,
309    }
310}