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