use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
#[default]
Unknown,
None,
Low,
Medium,
High,
Critical,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Unknown => write!(f, "unknown"),
Severity::None => write!(f, "none"),
Severity::Low => write!(f, "low"),
Severity::Medium => write!(f, "medium"),
Severity::High => write!(f, "high"),
Severity::Critical => write!(f, "critical"),
}
}
}
impl Severity {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" | "moderate" => Severity::Medium,
"low" => Severity::Low,
"none" | "info" | "informational" => Severity::None,
_ => Severity::Unknown,
}
}
pub fn meets_threshold(&self, threshold: &Severity) -> bool {
self >= threshold
}
pub fn color_code(&self) -> &'static str {
match self {
Severity::Critical => "\x1b[1;31m", Severity::High => "\x1b[31m", Severity::Medium => "\x1b[33m", Severity::Low => "\x1b[36m", Severity::None => "\x1b[37m", Severity::Unknown => "\x1b[90m", }
}
pub fn emoji(&self) -> &'static str {
match self {
Severity::Critical => "🔴",
Severity::High => "🟠",
Severity::Medium => "🟡",
Severity::Low => "🔵",
Severity::None => "⚪",
Severity::Unknown => "❓",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Advisory {
pub id: String,
pub aliases: Vec<String>,
pub title: String,
pub description: String,
pub severity: Severity,
pub cvss_score: Option<f32>,
pub cvss_vector: Option<String>,
pub url: Option<String>,
pub published: Option<String>,
pub updated: Option<String>,
pub cwe_ids: Vec<String>,
pub references: Vec<String>,
}
impl Advisory {
pub fn cve_id(&self) -> Option<&str> {
if self.id.starts_with("CVE-") {
return Some(&self.id);
}
self.aliases
.iter()
.find(|a| a.starts_with("CVE-"))
.map(|s| s.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AffectedPackage {
pub name: String,
pub version: String,
pub ecosystem: String,
pub affected_versions: Vec<String>,
pub patched_versions: Vec<String>,
pub recommended_version: Option<String>,
pub path: Vec<String>,
pub is_direct: bool,
}
impl AffectedPackage {
pub fn fix_action(&self) -> String {
if let Some(ref version) = self.recommended_version {
format!("Upgrade {} from {} to {}", self.name, self.version, version)
} else if !self.patched_versions.is_empty() {
format!(
"Upgrade {} from {} to one of: {}",
self.name,
self.version,
self.patched_versions.join(", ")
)
} else {
format!("No fix available for {} {}", self.name, self.version)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
pub advisory: Advisory,
pub affected_packages: Vec<AffectedPackage>,
pub source: String,
pub language: String,
pub fix_available: bool,
pub suppressed: bool,
pub suppression_reason: Option<String>,
}
impl Vulnerability {
pub fn new(
advisory: Advisory,
affected_packages: Vec<AffectedPackage>,
source: &str,
language: &str,
) -> Self {
let fix_available = affected_packages
.iter()
.any(|p| p.recommended_version.is_some() || !p.patched_versions.is_empty());
Self {
advisory,
affected_packages,
source: source.to_string(),
language: language.to_string(),
fix_available,
suppressed: false,
suppression_reason: None,
}
}
pub fn severity(&self) -> Severity {
self.advisory.severity
}
pub fn summary(&self) -> String {
let packages: Vec<_> = self
.affected_packages
.iter()
.map(|p| format!("{} {}", p.name, p.version))
.collect();
format!("{} in {}", self.advisory.id, packages.join(", "))
}
pub fn meets_severity_threshold(&self, threshold: &Severity) -> bool {
self.advisory.severity.meets_threshold(threshold)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_ordering() {
assert!(Severity::Critical > Severity::High);
assert!(Severity::High > Severity::Medium);
assert!(Severity::Medium > Severity::Low);
assert!(Severity::Low > Severity::None);
}
#[test]
fn test_severity_from_str() {
assert_eq!(Severity::from_str("critical"), Severity::Critical);
assert_eq!(Severity::from_str("HIGH"), Severity::High);
assert_eq!(Severity::from_str("moderate"), Severity::Medium);
assert_eq!(Severity::from_str("unknown_value"), Severity::Unknown);
}
#[test]
fn test_severity_threshold() {
assert!(Severity::Critical.meets_threshold(&Severity::High));
assert!(Severity::High.meets_threshold(&Severity::High));
assert!(!Severity::Medium.meets_threshold(&Severity::High));
}
}