1pub mod error;
47pub mod ping;
48pub mod policy;
49pub mod report;
50
51use std::{borrow::Cow, str::FromStr};
52
53use roxmltree::{Document, StringStorage};
54
55use crate::{error::FormatError, policy::Policy, report::Report};
56
57#[derive(Debug)]
64pub struct NessusClientDataV2<'input> {
65 pub policy: Policy<'input>,
67 pub report: Option<Report<'input>>,
70}
71
72impl<'input> NessusClientDataV2<'input> {
73 pub fn parse(xml: &'input str) -> Result<Self, FormatError> {
90 let doc = Document::parse(xml)?;
91
92 let root = doc.root_element();
93
94 if root.tag_name().name() != "NessusClientData_v2" {
95 return Err(FormatError::UnsupportedVersion);
96 }
97
98 let mut policy = None;
99 let mut report = None;
100
101 for child in root.children() {
102 match child.tag_name().name() {
103 "Policy" => {
104 if policy.is_some() {
105 return Err(FormatError::RepeatedTag("Policy"));
106 }
107 policy = Some(Policy::from_xml_node(child)?);
108 }
109 "Report" => {
110 if report.is_some() {
111 return Err(FormatError::RepeatedTag("Report"));
112 }
113 report = Some(Report::from_xml_node(child)?);
114 }
115 _ => assert_empty_text(child)?,
116 }
117 }
118
119 let policy = policy.ok_or(FormatError::MissingTag("Policy"))?;
120
121 Ok(Self { policy, report })
122 }
123}
124
125fn assert_empty_text(node: roxmltree::Node<'_, '_>) -> Result<(), FormatError> {
126 let Some(text) = node.text() else {
127 return Err(FormatError::UnexpectedNodeKind);
128 };
129
130 if !text.trim().is_empty() {
131 return Err(FormatError::UnexpectedNode(
132 format!("{}: {text}", node.tag_name().name()).into_boxed_str(),
133 ));
134 }
135
136 Ok(())
137}
138
139trait StringStorageExt<'input> {
140 fn to_str(&self) -> Result<&'input str, FormatError>;
141 fn to_cow(&self) -> Cow<'input, str>;
142}
143
144impl<'input> StringStorageExt<'input> for StringStorage<'input> {
145 fn to_str(&self) -> Result<&'input str, FormatError> {
146 match self {
147 StringStorage::Borrowed(s) => Ok(s),
148 StringStorage::Owned(s) => Err(FormatError::UnexpectedXmlAttribute(s.as_ref().into())),
151 }
152 }
153
154 fn to_cow(&self) -> Cow<'input, str> {
155 match self {
156 StringStorage::Borrowed(s) => Cow::Borrowed(s),
157 StringStorage::Owned(s) => Cow::Owned(String::from(s.as_ref())),
158 }
159 }
160}
161
162#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
168pub struct MacAddress {
169 bytes: [u8; 6],
170}
171
172impl MacAddress {
173 #[must_use]
175 pub const fn bytes(self) -> [u8; 6] {
176 self.bytes
177 }
178}
179
180impl FromStr for MacAddress {
181 type Err = FormatError;
182
183 fn from_str(s: &str) -> Result<Self, Self::Err> {
184 let mut octets = s.split(':');
185
186 let mac_address = Self {
187 bytes: [
188 parse_octet(octets.next().ok_or(FormatError::MacAddressParse)?)?,
189 parse_octet(octets.next().ok_or(FormatError::MacAddressParse)?)?,
190 parse_octet(octets.next().ok_or(FormatError::MacAddressParse)?)?,
191 parse_octet(octets.next().ok_or(FormatError::MacAddressParse)?)?,
192 parse_octet(octets.next().ok_or(FormatError::MacAddressParse)?)?,
193 parse_octet(octets.next().ok_or(FormatError::MacAddressParse)?)?,
194 ],
195 };
196
197 if octets.next().is_some() {
198 Err(FormatError::MacAddressParse)
199 } else {
200 Ok(mac_address)
201 }
202 }
203}
204
205fn parse_octet(input: &str) -> Result<u8, FormatError> {
206 let &[a, b] = input.as_bytes() else {
207 return Err(FormatError::MacAddressParse);
208 };
209
210 Ok((parse_hex_digit(a)? << 4) | parse_hex_digit(b)?)
211}
212
213const fn parse_hex_digit(ch: u8) -> Result<u8, FormatError> {
214 match ch {
215 b'0'..=b'9' => Ok(ch - b'0'),
216 b'A'..=b'F' => Ok((ch - b'A') + 10),
217 b'a'..=b'f' => Ok((ch - b'a') + 10),
218 _ => Err(FormatError::MacAddressParse),
219 }
220}
221
222impl std::fmt::Display for MacAddress {
223 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
224 write!(
225 f,
226 "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
227 self.bytes[0],
228 self.bytes[1],
229 self.bytes[2],
230 self.bytes[3],
231 self.bytes[4],
232 self.bytes[5]
233 )
234 }
235}
236
237impl std::fmt::Debug for MacAddress {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 write!(f, "\"{self}\"")
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use std::str::FromStr;
246
247 use super::{FormatError, MacAddress, NessusClientDataV2};
248
249 const MINIMAL_POLICY: &str = r"
250<Policy>
251 <policyName>p</policyName>
252 <Preferences>
253 <ServerPreferences>
254 <preference><name>whoami</name><value>u</value></preference>
255 <preference><name>scan_description</name><value>d</value></preference>
256 <preference><name>TARGET</name><value>127.0.0.1</value></preference>
257 <preference><name>port_range</name><value>default</value></preference>
258 <preference><name>scan_start_timestamp</name><value>1</value></preference>
259 <preference><name>plugin_set</name><value>;1;</value></preference>
260 <preference><name>name</name><value>n</value></preference>
261 </ServerPreferences>
262 <PluginsPreferences/>
263 </Preferences>
264 <FamilySelection/>
265 <IndividualPluginSelection/>
266</Policy>
267";
268
269 #[test]
270 fn parse_allows_missing_report_tag() {
271 let xml = format!("<NessusClientData_v2>{MINIMAL_POLICY}</NessusClientData_v2>");
272 let parsed = NessusClientDataV2::parse(&xml).expect("XML should parse");
273 assert!(parsed.report.is_none());
274 }
275
276 #[test]
277 fn parse_rejects_repeated_policy_tag() {
278 let xml =
279 format!("<NessusClientData_v2>{MINIMAL_POLICY}{MINIMAL_POLICY}</NessusClientData_v2>");
280 let err = NessusClientDataV2::parse(&xml).expect_err("duplicate Policy must fail");
281 assert!(matches!(err, FormatError::RepeatedTag("Policy")));
282 }
283
284 #[test]
285 fn parse_rejects_repeated_report_tag() {
286 let xml = format!(
287 "<NessusClientData_v2>{MINIMAL_POLICY}<Report name=\"r\"/><Report name=\"r2\"/></NessusClientData_v2>"
288 );
289 let err = NessusClientDataV2::parse(&xml).expect_err("duplicate Report must fail");
290 assert!(matches!(err, FormatError::RepeatedTag("Report")));
291 }
292
293 #[test]
294 fn parse_rejects_non_empty_unknown_top_level_text() {
295 let xml = format!(
296 "<NessusClientData_v2>{MINIMAL_POLICY}<extra>boom</extra></NessusClientData_v2>"
297 );
298 let err =
299 NessusClientDataV2::parse(&xml).expect_err("non-empty unknown node text must fail");
300 assert!(matches!(err, FormatError::UnexpectedNode(_)));
301 }
302
303 #[test]
304 fn mac_address_rejects_invalid_inputs() {
305 for input in [
306 "0a:1b:2c:3d:4e",
307 "0a:1b:2c:3d:4e:5f:6a",
308 "0a:1b:2c:3d:4e:zz",
309 "0:1b:2c:3d:4e:5f",
310 ] {
311 let err = MacAddress::from_str(input).expect_err("invalid MAC must fail");
312 assert!(matches!(err, FormatError::MacAddressParse));
313 }
314 }
315}