nessus_parser/
policy.rs

1use std::{borrow::Cow, collections::HashMap};
2
3use jiff::Timestamp;
4use roxmltree::{Node, StringStorage};
5
6use crate::{StringStorageExt, assert_empty_text, error::FormatError};
7
8/// Represents the `<Policy>` element within a Nessus report.
9///
10/// This struct holds the complete configuration of the scan policy,
11/// including its name, comments, and detailed preferences for server
12/// behavior and plugin selection.
13#[derive(Debug)]
14pub struct Policy<'input> {
15    /// The name of the policy (from the `<policyName>` tag).
16    pub policy_name: Cow<'input, str>,
17    /// Comments associated with the policy (from `<policyComments>`).
18    pub policy_comments: Option<Cow<'input, str>>,
19    /// Server and plugin preferences for the scan.
20    pub preferences: Preferences<'input>,
21    /// The selection of plugin families to be used in the scan.
22    pub family_selection: Vec<FamilyItem<'input>>,
23    /// The selection of individual plugins to be used in the scan.
24    pub individual_plugin_selection: Vec<PluginItem<'input>>,
25}
26
27impl<'input> Policy<'input> {
28    pub(crate) fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
29        let mut policy_name = None;
30        let mut policy_comments = None;
31        let mut preferences = None;
32        let mut family_selection = None;
33        let mut individual_plugin_selection = None;
34
35        for child in node.children() {
36            match child.tag_name().name() {
37                "policyName" => {
38                    if policy_name.is_some() {
39                        return Err(FormatError::RepeatedTag("policyName"));
40                    }
41                    policy_name = child.text_storage().map(StringStorageExt::to_cow);
42                }
43                "policyComments" => {
44                    if policy_comments.is_some() {
45                        return Err(FormatError::RepeatedTag("policyComments"));
46                    }
47                    policy_comments = child.text_storage().map(StringStorageExt::to_cow);
48                }
49                "Preferences" => {
50                    if preferences.is_some() {
51                        return Err(FormatError::RepeatedTag("Preferences"));
52                    }
53                    preferences = Some(Preferences::from_xml_node(child)?);
54                }
55                "FamilySelection" => {
56                    if family_selection.is_some() {
57                        return Err(FormatError::RepeatedTag("FamilySelection"));
58                    }
59
60                    let mut items = vec![];
61                    for child in child.children() {
62                        if child.tag_name().name() == "FamilyItem" {
63                            items.push(FamilyItem::from_xml_node(child)?);
64                        } else {
65                            assert_empty_text(child)?;
66                        }
67                    }
68                    family_selection = Some(items);
69                }
70                "IndividualPluginSelection" => {
71                    if individual_plugin_selection.is_some() {
72                        return Err(FormatError::RepeatedTag("IndividualPluginSelection"));
73                    }
74
75                    let mut items = vec![];
76                    for child in child.children() {
77                        if child.tag_name().name() == "PluginItem" {
78                            items.push(PluginItem::from_xml_node(child)?);
79                        } else {
80                            assert_empty_text(child)?;
81                        }
82                    }
83                    individual_plugin_selection = Some(items);
84                }
85                _ => assert_empty_text(child)?,
86            }
87        }
88
89        Ok(Self {
90            policy_name: policy_name.ok_or(FormatError::MissingTag("policyName"))?,
91            policy_comments,
92            preferences: preferences.ok_or(FormatError::MissingTag("Preferences"))?,
93            family_selection: family_selection.ok_or(FormatError::MissingTag("FamilySelection"))?,
94            individual_plugin_selection: individual_plugin_selection
95                .ok_or(FormatError::MissingTag("IndividualPluginSelection"))?,
96        })
97    }
98}
99
100/// Represents the `<Preferences>` element within a scan policy.
101///
102/// This acts as a container for both server-level and plugin-specific
103/// preferences that define the scan's behavior.
104#[derive(Debug)]
105pub struct Preferences<'a> {
106    /// A collection of server-wide settings for the scan.
107    pub server_preferences: ServerPreferences<'a>,
108    /// A collection of preferences specific to individual plugins.
109    pub plugins_preferences: Vec<PluginPreferenceItem<'a>>,
110}
111
112impl<'input> Preferences<'input> {
113    fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
114        let mut server_preferences = None;
115        let mut plugins_preferences = None;
116
117        for child in node.children() {
118            match child.tag_name().name() {
119                "ServerPreferences" => {
120                    if server_preferences.is_some() {
121                        return Err(FormatError::RepeatedTag("ServerPreferences"));
122                    }
123                    server_preferences = Some(ServerPreferences::from_xml_node(child)?);
124                }
125                "PluginsPreferences" => {
126                    if plugins_preferences.is_some() {
127                        return Err(FormatError::RepeatedTag("PluginsPreferences"));
128                    }
129                    let mut items = vec![];
130                    for item_node in child.children() {
131                        if item_node.tag_name().name() == "item" {
132                            items.push(PluginPreferenceItem::from_xml_node(item_node)?);
133                        } else {
134                            assert_empty_text(item_node)?;
135                        }
136                    }
137                    plugins_preferences = Some(items);
138                }
139                _ => assert_empty_text(child)?,
140            }
141        }
142
143        Ok(Self {
144            server_preferences: server_preferences
145                .ok_or(FormatError::MissingTag("ServerPreferences"))?,
146            plugins_preferences: plugins_preferences
147                .ok_or(FormatError::MissingTag("PluginsPreferences"))?,
148        })
149    }
150}
151
152/// Represents the `<ServerPreferences>` element, containing detailed
153/// settings for the Nessus scanner's behavior during the scan.
154#[derive(Debug)]
155pub struct ServerPreferences<'input> {
156    /// The user who launched the scan
157    pub whoami: Cow<'input, str>,
158    /// The user-defined name for the scan
159    pub scan_name: Option<Cow<'input, str>>,
160    /// The user-defined description for the scan
161    pub scan_description: Cow<'input, str>,
162    /// An alternative description field for the scan
163    pub description: Option<Cow<'input, str>>,
164    /// A list of targets for the scan (e.g., IP addresses, CIDR ranges).
165    // 1.2.3.4,192.168.0.0/24, ...
166    pub target: Vec<&'input str>,
167    /// The port range to be scanned (e.g., "1-65535", "default", "all").
168    pub port_range: &'input str,
169    /// The timestamp when the scan was initiated.
170    pub scan_start_timestamp_seconds: jiff::Timestamp,
171    /// The timestamp when the scan was completed.
172    pub scan_end_timestamp_seconds: Option<jiff::Timestamp>,
173    /// The set of all plugin IDs that were active for the scan.
174    // "...;28505;28497;28507;28502;28508;..." (gigantic list)
175    pub plugin_set: &'input str,
176    /// The name of the scan policy (e.g., "Advanced Scan").
177    pub name: Cow<'input, str>,
178    /// The discovery mode used for the scan (e.g., "portscan_common", "custom").
179    // None | Some("portscan_all") | Some("host_enumeration")
180    // | Some("custom") | Some("portscan_common") | Some("log4shell_thorough")
181    // | Some("identity_quick") | Some("log4shell_dc_normal")
182    pub discovery_mode: Option<&'input str>,
183    /// A map to hold any other server preferences not explicitly parsed into
184    /// other fields. The key is the preference name.
185    pub others: HashMap<&'input str, Option<Cow<'input, str>>>,
186}
187
188impl<'input> ServerPreferences<'input> {
189    #[allow(clippy::too_many_lines)]
190    fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
191        let mut whoami = None;
192        let mut scan_name = None;
193        let mut scan_description = None;
194        let mut description = None;
195        let mut target = None;
196        let mut port_range = None;
197        let mut scan_start_timestamp_seconds = None;
198        let mut scan_end_timestamp_seconds = None;
199        let mut plugin_set = None;
200        let mut name_name = None;
201        let mut discovery_mode = None;
202
203        let mut others = HashMap::new();
204
205        for child in node.children() {
206            if child.tag_name().name() != "preference" {
207                assert_empty_text(child)?;
208                continue;
209            }
210
211            let (name, value) = get_preference_name_value(child)?;
212
213            match name {
214                "whoami" => {
215                    if whoami.is_some() {
216                        return Err(FormatError::RepeatedTag("whoami"));
217                    }
218                    whoami = Some(value.to_cow());
219                }
220                "scan_name" => {
221                    if scan_name.is_some() {
222                        return Err(FormatError::RepeatedTag("scan_name"));
223                    }
224                    scan_name = Some(value.to_cow());
225                }
226                "scan_description" => {
227                    if scan_description.is_some() {
228                        return Err(FormatError::RepeatedTag("scan_description"));
229                    }
230                    scan_description = Some(value.to_cow());
231                }
232                "description" => {
233                    if description.is_some() {
234                        return Err(FormatError::RepeatedTag("description"));
235                    }
236                    description = Some(value.to_cow());
237                }
238                "TARGET" => {
239                    if target.is_some() {
240                        return Err(FormatError::RepeatedTag("TARGET"));
241                    }
242                    target = Some(value.to_str()?.split(',').collect());
243                }
244                "port_range" => {
245                    if port_range.is_some() {
246                        return Err(FormatError::RepeatedTag("port_range"));
247                    }
248                    port_range = Some(value.to_str()?);
249                }
250                "scan_start_timestamp" => {
251                    if scan_start_timestamp_seconds.is_some() {
252                        return Err(FormatError::RepeatedTag("scan_start_timestamp"));
253                    }
254                    scan_start_timestamp_seconds =
255                        Some(Timestamp::from_second(value.parse::<i64>()?)?);
256                }
257                "scan_end_timestamp" => {
258                    if scan_end_timestamp_seconds.is_some() {
259                        return Err(FormatError::RepeatedTag("scan_end_timestamp"));
260                    }
261                    scan_end_timestamp_seconds =
262                        Some(Timestamp::from_second(value.parse::<i64>()?)?);
263                }
264                "plugin_set" => {
265                    if plugin_set.is_some() {
266                        return Err(FormatError::RepeatedTag("plugin_set"));
267                    }
268
269                    plugin_set = Some(value.to_str()?);
270                }
271                "name" => {
272                    if name_name.is_some() {
273                        return Err(FormatError::RepeatedTag("name"));
274                    }
275                    name_name = Some(value.to_cow());
276                }
277                "discovery_mode" => {
278                    if discovery_mode.is_some() {
279                        return Err(FormatError::RepeatedTag("discovery_mode"));
280                    }
281                    discovery_mode = Some(value.to_str()?);
282                }
283                other_name => {
284                    others.insert(other_name, Some(value.to_cow()));
285                }
286            }
287        }
288
289        Ok(Self {
290            whoami: whoami.ok_or(FormatError::MissingTag("whoami"))?,
291            scan_name,
292            scan_description: scan_description
293                .ok_or(FormatError::MissingTag("scan_description"))?,
294            description,
295            target: target.ok_or(FormatError::MissingTag("TARGET"))?,
296            port_range: port_range.ok_or(FormatError::MissingTag("port_range"))?,
297            scan_start_timestamp_seconds: scan_start_timestamp_seconds
298                .ok_or(FormatError::MissingTag("scan_start_timestamp"))?,
299            scan_end_timestamp_seconds,
300            plugin_set: plugin_set.ok_or(FormatError::MissingTag("plugin_set"))?,
301            name: name_name.ok_or(FormatError::MissingTag("name"))?,
302            discovery_mode,
303            others,
304        })
305    }
306}
307
308fn get_preference_name_value<'input, 'a>(
309    child: Node<'a, 'input>,
310) -> Result<(&'input str, &'a StringStorage<'input>), FormatError> {
311    let mut name = None;
312    let mut value = None;
313
314    for sub_node in child.children() {
315        match sub_node.tag_name().name() {
316            "name" => {
317                if name.is_some() {
318                    return Err(FormatError::RepeatedTag("name"));
319                }
320                name = sub_node
321                    .text_storage()
322                    .map(StringStorageExt::to_str)
323                    .transpose()?;
324            }
325            "value" => {
326                if value.is_some() {
327                    return Err(FormatError::RepeatedTag("value"));
328                }
329                value = Some(
330                    sub_node
331                        .text_storage()
332                        .unwrap_or(&StringStorage::Borrowed("")),
333                );
334            }
335            _ => assert_empty_text(sub_node)?,
336        }
337    }
338
339    let name = name.ok_or(FormatError::MissingTag("name"))?;
340    let value = value.ok_or(FormatError::MissingTag("value"))?;
341
342    Ok((name, value))
343}
344
345/// Represents an individual `<item>` within `<PluginsPreferences>`.
346#[derive(Debug)]
347pub struct PluginPreferenceItem<'input> {
348    pub plugin_name: Cow<'input, str>,
349    pub plugin_id: u32,
350    pub full_name: Cow<'input, str>,
351    pub preference_name: Cow<'input, str>,
352    pub preference_type: Cow<'input, str>,
353    pub preference_values: Option<Cow<'input, str>>,
354    pub selected_value: Option<Cow<'input, str>>,
355}
356
357impl<'input> PluginPreferenceItem<'input> {
358    fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
359        let mut plugin_name = None;
360        let mut plugin_id = None;
361        let mut full_name = None;
362        let mut preference_name = None;
363        let mut preference_type = None;
364        let mut preference_values = None;
365        let mut selected_value = None;
366
367        for child in node.children() {
368            match child.tag_name().name() {
369                "pluginName" => {
370                    plugin_name = child.text_storage().map(StringStorageExt::to_cow);
371                }
372                "pluginId" => {
373                    let val = child.text().ok_or(FormatError::MissingTag("pluginId"))?;
374                    plugin_id = Some(val.parse()?);
375                }
376                "fullName" => {
377                    full_name = child.text_storage().map(StringStorageExt::to_cow);
378                }
379                "preferenceName" => {
380                    preference_name = child.text_storage().map(StringStorageExt::to_cow);
381                }
382                "preferenceType" => {
383                    preference_type = child.text_storage().map(StringStorageExt::to_cow);
384                }
385                "preferenceValues" => {
386                    preference_values = child.text_storage().map(StringStorageExt::to_cow);
387                }
388                "selectedValue" => {
389                    selected_value = child.text_storage().map(StringStorageExt::to_cow);
390                }
391                _ => assert_empty_text(child)?,
392            }
393        }
394
395        Ok(Self {
396            plugin_name: plugin_name.ok_or(FormatError::MissingTag("pluginName"))?,
397            plugin_id: plugin_id.ok_or(FormatError::MissingTag("pluginId"))?,
398            full_name: full_name.ok_or(FormatError::MissingTag("fullName"))?,
399            preference_name: preference_name.ok_or(FormatError::MissingTag("preferenceName"))?,
400            preference_type: preference_type.ok_or(FormatError::MissingTag("preferenceType"))?,
401            preference_values,
402            selected_value,
403        })
404    }
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408pub enum FamilyStatus {
409    Enabled,
410    Disabled,
411    Mixed,
412}
413
414impl std::str::FromStr for FamilyStatus {
415    type Err = FormatError;
416
417    fn from_str(s: &str) -> Result<Self, Self::Err> {
418        match s {
419            "enabled" => Ok(Self::Enabled),
420            "disabled" => Ok(Self::Disabled),
421            "mixed" => Ok(Self::Mixed),
422            _ => Err(FormatError::UnexpectedFormat("FamilyStatus")),
423        }
424    }
425}
426
427/// Represents an individual plugin family selection entry.
428#[derive(Debug)]
429pub struct FamilyItem<'input> {
430    pub family_name: Cow<'input, str>,
431    pub status: FamilyStatus,
432}
433
434impl<'input> FamilyItem<'input> {
435    fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
436        let mut family_name = None;
437        let mut status = None;
438
439        for child in node.children() {
440            match child.tag_name().name() {
441                "FamilyName" => {
442                    if family_name.is_some() {
443                        return Err(FormatError::RepeatedTag("FamilyName"));
444                    }
445                    family_name = child.text_storage().map(StringStorageExt::to_cow);
446                }
447                "Status" => {
448                    if status.is_some() {
449                        return Err(FormatError::RepeatedTag("Status"));
450                    }
451                    let val = child.text().ok_or(FormatError::MissingTag("Status"))?;
452                    status = Some(val.parse()?);
453                }
454                _ => assert_empty_text(child)?,
455            }
456        }
457
458        Ok(Self {
459            family_name: family_name.ok_or(FormatError::MissingTag("FamilyName"))?,
460            status: status.ok_or(FormatError::MissingTag("Status"))?,
461        })
462    }
463}
464
465/// Represents an individual plugin entry within `<IndividualPluginSelection>`.
466#[derive(Debug)]
467pub struct PluginItem<'input> {
468    pub plugin_id: u32,
469    pub plugin_name: Cow<'input, str>,
470    pub family: Cow<'input, str>,
471    pub status: FamilyStatus,
472}
473
474impl<'input> PluginItem<'input> {
475    fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
476        let mut plugin_id = None;
477        let mut plugin_name = None;
478        let mut family = None;
479        let mut status = None;
480
481        for child in node.children() {
482            match child.tag_name().name() {
483                "PluginId" => {
484                    if plugin_id.is_some() {
485                        return Err(FormatError::RepeatedTag("PluginId"));
486                    }
487                    let val = child.text().ok_or(FormatError::MissingTag("PluginId"))?;
488                    plugin_id = Some(val.parse()?);
489                }
490                "PluginName" => {
491                    if plugin_name.is_some() {
492                        return Err(FormatError::RepeatedTag("PluginName"));
493                    }
494                    plugin_name = child.text_storage().map(StringStorageExt::to_cow);
495                }
496                "Family" => {
497                    if family.is_some() {
498                        return Err(FormatError::RepeatedTag("Family"));
499                    }
500                    family = child.text_storage().map(StringStorageExt::to_cow);
501                }
502                "Status" => {
503                    if status.is_some() {
504                        return Err(FormatError::RepeatedTag("Status"));
505                    }
506                    let val = child.text().ok_or(FormatError::MissingTag("Status"))?;
507                    status = Some(val.parse()?);
508                }
509                _ => assert_empty_text(child)?,
510            }
511        }
512
513        Ok(Self {
514            plugin_id: plugin_id.ok_or(FormatError::MissingTag("PluginId"))?,
515            plugin_name: plugin_name.ok_or(FormatError::MissingTag("PluginName"))?,
516            family: family.ok_or(FormatError::MissingTag("Family"))?,
517            status: status.ok_or(FormatError::MissingTag("Status"))?,
518        })
519    }
520}