nessus_parser/report/
mod.rs

1use std::{borrow::Cow, collections::HashMap, net::IpAddr};
2
3use jiff::Timestamp;
4use roxmltree::{Node, StringStorage};
5
6use crate::{
7    MacAddress, StringStorageExt, assert_empty_text,
8    error::FormatError,
9    ping::PingOutcome,
10    report::item::{Item, PluginType, Protocol},
11};
12
13pub mod item;
14
15/// Represents the `<Report>` element, which contains the main body of the
16/// scan results.
17///
18/// This struct holds the report's name and a collection of `Host` structs,
19/// each detailing the findings for a single scanned target.
20#[derive(Debug)]
21pub struct Report<'input> {
22    /// The name of the report
23    pub name: Cow<'input, str>,
24    /// A vector of hosts that were scanned and included in the report.
25    pub hosts: Vec<Host<'input>>,
26}
27
28impl<'input> Report<'input> {
29    pub(crate) fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
30        let name = node
31            .attributes()
32            .find(|a| a.name() == "name")
33            .ok_or(FormatError::MissingAttribute("name"))?
34            .value_storage()
35            .to_cow();
36
37        let mut hosts = vec![];
38
39        for child in node.children() {
40            match child.tag_name().name() {
41                "ReportHost" => {
42                    hosts.push(Host::from_xml_node(child)?);
43                }
44                _ => assert_empty_text(child)?,
45            }
46        }
47
48        Ok(Self { name, hosts })
49    }
50}
51
52/// Represents a `<ReportHost>` element, containing all information gathered
53/// for a single host.
54#[derive(Debug)]
55pub struct Host<'input> {
56    /// The name of the host, typically its IP address or FQDN.
57    pub name: Cow<'input, str>,
58    /// A collection of metadata and properties discovered about the host.
59    pub properties: HostProperties<'input>,
60    /// A vector of `ReportItem` findings for this host, representing vulnerabilities,
61    /// information gathered, etc.
62    pub items: Vec<Item<'input>>,
63    /// The parsed outcome of the "Ping the remote host" plugin (ID 10180),
64    /// indicating the host's reachability status.
65    pub ping_outcome: Option<PingOutcome>,
66    /// The IP address of the scanner that performed the scan on this host,
67    /// extracted from the "Nessus Scan Information" plugin (ID 19506).
68    pub scanner_ip: Option<IpAddr>,
69    /// A sorted list of ports found to be open on the host.
70    pub open_ports: Vec<(u16, Protocol)>,
71}
72
73impl<'input> Host<'input> {
74    fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
75        const PING_THE_REMOTE_HOST_ID: u32 = 10180;
76        const NESSUS_SCAN_INFORMATION_ID: u32 = 19506;
77
78        let name = node
79            .attributes()
80            .find(|a| a.name() == "name")
81            .ok_or(FormatError::MissingAttribute("name"))?
82            .value_storage()
83            .to_cow();
84
85        let mut host_properties = None;
86        let mut items = vec![];
87
88        let mut ping_outcome = None;
89        let mut scanner_ip = None;
90
91        let mut open_ports = vec![];
92
93        for child in node.children() {
94            match child.tag_name().name() {
95                "HostProperties" => {
96                    if host_properties.is_some() {
97                        return Err(FormatError::RepeatedTag("HostProperties"));
98                    }
99                    host_properties = Some(HostProperties::from_xml_node(child)?);
100                }
101                "ReportItem" => {
102                    let item = Item::from_xml_node(child)?;
103                    match item.plugin_id {
104                        PING_THE_REMOTE_HOST_ID => {
105                            let plugin_output = item
106                                .plugin_output
107                                .as_ref()
108                                .ok_or(FormatError::MissingPluginOutput)?;
109                            if ping_outcome.is_some() {
110                                return Err(FormatError::RepeatedTag("Ping the remote host"));
111                            }
112                            ping_outcome = Some(PingOutcome::from_plugin_output(plugin_output)?);
113                        }
114                        NESSUS_SCAN_INFORMATION_ID => {
115                            let plugin_output = item
116                                .plugin_output
117                                .as_ref()
118                                .ok_or(FormatError::MissingPluginOutput)?;
119                            if scanner_ip.is_some() {
120                                return Err(FormatError::RepeatedTag("Nessus Scan Information"));
121                            }
122                            scanner_ip = Some(parse_scanner_ip(plugin_output)?);
123                        }
124                        // ignore plugins that don't verify ports
125                        10736 | 11111 | 14272 | 14274 => {}
126                        _ => {
127                            if item.port != 0 && item.plugin_type != PluginType::Local {
128                                open_ports.push((item.port, item.protocol));
129                            }
130                        }
131                    }
132
133                    items.push(item);
134                }
135                _ => assert_empty_text(child)?,
136            }
137        }
138
139        open_ports.sort_unstable();
140        open_ports.dedup();
141
142        Ok(Self {
143            name,
144            properties: host_properties.ok_or(FormatError::MissingTag("HostProperties"))?,
145            items,
146            ping_outcome,
147            scanner_ip,
148            open_ports,
149        })
150    }
151}
152
153fn parse_scanner_ip(plugin_output: &str) -> Result<IpAddr, FormatError> {
154    let (_, rest) = plugin_output
155        .split_once("Scanner IP : ")
156        .ok_or(FormatError::MissingAttribute("Scanner IP"))?;
157    let (ip, _) = rest
158        .split_once('\n')
159        .ok_or(FormatError::MissingAttribute("Scanner IP"))?;
160    Ok(ip.parse()?)
161}
162
163/// Represents the `<HostProperties>` element, a collection of key-value tags
164/// containing detailed metadata about a scanned host.
165#[derive(Debug)]
166pub struct HostProperties<'input> {
167    /// The primary IP address of the host (`host-ip`).
168    pub host_ip: IpAddr,
169    /// A timestamp string indicating when the scan of this host began (`HOST_START`).
170    pub host_start: &'input str,
171    /// A timestamp indicating when the scan of this host began (`HOST_START_TIMESTAMP`).
172    pub host_start_timestamp: Timestamp,
173    /// A timestamp string indicating when the scan of this host ended (`HOST_END`).
174    pub host_end: Option<&'input str>,
175    /// A timestamp indicating when the scan of this host ended (`HOST_END_TIMESTAMP`).
176    pub host_end_timestamp: Option<Timestamp>,
177
178    // Apache_sites
179    pub apache_sites: Option<Cow<'input, str>>,
180    // bios-uuid
181    pub bios_uuid: Option<&'input str>,
182    // Credentialed_Scan
183    pub credentialed_scan: Option<bool>,
184    // DDI_Dir_Scanner_Global_Duration
185    pub ddi_dir_scanner_global_duration: Option<u32>,
186    // DDI_Dir_Scanner_Global_Init
187    pub ddi_dir_scanner_global_init: Option<Timestamp>,
188    // dead_host: "1"
189    pub dead_host: Option<bool>,
190    // host-ad-config
191    pub host_ad_config: Option<Cow<'input, str>>,
192    /// The fully qualified domain name of the host (`host-fqdn`).
193    pub host_fqdn: Option<Cow<'input, str>>,
194    // host-fqdns
195    pub host_fqdns: Option<Cow<'input, str>>,
196    // host-rdns
197    pub host_rdns: Option<Cow<'input, str>>,
198    // hostname
199    pub hostname: Option<&'input str>,
200    // ignore_printer: "1"
201    pub ignore_printer: Option<bool>,
202    // IIS_sites
203    pub iis_sites: Option<Cow<'input, str>>,
204    // LastAuthenticatedResults
205    pub last_authenticated_results: Option<Timestamp>,
206    // LastUnauthenticatedResults
207    pub last_unauthenticated_results: Option<Timestamp>,
208    // local-checks-proto
209    pub local_checks_proto: Option<&'input str>,
210    // netbios-name
211    pub netbios_name: Option<Cow<'input, str>>,
212    /// The operating system identified on the host (`operating-system`).
213    pub operating_system: Option<&'input str>,
214    // operating-system-conf
215    pub operating_system_conf: Option<i32>,
216    // operating-system-method
217    pub operating_system_method: Option<&'input str>,
218    // operating-system-unsupported
219    pub operating_system_unsupported: Option<bool>,
220    // os: "linux" | "mac" | "other" | "windows"
221    pub os: Option<&'input str>,
222    // patch-summary-total-cves: Option<u32>
223    pub patch_summary_total_cves: Option<u32>,
224    // policy-used
225    pub policy_used: Option<Cow<'input, str>>,
226    // rexec-login-used
227    pub rexec_login_used: Option<&'input str>,
228    // rlogin-login-used
229    pub rlogin_login_used: Option<&'input str>,
230    // rsh-login-used
231    pub rsh_login_used: Option<&'input str>,
232    // smb-login-used
233    pub smb_login_used: Option<&'input str>,
234    // ssh-login-used
235    pub ssh_login_used: Option<&'input str>,
236    // telnet-login-used
237    pub telnet_login_used: Option<&'input str>,
238    // sinfp-ml-prediction
239    pub sinfp_ml_prediction: Option<Cow<'input, str>>,
240    // sinfp-signature:
241    // > P1:B11113:F0x12:W8192:O0204ffff:M1460:
242    // >    P2:B11113:F0x12:W8192:O0204ffff010303080402080affffffff44454144:M1460:
243    // >    P3:B00000:F0x00:W0:O0:M0
244    // >    P4:190400_7_p=53R
245    pub sinfp_signature: Option<&'input str>,
246    // ssh-fingerprint
247    pub ssh_fingerprint: Option<Cow<'input, str>>,
248    /// The system type, such as "general-purpose", "printer", etc. (`system-type`).
249    // > "unknown" | "general-purpose" | "hypervisor" | "firewall" | "router"
250    // > | "embedded" | "camera" | "switch" | "General" | "load-balancer"
251    // > | "wireless-access-point" | "printer"
252    pub system_type: Option<&'input str>,
253    // wmi-domain
254    pub wmi_domain: Option<&'input str>,
255    /// A list of MAC addresses discovered on the host (`mac-address`).
256    pub mac_address: Vec<MacAddress>,
257    /// A list of Common Platform Enumeration (CPE) strings (`cpe`, `cpe-0`, etc.).
258    // cpe-<u16?>
259    // > cpe:/o:<vendor>:<product?>
260    // > cpe:/o:<vendor>:<product>:<version?> -> <string?>
261    // > x-cpe:/h:<vendor>:<product>:<version>
262    // > x-cpe:/a:<vendor>:<product>:<version?>
263    pub cpe: Vec<Cow<'input, str>>,
264    /// A list of IP addresses representing the hops in a traceroute to the target.
265    // traceroute-hop-<u8>
266    pub traceroute: Vec<Option<IpAddr>>,
267    // netstat-listen-(tcp|udp)(4|6)-<u16>
268    pub netstat_listen: Vec<(&'input str, u16, &'input str)>,
269    // netstat-established-(tcp|udp)(4|6)-<u16>
270    pub netstat_established: Vec<(&'input str, u16, &'input str)>,
271    // patch-summary-txt-<32-hex-digits>
272    pub patch_summary_txt: Vec<(&'input str, Cow<'input, str>)>,
273    // enumerated-ports-<u16>-tcp: "open" (ports out of range)
274    pub enumerated_ports: Vec<(u16, Protocol, &'input str)>,
275    // patch-summary-cve-num-<32-hex-digits>
276    pub patch_summary_cve_num: Vec<(&'input str, u32)>,
277    // patch-summary-cves-<32-hex-digits>
278    pub patch_summary_cves: Vec<(&'input str, Vec<&'input str>)>,
279    // DDI_Dir_Scanner_Port_<u16>_Init
280    pub ddi_dir_scanner_port_init: Vec<(u16, Timestamp)>,
281    // DDI_Dir_Scanner_Port_<u16>_Pass_Start
282    pub ddi_dir_scanner_port_pass_start: Vec<(u16, Timestamp)>,
283    // DDI_Dir_Scanner_Port_<u16>_Duration
284    pub ddi_dir_scanner_port_duration: Vec<(u16, u32)>,
285    // DDI_Dir_Scanner_Port_<u16>_Pass_Timeout
286    pub ddi_dir_scanner_port_pass_timeout: Vec<(u16, Timestamp)>,
287    /// A map to hold any other host properties not explicitly parsed into
288    /// other fields. The key is the tag's `name` attribute.
289    // "MSXX-XXX" | "pd-XXXXXX-X", | "SecurityControls-X"
290    // "TAG" -> "<32 hex digits>"
291    pub others: HashMap<&'input str, Vec<Cow<'input, str>>>,
292}
293
294impl<'input> HostProperties<'input> {
295    #[expect(clippy::too_many_lines)]
296    fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
297        let mut host_ip = None;
298        let mut host_start = None;
299        let mut host_start_timestamp = None;
300        let mut host_end = None;
301        let mut host_end_timestamp = None;
302        let mut apache_sites = None;
303        let mut bios_uuid = None;
304        let mut credentialed_scan = None;
305        let mut ddi_dir_scanner_global_duration = None;
306        let mut ddi_dir_scanner_global_init = None;
307        let mut dead_host = None;
308        let mut host_ad_config = None;
309        let mut host_fqdn = None;
310        let mut host_fqdns = None;
311        let mut host_rdns = None;
312        let mut hostname = None;
313        let mut ignore_printer = None;
314        let mut iis_sites = None;
315        let mut last_authenticated_results = None;
316        let mut last_unauthenticated_results = None;
317        let mut local_checks_proto = None;
318        let mut netbios_name = None;
319        let mut operating_system = None;
320        let mut operating_system_conf = None;
321        let mut operating_system_method = None;
322        let mut operating_system_unsupported = None;
323        let mut os = None;
324        let mut patch_summary_total_cves = None;
325        let mut policy_used = None;
326        let mut rexec_login_used = None;
327        let mut rlogin_login_used = None;
328        let mut rsh_login_used = None;
329        let mut smb_login_used = None;
330        let mut ssh_login_used = None;
331        let mut telnet_login_used = None;
332        let mut sinfp_ml_prediction = None;
333        let mut sinfp_signature = None;
334        let mut ssh_fingerprint = None;
335        let mut system_type = None;
336        let mut wmi_domain = None;
337
338        let mut cpe = vec![];
339        let mut traceroute = vec![];
340        let mut mac_address = vec![];
341        let mut netstat_listen = vec![];
342        let mut netstat_established = vec![];
343        let mut patch_summary_txt = vec![];
344        let mut enumerated_ports = vec![];
345        let mut patch_summary_cve_num = vec![];
346        let mut patch_summary_cves = vec![];
347        let mut ddi_dir_scanner_port_init = vec![];
348        let mut ddi_dir_scanner_port_pass_start = vec![];
349        let mut ddi_dir_scanner_port_duration = vec![];
350        let mut ddi_dir_scanner_port_pass_timeout = vec![];
351
352        let mut others: HashMap<_, Vec<_>> = HashMap::new();
353
354        for child in node.children() {
355            if child.tag_name().name() != "tag" {
356                assert_empty_text(child)?;
357                continue;
358            }
359
360            let (name, Some(value)) = get_tag_name_value(child)? else {
361                continue;
362            };
363
364            match name {
365                "host-ip" => parse_value(&mut host_ip, "host-ip", value)?,
366                "Credentialed_Scan" => {
367                    parse_value(&mut credentialed_scan, "Credentialed_Scan", value)?;
368                }
369                "DDI_Dir_Scanner_Global_Duration" => parse_value(
370                    &mut ddi_dir_scanner_global_duration,
371                    "DDI_Dir_Scanner_Global_Duration",
372                    value,
373                )?,
374                "operating-system-conf" => {
375                    parse_value(&mut operating_system_conf, "operating-system-conf", value)?;
376                }
377                "operating-system-unsupported" => parse_value(
378                    &mut operating_system_unsupported,
379                    "operating-system-unsupported",
380                    value,
381                )?,
382                "patch-summary-total-cves" => parse_value(
383                    &mut patch_summary_total_cves,
384                    "patch-summary-total-cves",
385                    value,
386                )?,
387
388                "HOST_START" => str_value(&mut host_start, "HOST_START", value)?,
389                "HOST_END" => str_value(&mut host_end, "HOST_END", value)?,
390                "bios-uuid" => str_value(&mut bios_uuid, "bios-uuid", value)?,
391                "hostname" => str_value(&mut hostname, "hostname", value)?,
392                "local-checks-proto" => {
393                    str_value(&mut local_checks_proto, "local-checks-proto", value)?;
394                }
395                "operating-system" => str_value(&mut operating_system, "operating-system", value)?,
396                "operating-system-method" => str_value(
397                    &mut operating_system_method,
398                    "operating-system-method",
399                    value,
400                )?,
401                "os" => str_value(&mut os, "os", value)?,
402                "rexec-login-used" => str_value(&mut rexec_login_used, "rexec-login-used", value)?,
403                "rlogin-login-used" => {
404                    str_value(&mut rlogin_login_used, "rlogin-login-used", value)?;
405                }
406                "rsh-login-used" => str_value(&mut rsh_login_used, "rsh-login-used", value)?,
407                "smb-login-used" => str_value(&mut smb_login_used, "smb-login-used", value)?,
408                "ssh-login-used" => str_value(&mut ssh_login_used, "ssh-login-used", value)?,
409                "telnet-login-used" => {
410                    str_value(&mut telnet_login_used, "telnet-login-used", value)?;
411                }
412                "sinfp-signature" => str_value(&mut sinfp_signature, "sinfp-signature", value)?,
413                "system-type" => str_value(&mut system_type, "system-type", value)?,
414                "wmi-domain" => str_value(&mut wmi_domain, "wmi-domain", value)?,
415
416                "Apache_sites" => cow_value(&mut apache_sites, "Apache_sites", value)?,
417                "host-ad-config" => cow_value(&mut host_ad_config, "host-ad-config", value)?,
418                "host-fqdn" => cow_value(&mut host_fqdn, "host-fqdn", value)?,
419                "host-fqdns" => cow_value(&mut host_fqdns, "host-fqdns", value)?,
420                "host-rdns" => cow_value(&mut host_rdns, "host-rdns", value)?,
421                "IIS_sites" => cow_value(&mut iis_sites, "IIS_sites", value)?,
422                "netbios-name" => cow_value(&mut netbios_name, "netbios-name", value)?,
423                "policy-used" => cow_value(&mut policy_used, "policy-used", value)?,
424                "sinfp-ml-prediction" => {
425                    cow_value(&mut sinfp_ml_prediction, "sinfp-ml-prediction", value)?;
426                }
427                "ssh-fingerprint" => cow_value(&mut ssh_fingerprint, "ssh-fingerprint", value)?,
428
429                "HOST_START_TIMESTAMP" => {
430                    if host_start_timestamp.is_some() {
431                        return Err(FormatError::RepeatedTag("HOST_START_TIMESTAMP"));
432                    }
433                    host_start_timestamp = Some(Timestamp::from_second(value.parse::<i64>()?)?);
434                }
435                "HOST_END_TIMESTAMP" => {
436                    if host_end_timestamp.is_some() {
437                        return Err(FormatError::RepeatedTag("HOST_END_TIMESTAMP"));
438                    }
439                    host_end_timestamp = Some(Timestamp::from_second(value.parse::<i64>()?)?);
440                }
441                "DDI_Dir_Scanner_Global_Init" => {
442                    if ddi_dir_scanner_global_init.is_some() {
443                        return Err(FormatError::RepeatedTag("DDI_Dir_Scanner_Global_Init"));
444                    }
445                    ddi_dir_scanner_global_init =
446                        Some(Timestamp::from_second(value.parse::<i64>()?)?);
447                }
448                "LastAuthenticatedResults" => {
449                    if last_authenticated_results.is_some() {
450                        return Err(FormatError::RepeatedTag("LastAuthenticatedResults"));
451                    }
452                    last_authenticated_results =
453                        Some(Timestamp::from_second(value.parse::<i64>()?)?);
454                }
455                "LastUnauthenticatedResults" => {
456                    if last_unauthenticated_results.is_some() {
457                        return Err(FormatError::RepeatedTag("LastUnauthenticatedResults"));
458                    }
459                    last_unauthenticated_results =
460                        Some(Timestamp::from_second(value.parse::<i64>()?)?);
461                }
462
463                "dead_host" => {
464                    if dead_host.is_some() {
465                        return Err(FormatError::RepeatedTag("dead_host"));
466                    }
467                    dead_host = Some(value.as_str() == "1");
468                }
469                "ignore_printer" => {
470                    if ignore_printer.is_some() {
471                        return Err(FormatError::RepeatedTag("ignore_printer"));
472                    }
473                    ignore_printer = Some(value.as_str() == "1");
474                }
475
476                "mac-address" => {
477                    mac_address.reserve_exact((value.len() + 1) / 18);
478                    for mac in value.split('\n') {
479                        mac_address.push(mac.parse()?);
480                    }
481                }
482
483                "cpe" => cpe.push((0, value.to_cow())),
484
485                other_name => {
486                    if let Some(cpe_n) = other_name.strip_prefix("cpe-") {
487                        cpe.push((cpe_n.parse::<u16>()?, value.to_cow()));
488                    } else if let Some(port_and_suffix) =
489                        other_name.strip_prefix("DDI_Dir_Scanner_Port_")
490                        && let Some((port, suffix)) = port_and_suffix.split_once('_')
491                    {
492                        let port = port.parse()?;
493                        match suffix {
494                            "Duration" => {
495                                ddi_dir_scanner_port_duration.push((port, value.parse()?));
496                            }
497                            "Init" => ddi_dir_scanner_port_init
498                                .push((port, Timestamp::from_second(value.parse::<i64>()?)?)),
499                            "Pass_Start" => {
500                                ddi_dir_scanner_port_pass_start
501                                    .push((port, Timestamp::from_second(value.parse::<i64>()?)?));
502                            }
503                            "Pass_Timeout" => {
504                                ddi_dir_scanner_port_pass_timeout
505                                    .push((port, Timestamp::from_second(value.parse::<i64>()?)?));
506                            }
507                            _ => {
508                                return Err(FormatError::UnexpectedText(other_name.into()));
509                            }
510                        }
511                    } else if let Some(port_and_protocol) =
512                        other_name.strip_prefix("enumerated-ports-")
513                        && let Some((port, protocol)) = port_and_protocol.split_once('-')
514                    {
515                        enumerated_ports.push((port.parse()?, protocol.parse()?, value.to_str()?));
516                    } else if let Some(hex_str) = other_name.strip_prefix("patch-summary-cve-num-")
517                    {
518                        patch_summary_cve_num.push((hex_str, value.parse()?));
519                    } else if let Some(hex_str) = other_name.strip_prefix("patch-summary-cves-") {
520                        patch_summary_cves.push((hex_str, value.to_str()?.split(", ").collect()));
521                    } else if let Some(hex_str) = other_name.strip_prefix("patch-summary-txt-") {
522                        patch_summary_txt.push((hex_str, value.to_cow()));
523                    } else if let Some(hop_num) = other_name.strip_prefix("traceroute-hop-") {
524                        traceroute.push((hop_num.parse::<u8>()?, value.parse().ok()));
525                    } else if let Some(netstat_info) = other_name.strip_prefix("netstat-")
526                        && let Some((mode, rest)) = netstat_info.split_once('-')
527                        && let Some((protocol, num)) = rest.split_once('-')
528                    {
529                        let num = num.parse()?;
530                        let value = value.to_str()?;
531                        match mode {
532                            "listen" => netstat_listen.push((protocol, num, value)),
533                            "established" => netstat_established.push((protocol, num, value)),
534                            _ => {
535                                return Err(FormatError::UnexpectedText(other_name.into()));
536                            }
537                        }
538                    } else {
539                        others.entry(other_name).or_default().push(value.to_cow());
540                    }
541                }
542            }
543        }
544
545        cpe.sort_unstable();
546        traceroute.sort_unstable();
547        netstat_listen.sort_unstable();
548        netstat_established.sort_unstable();
549
550        let cpe = cpe.into_iter().map(|(_, cpe)| cpe).collect();
551        let traceroute = traceroute.into_iter().map(|(_, ip)| ip).collect();
552
553        Ok(Self {
554            host_ip: host_ip.ok_or(FormatError::MissingTag("host-ip"))?,
555            host_start: host_start.ok_or(FormatError::MissingTag("HOST_START"))?,
556            host_start_timestamp: host_start_timestamp
557                .ok_or(FormatError::MissingTag("HOST_START_TIMESTAMP"))?,
558            host_end,
559            host_end_timestamp,
560            apache_sites,
561            bios_uuid,
562            credentialed_scan,
563            ddi_dir_scanner_global_duration,
564            ddi_dir_scanner_global_init,
565            dead_host,
566            host_ad_config,
567            host_fqdn,
568            host_fqdns,
569            host_rdns,
570            hostname,
571            ignore_printer,
572            iis_sites,
573            last_authenticated_results,
574            last_unauthenticated_results,
575            local_checks_proto,
576            mac_address,
577            netbios_name,
578            operating_system,
579            operating_system_conf,
580            operating_system_method,
581            operating_system_unsupported,
582            os,
583            patch_summary_total_cves,
584            policy_used,
585            rexec_login_used,
586            rlogin_login_used,
587            rsh_login_used,
588            smb_login_used,
589            ssh_login_used,
590            telnet_login_used,
591            sinfp_ml_prediction,
592            sinfp_signature,
593            ssh_fingerprint,
594            system_type,
595            wmi_domain,
596            others,
597            cpe,
598            traceroute,
599            netstat_listen,
600            netstat_established,
601            patch_summary_txt,
602            enumerated_ports,
603            patch_summary_cve_num,
604            patch_summary_cves,
605            ddi_dir_scanner_port_init,
606            ddi_dir_scanner_port_pass_start,
607            ddi_dir_scanner_port_duration,
608            ddi_dir_scanner_port_pass_timeout,
609        })
610    }
611}
612
613fn parse_value<T>(
614    output: &mut Option<T>,
615    tag_name: &'static str,
616    value: &StringStorage,
617) -> Result<(), FormatError>
618where
619    T: std::str::FromStr,
620    FormatError: From<T::Err>,
621{
622    if output.is_some() {
623        return Err(FormatError::RepeatedTag(tag_name));
624    }
625    *output = Some(value.parse()?);
626
627    Ok(())
628}
629
630fn str_value<'input>(
631    output: &mut Option<&'input str>,
632    tag_name: &'static str,
633    value: &StringStorage<'input>,
634) -> Result<(), FormatError> {
635    if output.is_some() {
636        return Err(FormatError::RepeatedTag(tag_name));
637    }
638    *output = Some(value.to_str()?);
639
640    Ok(())
641}
642
643fn cow_value<'input>(
644    output: &mut Option<Cow<'input, str>>,
645    tag_name: &'static str,
646    value: &StringStorage<'input>,
647) -> Result<(), FormatError> {
648    if output.is_some() {
649        return Err(FormatError::RepeatedTag(tag_name));
650    }
651    *output = Some(value.to_cow());
652
653    Ok(())
654}
655
656fn get_tag_name_value<'input, 'a>(
657    child: Node<'a, 'input>,
658) -> Result<(&'input str, Option<&'a StringStorage<'input>>), FormatError> {
659    let name = child
660        .attributes()
661        .find(|a| a.name() == "name")
662        .ok_or(FormatError::MissingAttribute("name"))?
663        .value_storage()
664        .to_str()?;
665
666    let value = child.text_storage();
667
668    Ok((name, value))
669}