Skip to main content

vulnera_advisor/
models.rs

1//! Core data models for vulnerability advisories.
2//!
3//! This module defines the canonical [`Advisory`] struct and related types that form
4//! the unified data model for all vulnerability sources (GHSA, NVD, OSV, etc.).
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9/// A vulnerability advisory containing information about a security issue.
10///
11/// This is the canonical representation used internally, based on the OSV schema.
12/// All sources convert their data to this format.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Advisory {
15    /// Unique identifier (e.g., "GHSA-xxxx-xxxx-xxxx", "CVE-2024-1234").
16    pub id: String,
17    /// Brief summary of the vulnerability.
18    pub summary: Option<String>,
19    /// Detailed description of the vulnerability.
20    pub details: Option<String>,
21    /// List of affected packages and version ranges. Optional per OSV schema.
22    #[serde(default)]
23    pub affected: Vec<Affected>,
24    /// References to external resources (advisories, patches, etc.). Optional per OSV schema.
25    #[serde(default)]
26    pub references: Vec<Reference>,
27    /// When the advisory was first published.
28    pub published: Option<DateTime<Utc>>,
29    /// When the advisory was last modified.
30    pub modified: Option<DateTime<Utc>>,
31    /// Alternative identifiers (e.g., CVE aliases for GHSA).
32    pub aliases: Option<Vec<String>>,
33    /// Source-specific metadata.
34    pub database_specific: Option<serde_json::Value>,
35    /// Enrichment data from EPSS, CISA KEV, etc.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub enrichment: Option<Enrichment>,
38}
39
40/// Enrichment data aggregated from multiple sources.
41///
42/// This provides additional context for prioritization:
43/// - EPSS scores indicate exploit probability
44/// - KEV data indicates active exploitation
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
46pub struct Enrichment {
47    /// EPSS (Exploit Prediction Scoring System) probability score (0.0 - 1.0).
48    /// Higher values indicate higher likelihood of exploitation.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub epss_score: Option<f64>,
51    /// EPSS percentile (0.0 - 1.0) relative to all scored CVEs.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub epss_percentile: Option<f64>,
54    /// Date when EPSS score was calculated.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub epss_date: Option<DateTime<Utc>>,
57    /// Whether this CVE is in CISA's Known Exploited Vulnerabilities catalog.
58    #[serde(default)]
59    pub is_kev: bool,
60    /// CISA KEV due date for remediation (if applicable).
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub kev_due_date: Option<DateTime<Utc>>,
63    /// Date when CVE was added to KEV catalog.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub kev_date_added: Option<DateTime<Utc>>,
66    /// Whether known ransomware campaigns use this vulnerability.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub kev_ransomware: Option<bool>,
69    /// Extracted CVSS v3 base score (0.0 - 10.0) if available.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub cvss_v3_score: Option<f64>,
72    /// CVSS v3 severity level.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub cvss_v3_severity: Option<Severity>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Affected {
79    pub package: Package,
80    /// Version ranges affected (e.g., semver ranges).
81    #[serde(default)]
82    pub ranges: Vec<Range>,
83    /// Explicit list of affected versions. Optional per OSV schema.
84    #[serde(default)]
85    pub versions: Vec<String>,
86    pub ecosystem_specific: Option<serde_json::Value>,
87    pub database_specific: Option<serde_json::Value>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Package {
92    pub ecosystem: String,
93    pub name: String,
94    pub purl: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct Range {
99    #[serde(rename = "type")]
100    pub range_type: RangeType,
101    pub events: Vec<Event>,
102    pub repo: Option<String>,
103}
104
105/// Status of translating source-specific range semantics into canonical events.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "snake_case")]
108pub enum RangeTranslationStatus {
109    Exact,
110    Lossy,
111    Unsupported,
112    Invalid,
113}
114
115/// Diagnostic metadata for range translation.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct RangeTranslation {
118    pub source: String,
119    pub raw: Option<String>,
120    pub status: RangeTranslationStatus,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub reason: Option<String>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "UPPERCASE")]
127pub enum RangeType {
128    Semver,
129    Ecosystem,
130    Git,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum Event {
136    Introduced(String),
137    Fixed(String),
138    LastAffected(String),
139    Limit(String),
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct Reference {
144    #[serde(rename = "type")]
145    pub reference_type: ReferenceType,
146    pub url: String,
147}
148
149/// Reference types as defined in the OSV schema.
150/// Uses `#[serde(other)]` to gracefully handle unknown variants.
151#[derive(Debug, Clone, Serialize, Deserialize, Default)]
152#[serde(rename_all = "UPPERCASE")]
153pub enum ReferenceType {
154    Advisory,
155    Article,
156    Detection,
157    Discussion,
158    Evidence,
159    Fix,
160    Git,
161    Introduced,
162    Package,
163    Report,
164    Web,
165    /// Fallback for unknown/future reference types.
166    #[default]
167    #[serde(other)]
168    Other,
169}
170
171/// CVSS v3 severity levels.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
173#[serde(rename_all = "UPPERCASE")]
174pub enum Severity {
175    /// CVSS score 0.0
176    None,
177    /// CVSS score 0.1 - 3.9
178    Low,
179    /// CVSS score 4.0 - 6.9
180    Medium,
181    /// CVSS score 7.0 - 8.9
182    High,
183    /// CVSS score 9.0 - 10.0
184    Critical,
185}
186
187impl Severity {
188    /// Convert a CVSS v3 score to a severity level.
189    pub fn from_cvss_score(score: f64) -> Self {
190        match score {
191            s if s >= 9.0 => Self::Critical,
192            s if s >= 7.0 => Self::High,
193            s if s >= 4.0 => Self::Medium,
194            s if s > 0.0 => Self::Low,
195            _ => Self::None,
196        }
197    }
198
199    /// Get the minimum CVSS score for this severity level.
200    pub fn min_score(&self) -> f64 {
201        match self {
202            Self::None => 0.0,
203            Self::Low => 0.1,
204            Self::Medium => 4.0,
205            Self::High => 7.0,
206            Self::Critical => 9.0,
207        }
208    }
209}