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 { 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 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 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 let cpe = Vec::new(); 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 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
161async 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 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 let nvd_vulns = query_nvd(&client, &keyword).await;
193 all_vulns.extend(nvd_vulns);
194
195 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
281fn 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}