1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct VulnerabilityRef {
10 pub id: String,
12 pub source: VulnerabilitySource,
14 pub severity: Option<Severity>,
16 pub cvss: Vec<CvssScore>,
18 pub affected_versions: Vec<String>,
20 pub remediation: Option<Remediation>,
22 pub description: Option<String>,
24 pub cwes: Vec<String>,
26 pub published: Option<DateTime<Utc>>,
28 pub modified: Option<DateTime<Utc>>,
30 pub is_kev: bool,
32 pub kev_info: Option<KevInfo>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub vex_status: Option<VexStatus>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct KevInfo {
42 pub date_added: DateTime<Utc>,
44 pub due_date: DateTime<Utc>,
46 pub known_ransomware_use: bool,
48 pub required_action: String,
50 pub vendor_project: Option<String>,
52 pub product: Option<String>,
54}
55
56impl KevInfo {
57 #[must_use]
59 pub const fn new(date_added: DateTime<Utc>, due_date: DateTime<Utc>, required_action: String) -> Self {
60 Self {
61 date_added,
62 due_date,
63 known_ransomware_use: false,
64 required_action,
65 vendor_project: None,
66 product: None,
67 }
68 }
69
70 #[must_use]
72 pub fn is_overdue(&self) -> bool {
73 Utc::now() > self.due_date
74 }
75
76 #[must_use]
78 pub fn days_until_due(&self) -> i64 {
79 (self.due_date - Utc::now()).num_days()
80 }
81}
82
83impl VulnerabilityRef {
84 #[must_use]
86 pub const fn new(id: String, source: VulnerabilitySource) -> Self {
87 Self {
88 id,
89 source,
90 severity: None,
91 cvss: Vec::new(),
92 affected_versions: Vec::new(),
93 remediation: None,
94 description: None,
95 cwes: Vec::new(),
96 published: None,
97 modified: None,
98 is_kev: false,
99 kev_info: None,
100 vex_status: None,
101 }
102 }
103
104 #[must_use]
106 pub const fn is_actively_exploited(&self) -> bool {
107 self.is_kev
108 }
109
110 #[must_use]
112 pub fn is_ransomware_related(&self) -> bool {
113 self.kev_info
114 .as_ref()
115 .is_some_and(|k| k.known_ransomware_use)
116 }
117
118 #[must_use]
120 pub fn with_vex_status(mut self, vex: VexStatus) -> Self {
121 self.vex_status = Some(vex);
122 self
123 }
124
125 #[must_use]
127 pub fn max_cvss_score(&self) -> Option<f32> {
128 self.cvss
129 .iter()
130 .map(|c| c.base_score)
131 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
132 }
133}
134
135impl PartialEq for VulnerabilityRef {
136 fn eq(&self, other: &Self) -> bool {
137 self.id == other.id && self.source == other.source
138 }
139}
140
141impl Eq for VulnerabilityRef {}
142
143impl std::hash::Hash for VulnerabilityRef {
144 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
145 self.id.hash(state);
146 self.source.hash(state);
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
152pub enum VulnerabilitySource {
153 Nvd,
154 Ghsa,
155 Osv,
156 Snyk,
157 Sonatype,
158 VulnDb,
159 Cve,
160 Other(String),
161}
162
163impl fmt::Display for VulnerabilitySource {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 match self {
166 Self::Nvd => write!(f, "NVD"),
167 Self::Ghsa => write!(f, "GHSA"),
168 Self::Osv => write!(f, "OSV"),
169 Self::Snyk => write!(f, "Snyk"),
170 Self::Sonatype => write!(f, "Sonatype"),
171 Self::VulnDb => write!(f, "VulnDB"),
172 Self::Cve => write!(f, "CVE"),
173 Self::Other(s) => write!(f, "{s}"),
174 }
175 }
176}
177
178#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
180#[non_exhaustive]
181pub enum Severity {
182 Critical,
183 High,
184 Medium,
185 Low,
186 Info,
187 None,
188 #[default]
189 Unknown,
190}
191
192impl Severity {
193 #[must_use]
195 pub fn from_cvss(score: f32) -> Self {
196 match score {
197 s if s >= 9.0 => Self::Critical,
198 s if s >= 7.0 => Self::High,
199 s if s >= 4.0 => Self::Medium,
200 s if s >= 0.1 => Self::Low,
201 0.0 => Self::None,
202 _ => Self::Unknown,
203 }
204 }
205
206 #[must_use]
208 pub const fn priority(&self) -> u8 {
209 match self {
210 Self::Critical => 0,
211 Self::High => 1,
212 Self::Medium => 2,
213 Self::Low => 3,
214 Self::Info => 4,
215 Self::None => 5,
216 Self::Unknown => 6,
217 }
218 }
219}
220
221impl fmt::Display for Severity {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 match self {
224 Self::Critical => write!(f, "Critical"),
225 Self::High => write!(f, "High"),
226 Self::Medium => write!(f, "Medium"),
227 Self::Low => write!(f, "Low"),
228 Self::Info => write!(f, "Info"),
229 Self::None => write!(f, "None"),
230 Self::Unknown => write!(f, "Unknown"),
231 }
232 }
233}
234
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct CvssScore {
239 pub version: CvssVersion,
241 pub base_score: f32,
243 pub vector: Option<String>,
245 pub exploitability_score: Option<f32>,
247 pub impact_score: Option<f32>,
249}
250
251impl CvssScore {
252 #[must_use]
254 pub const fn new(version: CvssVersion, base_score: f32) -> Self {
255 Self {
256 version,
257 base_score,
258 vector: None,
259 exploitability_score: None,
260 impact_score: None,
261 }
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
267pub enum CvssVersion {
268 V2,
269 V3,
270 V31,
271 V4,
272}
273
274impl fmt::Display for CvssVersion {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 match self {
277 Self::V2 => write!(f, "2.0"),
278 Self::V3 => write!(f, "3.0"),
279 Self::V31 => write!(f, "3.1"),
280 Self::V4 => write!(f, "4.0"),
281 }
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct Remediation {
288 pub remediation_type: RemediationType,
290 pub description: Option<String>,
292 pub fixed_version: Option<String>,
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
298pub enum RemediationType {
299 Patch,
300 Upgrade,
301 Workaround,
302 Mitigation,
303 None,
304}
305
306impl fmt::Display for RemediationType {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 match self {
309 Self::Patch => write!(f, "Patch"),
310 Self::Upgrade => write!(f, "Upgrade"),
311 Self::Workaround => write!(f, "Workaround"),
312 Self::Mitigation => write!(f, "Mitigation"),
313 Self::None => write!(f, "None"),
314 }
315 }
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct VexStatus {
321 pub status: VexState,
323 pub justification: Option<VexJustification>,
325 pub action_statement: Option<String>,
327 pub impact_statement: Option<String>,
329 pub response: Option<VexResponse>,
331 pub detail: Option<String>,
333}
334
335impl VexStatus {
336 #[must_use]
338 pub const fn new(status: VexState) -> Self {
339 Self {
340 status,
341 justification: None,
342 action_statement: None,
343 impact_statement: None,
344 response: None,
345 detail: None,
346 }
347 }
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
352pub enum VexState {
353 Affected,
354 NotAffected,
355 Fixed,
356 UnderInvestigation,
357}
358
359impl fmt::Display for VexState {
360 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 match self {
362 Self::Affected => write!(f, "Affected"),
363 Self::NotAffected => write!(f, "Not Affected"),
364 Self::Fixed => write!(f, "Fixed"),
365 Self::UnderInvestigation => write!(f, "Under Investigation"),
366 }
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
372pub enum VexJustification {
373 ComponentNotPresent,
374 VulnerableCodeNotPresent,
375 VulnerableCodeNotInExecutePath,
376 VulnerableCodeCannotBeControlledByAdversary,
377 InlineMitigationsAlreadyExist,
378}
379
380impl fmt::Display for VexJustification {
381 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382 match self {
383 Self::ComponentNotPresent => write!(f, "Component not present"),
384 Self::VulnerableCodeNotPresent => write!(f, "Vulnerable code not present"),
385 Self::VulnerableCodeNotInExecutePath => {
386 write!(f, "Vulnerable code not in execute path")
387 }
388 Self::VulnerableCodeCannotBeControlledByAdversary => {
389 write!(f, "Vulnerable code cannot be controlled by adversary")
390 }
391 Self::InlineMitigationsAlreadyExist => {
392 write!(f, "Inline mitigations already exist")
393 }
394 }
395 }
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
400pub enum VexResponse {
401 CanNotFix,
402 WillNotFix,
403 Update,
404 Rollback,
405 Workaround,
406}
407
408impl fmt::Display for VexResponse {
409 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410 match self {
411 Self::CanNotFix => write!(f, "Can Not Fix"),
412 Self::WillNotFix => write!(f, "Will Not Fix"),
413 Self::Update => write!(f, "Update"),
414 Self::Rollback => write!(f, "Rollback"),
415 Self::Workaround => write!(f, "Workaround"),
416 }
417 }
418}