1use crate::discover::{DiscoverEngine, Host};
2use crate::error::Result;
3use crate::scan::{PortResult, PortScanner};
4use futures::stream::{self, StreamExt};
5use ipnet::Ipv4Net;
6use serde::Serialize;
7use std::sync::{
8 atomic::{AtomicUsize, Ordering},
9 Arc,
10};
11
12#[derive(Debug, Clone, Serialize)]
13pub struct SweepEntry {
14 pub host: Host,
15 pub open_ports: Vec<PortResult>,
16}
17
18#[derive(Debug, Clone, Copy)]
19pub enum SweepPhase {
20 DiscoverPing,
21 DiscoverResolve,
22 Scan,
23}
24
25#[derive(Debug, Clone)]
26pub struct SweepProgress {
27 pub phase: SweepPhase,
28 pub completed: usize,
29 pub total: usize,
30 pub found: usize,
31 pub ip: std::net::IpAddr,
32}
33
34pub struct SweepEngine {
35 discover: DiscoverEngine,
36 scanner: PortScanner,
37 scan_timeout_ms: u64,
38 host_concurrency: usize,
39}
40
41impl SweepEngine {
42 pub fn new(concurrency: usize) -> Self {
43 Self::new_with_timeouts(
44 concurrency,
45 crate::DEFAULT_PING_TIMEOUT_MS,
46 crate::DEFAULT_SCAN_TIMEOUT_MS,
47 crate::DEFAULT_DNS_TIMEOUT_MS,
48 )
49 }
50
51 pub fn new_with_timeouts(
52 concurrency: usize,
53 ping_timeout_ms: u64,
54 scan_timeout_ms: u64,
55 dns_timeout_ms: u64,
56 ) -> Self {
57 let concurrency = concurrency.clamp(1, crate::MAX_CONCURRENCY);
58 Self {
59 discover: DiscoverEngine::new_with_timeouts(
60 concurrency,
61 ping_timeout_ms,
62 dns_timeout_ms,
63 ),
64 scanner: PortScanner::new(concurrency),
65 scan_timeout_ms: scan_timeout_ms.max(1),
66 host_concurrency: concurrency,
67 }
68 }
69
70 pub async fn sweep(
71 &self,
72 subnet: Ipv4Net,
73 ports: Vec<u16>,
74 resolve_hostnames: bool,
75 ) -> Result<Vec<SweepEntry>> {
76 self.sweep_with_progress(subnet, ports, resolve_hostnames, None)
77 .await
78 }
79
80 pub async fn sweep_with_progress(
84 &self,
85 subnet: Ipv4Net,
86 ports: Vec<u16>,
87 resolve_hostnames: bool,
88 progress: Option<Arc<dyn Fn(SweepProgress) + Send + Sync>>,
89 ) -> Result<Vec<SweepEntry>> {
90 crate::validate_ports(&ports)?;
97 let discover_progress = progress.clone().map(|cb| {
98 Arc::new(move |p: crate::discover::DiscoverProgress| {
99 let phase = match p.phase {
100 crate::discover::DiscoverPhase::Ping => SweepPhase::DiscoverPing,
101 crate::discover::DiscoverPhase::Resolve => SweepPhase::DiscoverResolve,
102 };
103 cb(SweepProgress {
104 phase,
105 completed: p.completed,
106 total: p.total,
107 found: p.found,
108 ip: p.ip,
109 });
110 }) as Arc<dyn Fn(crate::discover::DiscoverProgress) + Send + Sync>
111 });
112
113 let hosts = self
114 .discover
115 .scan_subnet_with_progress(subnet, resolve_hostnames, discover_progress)
116 .await?;
117 let total_hosts = hosts.len();
118 let completed = Arc::new(AtomicUsize::new(0));
119 let open_hosts = Arc::new(AtomicUsize::new(0));
120 let ports = Arc::new(ports);
121
122 let scan_timeout_ms = self.scan_timeout_ms;
123 let scanner = self.scanner.clone();
124
125 let mut entries = stream::iter(hosts)
126 .map(|h| {
127 let scanner = scanner.clone();
128 let ports = ports.clone();
129 let completed = completed.clone();
130 let open_hosts = open_hosts.clone();
131 let progress = progress.clone();
132 async move {
133 let host_ip = h.ip;
134 let open_ports = scanner
139 .scan_host(host_ip, (*ports).clone(), scan_timeout_ms)
140 .await
141 .unwrap_or_default()
142 .into_iter()
143 .filter(|p| p.open)
144 .collect();
145
146 let entry = SweepEntry {
147 host: h,
148 open_ports,
149 };
150
151 let done = completed.fetch_add(1, Ordering::SeqCst) + 1;
152 if let Some(cb) = &progress {
153 let found = if entry.open_ports.is_empty() {
154 open_hosts.load(Ordering::SeqCst)
155 } else {
156 open_hosts.fetch_add(1, Ordering::SeqCst) + 1
157 };
158 cb(SweepProgress {
159 phase: SweepPhase::Scan,
160 completed: done,
161 total: total_hosts,
162 found,
163 ip: host_ip,
164 });
165 }
166
167 entry
168 }
169 })
170 .buffer_unordered(self.host_concurrency)
171 .collect::<Vec<SweepEntry>>()
172 .await;
173
174 entries.sort_by_key(|e| e.host.ip);
176 Ok(entries)
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::SweepEntry;
183 use crate::discover::Host;
184 use crate::scan::{PortResult, PortStatus};
185 use std::net::{IpAddr, Ipv4Addr};
186
187 #[test]
188 fn sweep_entry_preserves_host_inventory_and_open_ports() {
189 let entry = SweepEntry {
190 host: Host {
191 ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 125)),
192 hostname: Some("lamp.local".to_string()),
193 mac: Some("00:17:88:6E:6C:5C".to_string()),
194 vendor: Some("Philips Lighting BV".to_string()),
195 rtt_ms: Some(4),
196 found_by: crate::FoundBy::Probe,
197 hostname_source: Some(crate::NameSource::Mdns),
198 },
199 open_ports: vec![PortResult {
200 port: 443,
201 protocol: crate::scan::Protocol::Tcp,
202 open: true,
203 status: PortStatus::Open,
204 service: Some("https".to_string()),
205 product: None,
206 version: None,
207 latency_ms: Some(3),
208 banner: None,
209 http: None,
210 tls: None,
211 raw: None,
212 error: None,
213 }],
214 };
215
216 let value = serde_json::to_value(&entry).expect("serialize sweep entry");
217
218 assert_eq!(value["host"]["ip"], "192.168.1.125");
219 assert_eq!(value["host"]["hostname"], "lamp.local");
220 assert_eq!(value["host"]["mac"], "00:17:88:6E:6C:5C");
221 assert_eq!(value["host"]["vendor"], "Philips Lighting BV");
222 assert_eq!(value["host"]["rtt_ms"], 4);
223 assert_eq!(value["open_ports"][0]["port"], 443);
224 assert_eq!(value["open_ports"][0]["status"], "open");
225 }
226}