1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum AttackVector {
8 #[serde(rename = "N")]
10 Network,
11 #[serde(rename = "A")]
13 Adjacent,
14 #[serde(rename = "L")]
16 Local,
17 #[serde(rename = "P")]
19 Physical,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum AttackComplexity {
25 #[serde(rename = "L")]
27 Low,
28 #[serde(rename = "H")]
30 High,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum PrivilegesRequired {
36 #[serde(rename = "N")]
38 None,
39 #[serde(rename = "L")]
41 Low,
42 #[serde(rename = "H")]
44 High,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49pub enum UserInteraction {
50 #[serde(rename = "N")]
52 None,
53 #[serde(rename = "R")]
55 Required,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60pub enum Scope {
61 #[serde(rename = "U")]
63 Unchanged,
64 #[serde(rename = "C")]
66 Changed,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub enum Impact {
72 #[serde(rename = "N")]
74 None,
75 #[serde(rename = "L")]
77 Low,
78 #[serde(rename = "H")]
80 High,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(try_from = "String", into = "String")]
100pub struct CvssV3Vector {
101 pub attack_vector: AttackVector,
103 pub attack_complexity: AttackComplexity,
105 pub privileges_required: PrivilegesRequired,
107 pub user_interaction: UserInteraction,
109 pub scope: Scope,
111 pub confidentiality: Impact,
113 pub integrity: Impact,
115 pub availability: Impact,
117}
118
119impl CvssV3Vector {
120 #[allow(clippy::too_many_arguments)]
122 pub fn new(
123 attack_vector: AttackVector,
124 attack_complexity: AttackComplexity,
125 privileges_required: PrivilegesRequired,
126 user_interaction: UserInteraction,
127 scope: Scope,
128 confidentiality: Impact,
129 integrity: Impact,
130 availability: Impact,
131 ) -> Self {
132 Self {
133 attack_vector,
134 attack_complexity,
135 privileges_required,
136 user_interaction,
137 scope,
138 confidentiality,
139 integrity,
140 availability,
141 }
142 }
143
144 pub fn is_no_impact(&self) -> bool {
146 matches!(self.confidentiality, Impact::None)
147 && matches!(self.integrity, Impact::None)
148 && matches!(self.availability, Impact::None)
149 }
150
151 pub fn is_critical(&self) -> bool {
153 matches!(self.confidentiality, Impact::High)
154 && matches!(self.integrity, Impact::High)
155 && matches!(self.availability, Impact::High)
156 }
157}
158
159impl FromStr for CvssV3Vector {
160 type Err = String;
161
162 fn from_str(s: &str) -> Result<Self, Self::Err> {
163 let parts: Vec<&str> = s.split('/').collect();
164 if parts.len() != 8 {
165 return Err(format!("Expected 8 parts, got {}", parts.len()));
166 }
167
168 let mut av = None;
169 let mut ac = None;
170 let mut pr = None;
171 let mut ui = None;
172 let mut s_scope = None;
173 let mut c = None;
174 let mut i = None;
175 let mut a = None;
176
177 for part in parts {
178 let kv: Vec<&str> = part.split(':').collect();
179 if kv.len() != 2 {
180 return Err(format!("Invalid part: {}", part));
181 }
182
183 match kv[0] {
184 "AV" => av = Some(parse_av(kv[1])?),
185 "AC" => ac = Some(parse_ac(kv[1])?),
186 "PR" => pr = Some(parse_pr(kv[1])?),
187 "UI" => ui = Some(parse_ui(kv[1])?),
188 "S" => s_scope = Some(parse_scope(kv[1])?),
189 "C" => c = Some(parse_impact(kv[1])?),
190 "I" => i = Some(parse_impact(kv[1])?),
191 "A" => a = Some(parse_impact(kv[1])?),
192 _ => return Err(format!("Unknown metric: {}", kv[0])),
193 }
194 }
195
196 Ok(CvssV3Vector {
197 attack_vector: av.ok_or("Missing AV")?,
198 attack_complexity: ac.ok_or("Missing AC")?,
199 privileges_required: pr.ok_or("Missing PR")?,
200 user_interaction: ui.ok_or("Missing UI")?,
201 scope: s_scope.ok_or("Missing S")?,
202 confidentiality: c.ok_or("Missing C")?,
203 integrity: i.ok_or("Missing I")?,
204 availability: a.ok_or("Missing A")?,
205 })
206 }
207}
208
209impl fmt::Display for CvssV3Vector {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 write!(
212 f,
213 "AV:{}/AC:{}/PR:{}/UI:{}/S:{}/C:{}/I:{}/A:{}",
214 av_to_str(self.attack_vector),
215 ac_to_str(self.attack_complexity),
216 pr_to_str(self.privileges_required),
217 ui_to_str(self.user_interaction),
218 scope_to_str(self.scope),
219 impact_to_str(self.confidentiality),
220 impact_to_str(self.integrity),
221 impact_to_str(self.availability),
222 )
223 }
224}
225
226impl From<CvssV3Vector> for String {
227 fn from(vector: CvssV3Vector) -> String {
228 vector.to_string()
229 }
230}
231
232impl TryFrom<String> for CvssV3Vector {
233 type Error = String;
234
235 fn try_from(s: String) -> Result<Self, Self::Error> {
236 CvssV3Vector::from_str(&s)
237 }
238}
239
240fn parse_av(s: &str) -> Result<AttackVector, String> {
242 match s {
243 "N" => Ok(AttackVector::Network),
244 "A" => Ok(AttackVector::Adjacent),
245 "L" => Ok(AttackVector::Local),
246 "P" => Ok(AttackVector::Physical),
247 _ => Err(format!("Invalid AV value: {}", s)),
248 }
249}
250
251fn parse_ac(s: &str) -> Result<AttackComplexity, String> {
252 match s {
253 "L" => Ok(AttackComplexity::Low),
254 "H" => Ok(AttackComplexity::High),
255 _ => Err(format!("Invalid AC value: {}", s)),
256 }
257}
258
259fn parse_pr(s: &str) -> Result<PrivilegesRequired, String> {
260 match s {
261 "N" => Ok(PrivilegesRequired::None),
262 "L" => Ok(PrivilegesRequired::Low),
263 "H" => Ok(PrivilegesRequired::High),
264 _ => Err(format!("Invalid PR value: {}", s)),
265 }
266}
267
268fn parse_ui(s: &str) -> Result<UserInteraction, String> {
269 match s {
270 "N" => Ok(UserInteraction::None),
271 "R" => Ok(UserInteraction::Required),
272 _ => Err(format!("Invalid UI value: {}", s)),
273 }
274}
275
276fn parse_scope(s: &str) -> Result<Scope, String> {
277 match s {
278 "U" => Ok(Scope::Unchanged),
279 "C" => Ok(Scope::Changed),
280 _ => Err(format!("Invalid S value: {}", s)),
281 }
282}
283
284fn parse_impact(s: &str) -> Result<Impact, String> {
285 match s {
286 "N" => Ok(Impact::None),
287 "L" => Ok(Impact::Low),
288 "H" => Ok(Impact::High),
289 _ => Err(format!("Invalid impact value: {}", s)),
290 }
291}
292
293fn av_to_str(av: AttackVector) -> &'static str {
295 match av {
296 AttackVector::Network => "N",
297 AttackVector::Adjacent => "A",
298 AttackVector::Local => "L",
299 AttackVector::Physical => "P",
300 }
301}
302
303fn ac_to_str(ac: AttackComplexity) -> &'static str {
304 match ac {
305 AttackComplexity::Low => "L",
306 AttackComplexity::High => "H",
307 }
308}
309
310fn pr_to_str(pr: PrivilegesRequired) -> &'static str {
311 match pr {
312 PrivilegesRequired::None => "N",
313 PrivilegesRequired::Low => "L",
314 PrivilegesRequired::High => "H",
315 }
316}
317
318fn ui_to_str(ui: UserInteraction) -> &'static str {
319 match ui {
320 UserInteraction::None => "N",
321 UserInteraction::Required => "R",
322 }
323}
324
325fn scope_to_str(s: Scope) -> &'static str {
326 match s {
327 Scope::Unchanged => "U",
328 Scope::Changed => "C",
329 }
330}
331
332fn impact_to_str(i: Impact) -> &'static str {
333 match i {
334 Impact::None => "N",
335 Impact::Low => "L",
336 Impact::High => "H",
337 }
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct CvssV3MappingMetadata {
343 pub default: CvssV3Vector,
345}
346
347#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
349pub struct CvssV3MappingNode {
350 pub id: String,
352
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub cvss_v3: Option<CvssV3Vector>,
356
357 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 pub children: Vec<CvssV3MappingNode>,
360}
361
362impl CvssV3MappingNode {
363 pub fn has_cvss_mapping(&self) -> bool {
365 self.cvss_v3.is_some()
366 }
367
368 pub fn has_children(&self) -> bool {
370 !self.children.is_empty()
371 }
372
373 pub fn find_by_id(&self, vrt_id: &str) -> Option<&CvssV3MappingNode> {
375 if self.id == vrt_id {
376 return Some(self);
377 }
378
379 for child in &self.children {
380 if let Some(found) = child.find_by_id(vrt_id) {
381 return Some(found);
382 }
383 }
384
385 None
386 }
387
388 pub fn leaf_nodes(&self) -> Vec<&CvssV3MappingNode> {
390 let mut leaves = Vec::new();
391
392 if self.has_cvss_mapping() && !self.has_children() {
393 leaves.push(self);
394 }
395
396 for child in &self.children {
397 leaves.extend(child.leaf_nodes());
398 }
399
400 leaves
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
406pub struct CvssV3Mapping {
407 pub metadata: CvssV3MappingMetadata,
409
410 pub content: Vec<CvssV3MappingNode>,
412}
413
414impl CvssV3Mapping {
415 pub fn find_by_vrt_id(&self, vrt_id: &str) -> Option<&CvssV3MappingNode> {
417 for node in &self.content {
418 if let Some(found) = node.find_by_id(vrt_id) {
419 return Some(found);
420 }
421 }
422 None
423 }
424
425 pub fn lookup_cvss(&self, vrt_id: &str) -> Option<&CvssV3Vector> {
439 self.find_by_vrt_id(vrt_id)
440 .and_then(|node| node.cvss_v3.as_ref())
441 }
442
443 pub fn statistics(&self) -> CvssV3Statistics {
445 let mut stats = CvssV3Statistics::default();
446
447 for node in &self.content {
448 collect_stats(node, &mut stats);
449 }
450
451 stats
452 }
453}
454
455#[derive(Debug, Clone, Default, PartialEq, Eq)]
457pub struct CvssV3Statistics {
458 pub total_nodes: usize,
460 pub nodes_with_mappings: usize,
462 pub nodes_without_mappings: usize,
464}
465
466fn collect_stats(node: &CvssV3MappingNode, stats: &mut CvssV3Statistics) {
467 stats.total_nodes += 1;
468
469 if node.has_cvss_mapping() {
470 stats.nodes_with_mappings += 1;
471 } else {
472 stats.nodes_without_mappings += 1;
473 }
474
475 for child in &node.children {
476 collect_stats(child, stats);
477 }
478}