WP-20/21: contract and failing tests for vulnerability management
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
171
backend/crates/domain/src/vuln.rs
Normal file
171
backend/crates/domain/src/vuln.rs
Normal file
@ -0,0 +1,171 @@
|
||||
//! Vulnerability findings from scans of the OS and container images.
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Severity {
|
||||
Unknown,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Critical,
|
||||
}
|
||||
|
||||
impl Severity {
|
||||
pub const ALL: [Severity; 5] = [
|
||||
Severity::Critical,
|
||||
Severity::High,
|
||||
Severity::Medium,
|
||||
Severity::Low,
|
||||
Severity::Unknown,
|
||||
];
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Severity::Critical => "critical",
|
||||
Severity::High => "high",
|
||||
Severity::Medium => "medium",
|
||||
Severity::Low => "low",
|
||||
Severity::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Severity {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"critical" => Severity::Critical,
|
||||
"high" => Severity::High,
|
||||
"medium" => Severity::Medium,
|
||||
"low" => Severity::Low,
|
||||
_ => Severity::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TargetKind {
|
||||
Os,
|
||||
Image,
|
||||
}
|
||||
|
||||
impl TargetKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
TargetKind::Os => "os",
|
||||
TargetKind::Image => "image",
|
||||
}
|
||||
}
|
||||
pub fn parse(s: &str) -> Option<TargetKind> {
|
||||
match s {
|
||||
"os" => Some(TargetKind::Os),
|
||||
"image" => Some(TargetKind::Image),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FindingStatus {
|
||||
Open,
|
||||
Acknowledged,
|
||||
Fixed,
|
||||
}
|
||||
|
||||
impl FindingStatus {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
FindingStatus::Open => "open",
|
||||
FindingStatus::Acknowledged => "acknowledged",
|
||||
FindingStatus::Fixed => "fixed",
|
||||
}
|
||||
}
|
||||
pub fn parse(s: &str) -> Option<FindingStatus> {
|
||||
match s {
|
||||
"open" => Some(FindingStatus::Open),
|
||||
"acknowledged" => Some(FindingStatus::Acknowledged),
|
||||
"fixed" => Some(FindingStatus::Fixed),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One vulnerability as reported by the scanner, without lifecycle data.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RawFinding {
|
||||
pub cve_id: String,
|
||||
pub severity: Severity,
|
||||
pub package: String,
|
||||
pub installed_version: String,
|
||||
pub fixed_version: Option<String>,
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl RawFinding {
|
||||
/// Identity of a finding within a target.
|
||||
pub fn key(&self) -> String {
|
||||
format!(
|
||||
"{}|{}|{}",
|
||||
self.cve_id, self.package, self.installed_version
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Finding {
|
||||
pub id: Uuid,
|
||||
pub target_kind: TargetKind,
|
||||
/// "os" for the host, otherwise the image reference.
|
||||
pub target: String,
|
||||
#[serde(flatten)]
|
||||
pub raw: RawFinding,
|
||||
pub status: FindingStatus,
|
||||
pub first_seen: DateTime<Utc>,
|
||||
pub last_seen: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct FindingFilter {
|
||||
pub min_severity: Option<Severity>,
|
||||
pub target: Option<String>,
|
||||
pub status: Option<FindingStatus>,
|
||||
/// Include fixed findings (default: only open + acknowledged).
|
||||
pub include_fixed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct SeverityCounts {
|
||||
pub critical: usize,
|
||||
pub high: usize,
|
||||
pub medium: usize,
|
||||
pub low: usize,
|
||||
pub unknown: usize,
|
||||
}
|
||||
|
||||
impl SeverityCounts {
|
||||
pub fn add(&mut self, s: Severity) {
|
||||
match s {
|
||||
Severity::Critical => self.critical += 1,
|
||||
Severity::High => self.high += 1,
|
||||
Severity::Medium => self.medium += 1,
|
||||
Severity::Low => self.low += 1,
|
||||
Severity::Unknown => self.unknown += 1,
|
||||
}
|
||||
}
|
||||
pub fn total(&self) -> usize {
|
||||
self.critical + self.high + self.medium + self.low + self.unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of one scan run.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ScanReport {
|
||||
pub targets: Vec<String>,
|
||||
pub failed_targets: Vec<String>,
|
||||
pub total_open: usize,
|
||||
pub new_findings: Vec<Finding>,
|
||||
pub fixed: usize,
|
||||
}
|
||||
Reference in New Issue
Block a user