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