use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnerabilityRef {
pub id: String,
pub source: VulnerabilitySource,
pub severity: Option<Severity>,
pub cvss: Vec<CvssScore>,
pub affected_versions: Vec<String>,
pub remediation: Option<Remediation>,
pub description: Option<String>,
pub cwes: Vec<String>,
pub published: Option<DateTime<Utc>>,
pub modified: Option<DateTime<Utc>>,
pub is_kev: bool,
pub kev_info: Option<KevInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vex_status: Option<VexStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KevInfo {
pub date_added: DateTime<Utc>,
pub due_date: DateTime<Utc>,
pub known_ransomware_use: bool,
pub required_action: String,
pub vendor_project: Option<String>,
pub product: Option<String>,
}
impl KevInfo {
#[must_use]
pub const fn new(
date_added: DateTime<Utc>,
due_date: DateTime<Utc>,
required_action: String,
) -> Self {
Self {
date_added,
due_date,
known_ransomware_use: false,
required_action,
vendor_project: None,
product: None,
}
}
#[must_use]
pub fn is_overdue(&self) -> bool {
Utc::now() > self.due_date
}
#[must_use]
pub fn days_until_due(&self) -> i64 {
(self.due_date - Utc::now()).num_days()
}
}
impl VulnerabilityRef {
#[must_use]
pub const fn new(id: String, source: VulnerabilitySource) -> Self {
Self {
id,
source,
severity: None,
cvss: Vec::new(),
affected_versions: Vec::new(),
remediation: None,
description: None,
cwes: Vec::new(),
published: None,
modified: None,
is_kev: false,
kev_info: None,
vex_status: None,
}
}
#[must_use]
pub const fn is_actively_exploited(&self) -> bool {
self.is_kev
}
#[must_use]
pub fn is_ransomware_related(&self) -> bool {
self.kev_info
.as_ref()
.is_some_and(|k| k.known_ransomware_use)
}
#[must_use]
pub fn with_vex_status(mut self, vex: VexStatus) -> Self {
self.vex_status = Some(vex);
self
}
#[must_use]
pub fn max_cvss_score(&self) -> Option<f32> {
self.cvss
.iter()
.map(|c| c.base_score)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
}
impl PartialEq for VulnerabilityRef {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.source == other.source
}
}
impl Eq for VulnerabilityRef {}
impl std::hash::Hash for VulnerabilityRef {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.source.hash(state);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum VulnerabilitySource {
Nvd,
Ghsa,
Osv,
Snyk,
Sonatype,
VulnDb,
Cve,
Other(String),
}
impl fmt::Display for VulnerabilitySource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Nvd => write!(f, "NVD"),
Self::Ghsa => write!(f, "GHSA"),
Self::Osv => write!(f, "OSV"),
Self::Snyk => write!(f, "Snyk"),
Self::Sonatype => write!(f, "Sonatype"),
Self::VulnDb => write!(f, "VulnDB"),
Self::Cve => write!(f, "CVE"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Severity {
Critical,
High,
Medium,
Low,
Info,
None,
#[default]
Unknown,
}
impl Severity {
#[must_use]
pub fn from_cvss(score: f32) -> Self {
match score {
s if s >= 9.0 => Self::Critical,
s if s >= 7.0 => Self::High,
s if s >= 4.0 => Self::Medium,
s if s >= 0.1 => Self::Low,
0.0 => Self::None,
_ => Self::Unknown,
}
}
#[must_use]
pub const fn priority(&self) -> u8 {
match self {
Self::Critical => 0,
Self::High => 1,
Self::Medium => 2,
Self::Low => 3,
Self::Info => 4,
Self::None => 5,
Self::Unknown => 6,
}
}
}
impl std::str::FromStr for Severity {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_ascii_lowercase().as_str() {
"critical" => Self::Critical,
"high" => Self::High,
"medium" | "moderate" => Self::Medium,
"low" => Self::Low,
"info" | "informational" => Self::Info,
"none" => Self::None,
_ => Self::Unknown,
})
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Critical => write!(f, "Critical"),
Self::High => write!(f, "High"),
Self::Medium => write!(f, "Medium"),
Self::Low => write!(f, "Low"),
Self::Info => write!(f, "Info"),
Self::None => write!(f, "None"),
Self::Unknown => write!(f, "Unknown"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CvssScore {
pub version: CvssVersion,
pub base_score: f32,
pub vector: Option<String>,
pub exploitability_score: Option<f32>,
pub impact_score: Option<f32>,
}
impl CvssScore {
#[must_use]
pub const fn new(version: CvssVersion, base_score: f32) -> Self {
Self {
version,
base_score,
vector: None,
exploitability_score: None,
impact_score: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CvssVersion {
V2,
V3,
V31,
V4,
}
impl fmt::Display for CvssVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::V2 => write!(f, "2.0"),
Self::V3 => write!(f, "3.0"),
Self::V31 => write!(f, "3.1"),
Self::V4 => write!(f, "4.0"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Remediation {
pub remediation_type: RemediationType,
pub description: Option<String>,
pub fixed_version: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RemediationType {
Patch,
Upgrade,
Workaround,
Mitigation,
None,
}
impl fmt::Display for RemediationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Patch => write!(f, "Patch"),
Self::Upgrade => write!(f, "Upgrade"),
Self::Workaround => write!(f, "Workaround"),
Self::Mitigation => write!(f, "Mitigation"),
Self::None => write!(f, "None"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VexStatus {
pub status: VexState,
pub justification: Option<VexJustification>,
pub action_statement: Option<String>,
pub impact_statement: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub responses: Vec<VexResponse>,
pub detail: Option<String>,
}
impl VexStatus {
#[must_use]
pub const fn new(status: VexState) -> Self {
Self {
status,
justification: None,
action_statement: None,
impact_statement: None,
responses: Vec::new(),
detail: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum VexState {
Affected,
NotAffected,
Fixed,
UnderInvestigation,
}
impl fmt::Display for VexState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Affected => write!(f, "Affected"),
Self::NotAffected => write!(f, "Not Affected"),
Self::Fixed => write!(f, "Fixed"),
Self::UnderInvestigation => write!(f, "Under Investigation"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum VexJustification {
ComponentNotPresent,
VulnerableCodeNotPresent,
VulnerableCodeNotInExecutePath,
VulnerableCodeCannotBeControlledByAdversary,
InlineMitigationsAlreadyExist,
}
impl fmt::Display for VexJustification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ComponentNotPresent => write!(f, "Component not present"),
Self::VulnerableCodeNotPresent => write!(f, "Vulnerable code not present"),
Self::VulnerableCodeNotInExecutePath => {
write!(f, "Vulnerable code not in execute path")
}
Self::VulnerableCodeCannotBeControlledByAdversary => {
write!(f, "Vulnerable code cannot be controlled by adversary")
}
Self::InlineMitigationsAlreadyExist => {
write!(f, "Inline mitigations already exist")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum VexResponse {
CanNotFix,
WillNotFix,
Update,
Rollback,
Workaround,
}
impl fmt::Display for VexResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CanNotFix => write!(f, "Can Not Fix"),
Self::WillNotFix => write!(f, "Will Not Fix"),
Self::Update => write!(f, "Update"),
Self::Rollback => write!(f, "Rollback"),
Self::Workaround => write!(f, "Workaround"),
}
}
}