nessus-parser 0.3.0

A parser for `.nessus` (v2) XML reports
Documentation
use std::{fs, path::PathBuf, str::FromStr};

use nessus_parser::{MacAddress, NessusClientDataV2, error::FormatError};

fn fixture_path(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join(name)
}

#[test]
fn parses_anonymized_fixtures() {
    let fixtures = [
        "anonymized_minimal_alive.nessus",
        "anonymized_minimal_dead.nessus",
    ];

    for fixture in fixtures {
        let xml = fs::read_to_string(fixture_path(fixture)).expect("fixture should be readable");
        let parsed = NessusClientDataV2::parse(&xml)
            .unwrap_or_else(|err| panic!("failed parsing fixture {fixture}: {err}"));

        assert!(!parsed.policy.policy_name.trim().is_empty());

        let report = parsed
            .report
            .as_ref()
            .expect("fixtures should include a report");
        assert!(
            !report.hosts.is_empty(),
            "fixture {fixture} should have hosts"
        );

        for host in &report.hosts {
            assert!(
                !host.items.is_empty(),
                "fixture {fixture} has host without items"
            );
            assert!(!host.properties.host_start.trim().is_empty());
            assert!(host.properties.host_start_timestamp.as_second() > 0);
            assert!(
                host.scanner_ip.is_some(),
                "fixture {fixture} should include scanner_ip"
            );
            assert!(
                host.ping_outcome.is_some(),
                "fixture {fixture} should include ping outcome"
            );
        }
    }
}

#[test]
fn parses_structural_edge_case_fixtures() {
    let fixtures = [
        "anonymized_report_namespace_with_host.nessus",
        "anonymized_empty_report.nessus",
        "anonymized_attachment_and_rich_fields.nessus",
    ];

    for fixture in fixtures {
        let xml = fs::read_to_string(fixture_path(fixture)).expect("fixture should be readable");
        let parsed = NessusClientDataV2::parse(&xml)
            .unwrap_or_else(|err| panic!("failed parsing fixture {fixture}: {err}"));

        assert!(!parsed.policy.policy_name.trim().is_empty());
        assert!(
            parsed.report.is_some(),
            "fixture {fixture} should include a report"
        );
    }

    let empty_xml = fs::read_to_string(fixture_path("anonymized_empty_report.nessus"))
        .expect("fixture should be readable");
    let empty_parsed = NessusClientDataV2::parse(&empty_xml).expect("fixture should parse");
    let empty_report = empty_parsed.report.as_ref().expect("report should exist");
    assert!(
        empty_report.hosts.is_empty(),
        "empty report fixture should have no hosts"
    );

    let rich_xml = fs::read_to_string(fixture_path("anonymized_attachment_and_rich_fields.nessus"))
        .expect("fixture should be readable");
    let rich_parsed = NessusClientDataV2::parse(&rich_xml).expect("fixture should parse");
    let rich_report = rich_parsed.report.as_ref().expect("report should exist");
    let rich_host = rich_report
        .hosts
        .first()
        .expect("rich fixture should have one host");
    let rich_item = rich_host
        .items
        .first()
        .expect("rich fixture should have one report item");

    assert!(
        rich_item.others.contains_key("attachment"),
        "rich fixture should include attachment field"
    );
    assert!(
        rich_item.others.contains_key("synopsis"),
        "rich fixture should include synopsis field"
    );
    assert!(
        rich_item.others.contains_key("see_also"),
        "rich fixture should include see_also field"
    );
    assert!(
        rich_item.others.contains_key("cve"),
        "rich fixture should include cve field"
    );
}

#[test]
fn parse_rejects_non_nessus_root() {
    let xml = r#"<?xml version="1.0"?><not_nessus/>"#;
    let err = NessusClientDataV2::parse(xml).expect_err("must reject unsupported root");
    assert!(matches!(err, FormatError::UnsupportedVersion));
}

#[test]
fn parse_rejects_missing_policy_tag() {
    let xml = r#"<NessusClientData_v2>
  <Report name="Anonymized"></Report>
</NessusClientData_v2>"#;

    let err = NessusClientDataV2::parse(xml).expect_err("missing Policy should fail");
    assert!(matches!(err, FormatError::MissingTag("Policy")));
}

#[test]
fn mac_address_parsing_and_display_is_stable() {
    let mac = MacAddress::from_str("0a:1B:2c:3D:4e:5f").expect("valid MAC should parse");
    assert_eq!(mac.bytes(), [10, 27, 44, 61, 78, 95]);
    assert_eq!(mac.to_string(), "0A:1B:2C:3D:4E:5F");
}