use std::{borrow::Cow, collections::HashMap};
use jiff::Timestamp;
use roxmltree::{Node, StringStorage};
use crate::{StringStorageExt, assert_empty_text, error::FormatError};
#[derive(Debug)]
pub struct Policy<'input> {
pub policy_name: Cow<'input, str>,
pub policy_comments: Option<Cow<'input, str>>,
pub preferences: Preferences<'input>,
pub family_selection: Vec<FamilyItem<'input>>,
pub individual_plugin_selection: Vec<PluginItem<'input>>,
}
impl<'input> Policy<'input> {
pub(crate) fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
let mut policy_name = None;
let mut policy_comments = None;
let mut preferences = None;
let mut family_selection = None;
let mut individual_plugin_selection = None;
for child in node.children() {
match child.tag_name().name() {
"policyName" => {
if policy_name.is_some() {
return Err(FormatError::RepeatedTag("policyName"));
}
policy_name = child.text_storage().map(StringStorageExt::to_cow);
}
"policyComments" => {
if policy_comments.is_some() {
return Err(FormatError::RepeatedTag("policyComments"));
}
policy_comments = child.text_storage().map(StringStorageExt::to_cow);
}
"Preferences" => {
if preferences.is_some() {
return Err(FormatError::RepeatedTag("Preferences"));
}
preferences = Some(Preferences::from_xml_node(child)?);
}
"FamilySelection" => {
if family_selection.is_some() {
return Err(FormatError::RepeatedTag("FamilySelection"));
}
let mut items = vec![];
for child in child.children() {
if child.tag_name().name() == "FamilyItem" {
items.push(FamilyItem::from_xml_node(child)?);
} else {
assert_empty_text(child)?;
}
}
family_selection = Some(items);
}
"IndividualPluginSelection" => {
if individual_plugin_selection.is_some() {
return Err(FormatError::RepeatedTag("IndividualPluginSelection"));
}
let mut items = vec![];
for child in child.children() {
if child.tag_name().name() == "PluginItem" {
items.push(PluginItem::from_xml_node(child)?);
} else {
assert_empty_text(child)?;
}
}
individual_plugin_selection = Some(items);
}
_ => assert_empty_text(child)?,
}
}
Ok(Self {
policy_name: policy_name.ok_or(FormatError::MissingTag("policyName"))?,
policy_comments,
preferences: preferences.ok_or(FormatError::MissingTag("Preferences"))?,
family_selection: family_selection.ok_or(FormatError::MissingTag("FamilySelection"))?,
individual_plugin_selection: individual_plugin_selection
.ok_or(FormatError::MissingTag("IndividualPluginSelection"))?,
})
}
}
#[derive(Debug)]
pub struct Preferences<'a> {
pub server_preferences: ServerPreferences<'a>,
pub plugins_preferences: Vec<PluginPreferenceItem<'a>>,
}
impl<'input> Preferences<'input> {
fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
let mut server_preferences = None;
let mut plugins_preferences = None;
for child in node.children() {
match child.tag_name().name() {
"ServerPreferences" => {
if server_preferences.is_some() {
return Err(FormatError::RepeatedTag("ServerPreferences"));
}
server_preferences = Some(ServerPreferences::from_xml_node(child)?);
}
"PluginsPreferences" => {
if plugins_preferences.is_some() {
return Err(FormatError::RepeatedTag("PluginsPreferences"));
}
let mut items = vec![];
for item_node in child.children() {
if item_node.tag_name().name() == "item" {
items.push(PluginPreferenceItem::from_xml_node(item_node)?);
} else {
assert_empty_text(item_node)?;
}
}
plugins_preferences = Some(items);
}
_ => assert_empty_text(child)?,
}
}
Ok(Self {
server_preferences: server_preferences
.ok_or(FormatError::MissingTag("ServerPreferences"))?,
plugins_preferences: plugins_preferences
.ok_or(FormatError::MissingTag("PluginsPreferences"))?,
})
}
}
#[derive(Debug)]
pub struct ServerPreferences<'input> {
pub whoami: Cow<'input, str>,
pub scan_name: Option<Cow<'input, str>>,
pub scan_description: Cow<'input, str>,
pub description: Option<Cow<'input, str>>,
pub target: Vec<&'input str>,
pub port_range: &'input str,
pub scan_start_timestamp_seconds: jiff::Timestamp,
pub scan_end_timestamp_seconds: Option<jiff::Timestamp>,
pub plugin_set: &'input str,
pub name: Cow<'input, str>,
pub discovery_mode: Option<&'input str>,
pub others: HashMap<&'input str, Vec<Cow<'input, str>>>,
}
impl<'input> ServerPreferences<'input> {
#[allow(clippy::too_many_lines)]
fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
let mut whoami = None;
let mut scan_name = None;
let mut scan_description = None;
let mut description = None;
let mut target = None;
let mut port_range = None;
let mut scan_start_timestamp_seconds = None;
let mut scan_end_timestamp_seconds = None;
let mut plugin_set = None;
let mut name_name = None;
let mut discovery_mode = None;
let mut others: HashMap<&'input str, Vec<Cow<'input, str>>> = HashMap::new();
for child in node.children() {
if child.tag_name().name() != "preference" {
assert_empty_text(child)?;
continue;
}
let (name, value) = get_preference_name_value(child)?;
match name {
"whoami" => {
if whoami.is_some() {
return Err(FormatError::RepeatedTag("whoami"));
}
whoami = Some(value.to_cow());
}
"scan_name" => {
if scan_name.is_some() {
return Err(FormatError::RepeatedTag("scan_name"));
}
scan_name = Some(value.to_cow());
}
"scan_description" => {
if scan_description.is_some() {
return Err(FormatError::RepeatedTag("scan_description"));
}
scan_description = Some(value.to_cow());
}
"description" => {
if description.is_some() {
return Err(FormatError::RepeatedTag("description"));
}
description = Some(value.to_cow());
}
"TARGET" => {
if target.is_some() {
return Err(FormatError::RepeatedTag("TARGET"));
}
target = Some(value.to_str()?.split(',').collect());
}
"port_range" => {
if port_range.is_some() {
return Err(FormatError::RepeatedTag("port_range"));
}
port_range = Some(value.to_str()?);
}
"scan_start_timestamp" => {
if scan_start_timestamp_seconds.is_some() {
return Err(FormatError::RepeatedTag("scan_start_timestamp"));
}
scan_start_timestamp_seconds =
Some(Timestamp::from_second(value.parse::<i64>()?)?);
}
"scan_end_timestamp" => {
if scan_end_timestamp_seconds.is_some() {
return Err(FormatError::RepeatedTag("scan_end_timestamp"));
}
scan_end_timestamp_seconds =
Some(Timestamp::from_second(value.parse::<i64>()?)?);
}
"plugin_set" => {
if plugin_set.is_some() {
return Err(FormatError::RepeatedTag("plugin_set"));
}
plugin_set = Some(value.to_str()?);
}
"name" => {
if name_name.is_some() {
return Err(FormatError::RepeatedTag("name"));
}
name_name = Some(value.to_cow());
}
"discovery_mode" => {
if discovery_mode.is_some() {
return Err(FormatError::RepeatedTag("discovery_mode"));
}
discovery_mode = Some(value.to_str()?);
}
other_name => {
others.entry(other_name).or_default().push(value.to_cow());
}
}
}
Ok(Self {
whoami: whoami.ok_or(FormatError::MissingTag("whoami"))?,
scan_name,
scan_description: scan_description
.ok_or(FormatError::MissingTag("scan_description"))?,
description,
target: target.ok_or(FormatError::MissingTag("TARGET"))?,
port_range: port_range.ok_or(FormatError::MissingTag("port_range"))?,
scan_start_timestamp_seconds: scan_start_timestamp_seconds
.ok_or(FormatError::MissingTag("scan_start_timestamp"))?,
scan_end_timestamp_seconds,
plugin_set: plugin_set.ok_or(FormatError::MissingTag("plugin_set"))?,
name: name_name.ok_or(FormatError::MissingTag("name"))?,
discovery_mode,
others,
})
}
}
fn get_preference_name_value<'input, 'a>(
child: Node<'a, 'input>,
) -> Result<(&'input str, &'a StringStorage<'input>), FormatError> {
let mut name = None;
let mut value = None;
for sub_node in child.children() {
match sub_node.tag_name().name() {
"name" => {
if name.is_some() {
return Err(FormatError::RepeatedTag("name"));
}
name = sub_node
.text_storage()
.map(StringStorageExt::to_str)
.transpose()?;
}
"value" => {
if value.is_some() {
return Err(FormatError::RepeatedTag("value"));
}
value = Some(
sub_node
.text_storage()
.unwrap_or(&StringStorage::Borrowed("")),
);
}
_ => assert_empty_text(sub_node)?,
}
}
let name = name.ok_or(FormatError::MissingTag("name"))?;
let value = value.ok_or(FormatError::MissingTag("value"))?;
Ok((name, value))
}
#[derive(Debug)]
pub struct PluginPreferenceItem<'input> {
pub plugin_name: Cow<'input, str>,
pub plugin_id: u32,
pub full_name: Cow<'input, str>,
pub preference_name: Cow<'input, str>,
pub preference_type: Cow<'input, str>,
pub preference_values: Option<Cow<'input, str>>,
pub selected_value: Option<Cow<'input, str>>,
}
impl<'input> PluginPreferenceItem<'input> {
fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
let mut plugin_name = None;
let mut plugin_id = None;
let mut full_name = None;
let mut preference_name = None;
let mut preference_type = None;
let mut preference_values = None;
let mut selected_value = None;
for child in node.children() {
match child.tag_name().name() {
"pluginName" => {
if plugin_name.is_some() {
return Err(FormatError::RepeatedTag("pluginName"));
}
plugin_name = child.text_storage().map(StringStorageExt::to_cow);
}
"pluginId" => {
if plugin_id.is_some() {
return Err(FormatError::RepeatedTag("pluginId"));
}
let val = child.text().ok_or(FormatError::MissingTag("pluginId"))?;
plugin_id = Some(val.parse()?);
}
"fullName" => {
if full_name.is_some() {
return Err(FormatError::RepeatedTag("fullName"));
}
full_name = child.text_storage().map(StringStorageExt::to_cow);
}
"preferenceName" => {
if preference_name.is_some() {
return Err(FormatError::RepeatedTag("preferenceName"));
}
preference_name = child.text_storage().map(StringStorageExt::to_cow);
}
"preferenceType" => {
if preference_type.is_some() {
return Err(FormatError::RepeatedTag("preferenceType"));
}
preference_type = child.text_storage().map(StringStorageExt::to_cow);
}
"preferenceValues" => {
if preference_values.is_some() {
return Err(FormatError::RepeatedTag("preferenceValues"));
}
preference_values = child.text_storage().map(StringStorageExt::to_cow);
}
"selectedValue" => {
if selected_value.is_some() {
return Err(FormatError::RepeatedTag("selectedValue"));
}
selected_value = child.text_storage().map(StringStorageExt::to_cow);
}
_ => assert_empty_text(child)?,
}
}
Ok(Self {
plugin_name: plugin_name.ok_or(FormatError::MissingTag("pluginName"))?,
plugin_id: plugin_id.ok_or(FormatError::MissingTag("pluginId"))?,
full_name: full_name.ok_or(FormatError::MissingTag("fullName"))?,
preference_name: preference_name.ok_or(FormatError::MissingTag("preferenceName"))?,
preference_type: preference_type.ok_or(FormatError::MissingTag("preferenceType"))?,
preference_values,
selected_value,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FamilyStatus {
Enabled,
Disabled,
Mixed,
}
impl std::str::FromStr for FamilyStatus {
type Err = FormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"enabled" => Ok(Self::Enabled),
"disabled" => Ok(Self::Disabled),
"mixed" => Ok(Self::Mixed),
_ => Err(FormatError::UnexpectedFormat("FamilyStatus")),
}
}
}
#[derive(Debug)]
pub struct FamilyItem<'input> {
pub family_name: Cow<'input, str>,
pub status: FamilyStatus,
}
impl<'input> FamilyItem<'input> {
fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
let mut family_name = None;
let mut status = None;
for child in node.children() {
match child.tag_name().name() {
"FamilyName" => {
if family_name.is_some() {
return Err(FormatError::RepeatedTag("FamilyName"));
}
family_name = child.text_storage().map(StringStorageExt::to_cow);
}
"Status" => {
if status.is_some() {
return Err(FormatError::RepeatedTag("Status"));
}
let val = child.text().ok_or(FormatError::MissingTag("Status"))?;
status = Some(val.parse()?);
}
_ => assert_empty_text(child)?,
}
}
Ok(Self {
family_name: family_name.ok_or(FormatError::MissingTag("FamilyName"))?,
status: status.ok_or(FormatError::MissingTag("Status"))?,
})
}
}
#[derive(Debug)]
pub struct PluginItem<'input> {
pub plugin_id: u32,
pub plugin_name: Cow<'input, str>,
pub family: Cow<'input, str>,
pub status: FamilyStatus,
}
impl<'input> PluginItem<'input> {
fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
let mut plugin_id = None;
let mut plugin_name = None;
let mut family = None;
let mut status = None;
for child in node.children() {
match child.tag_name().name() {
"PluginId" => {
if plugin_id.is_some() {
return Err(FormatError::RepeatedTag("PluginId"));
}
let val = child.text().ok_or(FormatError::MissingTag("PluginId"))?;
plugin_id = Some(val.parse()?);
}
"PluginName" => {
if plugin_name.is_some() {
return Err(FormatError::RepeatedTag("PluginName"));
}
plugin_name = child.text_storage().map(StringStorageExt::to_cow);
}
"Family" => {
if family.is_some() {
return Err(FormatError::RepeatedTag("Family"));
}
family = child.text_storage().map(StringStorageExt::to_cow);
}
"Status" => {
if status.is_some() {
return Err(FormatError::RepeatedTag("Status"));
}
let val = child.text().ok_or(FormatError::MissingTag("Status"))?;
status = Some(val.parse()?);
}
_ => assert_empty_text(child)?,
}
}
Ok(Self {
plugin_id: plugin_id.ok_or(FormatError::MissingTag("PluginId"))?,
plugin_name: plugin_name.ok_or(FormatError::MissingTag("PluginName"))?,
family: family.ok_or(FormatError::MissingTag("Family"))?,
status: status.ok_or(FormatError::MissingTag("Status"))?,
})
}
}
#[cfg(test)]
mod tests {
use roxmltree::Document;
use crate::error::FormatError;
use super::{FamilyStatus, Policy};
fn parse_policy(xml: &str) -> Result<Policy<'_>, FormatError> {
let doc = Document::parse(xml).expect("test XML should parse");
let node = doc.root_element();
Policy::from_xml_node(node)
}
#[test]
fn rejects_missing_required_policy_sections() {
let xml = r"<Policy><policyName>p</policyName></Policy>";
let err = parse_policy(xml).expect_err("must fail");
assert!(matches!(err, FormatError::MissingTag("Preferences")));
}
#[test]
fn rejects_repeated_whoami_preference() {
let xml = r"
<Policy>
<policyName>p</policyName>
<Preferences>
<ServerPreferences>
<preference><name>whoami</name><value>u1</value></preference>
<preference><name>whoami</name><value>u2</value></preference>
<preference><name>scan_description</name><value>d</value></preference>
<preference><name>TARGET</name><value>127.0.0.1</value></preference>
<preference><name>port_range</name><value>default</value></preference>
<preference><name>scan_start_timestamp</name><value>1</value></preference>
<preference><name>plugin_set</name><value>;1;</value></preference>
<preference><name>name</name><value>n</value></preference>
</ServerPreferences>
<PluginsPreferences/>
</Preferences>
<FamilySelection/>
<IndividualPluginSelection/>
</Policy>
";
let err = parse_policy(xml).expect_err("must fail");
assert!(matches!(err, FormatError::RepeatedTag("whoami")));
}
#[test]
fn rejects_preference_item_missing_name_or_value() {
let missing_name = r"
<Policy>
<policyName>p</policyName>
<Preferences>
<ServerPreferences>
<preference><value>u</value></preference>
<preference><name>scan_description</name><value>d</value></preference>
<preference><name>TARGET</name><value>127.0.0.1</value></preference>
<preference><name>port_range</name><value>default</value></preference>
<preference><name>scan_start_timestamp</name><value>1</value></preference>
<preference><name>plugin_set</name><value>;1;</value></preference>
<preference><name>name</name><value>n</value></preference>
</ServerPreferences>
<PluginsPreferences/>
</Preferences>
<FamilySelection/>
<IndividualPluginSelection/>
</Policy>
";
let err = parse_policy(missing_name).expect_err("must fail");
assert!(matches!(err, FormatError::MissingTag("name")));
let missing_value = r"
<Policy>
<policyName>p</policyName>
<Preferences>
<ServerPreferences>
<preference><name>whoami</name></preference>
<preference><name>scan_description</name><value>d</value></preference>
<preference><name>TARGET</name><value>127.0.0.1</value></preference>
<preference><name>port_range</name><value>default</value></preference>
<preference><name>scan_start_timestamp</name><value>1</value></preference>
<preference><name>plugin_set</name><value>;1;</value></preference>
<preference><name>name</name><value>n</value></preference>
</ServerPreferences>
<PluginsPreferences/>
</Preferences>
<FamilySelection/>
<IndividualPluginSelection/>
</Policy>
";
let err = parse_policy(missing_value).expect_err("must fail");
assert!(matches!(err, FormatError::MissingTag("value")));
}
#[test]
fn family_status_parsing_works() {
assert!(matches!("enabled".parse(), Ok(FamilyStatus::Enabled)));
assert!(matches!("disabled".parse(), Ok(FamilyStatus::Disabled)));
assert!(matches!("mixed".parse(), Ok(FamilyStatus::Mixed)));
assert!(matches!(
"bad".parse::<FamilyStatus>(),
Err(FormatError::UnexpectedFormat("FamilyStatus"))
));
}
#[test]
fn minimal_policy_parses() {
let minimal_policy_xml = r"
<Policy>
<policyName>p</policyName>
<Preferences>
<ServerPreferences>
<preference><name>whoami</name><value>u</value></preference>
<preference><name>scan_description</name><value>d</value></preference>
<preference><name>TARGET</name><value>127.0.0.1</value></preference>
<preference><name>port_range</name><value>default</value></preference>
<preference><name>scan_start_timestamp</name><value>1</value></preference>
<preference><name>plugin_set</name><value>;1;</value></preference>
<preference><name>name</name><value>n</value></preference>
</ServerPreferences>
<PluginsPreferences/>
</Preferences>
<FamilySelection>
<FamilyItem>
<FamilyName>General</FamilyName>
<Status>enabled</Status>
</FamilyItem>
</FamilySelection>
<IndividualPluginSelection>
<PluginItem>
<PluginId>1</PluginId>
<PluginName>x</PluginName>
<Family>General</Family>
<Status>enabled</Status>
</PluginItem>
</IndividualPluginSelection>
</Policy>
";
let parsed = parse_policy(minimal_policy_xml).expect("must parse");
assert_eq!(parsed.policy_name, "p");
}
}