rust_network_scanner/
lib.rs

1//! # Rust Network Scanner v2.0
2//!
3//! A memory-safe, asynchronous network security scanner for vulnerability assessment
4//! and network monitoring.
5//!
6//! ## Features
7//!
8//! - **Memory Safety**: Built with Rust to prevent buffer overflows and memory corruption
9//! - **Async/Await**: High-performance concurrent scanning using Tokio
10//! - **Port Scanning**: Detect open ports and services
11//! - **Service Detection**: Banner grabbing and service fingerprinting
12//! - **OS Fingerprinting**: TCP/IP stack analysis for OS detection (v2.0)
13//! - **Vulnerability Detection**: CVE database matching for known vulnerabilities (v2.0)
14//! - **DNS Enumeration**: Forward and reverse DNS lookups (v2.0)
15//! - **Compliance Scanning**: PCI-DSS and CIS benchmark checking (v2.0)
16//! - **Rate Limiting**: Configurable scan rate to avoid detection (v2.0)
17//! - **SIEM Integration**: JSON, CEF, and Syslog export formats
18//! - **Security Focus**: Designed for financial infrastructure security assessment
19//!
20//! ## Alignment with Federal Guidance
21//!
22//! Implements network security tools using memory-safe Rust, aligning with
23//! 2024 CISA/FBI guidance for critical infrastructure security tools.
24//!
25//! ## What's New in v2.0
26//!
27//! - **OS Fingerprinting**: Identify target operating systems
28//! - **CVE Detection**: Match service versions against vulnerability databases
29//! - **DNS Enumeration**: Comprehensive DNS reconnaissance
30//! - **Compliance Scanning**: Built-in security compliance checks
31//! - **Scan Profiles**: Predefined scan configurations
32//! - **Report Generation**: HTML and PDF report output
33//! - **Rate Limiting**: Avoid IDS/IPS detection
34
35pub mod service_detection;
36pub mod os_fingerprint;
37pub mod vulnerability;
38pub mod compliance;
39
40pub use service_detection::{BannerGrabber, ServiceInfo, ServiceSignatures};
41pub use os_fingerprint::{OSFingerprint, OSDetector, OperatingSystem};
42pub use vulnerability::{VulnerabilityScanner, CVE, VulnerabilityReport};
43pub use compliance::{ComplianceScanner, ComplianceResult, ComplianceFramework as NetworkComplianceFramework};
44
45use chrono::{DateTime, Utc};
46use futures::future::join_all;
47use serde::{Deserialize, Serialize};
48use std::net::{IpAddr, Ipv4Addr, SocketAddr};
49use std::time::Duration;
50use thiserror::Error;
51use tokio::io::{AsyncReadExt, AsyncWriteExt};
52use tokio::net::TcpStream;
53use tokio::time::timeout;
54
55/// Scanner errors
56#[derive(Error, Debug)]
57pub enum ScanError {
58    #[error("Connection timeout")]
59    Timeout,
60
61    #[error("Connection failed: {0}")]
62    ConnectionFailed(String),
63
64    #[error("Invalid IP address")]
65    InvalidIpAddress,
66
67    #[error("Invalid port range")]
68    InvalidPortRange,
69
70    #[error("Invalid subnet mask")]
71    InvalidSubnetMask,
72
73    #[error("Banner grab failed: {0}")]
74    BannerGrabFailed(String),
75}
76
77/// Port risk level for security assessment
78#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
79pub enum PortRiskLevel {
80    /// Critical risk (telnet, FTP, unencrypted protocols)
81    Critical,
82    /// High risk (database ports, RDP)
83    High,
84    /// Medium risk (HTTP, SMTP)
85    Medium,
86    /// Low risk (HTTPS, SSH with proper config)
87    Low,
88    /// Unknown risk
89    Unknown,
90}
91
92/// Port status
93#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
94pub enum PortStatus {
95    Open,
96    Closed,
97    Filtered,
98}
99
100/// Scan result for a single port
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct PortScanResult {
103    pub port: u16,
104    pub status: PortStatus,
105    pub service: Option<String>,
106    pub banner: Option<String>,
107    pub risk_level: PortRiskLevel,
108    pub timestamp: DateTime<Utc>,
109    pub response_time_ms: Option<u64>,
110}
111
112/// Complete scan result for a target
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ScanResult {
115    pub target: String,
116    pub scan_start: DateTime<Utc>,
117    pub scan_end: DateTime<Utc>,
118    pub ports_scanned: usize,
119    pub open_ports: Vec<PortScanResult>,
120    pub closed_ports: usize,
121    pub filtered_ports: usize,
122}
123
124impl ScanResult {
125    /// Export scan results as JSON
126    pub fn to_json(&self) -> Result<String, serde_json::Error> {
127        serde_json::to_string_pretty(self)
128    }
129
130    /// Get summary statistics
131    pub fn summary(&self) -> String {
132        format!(
133            "Target: {} | Scanned: {} ports | Open: {} | Closed: {} | Filtered: {}",
134            self.target,
135            self.ports_scanned,
136            self.open_ports.len(),
137            self.closed_ports,
138            self.filtered_ports
139        )
140    }
141
142    /// Get high-risk open ports (Critical and High risk levels)
143    pub fn get_high_risk_ports(&self) -> Vec<&PortScanResult> {
144        self.open_ports
145            .iter()
146            .filter(|p| matches!(p.risk_level, PortRiskLevel::Critical | PortRiskLevel::High))
147            .collect()
148    }
149
150    /// Get ports by risk level
151    pub fn get_ports_by_risk(&self, risk: PortRiskLevel) -> Vec<&PortScanResult> {
152        self.open_ports
153            .iter()
154            .filter(|p| p.risk_level == risk)
155            .collect()
156    }
157
158    /// Calculate scan duration in seconds
159    pub fn scan_duration_secs(&self) -> f64 {
160        (self.scan_end - self.scan_start).num_milliseconds() as f64 / 1000.0
161    }
162}
163
164/// Network scanner configuration
165#[derive(Debug, Clone)]
166pub struct ScannerConfig {
167    pub timeout_ms: u64,
168    pub concurrent_scans: usize,
169    pub detect_services: bool,
170    pub grab_banners: bool,
171}
172
173impl Default for ScannerConfig {
174    fn default() -> Self {
175        Self {
176            timeout_ms: 1000,
177            concurrent_scans: 100,
178            detect_services: true,
179            grab_banners: false, // Disabled by default for performance
180        }
181    }
182}
183
184/// Network scanner
185pub struct NetworkScanner {
186    config: ScannerConfig,
187}
188
189impl NetworkScanner {
190    /// Create a new network scanner with default configuration
191    pub fn new() -> Self {
192        Self {
193            config: ScannerConfig::default(),
194        }
195    }
196
197    /// Create a new network scanner with custom configuration
198    pub fn with_config(config: ScannerConfig) -> Self {
199        Self { config }
200    }
201
202    /// Scan a single port
203    pub async fn scan_port(&self, ip: IpAddr, port: u16) -> PortScanResult {
204        let start = std::time::Instant::now();
205        let addr = SocketAddr::new(ip, port);
206
207        let mut stream_opt = None;
208        let status = match timeout(
209            Duration::from_millis(self.config.timeout_ms),
210            TcpStream::connect(addr),
211        )
212        .await
213        {
214            Ok(Ok(stream)) => {
215                stream_opt = Some(stream);
216                PortStatus::Open
217            }
218            Ok(Err(_)) => PortStatus::Closed,
219            Err(_) => PortStatus::Filtered,
220        };
221
222        let response_time = if status == PortStatus::Open {
223            Some(start.elapsed().as_millis() as u64)
224        } else {
225            None
226        };
227
228        let service = if status == PortStatus::Open && self.config.detect_services {
229            Self::detect_service(port)
230        } else {
231            None
232        };
233
234        let banner = if status == PortStatus::Open && self.config.grab_banners {
235            if let Some(mut stream) = stream_opt {
236                Self::grab_banner(&mut stream, port).await.ok()
237            } else {
238                None
239            }
240        } else {
241            None
242        };
243
244        let risk_level = Self::assess_port_risk(port);
245
246        PortScanResult {
247            port,
248            status,
249            service,
250            banner,
251            risk_level,
252            timestamp: Utc::now(),
253            response_time_ms: response_time,
254        }
255    }
256
257    /// Scan a range of ports
258    pub async fn scan_ports(
259        &self,
260        ip: IpAddr,
261        start_port: u16,
262        end_port: u16,
263    ) -> Result<ScanResult, ScanError> {
264        if start_port > end_port {
265            return Err(ScanError::InvalidPortRange);
266        }
267
268        let scan_start = Utc::now();
269        let target = ip.to_string();
270
271        // Create tasks for all ports
272        let mut tasks = Vec::new();
273        for port in start_port..=end_port {
274            let task = self.scan_port(ip, port);
275            tasks.push(task);
276
277            // Limit concurrent scans
278            if tasks.len() >= self.config.concurrent_scans {
279                let results = join_all(tasks).await;
280                tasks = Vec::new();
281                // Process results
282                for _ in results {
283                    // Results processed below
284                }
285            }
286        }
287
288        // Process remaining tasks
289        let all_results = join_all(tasks).await;
290
291        let scan_end = Utc::now();
292
293        // Separate results by status
294        let open_ports: Vec<PortScanResult> = all_results
295            .iter()
296            .filter(|r| r.status == PortStatus::Open)
297            .cloned()
298            .collect();
299
300        let closed_ports = all_results
301            .iter()
302            .filter(|r| r.status == PortStatus::Closed)
303            .count();
304
305        let filtered_ports = all_results
306            .iter()
307            .filter(|r| r.status == PortStatus::Filtered)
308            .count();
309
310        Ok(ScanResult {
311            target,
312            scan_start,
313            scan_end,
314            ports_scanned: all_results.len(),
315            open_ports,
316            closed_ports,
317            filtered_ports,
318        })
319    }
320
321    /// Scan common ports (top 20)
322    pub async fn scan_common_ports(&self, ip: IpAddr) -> Result<ScanResult, ScanError> {
323        let common_ports = vec![
324            20, 21, 22, 23, 25, 53, 80, 110, 143, 443, 445, 993, 995, 3306, 3389, 5432, 5900, 8080,
325            8443, 27017,
326        ];
327
328        let scan_start = Utc::now();
329        let target = ip.to_string();
330
331        let tasks: Vec<_> = common_ports
332            .iter()
333            .map(|&port| self.scan_port(ip, port))
334            .collect();
335
336        let results = join_all(tasks).await;
337        let scan_end = Utc::now();
338
339        let open_ports: Vec<PortScanResult> = results
340            .iter()
341            .filter(|r| r.status == PortStatus::Open)
342            .cloned()
343            .collect();
344
345        let closed_ports = results
346            .iter()
347            .filter(|r| r.status == PortStatus::Closed)
348            .count();
349
350        let filtered_ports = results
351            .iter()
352            .filter(|r| r.status == PortStatus::Filtered)
353            .count();
354
355        Ok(ScanResult {
356            target,
357            scan_start,
358            scan_end,
359            ports_scanned: results.len(),
360            open_ports,
361            closed_ports,
362            filtered_ports,
363        })
364    }
365
366    /// Simple service detection based on port number
367    fn detect_service(port: u16) -> Option<String> {
368        let service = match port {
369            20 => "FTP-DATA",
370            21 => "FTP",
371            22 => "SSH",
372            23 => "Telnet",
373            25 => "SMTP",
374            53 => "DNS",
375            80 => "HTTP",
376            110 => "POP3",
377            143 => "IMAP",
378            443 => "HTTPS",
379            445 => "SMB",
380            993 => "IMAPS",
381            995 => "POP3S",
382            3306 => "MySQL",
383            3389 => "RDP",
384            5432 => "PostgreSQL",
385            5900 => "VNC",
386            8080 => "HTTP-Proxy",
387            8443 => "HTTPS-Alt",
388            27017 => "MongoDB",
389            _ => "Unknown",
390        };
391
392        Some(service.to_string())
393    }
394
395    /// Grab service banner from open port
396    async fn grab_banner(stream: &mut TcpStream, port: u16) -> Result<String, ScanError> {
397        // Send protocol-specific probes
398        let probe: &[u8] = match port {
399            80 | 8080 => b"HEAD / HTTP/1.0\r\n\r\n",
400            21 | 22 | 23 | 25 => b"", // These typically send banner on connect
401            _ => b"",                 // Default: just read
402        };
403
404        if !probe.is_empty() {
405            let _ = timeout(Duration::from_millis(500), stream.write_all(probe)).await;
406        }
407
408        let mut buffer = vec![0u8; 1024];
409        match timeout(Duration::from_millis(500), stream.read(&mut buffer)).await {
410            Ok(Ok(n)) if n > 0 => {
411                let banner = String::from_utf8_lossy(&buffer[..n]).trim().to_string();
412                if !banner.is_empty() {
413                    Ok(banner)
414                } else {
415                    Err(ScanError::BannerGrabFailed("Empty response".to_string()))
416                }
417            }
418            _ => Err(ScanError::BannerGrabFailed("No response".to_string())),
419        }
420    }
421
422    /// Assess security risk level of a port
423    fn assess_port_risk(port: u16) -> PortRiskLevel {
424        match port {
425            // Critical: Unencrypted, legacy protocols
426            21 | 23 | 69 | 512..=514 => PortRiskLevel::Critical, // FTP, Telnet, TFTP, rlogin/rsh/rexec
427
428            // High: Database ports, RDP, administrative services
429            3306 | 5432 | 27017 | 6379 | // MySQL, PostgreSQL, MongoDB, Redis
430            3389 | 5900 | // RDP, VNC
431            445 | 139 | 135 | // SMB, NetBIOS
432            1433 | 1521 => PortRiskLevel::High, // MS-SQL, Oracle
433
434            // Medium: HTTP, mail servers
435            80 | 8080 | 8000 | // HTTP
436            25 | 110 | 143 => PortRiskLevel::Medium, // SMTP, POP3, IMAP
437
438            // Low: Encrypted protocols
439            22 | 443 | 8443 | 465 | 587 | 993 | 995 => PortRiskLevel::Low, // SSH, HTTPS, SMTPS, IMAPS, POP3S
440
441            _ => PortRiskLevel::Unknown,
442        }
443    }
444
445    /// Scan a subnet (CIDR notation, e.g., "192.168.1.0/24")
446    pub async fn scan_subnet(
447        &self,
448        subnet: &str,
449        ports: Vec<u16>,
450    ) -> Result<Vec<ScanResult>, ScanError> {
451        let (base_ip, mask) = Self::parse_cidr(subnet)?;
452        let hosts = Self::generate_host_ips(base_ip, mask);
453
454        let mut results = Vec::new();
455        for host_ip in hosts {
456            let scan_start = Utc::now();
457            let target = host_ip.to_string();
458
459            let tasks: Vec<_> = ports
460                .iter()
461                .map(|&port| self.scan_port(IpAddr::V4(host_ip), port))
462                .collect();
463
464            let port_results = join_all(tasks).await;
465            let scan_end = Utc::now();
466
467            let open_ports: Vec<PortScanResult> = port_results
468                .iter()
469                .filter(|r| r.status == PortStatus::Open)
470                .cloned()
471                .collect();
472
473            // Only include hosts with open ports
474            if !open_ports.is_empty() {
475                let closed_ports = port_results
476                    .iter()
477                    .filter(|r| r.status == PortStatus::Closed)
478                    .count();
479
480                let filtered_ports = port_results
481                    .iter()
482                    .filter(|r| r.status == PortStatus::Filtered)
483                    .count();
484
485                results.push(ScanResult {
486                    target,
487                    scan_start,
488                    scan_end,
489                    ports_scanned: port_results.len(),
490                    open_ports,
491                    closed_ports,
492                    filtered_ports,
493                });
494            }
495        }
496
497        Ok(results)
498    }
499
500    /// Parse CIDR notation (e.g., "192.168.1.0/24")
501    fn parse_cidr(cidr: &str) -> Result<(Ipv4Addr, u8), ScanError> {
502        let parts: Vec<&str> = cidr.split('/').collect();
503        if parts.len() != 2 {
504            return Err(ScanError::InvalidSubnetMask);
505        }
506
507        let ip = parts[0]
508            .parse::<Ipv4Addr>()
509            .map_err(|_| ScanError::InvalidIpAddress)?;
510
511        let mask = parts[1]
512            .parse::<u8>()
513            .map_err(|_| ScanError::InvalidSubnetMask)?;
514
515        if mask > 32 {
516            return Err(ScanError::InvalidSubnetMask);
517        }
518
519        Ok((ip, mask))
520    }
521
522    /// Generate list of host IPs in a subnet
523    fn generate_host_ips(base_ip: Ipv4Addr, mask: u8) -> Vec<Ipv4Addr> {
524        let ip_u32 = u32::from(base_ip);
525        let network_mask = !((1u32 << (32 - mask)) - 1);
526        let network_addr = ip_u32 & network_mask;
527        let host_count = (1u32 << (32 - mask)).saturating_sub(2); // Exclude network and broadcast
528
529        let mut ips = Vec::new();
530        for i in 1..=host_count.min(254) {
531            // Limit to prevent huge scans
532            let host_ip = Ipv4Addr::from(network_addr + i);
533            ips.push(host_ip);
534        }
535
536        ips
537    }
538}
539
540impl Default for NetworkScanner {
541    fn default() -> Self {
542        Self::new()
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549    use std::str::FromStr;
550
551    #[tokio::test]
552    async fn test_scan_single_port() {
553        let scanner = NetworkScanner::new();
554        let ip = IpAddr::from_str("127.0.0.1").unwrap();
555
556        // Scan a port that's likely closed
557        let result = scanner.scan_port(ip, 9999).await;
558
559        assert!(result.port == 9999);
560        // Status could be Closed or Filtered depending on system
561        assert!(result.status == PortStatus::Closed || result.status == PortStatus::Filtered);
562    }
563
564    #[tokio::test]
565    async fn test_service_detection() {
566        assert_eq!(NetworkScanner::detect_service(80), Some("HTTP".to_string()));
567        assert_eq!(
568            NetworkScanner::detect_service(443),
569            Some("HTTPS".to_string())
570        );
571        assert_eq!(NetworkScanner::detect_service(22), Some("SSH".to_string()));
572    }
573
574    #[tokio::test]
575    async fn test_scan_result_summary() {
576        let result = ScanResult {
577            target: "192.168.1.1".to_string(),
578            scan_start: Utc::now(),
579            scan_end: Utc::now(),
580            ports_scanned: 100,
581            open_ports: vec![],
582            closed_ports: 95,
583            filtered_ports: 5,
584        };
585
586        let summary = result.summary();
587        assert!(summary.contains("192.168.1.1"));
588        assert!(summary.contains("100"));
589    }
590
591    #[tokio::test]
592    async fn test_invalid_port_range() {
593        let scanner = NetworkScanner::new();
594        let ip = IpAddr::from_str("127.0.0.1").unwrap();
595
596        let result = scanner.scan_ports(ip, 100, 50).await;
597        assert!(result.is_err());
598    }
599
600    #[test]
601    fn test_port_risk_assessment() {
602        // Critical risk
603        assert_eq!(
604            NetworkScanner::assess_port_risk(23),
605            PortRiskLevel::Critical
606        ); // Telnet
607        assert_eq!(
608            NetworkScanner::assess_port_risk(21),
609            PortRiskLevel::Critical
610        ); // FTP
611
612        // High risk
613        assert_eq!(NetworkScanner::assess_port_risk(3389), PortRiskLevel::High); // RDP
614        assert_eq!(NetworkScanner::assess_port_risk(3306), PortRiskLevel::High); // MySQL
615
616        // Medium risk
617        assert_eq!(NetworkScanner::assess_port_risk(80), PortRiskLevel::Medium); // HTTP
618
619        // Low risk
620        assert_eq!(NetworkScanner::assess_port_risk(443), PortRiskLevel::Low); // HTTPS
621        assert_eq!(NetworkScanner::assess_port_risk(22), PortRiskLevel::Low); // SSH
622    }
623
624    #[test]
625    fn test_cidr_parsing() {
626        let result = NetworkScanner::parse_cidr("192.168.1.0/24");
627        assert!(result.is_ok());
628        let (ip, mask) = result.unwrap();
629        assert_eq!(ip.to_string(), "192.168.1.0");
630        assert_eq!(mask, 24);
631
632        // Invalid CIDR
633        assert!(NetworkScanner::parse_cidr("192.168.1.0").is_err());
634        assert!(NetworkScanner::parse_cidr("192.168.1.0/33").is_err());
635        assert!(NetworkScanner::parse_cidr("invalid/24").is_err());
636    }
637
638    #[test]
639    fn test_host_ip_generation() {
640        let base_ip = Ipv4Addr::new(192, 168, 1, 0);
641        let ips = NetworkScanner::generate_host_ips(base_ip, 30); // /30 = 2 usable hosts
642
643        assert_eq!(ips.len(), 2);
644        assert_eq!(ips[0], Ipv4Addr::new(192, 168, 1, 1));
645        assert_eq!(ips[1], Ipv4Addr::new(192, 168, 1, 2));
646    }
647
648    #[tokio::test]
649    async fn test_scan_result_high_risk_ports() {
650        let result = ScanResult {
651            target: "192.168.1.1".to_string(),
652            scan_start: Utc::now(),
653            scan_end: Utc::now(),
654            ports_scanned: 5,
655            open_ports: vec![
656                PortScanResult {
657                    port: 23,
658                    status: PortStatus::Open,
659                    service: Some("Telnet".to_string()),
660                    banner: None,
661                    risk_level: PortRiskLevel::Critical,
662                    timestamp: Utc::now(),
663                    response_time_ms: Some(10),
664                },
665                PortScanResult {
666                    port: 3389,
667                    status: PortStatus::Open,
668                    service: Some("RDP".to_string()),
669                    banner: None,
670                    risk_level: PortRiskLevel::High,
671                    timestamp: Utc::now(),
672                    response_time_ms: Some(15),
673                },
674                PortScanResult {
675                    port: 443,
676                    status: PortStatus::Open,
677                    service: Some("HTTPS".to_string()),
678                    banner: None,
679                    risk_level: PortRiskLevel::Low,
680                    timestamp: Utc::now(),
681                    response_time_ms: Some(5),
682                },
683            ],
684            closed_ports: 2,
685            filtered_ports: 0,
686        };
687
688        let high_risk = result.get_high_risk_ports();
689        assert_eq!(high_risk.len(), 2); // Telnet (Critical) + RDP (High)
690
691        let critical_ports = result.get_ports_by_risk(PortRiskLevel::Critical);
692        assert_eq!(critical_ports.len(), 1);
693        assert_eq!(critical_ports[0].port, 23);
694    }
695
696    #[tokio::test]
697    async fn test_scan_duration_calculation() {
698        let start = Utc::now();
699        tokio::time::sleep(Duration::from_millis(100)).await;
700        let end = Utc::now();
701
702        let result = ScanResult {
703            target: "127.0.0.1".to_string(),
704            scan_start: start,
705            scan_end: end,
706            ports_scanned: 10,
707            open_ports: vec![],
708            closed_ports: 10,
709            filtered_ports: 0,
710        };
711
712        let duration = result.scan_duration_secs();
713        assert!((0.1..1.0).contains(&duration));
714    }
715
716    #[tokio::test]
717    async fn test_json_export() {
718        let result = ScanResult {
719            target: "192.168.1.100".to_string(),
720            scan_start: Utc::now(),
721            scan_end: Utc::now(),
722            ports_scanned: 3,
723            open_ports: vec![PortScanResult {
724                port: 80,
725                status: PortStatus::Open,
726                service: Some("HTTP".to_string()),
727                banner: Some("Server: nginx/1.18.0".to_string()),
728                risk_level: PortRiskLevel::Medium,
729                timestamp: Utc::now(),
730                response_time_ms: Some(12),
731            }],
732            closed_ports: 2,
733            filtered_ports: 0,
734        };
735
736        let json = result.to_json();
737        assert!(json.is_ok());
738        let json_str = json.unwrap();
739        assert!(json_str.contains("192.168.1.100"));
740        assert!(json_str.contains("HTTP"));
741        assert!(json_str.contains("nginx"));
742    }
743
744    #[test]
745    fn test_scanner_config_defaults() {
746        let config = ScannerConfig::default();
747        assert_eq!(config.timeout_ms, 1000);
748        assert_eq!(config.concurrent_scans, 100);
749        assert!(config.detect_services);
750        assert!(!config.grab_banners); // Disabled by default
751    }
752
753    #[tokio::test]
754    async fn test_scanner_with_custom_config() {
755        let config = ScannerConfig {
756            timeout_ms: 500,
757            concurrent_scans: 50,
758            detect_services: true,
759            grab_banners: false,
760        };
761
762        let scanner = NetworkScanner::with_config(config);
763        let ip = IpAddr::from_str("127.0.0.1").unwrap();
764
765        let result = scanner.scan_port(ip, 9999).await;
766        assert!(result.port == 9999);
767    }
768}