WP-20/21: contract and failing tests for vulnerability management
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-02 22:40:48 +02:00
parent e6ef9b21d3
commit 5c6e09ad10
17 changed files with 1530 additions and 1 deletions

View 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,
}