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