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 {
68        let _ = t
69            .send(crate::ScanProgress {
70                module: "Nmap Zero-Day".into(),
71                percentage: 5.0,
72                message: "Resolving target IP for direct scanning...".into(),
73                status: "Info".into(),
74            })
75            .await;
76    }
77    let mut ipv4: Option<String> = None;
78    let mut ipv6: Option<String> = None;
79
80    if let Ok(addrs) = tokio::net::lookup_host(format!("{}:80", domain)).await {
81        for addr in addrs {
82            match addr.ip() {
83                std::net::IpAddr::V4(ip) if ipv4.is_none() => ipv4 = Some(ip.to_string()),
84                std::net::IpAddr::V6(ip) if ipv6.is_none() => ipv6 = Some(ip.to_string()),
85                _ => {}
86            }
87        }
88    }
89
90    let ip = ipv4.clone().unwrap_or_else(|| domain.to_string());
91
92    // ── Nmap Port Scan ──────────────────────────────────────────────────
93    if let Some(t) = &progress_tx {
94        let _ = t
95            .send(crate::ScanProgress {
96                module: "Nmap Zero-Day".into(),
97                percentage: 15.0,
98                message: "Executing Nmap fast scan on top 1000 ports...".into(),
99                status: "Info".into(),
100            })
101            .await;
102    }
103    let output = Command::new("nmap")
104        .args([
105            "-sV",
106            "-Pn",
107            "-A",
108            "-T5",
109            "--top-ports",
110            "1000",
111            "-oG",
112            "-",
113            &ip,
114        ])
115        .output()
116        .await?;
117
118    let stdout = String::from_utf8_lossy(&output.stdout);
119    let mut open_ports: Vec<PortInfo> = Vec::new();
120
121    if let Some(t) = &progress_tx {
122        let _ = t
123            .send(crate::ScanProgress {
124                module: "Nmap Zero-Day".into(),
125                percentage: 50.0,
126                message: "Parsing Nmap schema mapping...".into(),
127                status: "Info".into(),
128            })
129            .await;
130    }
131
132    // Parse grepable output: Host: x.x.x.x () Ports: 22/open/tcp//ssh//OpenSSH 8.9/, ...
133    for line in stdout.lines() {
134        if !line.contains("Ports:") {
135            continue;
136        }
137        if let Some(ports_section) = line.split("Ports: ").nth(1) {
138            for port_entry in ports_section.split(',') {
139                let parts: Vec<&str> = port_entry.trim().split('/').collect();
140                if parts.len() >= 5 && parts[1].trim() == "open" {
141                    let port: u16 = parts[0].trim().parse().unwrap_or(0);
142                    let service = parts[4].trim().to_string();
143                    let product = if parts.len() > 6 && !parts[6].trim().is_empty() {
144                        Some(parts[6].trim().to_string())
145                    } else {
146                        None
147                    };
148                    let version = if parts.len() > 6 {
149                        let p = parts[6].trim();
150                        let v = if parts.len() > 7 { parts[7].trim() } else { "" };
151                        format!("{} {}", p, v).trim().to_string()
152                    } else {
153                        String::new()
154                    };
155
156                    // Extract CPE from nmap XML output (if available in grepable)
157                    let cpe = Vec::new(); // CPE extraction requires XML output mode
158
159                    open_ports.push(PortInfo {
160                        port,
161                        state: "open".into(),
162                        service,
163                        version,
164                        product,
165                        cpe,
166                    });
167                }
168            }
169        }
170    }
171
172    // ── Vulnerability Lookup (NVD CVE) ──────────────────────────────────
173    if let Some(t) = &progress_tx {
174        let _ = t
175            .send(crate::ScanProgress {
176                module: "Nmap Zero-Day".into(),
177                percentage: 60.0,
178                message: "Initializing NVD Extractor...".into(),
179                status: "Info".into(),
180            })
181            .await;
182    }
183    let vulnerabilities = fetch_vulnerabilities(&open_ports, &progress_tx).await;
184
185    let scan_time = start.elapsed().as_secs_f64();
186
187    Ok(NmapScanResult {
188        domain: domain.to_string(),
189        ip,
190        scan_time_secs: scan_time,
191        dns_info: DnsInfo { ipv4, ipv6 },
192        open_ports,
193        vulnerabilities,
194    })
195}
196
197// ── CVE/Vulnerability Lookup ────────────────────────────────────────────────
198
199async fn fetch_vulnerabilities(
200    ports: &[PortInfo],
201    progress_tx: &Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
202) -> Vec<VulnerabilityInfo> {
203    let client = crate::http_client_builder()
204        .timeout(Duration::from_secs(20))
205        .build()
206        .unwrap_or_else(|_| Client::new());
207
208    let mut all_vulns = Vec::new();
209
210    for (i, port) in ports.iter().enumerate() {
211        if let Some(t) = progress_tx {
212            let _ = t
213                .send(crate::ScanProgress {
214                    module: "Nmap Zero-Day".into(),
215                    percentage: 60.0 + (40.0 * (i as f32 / ports.len().max(1) as f32)),
216                    message: format!("Matching CVEs for port {} ({})", port.port, port.service),
217                    status: "Info".into(),
218                })
219                .await;
220        }
221        // Build keyword from service + version/product
222        let keywords: Vec<&str> = [
223            port.service.as_str(),
224            port.product.as_deref().unwrap_or(""),
225            port.version.as_str(),
226        ]
227        .into_iter()
228        .filter(|s| !s.is_empty())
229        .collect();
230
231        if keywords.is_empty() {
232            continue;
233        }
234        let keyword = keywords.join(" ");
235
236        // ── NVD CVE Query ───────────────────────────────────────────
237        let nvd_vulns = query_nvd(&client, &keyword).await;
238        all_vulns.extend(nvd_vulns);
239
240        // ── Exploit-DB Query ────────────────────────────────────────
241        let exploit_vulns = query_exploit_db(&client, &keyword).await;
242        all_vulns.extend(exploit_vulns);
243    }
244
245    all_vulns
246}
247
248async fn query_nvd(client: &Client, keyword: &str) -> Vec<VulnerabilityInfo> {
249    let mut results = Vec::new();
250
251    let encoded = urlencoding::encode(keyword);
252    let url = format!("{}?keywordSearch={}&resultsPerPage=10", NVD_API, encoded);
253    let resp = match client.get(&url).send().await {
254        Ok(r) if r.status().is_success() => r,
255        _ => return results,
256    };
257
258    let body: Value = match resp.json().await {
259        Ok(v) => v,
260        Err(_) => return results,
261    };
262
263    if let Some(vulns) = body.get("vulnerabilities").and_then(|v| v.as_array()) {
264        for item in vulns {
265            let cve = match item.get("cve") {
266                Some(c) => c,
267                None => continue,
268            };
269            let id = cve
270                .get("id")
271                .and_then(|v| v.as_str())
272                .unwrap_or("N/A")
273                .to_string();
274            let description = cve
275                .get("descriptions")
276                .and_then(|d| d.as_array())
277                .and_then(|arr| arr.first())
278                .and_then(|d| d.get("value"))
279                .and_then(|v| v.as_str())
280                .unwrap_or("No description available")
281                .to_string();
282
283            let severity = calculate_severity(cve);
284
285            results.push(VulnerabilityInfo {
286                source: "NVD".into(),
287                vuln_type: "CVE".into(),
288                id,
289                description,
290                severity,
291            });
292        }
293    }
294
295    results
296}
297
298async fn query_exploit_db(client: &Client, keyword: &str) -> Vec<VulnerabilityInfo> {
299    let mut results = Vec::new();
300
301    let encoded = urlencoding::encode(keyword);
302    let url = format!("https://www.exploit-db.com/search?q={}", encoded);
303    if let Ok(resp) = client
304        .get(&url)
305        .header("User-Agent", "Mozilla/5.0")
306        .send()
307        .await
308    {
309        if resp.status().is_success() {
310            results.push(VulnerabilityInfo {
311                source: "Exploit-DB".into(),
312                vuln_type: "Exploit".into(),
313                id: "N/A".into(),
314                description: format!("Potential exploit for {}", keyword),
315                severity: SeverityInfo {
316                    level: "Unknown".into(),
317                    score: 0.0,
318                },
319            });
320        }
321    }
322
323    results
324}
325
326// ── Severity Calculation (CVSS v3.1) ────────────────────────────────────────
327
328fn calculate_severity(cve: &Value) -> SeverityInfo {
329    let base_score = cve
330        .get("metrics")
331        .and_then(|m| m.get("cvssMetricV31"))
332        .and_then(|v| v.as_array())
333        .and_then(|arr| arr.first())
334        .and_then(|m| m.get("cvssData"))
335        .and_then(|d| d.get("baseScore"))
336        .and_then(|s| s.as_f64())
337        .unwrap_or(0.0);
338
339    let level = if base_score >= 9.0 {
340        "Critical"
341    } else if base_score >= 7.0 {
342        "High"
343    } else if base_score >= 4.0 {
344        "Medium"
345    } else if base_score > 0.0 {
346        "Low"
347    } else {
348        "Unknown"
349    };
350
351    SeverityInfo {
352        level: level.into(),
353        score: base_score,
354    }
355}