bugcrowd_vrt/
cwe_mapping.rs

1use serde::{Deserialize, Serialize};
2
3/// A CWE (Common Weakness Enumeration) identifier
4///
5/// CWE IDs follow the format "CWE-{number}" (e.g., "CWE-79" for Cross-Site Scripting)
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct CweId(pub String);
8
9impl CweId {
10    /// Creates a new CWE ID
11    ///
12    /// # Example
13    /// ```
14    /// use bugcrowd_vrt::CweId;
15    ///
16    /// let cwe = CweId::new("CWE-79");
17    /// assert_eq!(cwe.as_str(), "CWE-79");
18    /// ```
19    pub fn new(id: impl Into<String>) -> Self {
20        Self(id.into())
21    }
22
23    /// Returns the CWE ID as a string slice
24    pub fn as_str(&self) -> &str {
25        &self.0
26    }
27
28    /// Extracts the numeric portion of the CWE ID
29    ///
30    /// # Example
31    /// ```
32    /// use bugcrowd_vrt::CweId;
33    ///
34    /// let cwe = CweId::new("CWE-79");
35    /// assert_eq!(cwe.number(), Some(79));
36    /// ```
37    pub fn number(&self) -> Option<u32> {
38        self.0.strip_prefix("CWE-")?.parse().ok()
39    }
40
41    /// Validates that the CWE ID follows the correct format
42    pub fn is_valid(&self) -> bool {
43        self.0.starts_with("CWE-") && self.number().is_some()
44    }
45}
46
47impl std::fmt::Display for CweId {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "{}", self.0)
50    }
51}
52
53impl From<String> for CweId {
54    fn from(s: String) -> Self {
55        Self(s)
56    }
57}
58
59impl From<&str> for CweId {
60    fn from(s: &str) -> Self {
61        Self(s.to_string())
62    }
63}
64
65/// Metadata for the CWE mapping
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct MappingMetadata {
68    /// Default CWE mapping (typically null)
69    pub default: Option<String>,
70}
71
72/// A node in the VRT to CWE mapping tree
73///
74/// This represents either a leaf mapping or a parent node with children.
75/// Each node maps a VRT ID to zero or more CWE IDs.
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct CweMappingNode {
78    /// VRT identifier (e.g., "cross_site_scripting_xss")
79    pub id: String,
80
81    /// Associated CWE identifiers (e.g., ["CWE-79"])
82    /// Can be null if no CWE mapping exists for this VRT ID
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub cwe: Option<Vec<CweId>>,
85
86    /// Child mappings (for hierarchical structure)
87    #[serde(default, skip_serializing_if = "Vec::is_empty")]
88    pub children: Vec<CweMappingNode>,
89}
90
91impl CweMappingNode {
92    /// Returns true if this node has CWE mappings
93    pub fn has_cwe_mapping(&self) -> bool {
94        self.cwe.as_ref().map_or(false, |cwes| !cwes.is_empty())
95    }
96
97    /// Returns true if this node has children
98    pub fn has_children(&self) -> bool {
99        !self.children.is_empty()
100    }
101
102    /// Recursively finds a mapping node by VRT ID
103    pub fn find_by_id(&self, vrt_id: &str) -> Option<&CweMappingNode> {
104        if self.id == vrt_id {
105            return Some(self);
106        }
107
108        for child in &self.children {
109            if let Some(found) = child.find_by_id(vrt_id) {
110                return Some(found);
111            }
112        }
113
114        None
115    }
116
117    /// Returns all CWE IDs associated with this node (non-recursive)
118    pub fn cwe_ids(&self) -> Vec<&CweId> {
119        self.cwe
120            .as_ref()
121            .map(|cwes| cwes.iter().collect())
122            .unwrap_or_default()
123    }
124
125    /// Returns all CWE IDs in the subtree rooted at this node (recursive)
126    pub fn all_cwe_ids(&self) -> Vec<&CweId> {
127        let mut ids = self.cwe_ids();
128
129        for child in &self.children {
130            ids.extend(child.all_cwe_ids());
131        }
132
133        ids
134    }
135
136    /// Returns all leaf nodes (nodes with CWE mappings but no children)
137    pub fn leaf_nodes(&self) -> Vec<&CweMappingNode> {
138        let mut leaves = Vec::new();
139
140        if self.has_cwe_mapping() && !self.has_children() {
141            leaves.push(self);
142        }
143
144        for child in &self.children {
145            leaves.extend(child.leaf_nodes());
146        }
147
148        leaves
149    }
150}
151
152/// The complete VRT to CWE mapping document
153///
154/// This represents the root structure of a CWE mapping file.
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct CweMapping {
157    /// Metadata about the mapping
158    pub metadata: MappingMetadata,
159
160    /// The mapping content (root nodes)
161    pub content: Vec<CweMappingNode>,
162}
163
164impl CweMapping {
165    /// Finds a mapping node by VRT ID across all root nodes
166    pub fn find_by_vrt_id(&self, vrt_id: &str) -> Option<&CweMappingNode> {
167        for node in &self.content {
168            if let Some(found) = node.find_by_id(vrt_id) {
169                return Some(found);
170            }
171        }
172        None
173    }
174
175    /// Looks up CWE IDs for a given VRT ID
176    ///
177    /// # Example
178    /// ```no_run
179    /// use bugcrowd_vrt::load_cwe_mapping_from_file;
180    ///
181    /// let mapping = load_cwe_mapping_from_file("cwe.mappings.json")
182    ///     .expect("Failed to load mapping");
183    ///
184    /// if let Some(cwes) = mapping.lookup_cwe("cross_site_scripting_xss") {
185    ///     for cwe in cwes {
186    ///         println!("CWE: {}", cwe);
187    ///     }
188    /// }
189    /// ```
190    pub fn lookup_cwe(&self, vrt_id: &str) -> Option<Vec<&CweId>> {
191        self.find_by_vrt_id(vrt_id)
192            .and_then(|node| node.cwe.as_ref())
193            .map(|cwes| cwes.iter().collect())
194    }
195
196    /// Returns all unique CWE IDs present in the mapping
197    pub fn all_cwe_ids(&self) -> Vec<&CweId> {
198        let mut ids = Vec::new();
199        for node in &self.content {
200            ids.extend(node.all_cwe_ids());
201        }
202
203        // Remove duplicates while preserving order
204        let mut seen = std::collections::HashSet::new();
205        ids.into_iter()
206            .filter(|id| seen.insert(id.as_str()))
207            .collect()
208    }
209
210    /// Returns statistics about the mapping
211    pub fn statistics(&self) -> MappingStatistics {
212        let mut stats = MappingStatistics::default();
213
214        for node in &self.content {
215            collect_stats(node, &mut stats);
216        }
217
218        stats
219    }
220}
221
222/// Statistics about a CWE mapping
223#[derive(Debug, Clone, Default, PartialEq, Eq)]
224pub struct MappingStatistics {
225    /// Total number of VRT nodes
226    pub total_nodes: usize,
227    /// Number of nodes with CWE mappings
228    pub nodes_with_mappings: usize,
229    /// Number of nodes without CWE mappings
230    pub nodes_without_mappings: usize,
231    /// Total number of unique CWE IDs
232    pub unique_cwe_ids: usize,
233}
234
235fn collect_stats(node: &CweMappingNode, stats: &mut MappingStatistics) {
236    stats.total_nodes += 1;
237
238    if node.has_cwe_mapping() {
239        stats.nodes_with_mappings += 1;
240    } else {
241        stats.nodes_without_mappings += 1;
242    }
243
244    for child in &node.children {
245        collect_stats(child, stats);
246    }
247}