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