use std::{borrow::Cow, collections::HashMap, net::IpAddr};
use jiff::Timestamp;
use roxmltree::{Node, StringStorage};
use crate::{
MacAddress, StringStorageExt, assert_empty_text,
error::FormatError,
ping::PingOutcome,
report::item::{Item, PluginType, Protocol},
};
pub mod item;
#[derive(Debug)]
pub struct Report<'input> {
pub name: Cow<'input, str>,
pub hosts: Vec<Host<'input>>,
}
impl<'input> Report<'input> {
pub(crate) fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
let name = node
.attributes()
.find(|a| a.name() == "name")
.ok_or(FormatError::MissingAttribute("name"))?
.value_storage()
.to_cow();
let mut hosts = vec![];
for child in node.children() {
match child.tag_name().name() {
"ReportHost" => {
hosts.push(Host::from_xml_node(child)?);
}
_ => assert_empty_text(child)?,
}
}
Ok(Self { name, hosts })
}
}
#[derive(Debug)]
pub struct Host<'input> {
pub name: Cow<'input, str>,
pub properties: HostProperties<'input>,
pub items: Vec<Item<'input>>,
pub ping_outcome: Option<PingOutcome>,
pub scanner_ip: Option<IpAddr>,
pub open_ports: Vec<(u16, Protocol)>,
}
impl<'input> Host<'input> {
fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
const PING_THE_REMOTE_HOST_ID: u32 = 10180;
const NESSUS_SCAN_INFORMATION_ID: u32 = 19506;
let name = node
.attributes()
.find(|a| a.name() == "name")
.ok_or(FormatError::MissingAttribute("name"))?
.value_storage()
.to_cow();
let mut host_properties = None;
let mut items = vec![];
let mut ping_outcome = None;
let mut scanner_ip = None;
let mut open_ports = vec![];
for child in node.children() {
match child.tag_name().name() {
"HostProperties" => {
if host_properties.is_some() {
return Err(FormatError::RepeatedTag("HostProperties"));
}
host_properties = Some(HostProperties::from_xml_node(child)?);
}
"ReportItem" => {
let item = Item::from_xml_node(child)?;
match item.plugin_id {
PING_THE_REMOTE_HOST_ID => {
let plugin_output = item
.plugin_output
.as_ref()
.ok_or(FormatError::MissingPluginOutput)?;
if ping_outcome.is_some() {
return Err(FormatError::RepeatedTag("Ping the remote host"));
}
ping_outcome = Some(PingOutcome::from_plugin_output(plugin_output)?);
}
NESSUS_SCAN_INFORMATION_ID => {
let plugin_output = item
.plugin_output
.as_ref()
.ok_or(FormatError::MissingPluginOutput)?;
if scanner_ip.is_some() {
return Err(FormatError::RepeatedTag("Nessus Scan Information"));
}
scanner_ip = Some(parse_scanner_ip(plugin_output)?);
}
10736 | 11111 | 14272 | 14274 => {}
_ => {
if item.port != 0 && item.plugin_type != PluginType::Local {
open_ports.push((item.port, item.protocol));
}
}
}
items.push(item);
}
_ => assert_empty_text(child)?,
}
}
open_ports.sort_unstable();
open_ports.dedup();
Ok(Self {
name,
properties: host_properties.ok_or(FormatError::MissingTag("HostProperties"))?,
items,
ping_outcome,
scanner_ip,
open_ports,
})
}
}
fn parse_scanner_ip(plugin_output: &str) -> Result<IpAddr, FormatError> {
let (_, rest) = plugin_output
.split_once("Scanner IP : ")
.ok_or(FormatError::MissingAttribute("Scanner IP"))?;
let (ip, _) = rest
.split_once('\n')
.ok_or(FormatError::MissingAttribute("Scanner IP"))?;
Ok(ip.parse()?)
}
#[derive(Debug)]
pub struct HostProperties<'input> {
pub host_ip: IpAddr,
pub host_start: &'input str,
pub host_start_timestamp: Timestamp,
pub host_end: Option<&'input str>,
pub host_end_timestamp: Option<Timestamp>,
pub apache_sites: Option<Cow<'input, str>>,
pub bios_uuid: Option<&'input str>,
pub credentialed_scan: Option<bool>,
pub ddi_dir_scanner_global_duration: Option<u32>,
pub ddi_dir_scanner_global_init: Option<Timestamp>,
pub dead_host: Option<bool>,
pub host_ad_config: Option<Cow<'input, str>>,
pub host_fqdn: Option<Cow<'input, str>>,
pub host_fqdns: Option<Cow<'input, str>>,
pub host_rdns: Option<Cow<'input, str>>,
pub hostname: Option<&'input str>,
pub ignore_printer: Option<bool>,
pub iis_sites: Option<Cow<'input, str>>,
pub last_authenticated_results: Option<Timestamp>,
pub last_unauthenticated_results: Option<Timestamp>,
pub local_checks_proto: Option<&'input str>,
pub netbios_name: Option<Cow<'input, str>>,
pub operating_system: Option<&'input str>,
pub operating_system_conf: Option<i32>,
pub operating_system_method: Option<&'input str>,
pub operating_system_unsupported: Option<bool>,
pub os: Option<&'input str>,
pub patch_summary_total_cves: Option<u32>,
pub policy_used: Option<Cow<'input, str>>,
pub rexec_login_used: Option<&'input str>,
pub rlogin_login_used: Option<&'input str>,
pub rsh_login_used: Option<&'input str>,
pub smb_login_used: Option<&'input str>,
pub ssh_login_used: Option<&'input str>,
pub telnet_login_used: Option<&'input str>,
pub sinfp_ml_prediction: Option<Cow<'input, str>>,
pub sinfp_signature: Option<&'input str>,
pub ssh_fingerprint: Option<Cow<'input, str>>,
pub system_type: Option<&'input str>,
pub wmi_domain: Option<&'input str>,
pub mac_address: Vec<MacAddress>,
pub cpe: Vec<Cow<'input, str>>,
pub traceroute: Vec<Option<IpAddr>>,
pub netstat_listen: Vec<(&'input str, u16, &'input str)>,
pub netstat_established: Vec<(&'input str, u16, &'input str)>,
pub patch_summary_txt: Vec<(&'input str, Cow<'input, str>)>,
pub enumerated_ports: Vec<(u16, Protocol, &'input str)>,
pub patch_summary_cve_num: Vec<(&'input str, u32)>,
pub patch_summary_cves: Vec<(&'input str, Vec<&'input str>)>,
pub ddi_dir_scanner_port_init: Vec<(u16, Timestamp)>,
pub ddi_dir_scanner_port_pass_start: Vec<(u16, Timestamp)>,
pub ddi_dir_scanner_port_duration: Vec<(u16, u32)>,
pub ddi_dir_scanner_port_pass_timeout: Vec<(u16, Timestamp)>,
pub others: HashMap<&'input str, Vec<Cow<'input, str>>>,
}
impl<'input> HostProperties<'input> {
#[expect(clippy::too_many_lines)]
fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
let mut host_ip = None;
let mut host_start = None;
let mut host_start_timestamp = None;
let mut host_end = None;
let mut host_end_timestamp = None;
let mut apache_sites = None;
let mut bios_uuid = None;
let mut credentialed_scan = None;
let mut ddi_dir_scanner_global_duration = None;
let mut ddi_dir_scanner_global_init = None;
let mut dead_host = None;
let mut host_ad_config = None;
let mut host_fqdn = None;
let mut host_fqdns = None;
let mut host_rdns = None;
let mut hostname = None;
let mut ignore_printer = None;
let mut iis_sites = None;
let mut last_authenticated_results = None;
let mut last_unauthenticated_results = None;
let mut local_checks_proto = None;
let mut netbios_name = None;
let mut operating_system = None;
let mut operating_system_conf = None;
let mut operating_system_method = None;
let mut operating_system_unsupported = None;
let mut os = None;
let mut patch_summary_total_cves = None;
let mut policy_used = None;
let mut rexec_login_used = None;
let mut rlogin_login_used = None;
let mut rsh_login_used = None;
let mut smb_login_used = None;
let mut ssh_login_used = None;
let mut telnet_login_used = None;
let mut sinfp_ml_prediction = None;
let mut sinfp_signature = None;
let mut ssh_fingerprint = None;
let mut system_type = None;
let mut wmi_domain = None;
let mut cpe = vec![];
let mut traceroute = vec![];
let mut mac_address = vec![];
let mut netstat_listen = vec![];
let mut netstat_established = vec![];
let mut patch_summary_txt = vec![];
let mut enumerated_ports = vec![];
let mut patch_summary_cve_num = vec![];
let mut patch_summary_cves = vec![];
let mut ddi_dir_scanner_port_init = vec![];
let mut ddi_dir_scanner_port_pass_start = vec![];
let mut ddi_dir_scanner_port_duration = vec![];
let mut ddi_dir_scanner_port_pass_timeout = vec![];
let mut others: HashMap<_, Vec<_>> = HashMap::new();
for child in node.children() {
if child.tag_name().name() != "tag" {
assert_empty_text(child)?;
continue;
}
let (name, Some(value)) = get_tag_name_value(child)? else {
continue;
};
match name {
"host-ip" => parse_value(&mut host_ip, "host-ip", value)?,
"Credentialed_Scan" => {
parse_value(&mut credentialed_scan, "Credentialed_Scan", value)?;
}
"DDI_Dir_Scanner_Global_Duration" => parse_value(
&mut ddi_dir_scanner_global_duration,
"DDI_Dir_Scanner_Global_Duration",
value,
)?,
"operating-system-conf" => {
parse_value(&mut operating_system_conf, "operating-system-conf", value)?;
}
"operating-system-unsupported" => parse_value(
&mut operating_system_unsupported,
"operating-system-unsupported",
value,
)?,
"patch-summary-total-cves" => parse_value(
&mut patch_summary_total_cves,
"patch-summary-total-cves",
value,
)?,
"HOST_START" => str_value(&mut host_start, "HOST_START", value)?,
"HOST_END" => str_value(&mut host_end, "HOST_END", value)?,
"bios-uuid" => str_value(&mut bios_uuid, "bios-uuid", value)?,
"hostname" => str_value(&mut hostname, "hostname", value)?,
"local-checks-proto" => {
str_value(&mut local_checks_proto, "local-checks-proto", value)?;
}
"operating-system" => str_value(&mut operating_system, "operating-system", value)?,
"operating-system-method" => str_value(
&mut operating_system_method,
"operating-system-method",
value,
)?,
"os" => str_value(&mut os, "os", value)?,
"rexec-login-used" => str_value(&mut rexec_login_used, "rexec-login-used", value)?,
"rlogin-login-used" => {
str_value(&mut rlogin_login_used, "rlogin-login-used", value)?;
}
"rsh-login-used" => str_value(&mut rsh_login_used, "rsh-login-used", value)?,
"smb-login-used" => str_value(&mut smb_login_used, "smb-login-used", value)?,
"ssh-login-used" => str_value(&mut ssh_login_used, "ssh-login-used", value)?,
"telnet-login-used" => {
str_value(&mut telnet_login_used, "telnet-login-used", value)?;
}
"sinfp-signature" => str_value(&mut sinfp_signature, "sinfp-signature", value)?,
"system-type" => str_value(&mut system_type, "system-type", value)?,
"wmi-domain" => str_value(&mut wmi_domain, "wmi-domain", value)?,
"Apache_sites" => cow_value(&mut apache_sites, "Apache_sites", value)?,
"host-ad-config" => cow_value(&mut host_ad_config, "host-ad-config", value)?,
"host-fqdn" => cow_value(&mut host_fqdn, "host-fqdn", value)?,
"host-fqdns" => cow_value(&mut host_fqdns, "host-fqdns", value)?,
"host-rdns" => cow_value(&mut host_rdns, "host-rdns", value)?,
"IIS_sites" => cow_value(&mut iis_sites, "IIS_sites", value)?,
"netbios-name" => cow_value(&mut netbios_name, "netbios-name", value)?,
"policy-used" => cow_value(&mut policy_used, "policy-used", value)?,
"sinfp-ml-prediction" => {
cow_value(&mut sinfp_ml_prediction, "sinfp-ml-prediction", value)?;
}
"ssh-fingerprint" => cow_value(&mut ssh_fingerprint, "ssh-fingerprint", value)?,
"HOST_START_TIMESTAMP" => {
if host_start_timestamp.is_some() {
return Err(FormatError::RepeatedTag("HOST_START_TIMESTAMP"));
}
host_start_timestamp = Some(Timestamp::from_second(value.parse::<i64>()?)?);
}
"HOST_END_TIMESTAMP" => {
if host_end_timestamp.is_some() {
return Err(FormatError::RepeatedTag("HOST_END_TIMESTAMP"));
}
host_end_timestamp = Some(Timestamp::from_second(value.parse::<i64>()?)?);
}
"DDI_Dir_Scanner_Global_Init" => {
if ddi_dir_scanner_global_init.is_some() {
return Err(FormatError::RepeatedTag("DDI_Dir_Scanner_Global_Init"));
}
ddi_dir_scanner_global_init =
Some(Timestamp::from_second(value.parse::<i64>()?)?);
}
"LastAuthenticatedResults" => {
if last_authenticated_results.is_some() {
return Err(FormatError::RepeatedTag("LastAuthenticatedResults"));
}
last_authenticated_results =
Some(Timestamp::from_second(value.parse::<i64>()?)?);
}
"LastUnauthenticatedResults" => {
if last_unauthenticated_results.is_some() {
return Err(FormatError::RepeatedTag("LastUnauthenticatedResults"));
}
last_unauthenticated_results =
Some(Timestamp::from_second(value.parse::<i64>()?)?);
}
"dead_host" => {
if dead_host.is_some() {
return Err(FormatError::RepeatedTag("dead_host"));
}
dead_host = Some(value.as_str() == "1");
}
"ignore_printer" => {
if ignore_printer.is_some() {
return Err(FormatError::RepeatedTag("ignore_printer"));
}
ignore_printer = Some(value.as_str() == "1");
}
"mac-address" => {
mac_address.reserve_exact((value.len() + 1) / 18);
for mac in value.split('\n') {
mac_address.push(mac.parse()?);
}
}
"cpe" => cpe.push((0, value.to_cow())),
other_name => {
if let Some(cpe_n) = other_name.strip_prefix("cpe-") {
cpe.push((cpe_n.parse::<u16>()?, value.to_cow()));
} else if let Some(port_and_suffix) =
other_name.strip_prefix("DDI_Dir_Scanner_Port_")
&& let Some((port, suffix)) = port_and_suffix.split_once('_')
{
let port = port.parse()?;
match suffix {
"Duration" => {
ddi_dir_scanner_port_duration.push((port, value.parse()?));
}
"Init" => ddi_dir_scanner_port_init
.push((port, Timestamp::from_second(value.parse::<i64>()?)?)),
"Pass_Start" => {
ddi_dir_scanner_port_pass_start
.push((port, Timestamp::from_second(value.parse::<i64>()?)?));
}
"Pass_Timeout" => {
ddi_dir_scanner_port_pass_timeout
.push((port, Timestamp::from_second(value.parse::<i64>()?)?));
}
_ => {
return Err(FormatError::UnexpectedText(other_name.into()));
}
}
} else if let Some(port_and_protocol) =
other_name.strip_prefix("enumerated-ports-")
&& let Some((port, protocol)) = port_and_protocol.split_once('-')
{
enumerated_ports.push((port.parse()?, protocol.parse()?, value.to_str()?));
} else if let Some(hex_str) = other_name.strip_prefix("patch-summary-cve-num-")
{
patch_summary_cve_num.push((hex_str, value.parse()?));
} else if let Some(hex_str) = other_name.strip_prefix("patch-summary-cves-") {
patch_summary_cves.push((hex_str, value.to_str()?.split(", ").collect()));
} else if let Some(hex_str) = other_name.strip_prefix("patch-summary-txt-") {
patch_summary_txt.push((hex_str, value.to_cow()));
} else if let Some(hop_num) = other_name.strip_prefix("traceroute-hop-") {
traceroute.push((hop_num.parse::<u8>()?, value.parse().ok()));
} else if let Some(netstat_info) = other_name.strip_prefix("netstat-")
&& let Some((mode, rest)) = netstat_info.split_once('-')
&& let Some((protocol, num)) = rest.split_once('-')
{
let num = num.parse()?;
let value = value.to_str()?;
match mode {
"listen" => netstat_listen.push((protocol, num, value)),
"established" => netstat_established.push((protocol, num, value)),
_ => {
return Err(FormatError::UnexpectedText(other_name.into()));
}
}
} else {
others.entry(other_name).or_default().push(value.to_cow());
}
}
}
}
cpe.sort_unstable();
traceroute.sort_unstable();
netstat_listen.sort_unstable();
netstat_established.sort_unstable();
let cpe = cpe.into_iter().map(|(_, cpe)| cpe).collect();
let traceroute = traceroute.into_iter().map(|(_, ip)| ip).collect();
Ok(Self {
host_ip: host_ip.ok_or(FormatError::MissingTag("host-ip"))?,
host_start: host_start.ok_or(FormatError::MissingTag("HOST_START"))?,
host_start_timestamp: host_start_timestamp
.ok_or(FormatError::MissingTag("HOST_START_TIMESTAMP"))?,
host_end,
host_end_timestamp,
apache_sites,
bios_uuid,
credentialed_scan,
ddi_dir_scanner_global_duration,
ddi_dir_scanner_global_init,
dead_host,
host_ad_config,
host_fqdn,
host_fqdns,
host_rdns,
hostname,
ignore_printer,
iis_sites,
last_authenticated_results,
last_unauthenticated_results,
local_checks_proto,
mac_address,
netbios_name,
operating_system,
operating_system_conf,
operating_system_method,
operating_system_unsupported,
os,
patch_summary_total_cves,
policy_used,
rexec_login_used,
rlogin_login_used,
rsh_login_used,
smb_login_used,
ssh_login_used,
telnet_login_used,
sinfp_ml_prediction,
sinfp_signature,
ssh_fingerprint,
system_type,
wmi_domain,
others,
cpe,
traceroute,
netstat_listen,
netstat_established,
patch_summary_txt,
enumerated_ports,
patch_summary_cve_num,
patch_summary_cves,
ddi_dir_scanner_port_init,
ddi_dir_scanner_port_pass_start,
ddi_dir_scanner_port_duration,
ddi_dir_scanner_port_pass_timeout,
})
}
}
fn parse_value<T>(
output: &mut Option<T>,
tag_name: &'static str,
value: &StringStorage,
) -> Result<(), FormatError>
where
T: std::str::FromStr,
FormatError: From<T::Err>,
{
if output.is_some() {
return Err(FormatError::RepeatedTag(tag_name));
}
*output = Some(value.parse()?);
Ok(())
}
fn str_value<'input>(
output: &mut Option<&'input str>,
tag_name: &'static str,
value: &StringStorage<'input>,
) -> Result<(), FormatError> {
if output.is_some() {
return Err(FormatError::RepeatedTag(tag_name));
}
*output = Some(value.to_str()?);
Ok(())
}
fn cow_value<'input>(
output: &mut Option<Cow<'input, str>>,
tag_name: &'static str,
value: &StringStorage<'input>,
) -> Result<(), FormatError> {
if output.is_some() {
return Err(FormatError::RepeatedTag(tag_name));
}
*output = Some(value.to_cow());
Ok(())
}
fn get_tag_name_value<'input, 'a>(
child: Node<'a, 'input>,
) -> Result<(&'input str, Option<&'a StringStorage<'input>>), FormatError> {
let name = child
.attributes()
.find(|a| a.name() == "name")
.ok_or(FormatError::MissingAttribute("name"))?
.value_storage()
.to_str()?;
let value = child.text_storage();
Ok((name, value))
}
#[cfg(test)]
mod tests {
use roxmltree::Document;
use crate::error::FormatError;
use super::{Host, item::Protocol, parse_scanner_ip};
fn minimal_report_item(plugin_id: u32, port: u16, protocol: &str, plugin_type: &str) -> String {
format!(
r#"<ReportItem pluginID="{plugin_id}" pluginName="x" port="{port}" protocol="{protocol}" svc_name="svc" severity="2" pluginFamily="General">
<solution>fix</solution>
<script_version>1.0</script_version>
<risk_factor>Medium</risk_factor>
<plugin_type>{plugin_type}</plugin_type>
<plugin_publication_date>2024/01/01</plugin_publication_date>
<plugin_modification_date>2024/01/02</plugin_modification_date>
<fname>x.nasl</fname>
<description>desc</description>
</ReportItem>"#
)
}
fn ping_item() -> String {
r#"<ReportItem pluginID="10180" pluginName="Ping the remote host" port="0" protocol="tcp" svc_name="general" severity="0" pluginFamily="General">
<solution>n/a</solution>
<script_version>1.0</script_version>
<risk_factor>None</risk_factor>
<plugin_type>summary</plugin_type>
<plugin_publication_date>2024/01/01</plugin_publication_date>
<plugin_modification_date>2024/01/02</plugin_modification_date>
<fname>ping.nasl</fname>
<description>desc</description>
<plugin_output>The remote host is up
The remote host replied to an ICMP echo packet</plugin_output>
</ReportItem>"#
.to_owned()
}
fn scanner_info_item(ip: &str) -> String {
format!(
r#"<ReportItem pluginID="19506" pluginName="Nessus Scan Information" port="0" protocol="tcp" svc_name="general" severity="0" pluginFamily="General">
<solution>n/a</solution>
<script_version>1.0</script_version>
<risk_factor>None</risk_factor>
<plugin_type>summary</plugin_type>
<plugin_publication_date>2024/01/01</plugin_publication_date>
<plugin_modification_date>2024/01/02</plugin_modification_date>
<fname>scan_info.nasl</fname>
<description>desc</description>
<plugin_output>Header
Scanner IP : {ip}
Tail</plugin_output>
</ReportItem>"#
)
}
fn host_xml(items: &[String], extra_tags: &str) -> String {
let items_xml = items.join("\n");
format!(
r#"<ReportHost name="127.0.0.1">
<HostProperties>
<tag name="host-ip">127.0.0.1</tag>
<tag name="HOST_START">start</tag>
<tag name="HOST_START_TIMESTAMP">1</tag>
{extra_tags}
</HostProperties>
{items_xml}
</ReportHost>"#
)
}
fn parse_host(xml: &str) -> Result<Host<'_>, FormatError> {
let doc = Document::parse(xml).expect("test XML should parse");
Host::from_xml_node(doc.root_element())
}
#[test]
fn parse_scanner_ip_handles_expected_and_error_paths() {
let ok = r"Information about this scan :
Nessus version : 10.8.4
Nessus build : 20028
Plugin feed version : 202507040825
Scanner edition used : Nessus
Scanner OS : LINUX
Scanner distribution : ubuntu1604-x86-64
Scan type : Normal
Scan name : anonymized scan from 192.0.2.0/24 (redacted)
Scan policy used : Host discovery ++
Scanner IP : 192.0.2.82
Port scanner(s) : nessus_syn_scanner
Port range : all
Ping RTT : 76.193 ms
Thorough tests : no
Experimental tests : no
Scan for Unpatched Vulnerabilities : no
Plugin debugging enabled : no
Paranoia level : 1
Report verbosity : 1
Safe checks : yes
Optimize the test : yes
Credentialed checks : no
Patch management checks : None
Display superseded patches : yes (supersedence plugin did not launch)
CGI scanning : disabled
Web application tests : disabled
Max hosts : 100
Max checks : 5
Recv timeout : 5
Backports : None
Allow post-scan editing : Yes
Nessus Plugin Signature Checking : Enabled
Audit File Signature Checking : Disabled
Scan Start Date : 2025/7/16 10:04 -05 (UTC -05:00)
Scan duration : 362 sec
Scan for malware : no
";
assert_eq!(
parse_scanner_ip(ok).expect("must parse").to_string(),
"192.0.2.82"
);
let missing_prefix = "Header\nNo scanner here";
assert!(matches!(
parse_scanner_ip(missing_prefix),
Err(FormatError::MissingAttribute("Scanner IP"))
));
let missing_newline = "Scanner IP : 10.0.0.1";
assert!(matches!(
parse_scanner_ip(missing_newline),
Err(FormatError::MissingAttribute("Scanner IP"))
));
let invalid_ip = "Header\nScanner IP : 999.999.999.999\nFooter";
assert!(matches!(
parse_scanner_ip(invalid_ip),
Err(FormatError::IpAddrParse(_))
));
}
#[test]
fn host_open_ports_are_filtered_sorted_and_deduped() {
let items = vec![
ping_item(),
scanner_info_item("10.0.0.2"),
minimal_report_item(123, 443, "tcp", "remote"),
minimal_report_item(124, 80, "tcp", "remote"),
minimal_report_item(124, 80, "tcp", "remote"),
minimal_report_item(125, 53, "udp", "local"),
minimal_report_item(11111, 22, "tcp", "remote"),
minimal_report_item(126, 0, "tcp", "remote"),
];
let xml = host_xml(&items, "");
let host = parse_host(&xml).expect("must parse");
assert_eq!(
host.open_ports,
vec![(80, Protocol::Tcp), (443, Protocol::Tcp)]
);
assert!(host.ping_outcome.is_some());
assert!(host.scanner_ip.is_some());
}
#[test]
fn host_allows_missing_special_plugins() {
let items = vec![
minimal_report_item(123, 443, "tcp", "remote"),
minimal_report_item(124, 53, "udp", "remote"),
];
let xml = host_xml(&items, "");
let host = parse_host(&xml).expect("must parse");
assert!(host.ping_outcome.is_none());
assert!(host.scanner_ip.is_none());
assert_eq!(
host.open_ports,
vec![(53, Protocol::Udp), (443, Protocol::Tcp)]
);
}
#[test]
fn host_rejects_repeated_special_plugins() {
let items = vec![
ping_item(),
ping_item(),
scanner_info_item("10.0.0.2"),
minimal_report_item(123, 443, "tcp", "remote"),
];
let xml = host_xml(&items, "");
let err = parse_host(&xml).expect_err("must fail");
assert!(matches!(
err,
FormatError::RepeatedTag("Ping the remote host")
));
}
#[test]
fn host_properties_bool_and_pattern_tags_parse() {
let items = vec![ping_item(), scanner_info_item("10.0.0.2")];
let extra = r#"
<tag name="dead_host">0</tag>
<tag name="ignore_printer">1</tag>
<tag name="enumerated-ports-8080-tcp">open</tag>
<tag name="traceroute-hop-1">8.8.8.8</tag>
<tag name="traceroute-hop-2">not-an-ip</tag>
<tag name="cpe-1">cpe:/o:test:os</tag>
"#;
let xml = host_xml(&items, extra);
let host = parse_host(&xml).expect("must parse");
assert_eq!(host.properties.dead_host, Some(false));
assert_eq!(host.properties.ignore_printer, Some(true));
assert_eq!(host.properties.enumerated_ports.len(), 1);
assert_eq!(host.properties.traceroute.len(), 2);
assert_eq!(host.properties.cpe.len(), 1);
}
}