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