use crate::shared::Result;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CvssScore(f32);
impl CvssScore {
pub fn new(score: f32) -> Result<Self> {
if score.is_nan() {
anyhow::bail!("CVSS score cannot be NaN");
}
if !(0.0..=10.0).contains(&score) {
anyhow::bail!("CVSS score must be between 0.0 and 10.0, got: {}", score);
}
Ok(Self(score))
}
pub fn value(&self) -> f32 {
self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Vulnerability {
id: String,
cvss_score: Option<CvssScore>,
severity: Severity,
fixed_version: Option<String>,
summary: Option<String>,
}
impl Vulnerability {
pub fn new(
id: String,
cvss_score: Option<CvssScore>,
severity: Severity,
fixed_version: Option<String>,
summary: Option<String>,
) -> Result<Self> {
if id.is_empty() {
anyhow::bail!("Vulnerability ID cannot be empty");
}
Ok(Self {
id,
cvss_score,
severity,
fixed_version,
summary,
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn cvss_score(&self) -> Option<CvssScore> {
self.cvss_score
}
pub fn severity(&self) -> Severity {
self.severity
}
pub fn fixed_version(&self) -> Option<&str> {
self.fixed_version.as_deref()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
None, Low, Medium, High, Critical, }
impl Severity {
pub fn from_cvss_score(score: CvssScore) -> Self {
let value = score.value();
match value {
0.0 => Self::None,
s if s < 4.0 => Self::Low,
s if s < 7.0 => Self::Medium,
s if s < 9.0 => Self::High,
_ => Self::Critical,
}
}
}
#[derive(Debug, Clone)]
pub struct PackageVulnerabilities {
package_name: String,
current_version: String,
vulnerabilities: Vec<Vulnerability>,
}
impl PackageVulnerabilities {
pub fn new(
package_name: String,
current_version: String,
vulnerabilities: Vec<Vulnerability>,
) -> Self {
Self {
package_name,
current_version,
vulnerabilities,
}
}
pub fn package_name(&self) -> &str {
&self.package_name
}
pub fn current_version(&self) -> &str {
&self.current_version
}
pub fn vulnerabilities(&self) -> &[Vulnerability] {
&self.vulnerabilities
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cvss_score_new_valid() {
let score = CvssScore::new(5.0);
assert!(score.is_ok());
assert_eq!(score.unwrap().value(), 5.0);
assert!(CvssScore::new(0.0).is_ok());
assert!(CvssScore::new(10.0).is_ok());
}
#[test]
fn test_cvss_score_invalid_too_high() {
let result = CvssScore::new(11.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("between 0.0 and 10.0"));
}
#[test]
fn test_cvss_score_invalid_negative() {
let result = CvssScore::new(-1.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("between 0.0 and 10.0"));
}
#[test]
fn test_cvss_score_invalid_nan() {
let result = CvssScore::new(f32::NAN);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be NaN"));
}
#[test]
fn test_vulnerability_new_valid() {
let cvss = CvssScore::new(9.8).unwrap();
let vuln = Vulnerability::new(
"CVE-2024-1234".to_string(),
Some(cvss),
Severity::Critical,
Some("2.0.0".to_string()),
Some("SQL injection vulnerability".to_string()),
);
assert!(vuln.is_ok());
let vuln = vuln.unwrap();
assert_eq!(vuln.id(), "CVE-2024-1234");
assert_eq!(vuln.cvss_score().map(|s| s.value()), Some(9.8));
assert_eq!(vuln.severity(), Severity::Critical);
assert_eq!(vuln.fixed_version(), Some("2.0.0"));
}
#[test]
fn test_vulnerability_empty_id() {
let cvss = CvssScore::new(5.0).unwrap();
let result = Vulnerability::new("".to_string(), Some(cvss), Severity::Medium, None, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("ID cannot be empty"));
}
#[test]
fn test_vulnerability_without_optional_fields() {
let vuln = Vulnerability::new(
"GHSA-xxxx-yyyy-zzzz".to_string(),
None,
Severity::High,
None,
None,
);
assert!(vuln.is_ok());
let vuln = vuln.unwrap();
assert_eq!(vuln.id(), "GHSA-xxxx-yyyy-zzzz");
assert_eq!(vuln.cvss_score(), None);
assert_eq!(vuln.fixed_version(), None);
}
#[test]
fn test_severity_from_cvss_score() {
assert_eq!(
Severity::from_cvss_score(CvssScore::new(0.0).unwrap()),
Severity::None
);
assert_eq!(
Severity::from_cvss_score(CvssScore::new(0.1).unwrap()),
Severity::Low
);
assert_eq!(
Severity::from_cvss_score(CvssScore::new(3.9).unwrap()),
Severity::Low
);
assert_eq!(
Severity::from_cvss_score(CvssScore::new(4.0).unwrap()),
Severity::Medium
);
assert_eq!(
Severity::from_cvss_score(CvssScore::new(6.9).unwrap()),
Severity::Medium
);
assert_eq!(
Severity::from_cvss_score(CvssScore::new(7.0).unwrap()),
Severity::High
);
assert_eq!(
Severity::from_cvss_score(CvssScore::new(8.9).unwrap()),
Severity::High
);
assert_eq!(
Severity::from_cvss_score(CvssScore::new(9.0).unwrap()),
Severity::Critical
);
assert_eq!(
Severity::from_cvss_score(CvssScore::new(10.0).unwrap()),
Severity::Critical
);
}
#[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_package_vulnerabilities_new() {
let vuln1 = Vulnerability::new(
"CVE-2024-1234".to_string(),
Some(CvssScore::new(9.8).unwrap()),
Severity::Critical,
Some("2.0.0".to_string()),
None,
)
.unwrap();
let vuln2 = Vulnerability::new(
"CVE-2024-5678".to_string(),
Some(CvssScore::new(5.0).unwrap()),
Severity::Medium,
Some("2.1.0".to_string()),
None,
)
.unwrap();
let pkg_vulns = PackageVulnerabilities::new(
"requests".to_string(),
"1.0.0".to_string(),
vec![vuln1, vuln2],
);
assert_eq!(pkg_vulns.package_name(), "requests");
assert_eq!(pkg_vulns.current_version(), "1.0.0");
assert_eq!(pkg_vulns.vulnerabilities().len(), 2);
}
}