nmap/
lib.rs

1use serde::{Deserialize, Serialize};
2use serde_xml_rs::from_str;
3
4/// Module for parsing and handling Nmap output files
5pub mod gnmap;
6
7/// Represents the type of address in an Nmap scan result
8#[derive(Debug, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "lowercase")]
10pub enum AddressType {
11    /// IPv4 address
12    IPv4,
13    /// IPv6 address
14    IPv6,
15    /// MAC address
16    MAC,
17}
18
19/// Represents the protocol used for port scanning
20#[derive(Debug, Serialize, Deserialize, PartialEq)]
21#[serde(rename_all = "lowercase")]
22pub enum PortProtocol {
23    /// IP protocol
24    Ip,
25    /// TCP protocol
26    Tcp,
27    /// UDP protocol
28    Udp,
29    /// SCTP protocol
30    Sctp,
31}
32
33/// Represents the state of a host in an Nmap scan
34#[derive(Debug, Serialize, Deserialize, PartialEq)]
35#[serde(rename_all = "lowercase")]
36pub enum HostState {
37    /// Host is up and responding
38    Up,
39    /// Host is down or not responding
40    Down,
41    /// Host state could not be determined
42    Unknown,
43    /// Host was skipped during scanning
44    Skipped,
45}
46
47/// Represents the state of a port in an Nmap scan
48#[derive(Debug, Serialize, Deserialize, PartialEq)]
49pub enum PortState {
50    /// Port is open and accepting connections
51    #[serde(rename = "open")]
52    Open,
53    /// Port is closed
54    #[serde(rename = "closed")]
55    Closed,
56    /// Port is filtered by a firewall or other network device
57    #[serde(rename = "filtered")]
58    Filtered,
59    /// Port is unfiltered but state could not be determined
60    #[serde(rename = "unfiltered")]
61    Unfiltered,
62    /// Port could be either open or filtered
63    #[serde(rename = "open|filtered")]
64    OpenFiltered,
65    /// Port could be either closed or filtered
66    #[serde(rename = "closed|filtered")]
67    ClosedFiltered,
68}
69
70/// Method used to determine service information
71#[derive(Debug, Serialize, Deserialize, PartialEq)]
72#[serde(rename_all = "lowercase")]
73pub enum ServiceMethod {
74    /// Service determined by port number lookup
75    Table,
76    /// Service determined by active probing
77    Probed,
78}
79
80/// Type of scan performed by Nmap
81#[derive(Debug, Serialize, Deserialize, PartialEq)]
82#[serde(rename_all = "lowercase")]
83pub enum ScanType {
84    /// TCP SYN scan
85    Syn,
86    /// TCP ACK scan
87    Ack,
88    /// FTP bounce scan
89    Bounce,
90    /// TCP connect scan
91    Connect,
92    /// TCP NULL scan
93    Null,
94    /// TCP XMAS scan
95    Xmas,
96    /// TCP Window scan
97    Window,
98    /// TCP Maimon scan
99    Maimon,
100    /// TCP FIN scan
101    Fin,
102    /// UDP scan
103    Udp,
104    /// SCTP INIT scan
105    SctpInit,
106    /// SCTP COOKIE-ECHO scan
107    SctpCookieEcho,
108    /// IP protocol scan
109    IpProto,
110}
111
112/// Information about the scan configuration
113#[derive(Debug, Serialize, Deserialize)]
114pub struct ScanInfo {
115    /// Type of scan performed
116    #[serde(rename = "type")]
117    pub scan_type: ScanType,
118    /// Protocol used for scanning
119    #[serde(rename = "protocol")]
120    pub protocol: PortProtocol,
121    /// Number of services scanned
122    #[serde(rename = "numservices")]
123    pub num_services: String,
124    /// List of services scanned
125    #[serde(rename = "services")]
126    pub services: Option<String>,
127}
128
129/// Main structure representing an Nmap scan run
130#[derive(Debug, Serialize, Deserialize)]
131pub struct NmapRun {
132    /// List of hosts scanned
133    #[serde(rename = "host")]
134    pub hosts: Vec<Host>,
135    /// Scan configuration information
136    #[serde(rename = "scaninfo")]
137    pub scan_info: Option<ScanInfo>,
138    /// Command line arguments used for the scan
139    #[serde(rename = "args")]
140    pub args: String,
141    /// Scanner name (usually "nmap")
142    #[serde(rename = "scanner")]
143    pub scanner: String,
144    /// Nmap version used
145    #[serde(rename = "version")]
146    pub version: String,
147    /// XML output version
148    #[serde(rename = "xmloutputversion")]
149    pub xml_output_version: String,
150    /// Unix timestamp when scan started
151    #[serde(rename = "start")]
152    pub start: Option<u64>,
153    /// Human-readable start time
154    #[serde(rename = "startstr")]
155    pub start_str: Option<String>,
156    /// Verbosity level information
157    #[serde(rename = "verbose", skip_serializing_if = "Option::is_none")]
158    pub verbose: Option<Verbose>,
159    /// Debugging level information
160    #[serde(rename = "debugging", skip_serializing_if = "Option::is_none")]
161    pub debugging: Option<Debugging>,
162    /// Statistics about the scan run
163    #[serde(rename = "runstats", skip_serializing_if = "Option::is_none")]
164    pub run_stats: Option<RunStats>,
165}
166
167/// Verbosity level configuration
168#[derive(Debug, Serialize, Deserialize)]
169pub struct Verbose {
170    /// Verbosity level (0-4)
171    #[serde(rename = "level")]
172    pub level: i32,
173}
174
175/// Debugging level configuration
176#[derive(Debug, Serialize, Deserialize)]
177pub struct Debugging {
178    /// Debug level (0-4)
179    #[serde(rename = "level")]
180    pub level: i32,
181}
182
183/// Statistics about the scan run
184#[derive(Debug, Serialize, Deserialize)]
185pub struct RunStats {
186    /// Information about scan completion
187    #[serde(rename = "finished")]
188    pub finished: Option<Finished>,
189    /// Host statistics
190    #[serde(rename = "hosts")]
191    pub hosts: Option<HostStats>,
192}
193
194/// Information about scan completion
195#[derive(Debug, Serialize, Deserialize)]
196pub struct Finished {
197    /// Unix timestamp when scan finished
198    #[serde(rename = "time")]
199    pub time: u64,
200    /// Human-readable finish time
201    #[serde(rename = "timestr")]
202    pub time_str: String,
203    /// Total time elapsed in seconds
204    #[serde(rename = "elapsed")]
205    pub elapsed: f64,
206    /// Summary of scan results
207    #[serde(rename = "summary")]
208    pub summary: String,
209    /// Exit status
210    #[serde(rename = "exit")]
211    pub exit: String,
212}
213
214/// Statistics about scanned hosts
215#[derive(Debug, Serialize, Deserialize)]
216pub struct HostStats {
217    /// Number of hosts that were up
218    #[serde(rename = "up")]
219    pub up: u32,
220    /// Number of hosts that were down
221    #[serde(rename = "down")]
222    pub down: u32,
223    /// Total number of hosts scanned
224    #[serde(rename = "total")]
225    pub total: u32,
226}
227
228/// Information about a scanned host
229#[derive(Debug, Serialize, Deserialize)]
230pub struct Host {
231    /// Host status information
232    #[serde(rename = "status")]
233    pub status: Status,
234    /// List of addresses associated with the host
235    #[serde(rename = "address")]
236    pub addresses: Vec<Address>,
237    /// Host names associated with the host
238    #[serde(rename = "hostnames")]
239    pub hostnames: Option<HostNames>,
240    /// Port scan results
241    #[serde(rename = "ports")]
242    pub ports: Option<Ports>,
243    /// Unix timestamp when host scan started
244    #[serde(rename = "starttime")]
245    pub start_time: Option<u64>,
246    /// Unix timestamp when host scan ended
247    #[serde(rename = "endtime")]
248    pub end_time: Option<u64>,
249}
250
251/// Host status information
252#[derive(Debug, Serialize, Deserialize)]
253pub struct Status {
254    /// State of the host
255    #[serde(rename = "state")]
256    pub state: HostState,
257    /// Reason for the state determination
258    #[serde(rename = "reason")]
259    pub reason: String,
260    /// TTL value from the reason determination
261    #[serde(rename = "reason_ttl")]
262    pub reason_ttl: String,
263}
264
265/// Address information for a host
266#[derive(Debug, Serialize, Deserialize)]
267pub struct Address {
268    /// The address value
269    #[serde(rename = "addr")]
270    pub addr: String,
271    /// Type of address
272    #[serde(rename = "addrtype")]
273    pub addrtype: AddressType,
274    /// Vendor name (for MAC addresses)
275    #[serde(rename = "vendor")]
276    pub vendor: Option<String>,
277}
278
279/// Collection of host names
280#[derive(Debug, Serialize, Deserialize)]
281pub struct HostNames {
282    /// List of host names
283    #[serde(rename = "hostname")]
284    pub hostnames: Option<Vec<HostName>>,
285}
286
287/// Individual host name information
288#[derive(Debug, Serialize, Deserialize)]
289pub struct HostName {
290    /// The host name
291    #[serde(rename = "name")]
292    pub name: String,
293    /// Type of host name
294    #[serde(rename = "type")]
295    pub hostname_type: HostNameType,
296}
297
298/// Type of host name
299#[derive(Debug, Serialize, Deserialize, PartialEq)]
300#[serde(rename_all = "UPPERCASE")]
301pub enum HostNameType {
302    /// User-specified host name
303    User,
304    /// PTR record from reverse DNS lookup
305    PTR,
306}
307
308/// Collection of port scan results
309#[derive(Debug, Serialize, Deserialize)]
310pub struct Ports {
311    /// List of scanned ports
312    #[serde(rename = "port")]
313    pub ports: Option<Vec<Port>>,
314    /// Information about ports not fully scanned
315    #[serde(rename = "extraports")]
316    pub extraports: Option<Vec<ExtraPorts>>,
317}
318
319/// Information about ports not fully scanned
320#[derive(Debug, Serialize, Deserialize)]
321pub struct ExtraPorts {
322    /// State of the extra ports
323    #[serde(rename = "state")]
324    pub state: PortState,
325    /// Number of ports in this state
326    #[serde(rename = "count")]
327    pub count: u32,
328    /// Additional reasons for port state
329    #[serde(rename = "extrareasons")]
330    pub extrareasons: Option<Vec<ExtraReasons>>,
331}
332
333/// Additional reasons for port state
334#[derive(Debug, Serialize, Deserialize)]
335pub struct ExtraReasons {
336    /// Reason description
337    #[serde(rename = "reason")]
338    pub reason: String,
339    /// Number of ports with this reason
340    #[serde(rename = "count")]
341    pub count: u32,
342    /// Protocol used
343    #[serde(rename = "proto")]
344    pub protocol: Option<PortProtocol>,
345    /// Port numbers affected
346    #[serde(rename = "ports")]
347    pub ports: Option<String>,
348}
349
350/// Information about a specific port
351#[derive(Debug, Serialize, Deserialize)]
352pub struct Port {
353    /// Protocol used
354    #[serde(rename = "protocol")]
355    pub protocol: PortProtocol,
356    /// Port number
357    #[serde(rename = "portid")]
358    pub port_id: u32,
359    /// Port state details
360    #[serde(rename = "state")]
361    pub state: PortStateDetails,
362    /// Service information
363    #[serde(rename = "service")]
364    pub service: Option<Service>,
365    /// NSE script results
366    #[serde(rename = "script")]
367    pub scripts: Option<Vec<Script>>,
368}
369
370/// Detailed port state information
371#[derive(Debug, Serialize, Deserialize)]
372pub struct PortStateDetails {
373    /// State of the port
374    #[serde(rename = "state")]
375    pub state: PortState,
376    /// Reason for the state determination
377    #[serde(rename = "reason")]
378    pub reason: String,
379    /// TTL value from the reason determination
380    #[serde(rename = "reason_ttl")]
381    pub reason_ttl: String,
382    /// IP address that provided the reason
383    #[serde(rename = "reason_ip")]
384    pub reason_ip: Option<String>,
385}
386
387/// Service information for a port
388#[derive(Debug, Serialize, Deserialize)]
389pub struct Service {
390    /// Service name
391    #[serde(rename = "name")]
392    pub name: String,
393    /// Product name
394    #[serde(rename = "product")]
395    pub product: Option<String>,
396    /// Product version
397    #[serde(rename = "version")]
398    pub version: Option<String>,
399    /// Additional service information
400    #[serde(rename = "extrainfo")]
401    pub extra_info: Option<String>,
402    /// Method used to determine service
403    #[serde(rename = "method")]
404    pub method: ServiceMethod,
405    /// Confidence level in service detection (0-10)
406    #[serde(rename = "conf")]
407    pub confidence: u8,
408    /// Operating system type
409    #[serde(rename = "ostype")]
410    pub os_type: Option<String>,
411    /// Device type
412    #[serde(rename = "devicetype")]
413    pub device_type: Option<String>,
414    /// Tunnel type (if service is tunneled)
415    #[serde(rename = "tunnel")]
416    pub tunnel: Option<String>,
417    /// Common Platform Enumeration (CPE) names
418    #[serde(rename = "cpe")]
419    pub cpes: Option<Vec<String>>,
420}
421
422/// NSE script results
423#[derive(Debug, Serialize, Deserialize)]
424pub struct Script {
425    /// Script identifier
426    #[serde(rename = "id")]
427    pub id: String,
428    /// Script output
429    #[serde(rename = "output")]
430    pub output: String,
431}
432
433/// Parses Nmap XML output into a structured format
434///
435/// # Examples
436///
437/// ```
438/// use nmap::parse_nmap_xml;
439///
440/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
441/// <nmaprun scanner="nmap" args="nmap -sS -p 80 example.com" version="7.92" xmloutputversion="1.05">
442///   <host>
443///     <status state="up" reason="syn-ack" reason_ttl="0"/>
444///     <address addr="192.168.1.1" addrtype="ipv4"/>
445///     <ports>
446///       <port protocol="tcp" portid="80">
447///         <state state="open" reason="syn-ack" reason_ttl="64"/>
448///         <service name="http" method="table" conf="3"/>
449///       </port>
450///     </ports>
451///   </host>
452/// </nmaprun>"#;
453///
454/// let result = parse_nmap_xml(xml);
455/// assert!(result.is_ok());
456///
457/// let nmap_run = result.unwrap();
458/// assert_eq!(nmap_run.scanner, "nmap");
459/// assert_eq!(nmap_run.version, "7.92");
460/// assert_eq!(nmap_run.hosts.len(), 1);
461///
462/// let host = &nmap_run.hosts[0];
463/// assert_eq!(host.addresses[0].addr, "192.168.1.1");
464/// assert_eq!(host.addresses[0].addrtype, nmap::AddressType::IPv4);
465/// ```
466pub fn parse_nmap_xml(xml_content: &str) -> Result<NmapRun, Box<dyn std::error::Error>> {
467    let nmap_run: NmapRun = from_str(xml_content)?;
468    Ok(nmap_run)
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn test_parse_status() {
477        let xml = r#"<status state="up" reason="arp-response" reason_ttl="0"/>"#;
478        let status: Status = from_str(xml).expect("Failed to parse Status");
479        assert_eq!(status.state, HostState::Up);
480        assert_eq!(status.reason, "arp-response");
481        assert_eq!(status.reason_ttl, "0");
482    }
483
484    #[test]
485    fn test_parse_port() {
486        let xml = r#"<port protocol="tcp" portid="22">
487            <state state="open" reason="syn-ack" reason_ttl="64"/>
488            <service name="ssh" product="OpenSSH" version="8.2p1" extrainfo="Ubuntu 4ubuntu0.5" method="probed" conf="10"/>
489        </port>"#;
490
491        let port: Port = from_str(xml).expect("Failed to parse Port");
492
493        assert_eq!(port.protocol, PortProtocol::Tcp);
494        assert_eq!(port.port_id, 22);
495        assert_eq!(port.state.state, PortState::Open);
496        assert_eq!(port.state.reason, "syn-ack");
497        assert_eq!(port.state.reason_ttl, "64");
498
499        let service = port.service.expect("Expected service to be present");
500        assert_eq!(service.name, "ssh");
501        assert_eq!(service.product, Some("OpenSSH".to_string()));
502        assert_eq!(service.version, Some("8.2p1".to_string()));
503        assert_eq!(service.extra_info, Some("Ubuntu 4ubuntu0.5".to_string()));
504        assert_eq!(service.method, ServiceMethod::Probed);
505        assert_eq!(service.confidence, 10);
506    }
507
508    #[test]
509    fn test_parse_nmap_xml() {
510        let xml_content = r#"<?xml version="1.0" encoding="UTF-8"?>
511<nmaprun scanner="nmap" args="nmap -sS -Pn -A -O -T4 -oA local 1.2.3.4/24" 
512         start="1741619519" startstr="Mon Mar 10 10:11:59 2025" 
513         version="7.92" xmloutputversion="1.05">
514    <scaninfo type="syn" protocol="tcp" numservices="1" services="10"/>
515    <host starttime="1741619528" endtime="1741619840">
516        <status state="up" reason="arp-response" reason_ttl="0"/>
517        <address addr="1.1.1.1" addrtype="ipv4"/>
518        <address addr="AA:AA:AA:AA:AA:AA" addrtype="mac" vendor="A"/>
519        <hostnames>
520            <hostname name="test.local" type="PTR"/>
521        </hostnames>
522        <ports>
523            <port protocol="tcp" portid="22">
524                <state state="open" reason="syn-ack" reason_ttl="64"/>
525                <service name="ssh" product="OpenSSH" version="8.2p1" extrainfo="Ubuntu 4ubuntu0.5" method="probed" conf="10"/>
526            </port>
527            <port protocol="tcp" portid="80">
528                <state state="open" reason="syn-ack" reason_ttl="64"/>
529                <service name="http" product="nginx" version="1.18.0" method="probed" conf="10"/>
530            </port>
531            <extraports state="filtered" count="998">
532                <extrareasons reason="no-response" count="998" proto="tcp" ports="1-21,23-79,81-65535"/>
533            </extraports>
534        </ports>
535    </host>
536    <host>
537        <status state="up" reason="arp-response" reason_ttl="0"/>
538        <address addr="2.2.2.2" addrtype="ipv4"/>
539        <address addr="BB:BB:BB:BB:BB:BB" addrtype="mac" vendor="B"/>
540        <ports>
541            <port protocol="tcp" portid="443">
542                <state state="open" reason="syn-ack" reason_ttl="64"/>
543                <service name="https" product="Apache httpd" version="2.4.41" method="probed" conf="10"/>
544            </port>
545        </ports>
546    </host>
547</nmaprun>"#;
548
549        let nmap_run = parse_nmap_xml(xml_content).expect("Failed to parse XML");
550
551        assert_eq!(nmap_run.args, "nmap -sS -Pn -A -O -T4 -oA local 1.2.3.4/24");
552        assert_eq!(nmap_run.scanner, "nmap");
553        assert_eq!(nmap_run.version, "7.92");
554        assert_eq!(nmap_run.xml_output_version, "1.05");
555        assert_eq!(nmap_run.start, Some(1741619519));
556        assert_eq!(
557            nmap_run.start_str,
558            Some("Mon Mar 10 10:11:59 2025".to_string())
559        );
560
561        // Test scaninfo
562        if let Some(scan_info) = &nmap_run.scan_info {
563            assert_eq!(scan_info.scan_type, ScanType::Syn);
564            assert_eq!(scan_info.protocol, PortProtocol::Tcp);
565            assert!(scan_info.services.is_some());
566        } else {
567            panic!("Expected scaninfo to be present");
568        }
569
570        assert_eq!(nmap_run.hosts.len(), 2);
571
572        let host0 = &nmap_run.hosts[0];
573        assert_eq!(host0.status.state, HostState::Up);
574        assert_eq!(host0.status.reason, "arp-response");
575        assert_eq!(host0.status.reason_ttl, "0");
576        assert_eq!(host0.addresses[0].addr, "1.1.1.1");
577        assert_eq!(host0.addresses[0].addrtype, AddressType::IPv4);
578        assert_eq!(host0.addresses[1].addrtype, AddressType::MAC);
579        assert_eq!(host0.addresses[1].vendor, Some("A".to_string()));
580        assert_eq!(host0.start_time, Some(1741619528));
581        assert_eq!(host0.end_time, Some(1741619840));
582
583        if let Some(hostnames) = &host0.hostnames {
584            if let Some(hostname_vec) = &hostnames.hostnames {
585                assert_eq!(hostname_vec[0].name, "test.local");
586                assert_eq!(hostname_vec[0].hostname_type, HostNameType::PTR);
587            } else {
588                panic!("Expected hostname vector to be present");
589            }
590        } else {
591            panic!("Expected hostnames for first host");
592        }
593
594        // Test ports for host0
595        if let Some(ports) = &host0.ports {
596            if let Some(port_vec) = &ports.ports {
597                assert_eq!(port_vec.len(), 2);
598
599                // Check first port (SSH)
600                let ssh_port = &port_vec[0];
601                assert_eq!(ssh_port.protocol, PortProtocol::Tcp);
602                assert_eq!(ssh_port.port_id, 22);
603                assert_eq!(ssh_port.state.state, PortState::Open);
604                assert_eq!(ssh_port.state.reason, "syn-ack");
605
606                if let Some(service) = &ssh_port.service {
607                    assert_eq!(service.name, "ssh");
608                    assert_eq!(service.product, Some("OpenSSH".to_string()));
609                    assert_eq!(service.version, Some("8.2p1".to_string()));
610                    assert_eq!(service.method, ServiceMethod::Probed);
611                    assert_eq!(service.confidence, 10);
612                } else {
613                    panic!("Expected service info for SSH port");
614                }
615
616                // Check second port (HTTP)
617                let http_port = &port_vec[1];
618                assert_eq!(http_port.protocol, PortProtocol::Tcp);
619                assert_eq!(http_port.port_id, 80);
620
621                if let Some(service) = &http_port.service {
622                    assert_eq!(service.name, "http");
623                    assert_eq!(service.product, Some("nginx".to_string()));
624                    assert_eq!(service.method, ServiceMethod::Probed);
625                } else {
626                    panic!("Expected service info for HTTP port");
627                }
628            } else {
629                panic!("Expected port vector to be present");
630            }
631
632            // Check extraports
633            if let Some(extraports) = &ports.extraports {
634                assert_eq!(extraports[0].state, PortState::Filtered);
635                assert_eq!(extraports[0].count, 998);
636
637                if let Some(extrareasons) = &extraports[0].extrareasons {
638                    assert_eq!(extrareasons[0].reason, "no-response");
639                    assert_eq!(extrareasons[0].count, 998);
640                } else {
641                    panic!("Expected extrareasons in extraports");
642                }
643            } else {
644                panic!("Expected extraports information");
645            }
646        } else {
647            panic!("Expected ports information for host0");
648        }
649
650        // Test ports for host1
651        let host1 = &nmap_run.hosts[1];
652        if let Some(ports) = &host1.ports {
653            if let Some(port_vec) = &ports.ports {
654                assert_eq!(port_vec.len(), 1);
655
656                let https_port = &port_vec[0];
657                assert_eq!(https_port.protocol, PortProtocol::Tcp);
658                assert_eq!(https_port.port_id, 443);
659                assert_eq!(https_port.state.state, PortState::Open);
660
661                if let Some(service) = &https_port.service {
662                    assert_eq!(service.name, "https");
663                    assert_eq!(service.product, Some("Apache httpd".to_string()));
664                    assert_eq!(service.version, Some("2.4.41".to_string()));
665                    assert_eq!(service.method, ServiceMethod::Probed);
666                } else {
667                    panic!("Expected service info for HTTPS port");
668                }
669            } else {
670                panic!("Expected port vector to be present");
671            }
672        } else {
673            panic!("Expected ports information for host1");
674        }
675    }
676
677    #[test]
678    fn test_serialize_port() {
679        let port = Port {
680            protocol: PortProtocol::Tcp,
681            port_id: 80,
682            state: PortStateDetails {
683                state: PortState::Open,
684                reason: "syn-ack".to_string(),
685                reason_ttl: "64".to_string(),
686                reason_ip: None,
687            },
688            service: Some(Service {
689                name: "http".to_string(),
690                product: Some("nginx".to_string()),
691                version: Some("1.18.0".to_string()),
692                extra_info: None,
693                method: ServiceMethod::Probed,
694                confidence: 10,
695                os_type: None,
696                device_type: None,
697                tunnel: None,
698                cpes: None,
699            }),
700            scripts: None,
701        };
702
703        // Serialize to JSON instead of XML
704        let json = serde_json::to_string_pretty(&port).expect("Failed to serialize Port to JSON");
705        println!("Serialized JSON:\n{}", json);
706
707        // Verify JSON contains expected values
708        assert!(json.contains(r#""protocol": "tcp"#));
709        assert!(json.contains(r#""portid": 80"#));
710        assert!(json.contains(r#""state": "open"#));
711        assert!(json.contains(r#""name": "http"#));
712        assert!(json.contains(r#""product": "nginx"#));
713        assert!(json.contains(r#""version": "1.18.0"#));
714        assert!(json.contains(r#""method": "probed"#));
715        assert!(json.contains(r#""conf": 10"#));
716
717        // Test round-trip with JSON
718        let parsed_port: Port =
719            serde_json::from_str(&json).expect("Failed to parse serialized Port JSON");
720        assert_eq!(parsed_port.protocol, port.protocol);
721        assert_eq!(parsed_port.port_id, port.port_id);
722        assert_eq!(parsed_port.state.state, port.state.state);
723        assert_eq!(parsed_port.state.reason, port.state.reason);
724        assert_eq!(parsed_port.state.reason_ttl, port.state.reason_ttl);
725
726        let parsed_service = parsed_port.service.expect("Expected service to be present");
727        let original_service = port.service.expect("Expected service to be present");
728        assert_eq!(parsed_service.name, original_service.name);
729        assert_eq!(parsed_service.product, original_service.product);
730        assert_eq!(parsed_service.version, original_service.version);
731        assert_eq!(parsed_service.method, original_service.method);
732        assert_eq!(parsed_service.confidence, original_service.confidence);
733    }
734}