1use serde::{Deserialize, Serialize};
2use serde_xml_rs::from_str;
3
4pub mod gnmap;
6
7#[derive(Debug, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "lowercase")]
10pub enum AddressType {
11 IPv4,
13 IPv6,
15 MAC,
17}
18
19#[derive(Debug, Serialize, Deserialize, PartialEq)]
21#[serde(rename_all = "lowercase")]
22pub enum PortProtocol {
23 Ip,
25 Tcp,
27 Udp,
29 Sctp,
31}
32
33#[derive(Debug, Serialize, Deserialize, PartialEq)]
35#[serde(rename_all = "lowercase")]
36pub enum HostState {
37 Up,
39 Down,
41 Unknown,
43 Skipped,
45}
46
47#[derive(Debug, Serialize, Deserialize, PartialEq)]
49pub enum PortState {
50 #[serde(rename = "open")]
52 Open,
53 #[serde(rename = "closed")]
55 Closed,
56 #[serde(rename = "filtered")]
58 Filtered,
59 #[serde(rename = "unfiltered")]
61 Unfiltered,
62 #[serde(rename = "open|filtered")]
64 OpenFiltered,
65 #[serde(rename = "closed|filtered")]
67 ClosedFiltered,
68}
69
70#[derive(Debug, Serialize, Deserialize, PartialEq)]
72#[serde(rename_all = "lowercase")]
73pub enum ServiceMethod {
74 Table,
76 Probed,
78}
79
80#[derive(Debug, Serialize, Deserialize, PartialEq)]
82#[serde(rename_all = "lowercase")]
83pub enum ScanType {
84 Syn,
86 Ack,
88 Bounce,
90 Connect,
92 Null,
94 Xmas,
96 Window,
98 Maimon,
100 Fin,
102 Udp,
104 SctpInit,
106 SctpCookieEcho,
108 IpProto,
110}
111
112#[derive(Debug, Serialize, Deserialize)]
114pub struct ScanInfo {
115 #[serde(rename = "type")]
117 pub scan_type: ScanType,
118 #[serde(rename = "protocol")]
120 pub protocol: PortProtocol,
121 #[serde(rename = "numservices")]
123 pub num_services: String,
124 #[serde(rename = "services")]
126 pub services: Option<String>,
127}
128
129#[derive(Debug, Serialize, Deserialize)]
131pub struct NmapRun {
132 #[serde(rename = "host")]
134 pub hosts: Vec<Host>,
135 #[serde(rename = "scaninfo")]
137 pub scan_info: Option<ScanInfo>,
138 #[serde(rename = "args")]
140 pub args: String,
141 #[serde(rename = "scanner")]
143 pub scanner: String,
144 #[serde(rename = "version")]
146 pub version: String,
147 #[serde(rename = "xmloutputversion")]
149 pub xml_output_version: String,
150 #[serde(rename = "start")]
152 pub start: Option<u64>,
153 #[serde(rename = "startstr")]
155 pub start_str: Option<String>,
156 #[serde(rename = "verbose", skip_serializing_if = "Option::is_none")]
158 pub verbose: Option<Verbose>,
159 #[serde(rename = "debugging", skip_serializing_if = "Option::is_none")]
161 pub debugging: Option<Debugging>,
162 #[serde(rename = "runstats", skip_serializing_if = "Option::is_none")]
164 pub run_stats: Option<RunStats>,
165}
166
167#[derive(Debug, Serialize, Deserialize)]
169pub struct Verbose {
170 #[serde(rename = "level")]
172 pub level: i32,
173}
174
175#[derive(Debug, Serialize, Deserialize)]
177pub struct Debugging {
178 #[serde(rename = "level")]
180 pub level: i32,
181}
182
183#[derive(Debug, Serialize, Deserialize)]
185pub struct RunStats {
186 #[serde(rename = "finished")]
188 pub finished: Option<Finished>,
189 #[serde(rename = "hosts")]
191 pub hosts: Option<HostStats>,
192}
193
194#[derive(Debug, Serialize, Deserialize)]
196pub struct Finished {
197 #[serde(rename = "time")]
199 pub time: u64,
200 #[serde(rename = "timestr")]
202 pub time_str: String,
203 #[serde(rename = "elapsed")]
205 pub elapsed: f64,
206 #[serde(rename = "summary")]
208 pub summary: String,
209 #[serde(rename = "exit")]
211 pub exit: String,
212}
213
214#[derive(Debug, Serialize, Deserialize)]
216pub struct HostStats {
217 #[serde(rename = "up")]
219 pub up: u32,
220 #[serde(rename = "down")]
222 pub down: u32,
223 #[serde(rename = "total")]
225 pub total: u32,
226}
227
228#[derive(Debug, Serialize, Deserialize)]
230pub struct Host {
231 #[serde(rename = "status")]
233 pub status: Status,
234 #[serde(rename = "address")]
236 pub addresses: Vec<Address>,
237 #[serde(rename = "hostnames")]
239 pub hostnames: Option<HostNames>,
240 #[serde(rename = "ports")]
242 pub ports: Option<Ports>,
243 #[serde(rename = "starttime")]
245 pub start_time: Option<u64>,
246 #[serde(rename = "endtime")]
248 pub end_time: Option<u64>,
249}
250
251#[derive(Debug, Serialize, Deserialize)]
253pub struct Status {
254 #[serde(rename = "state")]
256 pub state: HostState,
257 #[serde(rename = "reason")]
259 pub reason: String,
260 #[serde(rename = "reason_ttl")]
262 pub reason_ttl: String,
263}
264
265#[derive(Debug, Serialize, Deserialize)]
267pub struct Address {
268 #[serde(rename = "addr")]
270 pub addr: String,
271 #[serde(rename = "addrtype")]
273 pub addrtype: AddressType,
274 #[serde(rename = "vendor")]
276 pub vendor: Option<String>,
277}
278
279#[derive(Debug, Serialize, Deserialize)]
281pub struct HostNames {
282 #[serde(rename = "hostname")]
284 pub hostnames: Option<Vec<HostName>>,
285}
286
287#[derive(Debug, Serialize, Deserialize)]
289pub struct HostName {
290 #[serde(rename = "name")]
292 pub name: String,
293 #[serde(rename = "type")]
295 pub hostname_type: HostNameType,
296}
297
298#[derive(Debug, Serialize, Deserialize, PartialEq)]
300#[serde(rename_all = "UPPERCASE")]
301pub enum HostNameType {
302 User,
304 PTR,
306}
307
308#[derive(Debug, Serialize, Deserialize)]
310pub struct Ports {
311 #[serde(rename = "port")]
313 pub ports: Option<Vec<Port>>,
314 #[serde(rename = "extraports")]
316 pub extraports: Option<Vec<ExtraPorts>>,
317}
318
319#[derive(Debug, Serialize, Deserialize)]
321pub struct ExtraPorts {
322 #[serde(rename = "state")]
324 pub state: PortState,
325 #[serde(rename = "count")]
327 pub count: u32,
328 #[serde(rename = "extrareasons")]
330 pub extrareasons: Option<Vec<ExtraReasons>>,
331}
332
333#[derive(Debug, Serialize, Deserialize)]
335pub struct ExtraReasons {
336 #[serde(rename = "reason")]
338 pub reason: String,
339 #[serde(rename = "count")]
341 pub count: u32,
342 #[serde(rename = "proto")]
344 pub protocol: Option<PortProtocol>,
345 #[serde(rename = "ports")]
347 pub ports: Option<String>,
348}
349
350#[derive(Debug, Serialize, Deserialize)]
352pub struct Port {
353 #[serde(rename = "protocol")]
355 pub protocol: PortProtocol,
356 #[serde(rename = "portid")]
358 pub port_id: u32,
359 #[serde(rename = "state")]
361 pub state: PortStateDetails,
362 #[serde(rename = "service")]
364 pub service: Option<Service>,
365 #[serde(rename = "script")]
367 pub scripts: Option<Vec<Script>>,
368}
369
370#[derive(Debug, Serialize, Deserialize)]
372pub struct PortStateDetails {
373 #[serde(rename = "state")]
375 pub state: PortState,
376 #[serde(rename = "reason")]
378 pub reason: String,
379 #[serde(rename = "reason_ttl")]
381 pub reason_ttl: String,
382 #[serde(rename = "reason_ip")]
384 pub reason_ip: Option<String>,
385}
386
387#[derive(Debug, Serialize, Deserialize)]
389pub struct Service {
390 #[serde(rename = "name")]
392 pub name: String,
393 #[serde(rename = "product")]
395 pub product: Option<String>,
396 #[serde(rename = "version")]
398 pub version: Option<String>,
399 #[serde(rename = "extrainfo")]
401 pub extra_info: Option<String>,
402 #[serde(rename = "method")]
404 pub method: ServiceMethod,
405 #[serde(rename = "conf")]
407 pub confidence: u8,
408 #[serde(rename = "ostype")]
410 pub os_type: Option<String>,
411 #[serde(rename = "devicetype")]
413 pub device_type: Option<String>,
414 #[serde(rename = "tunnel")]
416 pub tunnel: Option<String>,
417 #[serde(rename = "cpe")]
419 pub cpes: Option<Vec<String>>,
420}
421
422#[derive(Debug, Serialize, Deserialize)]
424pub struct Script {
425 #[serde(rename = "id")]
427 pub id: String,
428 #[serde(rename = "output")]
430 pub output: String,
431}
432
433pub 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 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 if let Some(ports) = &host0.ports {
596 if let Some(port_vec) = &ports.ports {
597 assert_eq!(port_vec.len(), 2);
598
599 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 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 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 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 let json = serde_json::to_string_pretty(&port).expect("Failed to serialize Port to JSON");
705 println!("Serialized JSON:\n{}", json);
706
707 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 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}