bugcrowd_vrt/
cvss_v3.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5/// CVSS v3.x Attack Vector
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum AttackVector {
8    /// Network (N) - Exploitable remotely
9    #[serde(rename = "N")]
10    Network,
11    /// Adjacent (A) - Requires local network access
12    #[serde(rename = "A")]
13    Adjacent,
14    /// Local (L) - Requires local access
15    #[serde(rename = "L")]
16    Local,
17    /// Physical (P) - Requires physical access
18    #[serde(rename = "P")]
19    Physical,
20}
21
22/// CVSS v3.x Attack Complexity
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum AttackComplexity {
25    /// Low (L) - No special conditions
26    #[serde(rename = "L")]
27    Low,
28    /// High (H) - Requires special conditions
29    #[serde(rename = "H")]
30    High,
31}
32
33/// CVSS v3.x Privileges Required
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum PrivilegesRequired {
36    /// None (N) - No privileges required
37    #[serde(rename = "N")]
38    None,
39    /// Low (L) - Basic user privileges
40    #[serde(rename = "L")]
41    Low,
42    /// High (H) - Admin/elevated privileges
43    #[serde(rename = "H")]
44    High,
45}
46
47/// CVSS v3.x User Interaction
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49pub enum UserInteraction {
50    /// None (N) - No user interaction required
51    #[serde(rename = "N")]
52    None,
53    /// Required (R) - User interaction required
54    #[serde(rename = "R")]
55    Required,
56}
57
58/// CVSS v3.x Scope
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60pub enum Scope {
61    /// Unchanged (U) - Scope doesn't change
62    #[serde(rename = "U")]
63    Unchanged,
64    /// Changed (C) - Scope changes
65    #[serde(rename = "C")]
66    Changed,
67}
68
69/// CVSS v3.x Impact level (for C, I, A)
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub enum Impact {
72    /// None (N) - No impact
73    #[serde(rename = "N")]
74    None,
75    /// Low (L) - Low impact
76    #[serde(rename = "L")]
77    Low,
78    /// High (H) - High impact
79    #[serde(rename = "H")]
80    High,
81}
82
83/// A parsed CVSS v3.x vector string
84///
85/// CVSS v3 vectors follow the format:
86/// `AV:[NALP]/AC:[LH]/PR:[NLH]/UI:[NR]/S:[UC]/C:[NLH]/I:[NLH]/A:[NLH]`
87///
88/// # Example
89/// ```
90/// use bugcrowd_vrt::CvssV3Vector;
91/// use std::str::FromStr;
92///
93/// let vector = CvssV3Vector::from_str("AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")
94///     .expect("Invalid CVSS vector");
95///
96/// assert_eq!(vector.to_string(), "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H");
97/// ```
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(try_from = "String", into = "String")]
100pub struct CvssV3Vector {
101    /// Attack Vector
102    pub attack_vector: AttackVector,
103    /// Attack Complexity
104    pub attack_complexity: AttackComplexity,
105    /// Privileges Required
106    pub privileges_required: PrivilegesRequired,
107    /// User Interaction
108    pub user_interaction: UserInteraction,
109    /// Scope
110    pub scope: Scope,
111    /// Confidentiality Impact
112    pub confidentiality: Impact,
113    /// Integrity Impact
114    pub integrity: Impact,
115    /// Availability Impact
116    pub availability: Impact,
117}
118
119impl CvssV3Vector {
120    /// Creates a new CVSS v3 vector with all fields
121    #[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    /// Returns true if this vector represents no impact (all CIA are None)
145    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    /// Returns true if this is a critical severity vector (all CIA are High)
152    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
240// Helper parsing functions
241fn 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
293// Helper display functions
294fn 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/// Metadata for the CVSS v3 mapping
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct CvssV3MappingMetadata {
343    /// Default CVSS v3 vector
344    pub default: CvssV3Vector,
345}
346
347/// A node in the VRT to CVSS v3 mapping tree
348#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
349pub struct CvssV3MappingNode {
350    /// VRT identifier (e.g., "cross_site_scripting_xss")
351    pub id: String,
352
353    /// Associated CVSS v3 vector
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub cvss_v3: Option<CvssV3Vector>,
356
357    /// Child mappings (for hierarchical structure)
358    #[serde(default, skip_serializing_if = "Vec::is_empty")]
359    pub children: Vec<CvssV3MappingNode>,
360}
361
362impl CvssV3MappingNode {
363    /// Returns true if this node has a CVSS v3 mapping
364    pub fn has_cvss_mapping(&self) -> bool {
365        self.cvss_v3.is_some()
366    }
367
368    /// Returns true if this node has children
369    pub fn has_children(&self) -> bool {
370        !self.children.is_empty()
371    }
372
373    /// Recursively finds a mapping node by VRT ID
374    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    /// Returns all leaf nodes (nodes with CVSS mappings but no children)
389    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/// The complete VRT to CVSS v3 mapping document
405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
406pub struct CvssV3Mapping {
407    /// Metadata about the mapping
408    pub metadata: CvssV3MappingMetadata,
409
410    /// The mapping content (root nodes)
411    pub content: Vec<CvssV3MappingNode>,
412}
413
414impl CvssV3Mapping {
415    /// Finds a mapping node by VRT ID across all root nodes
416    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    /// Looks up the CVSS v3 vector for a given VRT ID
426    ///
427    /// # Example
428    /// ```no_run
429    /// use bugcrowd_vrt::load_cvss_v3_mapping_from_file;
430    ///
431    /// let mapping = load_cvss_v3_mapping_from_file("cvss_v3.json")
432    ///     .expect("Failed to load mapping");
433    ///
434    /// if let Some(vector) = mapping.lookup_cvss("cross_site_scripting_xss") {
435    ///     println!("CVSS Vector: {}", vector);
436    /// }
437    /// ```
438    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    /// Returns statistics about the mapping
444    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/// Statistics about a CVSS v3 mapping
456#[derive(Debug, Clone, Default, PartialEq, Eq)]
457pub struct CvssV3Statistics {
458    /// Total number of VRT nodes
459    pub total_nodes: usize,
460    /// Number of nodes with CVSS mappings
461    pub nodes_with_mappings: usize,
462    /// Number of nodes without CVSS mappings
463    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}