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(
60 date_added: DateTime<Utc>,
61 due_date: DateTime<Utc>,
62 required_action: String,
63 ) -> Self {
64 Self {
65 date_added,
66 due_date,
67 known_ransomware_use: false,
68 required_action,
69 vendor_project: None,
70 product: None,
71 }
72 }
73
74 #[must_use]
76 pub fn is_overdue(&self) -> bool {
77 Utc::now() > self.due_date
78 }
79
80 #[must_use]
82 pub fn days_until_due(&self) -> i64 {
83 (self.due_date - Utc::now()).num_days()
84 }
85}
86
87impl VulnerabilityRef {
88 #[must_use]
90 pub const fn new(id: String, source: VulnerabilitySource) -> Self {
91 Self {
92 id,
93 source,
94 severity: None,
95 cvss: Vec::new(),
96 affected_versions: Vec::new(),
97 remediation: None,
98 description: None,
99 cwes: Vec::new(),
100 published: None,
101 modified: None,
102 is_kev: false,
103 kev_info: None,
104 vex_status: None,
105 }
106 }
107
108 #[must_use]
110 pub const fn is_actively_exploited(&self) -> bool {
111 self.is_kev
112 }
113
114 #[must_use]
116 pub fn is_ransomware_related(&self) -> bool {
117 self.kev_info
118 .as_ref()
119 .is_some_and(|k| k.known_ransomware_use)
120 }
121
122 #[must_use]
124 pub fn with_vex_status(mut self, vex: VexStatus) -> Self {
125 self.vex_status = Some(vex);
126 self
127 }
128
129 #[must_use]
131 pub fn max_cvss_score(&self) -> Option<f32> {
132 self.cvss
133 .iter()
134 .map(|c| c.base_score)
135 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
136 }
137}
138
139impl PartialEq for VulnerabilityRef {
140 fn eq(&self, other: &Self) -> bool {
141 self.id == other.id && self.source == other.source
142 }
143}
144
145impl Eq for VulnerabilityRef {}
146
147impl std::hash::Hash for VulnerabilityRef {
148 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
149 self.id.hash(state);
150 self.source.hash(state);
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
156#[non_exhaustive]
157pub enum VulnerabilitySource {
158 Nvd,
159 Ghsa,
160 Osv,
161 Snyk,
162 Sonatype,
163 VulnDb,
164 Cve,
165 Other(String),
166}
167
168impl fmt::Display for VulnerabilitySource {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self {
171 Self::Nvd => write!(f, "NVD"),
172 Self::Ghsa => write!(f, "GHSA"),
173 Self::Osv => write!(f, "OSV"),
174 Self::Snyk => write!(f, "Snyk"),
175 Self::Sonatype => write!(f, "Sonatype"),
176 Self::VulnDb => write!(f, "VulnDB"),
177 Self::Cve => write!(f, "CVE"),
178 Self::Other(s) => write!(f, "{s}"),
179 }
180 }
181}
182
183#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
185#[non_exhaustive]
186pub enum Severity {
187 Critical,
188 High,
189 Medium,
190 Low,
191 Info,
192 None,
193 #[default]
194 Unknown,
195}
196
197impl Severity {
198 #[must_use]
200 pub fn from_cvss(score: f32) -> Self {
201 match score {
202 s if s >= 9.0 => Self::Critical,
203 s if s >= 7.0 => Self::High,
204 s if s >= 4.0 => Self::Medium,
205 s if s >= 0.1 => Self::Low,
206 0.0 => Self::None,
207 _ => Self::Unknown,
208 }
209 }
210
211 #[must_use]
213 pub const fn priority(&self) -> u8 {
214 match self {
215 Self::Critical => 0,
216 Self::High => 1,
217 Self::Medium => 2,
218 Self::Low => 3,
219 Self::Info => 4,
220 Self::None => 5,
221 Self::Unknown => 6,
222 }
223 }
224}
225
226impl std::str::FromStr for Severity {
227 type Err = std::convert::Infallible;
228
229 fn from_str(s: &str) -> Result<Self, Self::Err> {
230 Ok(match s.to_ascii_lowercase().as_str() {
231 "critical" => Self::Critical,
232 "high" => Self::High,
233 "medium" | "moderate" => Self::Medium,
234 "low" => Self::Low,
235 "info" | "informational" => Self::Info,
236 "none" => Self::None,
237 _ => Self::Unknown,
238 })
239 }
240}
241
242impl fmt::Display for Severity {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match self {
245 Self::Critical => write!(f, "Critical"),
246 Self::High => write!(f, "High"),
247 Self::Medium => write!(f, "Medium"),
248 Self::Low => write!(f, "Low"),
249 Self::Info => write!(f, "Info"),
250 Self::None => write!(f, "None"),
251 Self::Unknown => write!(f, "Unknown"),
252 }
253 }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct CvssScore {
259 pub version: CvssVersion,
261 pub base_score: f32,
263 pub vector: Option<String>,
265 pub exploitability_score: Option<f32>,
267 pub impact_score: Option<f32>,
269}
270
271impl CvssScore {
272 #[must_use]
274 pub const fn new(version: CvssVersion, base_score: f32) -> Self {
275 Self {
276 version,
277 base_score,
278 vector: None,
279 exploitability_score: None,
280 impact_score: None,
281 }
282 }
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
287pub enum CvssVersion {
288 V2,
289 V3,
290 V31,
291 V4,
292}
293
294impl fmt::Display for CvssVersion {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 match self {
297 Self::V2 => write!(f, "2.0"),
298 Self::V3 => write!(f, "3.0"),
299 Self::V31 => write!(f, "3.1"),
300 Self::V4 => write!(f, "4.0"),
301 }
302 }
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct Remediation {
308 pub remediation_type: RemediationType,
310 pub description: Option<String>,
312 pub fixed_version: Option<String>,
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
318pub enum RemediationType {
319 Patch,
320 Upgrade,
321 Workaround,
322 Mitigation,
323 None,
324}
325
326impl fmt::Display for RemediationType {
327 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328 match self {
329 Self::Patch => write!(f, "Patch"),
330 Self::Upgrade => write!(f, "Upgrade"),
331 Self::Workaround => write!(f, "Workaround"),
332 Self::Mitigation => write!(f, "Mitigation"),
333 Self::None => write!(f, "None"),
334 }
335 }
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct VexStatus {
341 pub status: VexState,
343 pub justification: Option<VexJustification>,
345 pub action_statement: Option<String>,
347 pub impact_statement: Option<String>,
349 #[serde(default, skip_serializing_if = "Vec::is_empty")]
351 pub responses: Vec<VexResponse>,
352 pub detail: Option<String>,
354}
355
356impl VexStatus {
357 #[must_use]
359 pub const fn new(status: VexState) -> Self {
360 Self {
361 status,
362 justification: None,
363 action_statement: None,
364 impact_statement: None,
365 responses: Vec::new(),
366 detail: None,
367 }
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
373pub enum VexState {
374 Affected,
375 NotAffected,
376 Fixed,
377 UnderInvestigation,
378}
379
380impl fmt::Display for VexState {
381 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382 match self {
383 Self::Affected => write!(f, "Affected"),
384 Self::NotAffected => write!(f, "Not Affected"),
385 Self::Fixed => write!(f, "Fixed"),
386 Self::UnderInvestigation => write!(f, "Under Investigation"),
387 }
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
393pub enum VexJustification {
394 ComponentNotPresent,
395 VulnerableCodeNotPresent,
396 VulnerableCodeNotInExecutePath,
397 VulnerableCodeCannotBeControlledByAdversary,
398 InlineMitigationsAlreadyExist,
399}
400
401impl fmt::Display for VexJustification {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 match self {
404 Self::ComponentNotPresent => write!(f, "Component not present"),
405 Self::VulnerableCodeNotPresent => write!(f, "Vulnerable code not present"),
406 Self::VulnerableCodeNotInExecutePath => {
407 write!(f, "Vulnerable code not in execute path")
408 }
409 Self::VulnerableCodeCannotBeControlledByAdversary => {
410 write!(f, "Vulnerable code cannot be controlled by adversary")
411 }
412 Self::InlineMitigationsAlreadyExist => {
413 write!(f, "Inline mitigations already exist")
414 }
415 }
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
421pub enum VexResponse {
422 CanNotFix,
423 WillNotFix,
424 Update,
425 Rollback,
426 Workaround,
427}
428
429impl fmt::Display for VexResponse {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 match self {
432 Self::CanNotFix => write!(f, "Can Not Fix"),
433 Self::WillNotFix => write!(f, "Will Not Fix"),
434 Self::Update => write!(f, "Update"),
435 Self::Rollback => write!(f, "Rollback"),
436 Self::Workaround => write!(f, "Workaround"),
437 }
438 }
439}