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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "UPPERCASE")]
107pub enum RangeType {
108    Semver,
109    Ecosystem,
110    Git,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum Event {
116    Introduced(String),
117    Fixed(String),
118    LastAffected(String),
119    Limit(String),
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct Reference {
124    #[serde(rename = "type")]
125    pub reference_type: ReferenceType,
126    pub url: String,
127}
128
129/// Reference types as defined in the OSV schema.
130/// Uses `#[serde(other)]` to gracefully handle unknown variants.
131#[derive(Debug, Clone, Serialize, Deserialize, Default)]
132#[serde(rename_all = "UPPERCASE")]
133pub enum ReferenceType {
134    Advisory,
135    Article,
136    Detection,
137    Discussion,
138    Evidence,
139    Fix,
140    Git,
141    Introduced,
142    Package,
143    Report,
144    Web,
145    /// Fallback for unknown/future reference types.
146    #[default]
147    #[serde(other)]
148    Other,
149}
150
151/// CVSS v3 severity levels.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
153#[serde(rename_all = "UPPERCASE")]
154pub enum Severity {
155    /// CVSS score 0.0
156    None,
157    /// CVSS score 0.1 - 3.9
158    Low,
159    /// CVSS score 4.0 - 6.9
160    Medium,
161    /// CVSS score 7.0 - 8.9
162    High,
163    /// CVSS score 9.0 - 10.0
164    Critical,
165}
166
167impl Severity {
168    /// Convert a CVSS v3 score to a severity level.
169    pub fn from_cvss_score(score: f64) -> Self {
170        match score {
171            s if s >= 9.0 => Self::Critical,
172            s if s >= 7.0 => Self::High,
173            s if s >= 4.0 => Self::Medium,
174            s if s > 0.0 => Self::Low,
175            _ => Self::None,
176        }
177    }
178
179    /// Get the minimum CVSS score for this severity level.
180    pub fn min_score(&self) -> f64 {
181        match self {
182            Self::None => 0.0,
183            Self::Low => 0.1,
184            Self::Medium => 4.0,
185            Self::High => 7.0,
186            Self::Critical => 9.0,
187        }
188    }
189}