nessus_parser/
lib.rs

1pub mod error;
2pub mod ping;
3pub mod policy;
4pub mod report;
5
6use std::{borrow::Cow, str::FromStr};
7
8use roxmltree::{Document, StringStorage};
9
10use crate::{error::FormatError, policy::Policy, report::Report};
11
12// TODO: Add unit tests
13// TODO: Add example usage
14
15/// Represents the root of a Nessus v2 XML report, corresponding to the
16/// `<NessusClientData_v2>` element.
17///
18/// This is the main entry point for a parsed `.nessus` file. It contains the
19/// scan policy that was used and the report itself, which holds the results
20/// of the scan.
21#[derive(Debug)]
22pub struct NessusClientDataV2<'input> {
23    /// The policy configuration used for the Nessus scan.
24    pub policy: Policy<'input>,
25    /// The results of the scan, containing all discovered hosts and their
26    /// vulnerabilities.
27    pub report: Option<Report<'input>>,
28}
29
30impl<'input> NessusClientDataV2<'input> {
31    /// Parses a string containing a `.nessus` (v2) XML report.
32    ///
33    /// This function is the main entry point for the parser. It takes the entire
34    /// XML content of a `.nessus` file as a string slice and attempts to parse
35    /// it into a structured `NessusClientDataV2` object.
36    ///
37    /// # Errors
38    ///
39    /// Returns a `FormatError` if the input string is not a valid Nessus v2
40    /// report. This can happen for several reasons, including:
41    /// - The XML is malformed.
42    /// - The root element is not `<NessusClientData_v2>`.
43    /// - Required elements or attributes (e.g., `<Policy>`) are missing.
44    /// - Elements that should be unique appear multiple times.
45    /// - Data cannot be converted to the expected type (e.g., a non-integer
46    ///   value for a port number).
47    pub fn parse(xml: &'input str) -> Result<Self, FormatError> {
48        let doc = Document::parse(xml)?;
49
50        let root = doc.root_element();
51
52        if root.tag_name().name() != "NessusClientData_v2" {
53            return Err(FormatError::UnsupportedVersion);
54        }
55
56        let mut policy = None;
57        let mut report = None;
58
59        for child in root.children() {
60            match child.tag_name().name() {
61                "Policy" => {
62                    if policy.is_some() {
63                        return Err(FormatError::RepeatedTag("Policy"));
64                    }
65                    policy = Some(Policy::from_xml_node(child)?);
66                }
67                "Report" => {
68                    if report.is_some() {
69                        return Err(FormatError::RepeatedTag("Report"));
70                    }
71                    report = Some(Report::from_xml_node(child)?);
72                }
73                _ => assert_empty_text(child)?,
74            }
75        }
76
77        let policy = policy.ok_or(FormatError::MissingTag("Policy"))?;
78
79        Ok(Self { policy, report })
80    }
81}
82
83fn assert_empty_text(node: roxmltree::Node<'_, '_>) -> Result<(), FormatError> {
84    let Some(text) = node.text() else {
85        return Err(FormatError::UnexpectedNodeKind);
86    };
87
88    if !text.trim().is_empty() {
89        return Err(FormatError::UnexpectedNode(
90            format!("{}: {text}", node.tag_name().name()).into_boxed_str(),
91        ));
92    }
93
94    Ok(())
95}
96
97trait StringStorageExt<'input> {
98    fn to_str(&self) -> Result<&'input str, FormatError>;
99    fn to_cow(&self) -> Cow<'input, str>;
100}
101
102impl<'input> StringStorageExt<'input> for StringStorage<'input> {
103    fn to_str(&self) -> Result<&'input str, FormatError> {
104        match self {
105            StringStorage::Borrowed(s) => Ok(s),
106            StringStorage::Owned(s) => Err(FormatError::UnexpectedXmlAttribute(s.as_ref().into())),
107        }
108    }
109
110    fn to_cow(&self) -> Cow<'input, str> {
111        match self {
112            StringStorage::Borrowed(s) => Cow::Borrowed(s),
113            StringStorage::Owned(s) => Cow::Owned(String::from(s.as_ref())),
114        }
115    }
116}
117
118/// A utility struct for representing a standard 6-byte MAC address.
119///
120/// It provides functionality for parsing from the common colon-separated
121/// hexadecimal format (e.g., "00:1A:2B:3C:4D:5E") and for displaying
122/// in the same format.
123#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
124pub struct MacAddress {
125    bytes: [u8; 6],
126}
127
128impl MacAddress {
129    #[must_use]
130    pub const fn bytes(self) -> [u8; 6] {
131        self.bytes
132    }
133}
134
135impl FromStr for MacAddress {
136    type Err = FormatError;
137
138    fn from_str(s: &str) -> Result<Self, Self::Err> {
139        let mut octets = s.split(':');
140
141        let mac_address = Self {
142            bytes: [
143                parse_octet(octets.next().ok_or(FormatError::MacAdressParse)?)?,
144                parse_octet(octets.next().ok_or(FormatError::MacAdressParse)?)?,
145                parse_octet(octets.next().ok_or(FormatError::MacAdressParse)?)?,
146                parse_octet(octets.next().ok_or(FormatError::MacAdressParse)?)?,
147                parse_octet(octets.next().ok_or(FormatError::MacAdressParse)?)?,
148                parse_octet(octets.next().ok_or(FormatError::MacAdressParse)?)?,
149            ],
150        };
151
152        if octets.next().is_some() {
153            Err(FormatError::MacAdressParse)
154        } else {
155            Ok(mac_address)
156        }
157    }
158}
159
160fn parse_octet(input: &str) -> Result<u8, FormatError> {
161    let &[a, b] = input.as_bytes() else {
162        return Err(FormatError::MacAdressParse);
163    };
164
165    Ok((parse_hex_digit(a)? << 4) | parse_hex_digit(b)?)
166}
167
168const fn parse_hex_digit(ch: u8) -> Result<u8, FormatError> {
169    match ch {
170        b'0'..=b'9' => Ok(ch - b'0'),
171        b'A'..=b'F' => Ok((ch - b'A') + 10),
172        b'a'..=b'f' => Ok((ch - b'a') + 10),
173        _ => Err(FormatError::MacAdressParse),
174    }
175}
176
177impl std::fmt::Display for MacAddress {
178    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
179        let _ = write!(
180            f,
181            "{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}",
182            self.bytes[0],
183            self.bytes[1],
184            self.bytes[2],
185            self.bytes[3],
186            self.bytes[4],
187            self.bytes[5]
188        );
189
190        Ok(())
191    }
192}
193
194impl std::fmt::Debug for MacAddress {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        write!(f, "\"{self}\"")
197    }
198}