1use std::{borrow::Cow, collections::HashMap};
4
5use jiff::Timestamp;
6use roxmltree::{Node, StringStorage};
7
8use crate::{StringStorageExt, assert_empty_text, error::FormatError};
9
10#[derive(Debug)]
16pub struct Policy<'input> {
17 pub policy_name: Cow<'input, str>,
19 pub policy_comments: Option<Cow<'input, str>>,
21 pub preferences: Preferences<'input>,
23 pub family_selection: Vec<FamilyItem<'input>>,
25 pub individual_plugin_selection: Vec<PluginItem<'input>>,
27}
28
29impl<'input> Policy<'input> {
30 pub(crate) fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
31 let mut policy_name = None;
32 let mut policy_comments = None;
33 let mut preferences = None;
34 let mut family_selection = None;
35 let mut individual_plugin_selection = None;
36
37 for child in node.children() {
38 match child.tag_name().name() {
39 "policyName" => {
40 if policy_name.is_some() {
41 return Err(FormatError::RepeatedTag("policyName"));
42 }
43 policy_name = child.text_storage().map(StringStorageExt::to_cow);
44 }
45 "policyComments" => {
46 if policy_comments.is_some() {
47 return Err(FormatError::RepeatedTag("policyComments"));
48 }
49 policy_comments = child.text_storage().map(StringStorageExt::to_cow);
50 }
51 "Preferences" => {
52 if preferences.is_some() {
53 return Err(FormatError::RepeatedTag("Preferences"));
54 }
55 preferences = Some(Preferences::from_xml_node(child)?);
56 }
57 "FamilySelection" => {
58 if family_selection.is_some() {
59 return Err(FormatError::RepeatedTag("FamilySelection"));
60 }
61
62 let mut items = vec![];
63 for child in child.children() {
64 if child.tag_name().name() == "FamilyItem" {
65 items.push(FamilyItem::from_xml_node(child)?);
66 } else {
67 assert_empty_text(child)?;
68 }
69 }
70 family_selection = Some(items);
71 }
72 "IndividualPluginSelection" => {
73 if individual_plugin_selection.is_some() {
74 return Err(FormatError::RepeatedTag("IndividualPluginSelection"));
75 }
76
77 let mut items = vec![];
78 for child in child.children() {
79 if child.tag_name().name() == "PluginItem" {
80 items.push(PluginItem::from_xml_node(child)?);
81 } else {
82 assert_empty_text(child)?;
83 }
84 }
85 individual_plugin_selection = Some(items);
86 }
87 _ => assert_empty_text(child)?,
88 }
89 }
90
91 Ok(Self {
92 policy_name: policy_name.ok_or(FormatError::MissingTag("policyName"))?,
93 policy_comments,
94 preferences: preferences.ok_or(FormatError::MissingTag("Preferences"))?,
95 family_selection: family_selection.ok_or(FormatError::MissingTag("FamilySelection"))?,
96 individual_plugin_selection: individual_plugin_selection
97 .ok_or(FormatError::MissingTag("IndividualPluginSelection"))?,
98 })
99 }
100}
101
102#[derive(Debug)]
107pub struct Preferences<'a> {
108 pub server_preferences: ServerPreferences<'a>,
110 pub plugins_preferences: Vec<PluginPreferenceItem<'a>>,
112}
113
114impl<'input> Preferences<'input> {
115 fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
116 let mut server_preferences = None;
117 let mut plugins_preferences = None;
118
119 for child in node.children() {
120 match child.tag_name().name() {
121 "ServerPreferences" => {
122 if server_preferences.is_some() {
123 return Err(FormatError::RepeatedTag("ServerPreferences"));
124 }
125 server_preferences = Some(ServerPreferences::from_xml_node(child)?);
126 }
127 "PluginsPreferences" => {
128 if plugins_preferences.is_some() {
129 return Err(FormatError::RepeatedTag("PluginsPreferences"));
130 }
131 let mut items = vec![];
132 for item_node in child.children() {
133 if item_node.tag_name().name() == "item" {
134 items.push(PluginPreferenceItem::from_xml_node(item_node)?);
135 } else {
136 assert_empty_text(item_node)?;
137 }
138 }
139 plugins_preferences = Some(items);
140 }
141 _ => assert_empty_text(child)?,
142 }
143 }
144
145 Ok(Self {
146 server_preferences: server_preferences
147 .ok_or(FormatError::MissingTag("ServerPreferences"))?,
148 plugins_preferences: plugins_preferences
149 .ok_or(FormatError::MissingTag("PluginsPreferences"))?,
150 })
151 }
152}
153
154#[derive(Debug)]
157pub struct ServerPreferences<'input> {
158 pub whoami: Cow<'input, str>,
160 pub scan_name: Option<Cow<'input, str>>,
162 pub scan_description: Cow<'input, str>,
164 pub description: Option<Cow<'input, str>>,
166 pub target: Vec<&'input str>,
169 pub port_range: &'input str,
171 pub scan_start_timestamp_seconds: jiff::Timestamp,
173 pub scan_end_timestamp_seconds: Option<jiff::Timestamp>,
175 pub plugin_set: &'input str,
178 pub name: Cow<'input, str>,
180 pub discovery_mode: Option<&'input str>,
185 pub others: HashMap<&'input str, Vec<Cow<'input, str>>>,
188}
189
190impl<'input> ServerPreferences<'input> {
191 #[allow(clippy::too_many_lines)]
192 fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
193 let mut whoami = None;
194 let mut scan_name = None;
195 let mut scan_description = None;
196 let mut description = None;
197 let mut target = None;
198 let mut port_range = None;
199 let mut scan_start_timestamp_seconds = None;
200 let mut scan_end_timestamp_seconds = None;
201 let mut plugin_set = None;
202 let mut name_name = None;
203 let mut discovery_mode = None;
204
205 let mut others: HashMap<&'input str, Vec<Cow<'input, str>>> = HashMap::new();
206
207 for child in node.children() {
208 if child.tag_name().name() != "preference" {
209 assert_empty_text(child)?;
210 continue;
211 }
212
213 let (name, value) = get_preference_name_value(child)?;
214
215 match name {
216 "whoami" => {
217 if whoami.is_some() {
218 return Err(FormatError::RepeatedTag("whoami"));
219 }
220 whoami = Some(value.to_cow());
221 }
222 "scan_name" => {
223 if scan_name.is_some() {
224 return Err(FormatError::RepeatedTag("scan_name"));
225 }
226 scan_name = Some(value.to_cow());
227 }
228 "scan_description" => {
229 if scan_description.is_some() {
230 return Err(FormatError::RepeatedTag("scan_description"));
231 }
232 scan_description = Some(value.to_cow());
233 }
234 "description" => {
235 if description.is_some() {
236 return Err(FormatError::RepeatedTag("description"));
237 }
238 description = Some(value.to_cow());
239 }
240 "TARGET" => {
241 if target.is_some() {
242 return Err(FormatError::RepeatedTag("TARGET"));
243 }
244 target = Some(value.to_str()?.split(',').collect());
245 }
246 "port_range" => {
247 if port_range.is_some() {
248 return Err(FormatError::RepeatedTag("port_range"));
249 }
250 port_range = Some(value.to_str()?);
251 }
252 "scan_start_timestamp" => {
253 if scan_start_timestamp_seconds.is_some() {
254 return Err(FormatError::RepeatedTag("scan_start_timestamp"));
255 }
256 scan_start_timestamp_seconds =
257 Some(Timestamp::from_second(value.parse::<i64>()?)?);
258 }
259 "scan_end_timestamp" => {
260 if scan_end_timestamp_seconds.is_some() {
261 return Err(FormatError::RepeatedTag("scan_end_timestamp"));
262 }
263 scan_end_timestamp_seconds =
264 Some(Timestamp::from_second(value.parse::<i64>()?)?);
265 }
266 "plugin_set" => {
267 if plugin_set.is_some() {
268 return Err(FormatError::RepeatedTag("plugin_set"));
269 }
270
271 plugin_set = Some(value.to_str()?);
272 }
273 "name" => {
274 if name_name.is_some() {
275 return Err(FormatError::RepeatedTag("name"));
276 }
277 name_name = Some(value.to_cow());
278 }
279 "discovery_mode" => {
280 if discovery_mode.is_some() {
281 return Err(FormatError::RepeatedTag("discovery_mode"));
282 }
283 discovery_mode = Some(value.to_str()?);
284 }
285 other_name => {
286 others.entry(other_name).or_default().push(value.to_cow());
287 }
288 }
289 }
290
291 Ok(Self {
292 whoami: whoami.ok_or(FormatError::MissingTag("whoami"))?,
293 scan_name,
294 scan_description: scan_description
295 .ok_or(FormatError::MissingTag("scan_description"))?,
296 description,
297 target: target.ok_or(FormatError::MissingTag("TARGET"))?,
298 port_range: port_range.ok_or(FormatError::MissingTag("port_range"))?,
299 scan_start_timestamp_seconds: scan_start_timestamp_seconds
300 .ok_or(FormatError::MissingTag("scan_start_timestamp"))?,
301 scan_end_timestamp_seconds,
302 plugin_set: plugin_set.ok_or(FormatError::MissingTag("plugin_set"))?,
303 name: name_name.ok_or(FormatError::MissingTag("name"))?,
304 discovery_mode,
305 others,
306 })
307 }
308}
309
310fn get_preference_name_value<'input, 'a>(
311 child: Node<'a, 'input>,
312) -> Result<(&'input str, &'a StringStorage<'input>), FormatError> {
313 let mut name = None;
314 let mut value = None;
315
316 for sub_node in child.children() {
317 match sub_node.tag_name().name() {
318 "name" => {
319 if name.is_some() {
320 return Err(FormatError::RepeatedTag("name"));
321 }
322 name = sub_node
323 .text_storage()
324 .map(StringStorageExt::to_str)
325 .transpose()?;
326 }
327 "value" => {
328 if value.is_some() {
329 return Err(FormatError::RepeatedTag("value"));
330 }
331 value = Some(
332 sub_node
333 .text_storage()
334 .unwrap_or(&StringStorage::Borrowed("")),
335 );
336 }
337 _ => assert_empty_text(sub_node)?,
338 }
339 }
340
341 let name = name.ok_or(FormatError::MissingTag("name"))?;
342 let value = value.ok_or(FormatError::MissingTag("value"))?;
343
344 Ok((name, value))
345}
346
347#[derive(Debug)]
349pub struct PluginPreferenceItem<'input> {
350 pub plugin_name: Cow<'input, str>,
351 pub plugin_id: u32,
352 pub full_name: Cow<'input, str>,
353 pub preference_name: Cow<'input, str>,
354 pub preference_type: Cow<'input, str>,
355 pub preference_values: Option<Cow<'input, str>>,
356 pub selected_value: Option<Cow<'input, str>>,
357}
358
359impl<'input> PluginPreferenceItem<'input> {
360 fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
361 let mut plugin_name = None;
362 let mut plugin_id = None;
363 let mut full_name = None;
364 let mut preference_name = None;
365 let mut preference_type = None;
366 let mut preference_values = None;
367 let mut selected_value = None;
368
369 for child in node.children() {
370 match child.tag_name().name() {
371 "pluginName" => {
372 if plugin_name.is_some() {
373 return Err(FormatError::RepeatedTag("pluginName"));
374 }
375 plugin_name = child.text_storage().map(StringStorageExt::to_cow);
376 }
377 "pluginId" => {
378 if plugin_id.is_some() {
379 return Err(FormatError::RepeatedTag("pluginId"));
380 }
381 let val = child.text().ok_or(FormatError::MissingTag("pluginId"))?;
382 plugin_id = Some(val.parse()?);
383 }
384 "fullName" => {
385 if full_name.is_some() {
386 return Err(FormatError::RepeatedTag("fullName"));
387 }
388 full_name = child.text_storage().map(StringStorageExt::to_cow);
389 }
390 "preferenceName" => {
391 if preference_name.is_some() {
392 return Err(FormatError::RepeatedTag("preferenceName"));
393 }
394 preference_name = child.text_storage().map(StringStorageExt::to_cow);
395 }
396 "preferenceType" => {
397 if preference_type.is_some() {
398 return Err(FormatError::RepeatedTag("preferenceType"));
399 }
400 preference_type = child.text_storage().map(StringStorageExt::to_cow);
401 }
402 "preferenceValues" => {
403 if preference_values.is_some() {
404 return Err(FormatError::RepeatedTag("preferenceValues"));
405 }
406 preference_values = child.text_storage().map(StringStorageExt::to_cow);
407 }
408 "selectedValue" => {
409 if selected_value.is_some() {
410 return Err(FormatError::RepeatedTag("selectedValue"));
411 }
412 selected_value = child.text_storage().map(StringStorageExt::to_cow);
413 }
414 _ => assert_empty_text(child)?,
415 }
416 }
417
418 Ok(Self {
419 plugin_name: plugin_name.ok_or(FormatError::MissingTag("pluginName"))?,
420 plugin_id: plugin_id.ok_or(FormatError::MissingTag("pluginId"))?,
421 full_name: full_name.ok_or(FormatError::MissingTag("fullName"))?,
422 preference_name: preference_name.ok_or(FormatError::MissingTag("preferenceName"))?,
423 preference_type: preference_type.ok_or(FormatError::MissingTag("preferenceType"))?,
424 preference_values,
425 selected_value,
426 })
427 }
428}
429
430#[derive(Debug, Clone, Copy, PartialEq, Eq)]
431pub enum FamilyStatus {
432 Enabled,
433 Disabled,
434 Mixed,
435}
436
437impl std::str::FromStr for FamilyStatus {
438 type Err = FormatError;
439
440 fn from_str(s: &str) -> Result<Self, Self::Err> {
441 match s {
442 "enabled" => Ok(Self::Enabled),
443 "disabled" => Ok(Self::Disabled),
444 "mixed" => Ok(Self::Mixed),
445 _ => Err(FormatError::UnexpectedFormat("FamilyStatus")),
446 }
447 }
448}
449
450#[derive(Debug)]
452pub struct FamilyItem<'input> {
453 pub family_name: Cow<'input, str>,
454 pub status: FamilyStatus,
455}
456
457impl<'input> FamilyItem<'input> {
458 fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
459 let mut family_name = None;
460 let mut status = None;
461
462 for child in node.children() {
463 match child.tag_name().name() {
464 "FamilyName" => {
465 if family_name.is_some() {
466 return Err(FormatError::RepeatedTag("FamilyName"));
467 }
468 family_name = child.text_storage().map(StringStorageExt::to_cow);
469 }
470 "Status" => {
471 if status.is_some() {
472 return Err(FormatError::RepeatedTag("Status"));
473 }
474 let val = child.text().ok_or(FormatError::MissingTag("Status"))?;
475 status = Some(val.parse()?);
476 }
477 _ => assert_empty_text(child)?,
478 }
479 }
480
481 Ok(Self {
482 family_name: family_name.ok_or(FormatError::MissingTag("FamilyName"))?,
483 status: status.ok_or(FormatError::MissingTag("Status"))?,
484 })
485 }
486}
487
488#[derive(Debug)]
490pub struct PluginItem<'input> {
491 pub plugin_id: u32,
492 pub plugin_name: Cow<'input, str>,
493 pub family: Cow<'input, str>,
494 pub status: FamilyStatus,
495}
496
497impl<'input> PluginItem<'input> {
498 fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
499 let mut plugin_id = None;
500 let mut plugin_name = None;
501 let mut family = None;
502 let mut status = None;
503
504 for child in node.children() {
505 match child.tag_name().name() {
506 "PluginId" => {
507 if plugin_id.is_some() {
508 return Err(FormatError::RepeatedTag("PluginId"));
509 }
510 let val = child.text().ok_or(FormatError::MissingTag("PluginId"))?;
511 plugin_id = Some(val.parse()?);
512 }
513 "PluginName" => {
514 if plugin_name.is_some() {
515 return Err(FormatError::RepeatedTag("PluginName"));
516 }
517 plugin_name = child.text_storage().map(StringStorageExt::to_cow);
518 }
519 "Family" => {
520 if family.is_some() {
521 return Err(FormatError::RepeatedTag("Family"));
522 }
523 family = child.text_storage().map(StringStorageExt::to_cow);
524 }
525 "Status" => {
526 if status.is_some() {
527 return Err(FormatError::RepeatedTag("Status"));
528 }
529 let val = child.text().ok_or(FormatError::MissingTag("Status"))?;
530 status = Some(val.parse()?);
531 }
532 _ => assert_empty_text(child)?,
533 }
534 }
535
536 Ok(Self {
537 plugin_id: plugin_id.ok_or(FormatError::MissingTag("PluginId"))?,
538 plugin_name: plugin_name.ok_or(FormatError::MissingTag("PluginName"))?,
539 family: family.ok_or(FormatError::MissingTag("Family"))?,
540 status: status.ok_or(FormatError::MissingTag("Status"))?,
541 })
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use roxmltree::Document;
548
549 use crate::error::FormatError;
550
551 use super::{FamilyStatus, Policy};
552
553 fn parse_policy(xml: &str) -> Result<Policy<'_>, FormatError> {
554 let doc = Document::parse(xml).expect("test XML should parse");
555 let node = doc.root_element();
556 Policy::from_xml_node(node)
557 }
558
559 #[test]
560 fn rejects_missing_required_policy_sections() {
561 let xml = r"<Policy><policyName>p</policyName></Policy>";
562 let err = parse_policy(xml).expect_err("must fail");
563 assert!(matches!(err, FormatError::MissingTag("Preferences")));
564 }
565
566 #[test]
567 fn rejects_repeated_whoami_preference() {
568 let xml = r"
569<Policy>
570 <policyName>p</policyName>
571 <Preferences>
572 <ServerPreferences>
573 <preference><name>whoami</name><value>u1</value></preference>
574 <preference><name>whoami</name><value>u2</value></preference>
575 <preference><name>scan_description</name><value>d</value></preference>
576 <preference><name>TARGET</name><value>127.0.0.1</value></preference>
577 <preference><name>port_range</name><value>default</value></preference>
578 <preference><name>scan_start_timestamp</name><value>1</value></preference>
579 <preference><name>plugin_set</name><value>;1;</value></preference>
580 <preference><name>name</name><value>n</value></preference>
581 </ServerPreferences>
582 <PluginsPreferences/>
583 </Preferences>
584 <FamilySelection/>
585 <IndividualPluginSelection/>
586</Policy>
587";
588 let err = parse_policy(xml).expect_err("must fail");
589 assert!(matches!(err, FormatError::RepeatedTag("whoami")));
590 }
591
592 #[test]
593 fn rejects_preference_item_missing_name_or_value() {
594 let missing_name = r"
595<Policy>
596 <policyName>p</policyName>
597 <Preferences>
598 <ServerPreferences>
599 <preference><value>u</value></preference>
600 <preference><name>scan_description</name><value>d</value></preference>
601 <preference><name>TARGET</name><value>127.0.0.1</value></preference>
602 <preference><name>port_range</name><value>default</value></preference>
603 <preference><name>scan_start_timestamp</name><value>1</value></preference>
604 <preference><name>plugin_set</name><value>;1;</value></preference>
605 <preference><name>name</name><value>n</value></preference>
606 </ServerPreferences>
607 <PluginsPreferences/>
608 </Preferences>
609 <FamilySelection/>
610 <IndividualPluginSelection/>
611</Policy>
612";
613 let err = parse_policy(missing_name).expect_err("must fail");
614 assert!(matches!(err, FormatError::MissingTag("name")));
615
616 let missing_value = r"
617<Policy>
618 <policyName>p</policyName>
619 <Preferences>
620 <ServerPreferences>
621 <preference><name>whoami</name></preference>
622 <preference><name>scan_description</name><value>d</value></preference>
623 <preference><name>TARGET</name><value>127.0.0.1</value></preference>
624 <preference><name>port_range</name><value>default</value></preference>
625 <preference><name>scan_start_timestamp</name><value>1</value></preference>
626 <preference><name>plugin_set</name><value>;1;</value></preference>
627 <preference><name>name</name><value>n</value></preference>
628 </ServerPreferences>
629 <PluginsPreferences/>
630 </Preferences>
631 <FamilySelection/>
632 <IndividualPluginSelection/>
633</Policy>
634";
635 let err = parse_policy(missing_value).expect_err("must fail");
636 assert!(matches!(err, FormatError::MissingTag("value")));
637 }
638
639 #[test]
640 fn family_status_parsing_works() {
641 assert!(matches!("enabled".parse(), Ok(FamilyStatus::Enabled)));
642 assert!(matches!("disabled".parse(), Ok(FamilyStatus::Disabled)));
643 assert!(matches!("mixed".parse(), Ok(FamilyStatus::Mixed)));
644 assert!(matches!(
645 "bad".parse::<FamilyStatus>(),
646 Err(FormatError::UnexpectedFormat("FamilyStatus"))
647 ));
648 }
649
650 #[test]
651 fn minimal_policy_parses() {
652 let minimal_policy_xml = r"
653<Policy>
654 <policyName>p</policyName>
655 <Preferences>
656 <ServerPreferences>
657 <preference><name>whoami</name><value>u</value></preference>
658 <preference><name>scan_description</name><value>d</value></preference>
659 <preference><name>TARGET</name><value>127.0.0.1</value></preference>
660 <preference><name>port_range</name><value>default</value></preference>
661 <preference><name>scan_start_timestamp</name><value>1</value></preference>
662 <preference><name>plugin_set</name><value>;1;</value></preference>
663 <preference><name>name</name><value>n</value></preference>
664 </ServerPreferences>
665 <PluginsPreferences/>
666 </Preferences>
667 <FamilySelection>
668 <FamilyItem>
669 <FamilyName>General</FamilyName>
670 <Status>enabled</Status>
671 </FamilyItem>
672 </FamilySelection>
673 <IndividualPluginSelection>
674 <PluginItem>
675 <PluginId>1</PluginId>
676 <PluginName>x</PluginName>
677 <Family>General</Family>
678 <Status>enabled</Status>
679 </PluginItem>
680 </IndividualPluginSelection>
681</Policy>
682";
683 let parsed = parse_policy(minimal_policy_xml).expect("must parse");
684 assert_eq!(parsed.policy_name, "p");
685 }
686}