1use reqwest::Client;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::time::{Duration, Instant};
5use tokio::process::Command;
6
7#[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
54const NVD_API: &str = "https://services.nvd.nist.gov/rest/json/cves/2.0";
57
58pub 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 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 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 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 let cpe = Vec::new(); 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 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
197async 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 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 let nvd_vulns = query_nvd(&client, &keyword).await;
238 all_vulns.extend(nvd_vulns);
239
240 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
326fn 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}