1use std::{borrow::Cow, collections::HashMap, str::FromStr};
2
3use jiff::civil::Date;
4use roxmltree::Node;
5
6use crate::{StringStorageExt, error::FormatError};
7
8#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
9pub enum Protocol {
10 Tcp,
11 Udp,
12 Icmp,
13}
14
15impl Protocol {
16 #[must_use]
17 pub const fn as_str(self) -> &'static str {
18 match self {
19 Self::Tcp => "tcp",
20 Self::Udp => "udp",
21 Self::Icmp => "icmp",
22 }
23 }
24}
25
26impl FromStr for Protocol {
27 type Err = FormatError;
28
29 fn from_str(s: &str) -> Result<Self, Self::Err> {
30 match s {
31 "tcp" => Ok(Self::Tcp),
32 "udp" => Ok(Self::Udp),
33 "icmp" => Ok(Self::Icmp),
34 other => Err(FormatError::UnexpectedProtocol(other.into())),
35 }
36 }
37}
38
39#[derive(Debug, PartialEq, Eq, Clone, Copy)]
40pub enum PluginType {
41 Summary,
42 Remote,
43 Combined,
44 Local,
45}
46
47impl FromStr for PluginType {
48 type Err = FormatError;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 match s {
52 "summary" => Ok(Self::Summary),
53 "remote" => Ok(Self::Remote),
54 "combined" => Ok(Self::Combined),
55 "local" => Ok(Self::Local),
56 other => Err(FormatError::UnexpectedPluginType(other.into())),
57 }
58 }
59}
60
61#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
62pub enum Level {
63 None = 0,
64 Low = 1,
65 Medium = 2,
66 High = 3,
67 Critical = 4,
68}
69
70impl Level {
71 fn from_int(int: &str) -> Result<Self, FormatError> {
72 match int {
73 "0" => Ok(Self::None),
74 "1" => Ok(Self::Low),
75 "2" => Ok(Self::Medium),
76 "3" => Ok(Self::High),
77 "4" => Ok(Self::Critical),
78 other => Err(FormatError::UnexpectedLevel(other.into())),
79 }
80 }
81
82 fn from_text(s: &str) -> Result<Self, FormatError> {
83 match s {
84 "None" => Ok(Self::None),
85 "Low" => Ok(Self::Low),
86 "Medium" => Ok(Self::Medium),
87 "High" => Ok(Self::High),
88 "Critical" => Ok(Self::Critical),
89 other => Err(FormatError::UnexpectedLevel(other.into())),
90 }
91 }
92}
93
94#[derive(Debug)]
97pub struct Item<'input> {
98 pub plugin_id: u32,
100 pub plugin_name: Cow<'input, str>,
102 pub port: u16,
104 pub protocol: Protocol,
106 pub svc_name: &'input str,
108 pub severity: Level,
110 pub plugin_family: &'input str,
112 pub plugin_output: Option<Cow<'input, str>>,
114
115 pub solution: Cow<'input, str>,
117 pub script_version: &'input str,
120 pub risk_factor: Level,
122 pub plugin_type: PluginType,
124 pub plugin_publication_date: jiff::civil::Date,
126 pub plugin_modification_date: jiff::civil::Date,
128 pub fname: &'input str,
130 pub description: Cow<'input, str>,
132 pub exploit_available: bool,
134 pub exploited_by_nessus: bool,
136 pub exploitability_ease: Option<&'input str>,
139
140 pub agent: Option<&'input str>,
142
143 pub cvss_vector: Option<&'input str>,
145 pub cvss_temporal_vector: Option<&'input str>,
147 pub cvss3_vector: Option<&'input str>,
149 pub cvss3_temporal_vector: Option<&'input str>,
151 pub cvss4_vector: Option<&'input str>,
153 pub cvss4_threat_vector: Option<&'input str>,
155
156 pub others: HashMap<&'input str, Vec<Cow<'input, str>>>,
159}
160
161impl<'input> Item<'input> {
162 #[expect(clippy::too_many_lines, clippy::similar_names)]
163 pub(crate) fn from_xml_node(node: Node<'_, 'input>) -> Result<Self, FormatError> {
164 let mut plugin_id = None;
165 let mut plugin_name = None;
166 let mut port = None;
167 let mut protocol = None;
168 let mut svc_name = None;
169 let mut severity = None;
170 let mut plugin_family = None;
171
172 for attribute in node.attributes() {
173 match attribute.name() {
174 "pluginID" => {
175 if plugin_id.is_some() {
176 return Err(FormatError::RepeatedTag("pluginID"));
177 }
178 plugin_id = Some(attribute.value_storage().parse()?);
179 }
180 "pluginName" => {
181 if plugin_name.is_some() {
182 return Err(FormatError::RepeatedTag("pluginName"));
183 }
184 plugin_name = Some(attribute.value_storage().to_cow());
185 }
186 "port" => {
187 if port.is_some() {
188 return Err(FormatError::RepeatedTag("port"));
189 }
190 port = Some(attribute.value().parse()?);
191 }
192 "protocol" => {
193 if protocol.is_some() {
194 return Err(FormatError::RepeatedTag("protocol"));
195 }
196 protocol = Some(attribute.value_storage().parse()?);
197 }
198 "svc_name" => {
199 if svc_name.is_some() {
200 return Err(FormatError::RepeatedTag("svc_name"));
201 }
202 svc_name = Some(attribute.value_storage().to_str()?);
203 }
204 "severity" => {
205 if severity.is_some() {
206 return Err(FormatError::RepeatedTag("severity"));
207 }
208 severity = Some(Level::from_int(attribute.value())?);
209 }
210 "pluginFamily" => {
211 if plugin_family.is_some() {
212 return Err(FormatError::RepeatedTag("pluginFamily"));
213 }
214 plugin_family = Some(attribute.value_storage().to_str()?);
215 }
216
217 other => return Err(FormatError::UnexpectedXmlAttribute(other.into())),
218 }
219 }
220
221 let mut plugin_output = None;
222
223 let mut solution = None;
224 let mut script_version = None;
225 let mut risk_factor = None;
226 let mut plugin_type = None;
227 let mut plugin_publication_date = None;
228 let mut plugin_modification_date = None;
229 let mut fname = None;
230 let mut description = None;
231
232 let mut agent = None;
233 let mut cvss_vector = None;
234 let mut cvss3_vector = None;
235 let mut cvss_temporal_vector = None;
236 let mut cvss3_temporal_vector = None;
237 let mut cvss4_vector = None;
238 let mut cvss4_threat_vector = None;
239 let mut exploitability_ease = None;
240 let mut exploit_available = None;
241 let mut exploited_by_nessus = None;
242
243 let mut others: HashMap<_, Vec<_>> = HashMap::new();
244
245 for child in node.children() {
246 if child.is_text() {
247 if let Some(text) = child.text()
248 && !text.trim().is_empty()
249 {
250 return Err(FormatError::UnexpectedText(text.into()));
251 }
252 continue;
253 }
254
255 let name = child.tag_name().name();
256 if let Some(value) = child.text_storage() {
257 match name {
258 "plugin_output" => {
259 if plugin_output.is_some() {
260 return Err(FormatError::RepeatedTag("plugin_output"));
261 }
262 plugin_output = Some(value.to_cow());
263 }
264 "solution" => {
265 if solution.is_some() {
266 return Err(FormatError::RepeatedTag("solution"));
267 }
268 solution = Some(value.to_cow());
269 }
270 "description" => {
271 if description.is_some() {
272 return Err(FormatError::RepeatedTag("description"));
273 }
274 description = Some(value.to_cow());
275 }
276
277 "script_version" => {
278 if script_version.is_some() {
279 return Err(FormatError::RepeatedTag("script_version"));
280 }
281 script_version = Some(value.to_str()?);
282 }
283 "risk_factor" => {
284 if risk_factor.is_some() {
285 return Err(FormatError::RepeatedTag("risk_factor"));
286 }
287 risk_factor = Some(Level::from_text(value.as_str())?);
288 }
289 "plugin_type" => {
290 if plugin_type.is_some() {
291 return Err(FormatError::RepeatedTag("plugin_type"));
292 }
293 plugin_type = Some(value.parse()?);
294 }
295 "plugin_publication_date" => {
296 if plugin_publication_date.is_some() {
297 return Err(FormatError::RepeatedTag("plugin_publication_date"));
298 }
299 plugin_publication_date = Some(Date::strptime("%Y/%m/%d", value.as_str())?);
300 }
301 "plugin_modification_date" => {
302 if plugin_modification_date.is_some() {
303 return Err(FormatError::RepeatedTag("plugin_modification_date"));
304 }
305 plugin_modification_date =
306 Some(Date::strptime("%Y/%m/%d", value.as_str())?);
307 }
308 "fname" => {
309 if fname.is_some() {
310 return Err(FormatError::RepeatedTag("fname"));
311 }
312 fname = Some(value.to_str()?);
313 }
314
315 "agent" => {
316 if agent.is_some() {
317 return Err(FormatError::RepeatedTag("agent"));
318 }
319 agent = Some(value.to_str()?);
320 }
321 "cvss_vector" => {
322 if cvss_vector.is_some() {
323 return Err(FormatError::RepeatedTag("cvss_vector"));
324 }
325 cvss_vector = Some(value.to_str()?);
326 }
327 "cvss3_vector" => {
328 if cvss3_vector.is_some() {
329 return Err(FormatError::RepeatedTag("cvss3_vector"));
330 }
331 cvss3_vector = Some(value.to_str()?);
332 }
333 "cvss_temporal_vector" => {
334 if cvss_temporal_vector.is_some() {
335 return Err(FormatError::RepeatedTag("cvss_temporal_vector"));
336 }
337 cvss_temporal_vector = Some(value.to_str()?);
338 }
339 "cvss3_temporal_vector" => {
340 if cvss3_temporal_vector.is_some() {
341 return Err(FormatError::RepeatedTag("cvss3_temporal_vector"));
342 }
343 cvss3_temporal_vector = Some(value.to_str()?);
344 }
345 "cvss4_vector" => {
346 if cvss4_vector.is_some() {
347 return Err(FormatError::RepeatedTag("cvss4_vector"));
348 }
349 cvss4_vector = Some(value.to_str()?);
350 }
351 "cvss4_threat_vector" => {
352 if cvss4_threat_vector.is_some() {
353 return Err(FormatError::RepeatedTag("cvss4_threat_vector"));
354 }
355 cvss4_threat_vector = Some(value.to_str()?);
356 }
357 "exploitability_ease" => {
358 if exploitability_ease.is_some() {
359 return Err(FormatError::RepeatedTag("exploitability_ease"));
360 }
361 exploitability_ease = Some(value.to_str()?);
362 }
363 "exploit_available" => {
364 if exploit_available.is_some() {
365 return Err(FormatError::RepeatedTag("exploit_available"));
366 }
367 exploit_available = Some(value.as_str() == "true");
368 }
369 "exploited_by_nessus" => {
370 if exploited_by_nessus.is_some() {
371 return Err(FormatError::RepeatedTag("exploited_by_nessus"));
372 }
373 exploited_by_nessus = Some(value.as_str() == "true");
374 }
375
376 _ => others.entry(name).or_default().push(value.to_cow()),
377 }
378 }
379 }
380
381 Ok(Self {
382 plugin_id: plugin_id.ok_or(FormatError::MissingAttribute("pluginID"))?,
383 plugin_name: plugin_name.ok_or(FormatError::MissingAttribute("pluginName"))?,
384 port: port.ok_or(FormatError::MissingAttribute("port"))?,
385 protocol: protocol.ok_or(FormatError::MissingAttribute("protocol"))?,
386 svc_name: svc_name.ok_or(FormatError::MissingAttribute("svc_name"))?,
387 severity: severity.ok_or(FormatError::MissingAttribute("severity"))?,
388 plugin_family: plugin_family.ok_or(FormatError::MissingAttribute("pluginFamily"))?,
389 solution: solution.ok_or(FormatError::MissingTag("solution"))?,
390 script_version: script_version.ok_or(FormatError::MissingTag("script_version"))?,
391 risk_factor: risk_factor.ok_or(FormatError::MissingTag("risk_factor"))?,
392 plugin_type: plugin_type.ok_or(FormatError::MissingTag("plugin_type"))?,
393 plugin_publication_date: plugin_publication_date
394 .ok_or(FormatError::MissingTag("plugin_publication_date"))?,
395 plugin_modification_date: plugin_modification_date
396 .ok_or(FormatError::MissingTag("plugin_modification_date"))?,
397 fname: fname.ok_or(FormatError::MissingTag("fname"))?,
398 description: description.ok_or(FormatError::MissingTag("description"))?,
399 plugin_output,
400 agent,
401 cvss_vector,
402 cvss3_vector,
403 cvss_temporal_vector,
404 cvss3_temporal_vector,
405 cvss4_vector,
406 cvss4_threat_vector,
407 exploitability_ease,
408 exploit_available: exploit_available == Some(true),
409 exploited_by_nessus: exploited_by_nessus == Some(true),
410 others,
411 })
412 }
413}