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.
22    pub affected: Vec<Affected>,
23    /// References to external resources (advisories, patches, etc.).
24    pub references: Vec<Reference>,
25    /// When the advisory was first published.
26    pub published: Option<DateTime<Utc>>,
27    /// When the advisory was last modified.
28    pub modified: Option<DateTime<Utc>>,
29    /// Alternative identifiers (e.g., CVE aliases for GHSA).
30    pub aliases: Option<Vec<String>>,
31    /// Source-specific metadata.
32    pub database_specific: Option<serde_json::Value>,
33    /// Enrichment data from EPSS, CISA KEV, etc.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub enrichment: Option<Enrichment>,
36}
37
38/// Enrichment data aggregated from multiple sources.
39///
40/// This provides additional context for prioritization:
41/// - EPSS scores indicate exploit probability
42/// - KEV data indicates active exploitation
43#[derive(Debug, Clone, Default, Serialize, Deserialize)]
44pub struct Enrichment {
45    /// EPSS (Exploit Prediction Scoring System) probability score (0.0 - 1.0).
46    /// Higher values indicate higher likelihood of exploitation.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub epss_score: Option<f64>,
49    /// EPSS percentile (0.0 - 1.0) relative to all scored CVEs.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub epss_percentile: Option<f64>,
52    /// Date when EPSS score was calculated.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub epss_date: Option<DateTime<Utc>>,
55    /// Whether this CVE is in CISA's Known Exploited Vulnerabilities catalog.
56    #[serde(default)]
57    pub is_kev: bool,
58    /// CISA KEV due date for remediation (if applicable).
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub kev_due_date: Option<DateTime<Utc>>,
61    /// Date when CVE was added to KEV catalog.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub kev_date_added: Option<DateTime<Utc>>,
64    /// Whether known ransomware campaigns use this vulnerability.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub kev_ransomware: Option<bool>,
67    /// Extracted CVSS v3 base score (0.0 - 10.0) if available.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub cvss_v3_score: Option<f64>,
70    /// CVSS v3 severity level.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub cvss_v3_severity: Option<Severity>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Affected {
77    pub package: Package,
78    pub ranges: Vec<Range>,
79    pub versions: Vec<String>,
80    pub ecosystem_specific: Option<serde_json::Value>,
81    pub database_specific: Option<serde_json::Value>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Package {
86    pub ecosystem: String,
87    pub name: String,
88    pub purl: Option<String>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct Range {
93    #[serde(rename = "type")]
94    pub range_type: RangeType,
95    pub events: Vec<Event>,
96    pub repo: Option<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "UPPERCASE")]
101pub enum RangeType {
102    Semver,
103    Ecosystem,
104    Git,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum Event {
110    Introduced(String),
111    Fixed(String),
112    LastAffected(String),
113    Limit(String),
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Reference {
118    #[serde(rename = "type")]
119    pub reference_type: ReferenceType,
120    pub url: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "UPPERCASE")]
125pub enum ReferenceType {
126    Advisory,
127    Article,
128    Report,
129    Fix,
130    Git,
131    Package,
132    Web,
133    Other,
134}
135
136/// CVSS v3 severity levels.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
138#[serde(rename_all = "UPPERCASE")]
139pub enum Severity {
140    /// CVSS score 0.0
141    None,
142    /// CVSS score 0.1 - 3.9
143    Low,
144    /// CVSS score 4.0 - 6.9
145    Medium,
146    /// CVSS score 7.0 - 8.9
147    High,
148    /// CVSS score 9.0 - 10.0
149    Critical,
150}
151
152impl Severity {
153    /// Convert a CVSS v3 score to a severity level.
154    pub fn from_cvss_score(score: f64) -> Self {
155        match score {
156            s if s >= 9.0 => Self::Critical,
157            s if s >= 7.0 => Self::High,
158            s if s >= 4.0 => Self::Medium,
159            s if s > 0.0 => Self::Low,
160            _ => Self::None,
161        }
162    }
163
164    /// Get the minimum CVSS score for this severity level.
165    pub fn min_score(&self) -> f64 {
166        match self {
167            Self::None => 0.0,
168            Self::Low => 0.1,
169            Self::Medium => 4.0,
170            Self::High => 7.0,
171            Self::Critical => 9.0,
172        }
173    }
174}