use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CweId(pub String);
impl CweId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn number(&self) -> Option<u32> {
self.0.strip_prefix("CWE-")?.parse().ok()
}
pub fn is_valid(&self) -> bool {
self.0.starts_with("CWE-") && self.number().is_some()
}
}
impl std::fmt::Display for CweId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for CweId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for CweId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MappingMetadata {
pub default: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CweMappingNode {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwe: Option<Vec<CweId>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<CweMappingNode>,
}
impl CweMappingNode {
pub fn has_cwe_mapping(&self) -> bool {
self.cwe.as_ref().map_or(false, |cwes| !cwes.is_empty())
}
pub fn has_children(&self) -> bool {
!self.children.is_empty()
}
pub fn find_by_id(&self, vrt_id: &str) -> Option<&CweMappingNode> {
if self.id == vrt_id {
return Some(self);
}
for child in &self.children {
if let Some(found) = child.find_by_id(vrt_id) {
return Some(found);
}
}
None
}
pub fn cwe_ids(&self) -> Vec<&CweId> {
self.cwe
.as_ref()
.map(|cwes| cwes.iter().collect())
.unwrap_or_default()
}
pub fn all_cwe_ids(&self) -> Vec<&CweId> {
let mut ids = self.cwe_ids();
for child in &self.children {
ids.extend(child.all_cwe_ids());
}
ids
}
pub fn leaf_nodes(&self) -> Vec<&CweMappingNode> {
let mut leaves = Vec::new();
if self.has_cwe_mapping() && !self.has_children() {
leaves.push(self);
}
for child in &self.children {
leaves.extend(child.leaf_nodes());
}
leaves
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CweMapping {
pub metadata: MappingMetadata,
pub content: Vec<CweMappingNode>,
}
impl CweMapping {
pub fn find_by_vrt_id(&self, vrt_id: &str) -> Option<&CweMappingNode> {
for node in &self.content {
if let Some(found) = node.find_by_id(vrt_id) {
return Some(found);
}
}
None
}
pub fn lookup_cwe(&self, vrt_id: &str) -> Option<Vec<&CweId>> {
self.find_by_vrt_id(vrt_id)
.and_then(|node| node.cwe.as_ref())
.map(|cwes| cwes.iter().collect())
}
pub fn all_cwe_ids(&self) -> Vec<&CweId> {
let mut ids = Vec::new();
for node in &self.content {
ids.extend(node.all_cwe_ids());
}
let mut seen = std::collections::HashSet::new();
ids.into_iter()
.filter(|id| seen.insert(id.as_str()))
.collect()
}
pub fn statistics(&self) -> MappingStatistics {
let mut stats = MappingStatistics::default();
for node in &self.content {
collect_stats(node, &mut stats);
}
stats
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MappingStatistics {
pub total_nodes: usize,
pub nodes_with_mappings: usize,
pub nodes_without_mappings: usize,
pub unique_cwe_ids: usize,
}
fn collect_stats(node: &CweMappingNode, stats: &mut MappingStatistics) {
stats.total_nodes += 1;
if node.has_cwe_mapping() {
stats.nodes_with_mappings += 1;
} else {
stats.nodes_without_mappings += 1;
}
for child in &node.children {
collect_stats(child, stats);
}
}