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