Skip to main content

sbom_tools/model/
vulnerability.rs

1//! Vulnerability data structures.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7/// Reference to a vulnerability affecting a component
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct VulnerabilityRef {
10    /// Vulnerability identifier (CVE, GHSA, etc.)
11    pub id: String,
12    /// Source database
13    pub source: VulnerabilitySource,
14    /// Severity level
15    pub severity: Option<Severity>,
16    /// CVSS scores
17    pub cvss: Vec<CvssScore>,
18    /// Affected version ranges
19    pub affected_versions: Vec<String>,
20    /// Remediation information
21    pub remediation: Option<Remediation>,
22    /// Description
23    pub description: Option<String>,
24    /// CWE identifiers
25    pub cwes: Vec<String>,
26    /// Published date
27    pub published: Option<DateTime<Utc>>,
28    /// Last modified date
29    pub modified: Option<DateTime<Utc>>,
30    /// Whether this CVE is in CISA's Known Exploited Vulnerabilities catalog
31    pub is_kev: bool,
32    /// KEV-specific metadata if applicable
33    pub kev_info: Option<KevInfo>,
34    /// Per-vulnerability VEX status (from external VEX documents or embedded analysis)
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub vex_status: Option<VexStatus>,
37}
38
39/// CISA Known Exploited Vulnerabilities (KEV) catalog information
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct KevInfo {
42    /// Date added to KEV catalog
43    pub date_added: DateTime<Utc>,
44    /// Due date for remediation (per CISA directive)
45    pub due_date: DateTime<Utc>,
46    /// Whether known to be used in ransomware campaigns
47    pub known_ransomware_use: bool,
48    /// Required action description
49    pub required_action: String,
50    /// Vendor/project name
51    pub vendor_project: Option<String>,
52    /// Product name
53    pub product: Option<String>,
54}
55
56impl KevInfo {
57    /// Create new KEV info
58    #[must_use] 
59    pub const fn new(date_added: DateTime<Utc>, due_date: DateTime<Utc>, required_action: String) -> Self {
60        Self {
61            date_added,
62            due_date,
63            known_ransomware_use: false,
64            required_action,
65            vendor_project: None,
66            product: None,
67        }
68    }
69
70    /// Check if remediation is overdue
71    #[must_use] 
72    pub fn is_overdue(&self) -> bool {
73        Utc::now() > self.due_date
74    }
75
76    /// Days until due date (negative if overdue)
77    #[must_use] 
78    pub fn days_until_due(&self) -> i64 {
79        (self.due_date - Utc::now()).num_days()
80    }
81}
82
83impl VulnerabilityRef {
84    /// Create a new vulnerability reference
85    #[must_use] 
86    pub const fn new(id: String, source: VulnerabilitySource) -> Self {
87        Self {
88            id,
89            source,
90            severity: None,
91            cvss: Vec::new(),
92            affected_versions: Vec::new(),
93            remediation: None,
94            description: None,
95            cwes: Vec::new(),
96            published: None,
97            modified: None,
98            is_kev: false,
99            kev_info: None,
100            vex_status: None,
101        }
102    }
103
104    /// Check if this vulnerability is actively exploited (KEV)
105    #[must_use] 
106    pub const fn is_actively_exploited(&self) -> bool {
107        self.is_kev
108    }
109
110    /// Check if this is a ransomware-related KEV entry
111    #[must_use] 
112    pub fn is_ransomware_related(&self) -> bool {
113        self.kev_info
114            .as_ref()
115            .is_some_and(|k| k.known_ransomware_use)
116    }
117
118    /// Set per-vulnerability VEX status
119    #[must_use]
120    pub fn with_vex_status(mut self, vex: VexStatus) -> Self {
121        self.vex_status = Some(vex);
122        self
123    }
124
125    /// Get the highest CVSS score
126    #[must_use]
127    pub fn max_cvss_score(&self) -> Option<f32> {
128        self.cvss
129            .iter()
130            .map(|c| c.base_score)
131            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
132    }
133}
134
135impl PartialEq for VulnerabilityRef {
136    fn eq(&self, other: &Self) -> bool {
137        self.id == other.id && self.source == other.source
138    }
139}
140
141impl Eq for VulnerabilityRef {}
142
143impl std::hash::Hash for VulnerabilityRef {
144    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
145        self.id.hash(state);
146        self.source.hash(state);
147    }
148}
149
150/// Vulnerability database source
151#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
152pub enum VulnerabilitySource {
153    Nvd,
154    Ghsa,
155    Osv,
156    Snyk,
157    Sonatype,
158    VulnDb,
159    Cve,
160    Other(String),
161}
162
163impl fmt::Display for VulnerabilitySource {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        match self {
166            Self::Nvd => write!(f, "NVD"),
167            Self::Ghsa => write!(f, "GHSA"),
168            Self::Osv => write!(f, "OSV"),
169            Self::Snyk => write!(f, "Snyk"),
170            Self::Sonatype => write!(f, "Sonatype"),
171            Self::VulnDb => write!(f, "VulnDB"),
172            Self::Cve => write!(f, "CVE"),
173            Self::Other(s) => write!(f, "{s}"),
174        }
175    }
176}
177
178/// Severity level
179#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
180#[non_exhaustive]
181pub enum Severity {
182    Critical,
183    High,
184    Medium,
185    Low,
186    Info,
187    None,
188    #[default]
189    Unknown,
190}
191
192impl Severity {
193    /// Create severity from CVSS score
194    #[must_use] 
195    pub fn from_cvss(score: f32) -> Self {
196        match score {
197            s if s >= 9.0 => Self::Critical,
198            s if s >= 7.0 => Self::High,
199            s if s >= 4.0 => Self::Medium,
200            s if s >= 0.1 => Self::Low,
201            0.0 => Self::None,
202            _ => Self::Unknown,
203        }
204    }
205
206    /// Get numeric priority (lower is more severe)
207    #[must_use] 
208    pub const fn priority(&self) -> u8 {
209        match self {
210            Self::Critical => 0,
211            Self::High => 1,
212            Self::Medium => 2,
213            Self::Low => 3,
214            Self::Info => 4,
215            Self::None => 5,
216            Self::Unknown => 6,
217        }
218    }
219}
220
221impl fmt::Display for Severity {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match self {
224            Self::Critical => write!(f, "Critical"),
225            Self::High => write!(f, "High"),
226            Self::Medium => write!(f, "Medium"),
227            Self::Low => write!(f, "Low"),
228            Self::Info => write!(f, "Info"),
229            Self::None => write!(f, "None"),
230            Self::Unknown => write!(f, "Unknown"),
231        }
232    }
233}
234
235
236/// CVSS score information
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct CvssScore {
239    /// CVSS version
240    pub version: CvssVersion,
241    /// Base score (0.0 - 10.0)
242    pub base_score: f32,
243    /// Attack vector
244    pub vector: Option<String>,
245    /// Exploitability score
246    pub exploitability_score: Option<f32>,
247    /// Impact score
248    pub impact_score: Option<f32>,
249}
250
251impl CvssScore {
252    /// Create a new CVSS score
253    #[must_use] 
254    pub const fn new(version: CvssVersion, base_score: f32) -> Self {
255        Self {
256            version,
257            base_score,
258            vector: None,
259            exploitability_score: None,
260            impact_score: None,
261        }
262    }
263}
264
265/// CVSS version
266#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
267pub enum CvssVersion {
268    V2,
269    V3,
270    V31,
271    V4,
272}
273
274impl fmt::Display for CvssVersion {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        match self {
277            Self::V2 => write!(f, "2.0"),
278            Self::V3 => write!(f, "3.0"),
279            Self::V31 => write!(f, "3.1"),
280            Self::V4 => write!(f, "4.0"),
281        }
282    }
283}
284
285/// Remediation information
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct Remediation {
288    /// Remediation type
289    pub remediation_type: RemediationType,
290    /// Description
291    pub description: Option<String>,
292    /// Fixed version
293    pub fixed_version: Option<String>,
294}
295
296/// Remediation type
297#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
298pub enum RemediationType {
299    Patch,
300    Upgrade,
301    Workaround,
302    Mitigation,
303    None,
304}
305
306impl fmt::Display for RemediationType {
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        match self {
309            Self::Patch => write!(f, "Patch"),
310            Self::Upgrade => write!(f, "Upgrade"),
311            Self::Workaround => write!(f, "Workaround"),
312            Self::Mitigation => write!(f, "Mitigation"),
313            Self::None => write!(f, "None"),
314        }
315    }
316}
317
318/// VEX (Vulnerability Exploitability eXchange) status
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct VexStatus {
321    /// VEX state
322    pub status: VexState,
323    /// Justification for the status
324    pub justification: Option<VexJustification>,
325    /// Action statement
326    pub action_statement: Option<String>,
327    /// Impact statement
328    pub impact_statement: Option<String>,
329    /// Response type
330    pub response: Option<VexResponse>,
331    /// Details
332    pub detail: Option<String>,
333}
334
335impl VexStatus {
336    /// Create a new VEX status
337    #[must_use] 
338    pub const fn new(status: VexState) -> Self {
339        Self {
340            status,
341            justification: None,
342            action_statement: None,
343            impact_statement: None,
344            response: None,
345            detail: None,
346        }
347    }
348}
349
350/// VEX state
351#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
352pub enum VexState {
353    Affected,
354    NotAffected,
355    Fixed,
356    UnderInvestigation,
357}
358
359impl fmt::Display for VexState {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        match self {
362            Self::Affected => write!(f, "Affected"),
363            Self::NotAffected => write!(f, "Not Affected"),
364            Self::Fixed => write!(f, "Fixed"),
365            Self::UnderInvestigation => write!(f, "Under Investigation"),
366        }
367    }
368}
369
370/// VEX justification for `not_affected` status
371#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
372pub enum VexJustification {
373    ComponentNotPresent,
374    VulnerableCodeNotPresent,
375    VulnerableCodeNotInExecutePath,
376    VulnerableCodeCannotBeControlledByAdversary,
377    InlineMitigationsAlreadyExist,
378}
379
380impl fmt::Display for VexJustification {
381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382        match self {
383            Self::ComponentNotPresent => write!(f, "Component not present"),
384            Self::VulnerableCodeNotPresent => write!(f, "Vulnerable code not present"),
385            Self::VulnerableCodeNotInExecutePath => {
386                write!(f, "Vulnerable code not in execute path")
387            }
388            Self::VulnerableCodeCannotBeControlledByAdversary => {
389                write!(f, "Vulnerable code cannot be controlled by adversary")
390            }
391            Self::InlineMitigationsAlreadyExist => {
392                write!(f, "Inline mitigations already exist")
393            }
394        }
395    }
396}
397
398/// VEX response type
399#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
400pub enum VexResponse {
401    CanNotFix,
402    WillNotFix,
403    Update,
404    Rollback,
405    Workaround,
406}
407
408impl fmt::Display for VexResponse {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        match self {
411            Self::CanNotFix => write!(f, "Can Not Fix"),
412            Self::WillNotFix => write!(f, "Will Not Fix"),
413            Self::Update => write!(f, "Update"),
414            Self::Rollback => write!(f, "Rollback"),
415            Self::Workaround => write!(f, "Workaround"),
416        }
417    }
418}