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) -> Result<NmapScanResult, Box<dyn std::error::Error + Send + Sync>> {
63 let start = Instant::now();
64
65 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 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 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 let cpe = Vec::new(); 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 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
155async 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 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 let nvd_vulns = query_nvd(&client, &keyword).await;
183 all_vulns.extend(nvd_vulns);
184
185 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
271fn 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}