use super::check::CheckResult;
use super::{CommandResult, Dependency, DependencyKind, Ecosystem};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Unknown,
Low,
Medium,
High,
Critical,
}
impl Severity {
pub fn from_cvss(score: f64) -> Self {
if score >= 9.0 {
Self::Critical
} else if score >= 7.0 {
Self::High
} else if score >= 4.0 {
Self::Medium
} else if score > 0.0 {
Self::Low
} else {
Self::Unknown
}
}
pub fn from_str_label(s: &str) -> Self {
match s.to_uppercase().as_str() {
"CRITICAL" => Self::Critical,
"HIGH" => Self::High,
"MEDIUM" | "MODERATE" => Self::Medium,
"LOW" => Self::Low,
_ => Self::Unknown,
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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::Unknown => write!(f, "UNKNOWN"),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Vulnerability {
pub id: String,
pub aliases: Vec<String>,
pub summary: String,
pub severity: Severity,
pub url: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AuditResult {
pub dep: Dependency,
pub version: String,
pub vulnerabilities: Vec<Vulnerability>,
}
impl CommandResult for AuditResult {
fn ecosystem(&self) -> Ecosystem {
self.dep.ecosystem
}
fn kind(&self) -> DependencyKind {
self.dep.kind
}
fn artifact(&self) -> &str {
&self.dep.artifact
}
fn property(&self) -> Option<&str> {
self.dep.property.as_deref()
}
fn source(&self) -> &str {
&self.dep.source
}
}
impl AuditResult {
pub fn from_version_result(r: &CheckResult, vulnerabilities: Vec<Vulnerability>) -> Self {
Self {
dep: r.dep.clone(),
version: r.current_version.clone(),
vulnerabilities,
}
}
pub fn max_severity(&self) -> Severity {
self.vulnerabilities
.iter()
.map(|v| v.severity)
.max()
.unwrap_or(Severity::Unknown)
}
pub fn is_vulnerable(&self) -> bool {
!self.vulnerabilities.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::check::CheckResult;
use crate::model::{Dependency, DependencyKind, Ecosystem};
fn make_dep() -> Dependency {
Dependency::new(
Ecosystem::Maven,
DependencyKind::Dependency,
"org.example:lib".into(),
Some("version.lib".into()),
"pom.xml".into(),
)
}
fn make_vuln(id: &str, severity: Severity) -> Vulnerability {
Vulnerability {
id: id.into(),
aliases: Vec::new(),
summary: String::new(),
severity,
url: None,
}
}
#[test]
fn is_vulnerable_with_vulns() {
let result = AuditResult {
dep: make_dep(),
version: "1.0.0".into(),
vulnerabilities: vec![make_vuln("CVE-1", Severity::High)],
};
assert!(result.is_vulnerable());
}
#[test]
fn is_vulnerable_without_vulns() {
let result = AuditResult {
dep: make_dep(),
version: "1.0.0".into(),
vulnerabilities: Vec::new(),
};
assert!(!result.is_vulnerable());
}
#[test]
fn max_severity_returns_highest() {
let result = AuditResult {
dep: make_dep(),
version: "1.0.0".into(),
vulnerabilities: vec![
make_vuln("V1", Severity::Low),
make_vuln("V2", Severity::Critical),
make_vuln("V3", Severity::Medium),
],
};
assert_eq!(result.max_severity(), Severity::Critical);
}
#[test]
fn max_severity_empty_returns_unknown() {
let result = AuditResult {
dep: make_dep(),
version: "1.0.0".into(),
vulnerabilities: Vec::new(),
};
assert_eq!(result.max_severity(), Severity::Unknown);
}
#[test]
fn from_version_result_copies_fields() {
let check = CheckResult::checked(make_dep(), "1.0.0".into(), "2.0.0".into(), true);
let vulns = vec![make_vuln("CVE-1", Severity::High)];
let audit = AuditResult::from_version_result(&check, vulns);
assert_eq!(audit.version, "1.0.0");
assert_eq!(audit.artifact(), "org.example:lib");
assert_eq!(audit.property(), Some("version.lib"));
assert_eq!(audit.source(), "pom.xml");
assert_eq!(audit.vulnerabilities.len(), 1);
}
}