use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; /// Status of a CVE notification #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum NotificationStatus { /// Newly created, not yet seen by the user New, /// User has seen it (e.g., opened the notification panel) Read, /// User has explicitly acknowledged/dismissed it Dismissed, } /// Severity level for notification filtering #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "lowercase")] pub enum NotificationSeverity { Low, Medium, High, Critical, } /// A notification about a newly discovered CVE affecting a tracked dependency. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CveNotification { #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] pub id: Option, /// The CVE/GHSA identifier pub cve_id: String, /// Repository where the vulnerable dependency is used pub repo_id: String, /// Repository name (denormalized for display) pub repo_name: String, /// Affected package name pub package_name: String, /// Affected version pub package_version: String, /// Human-readable severity pub severity: NotificationSeverity, /// CVSS score if available pub cvss_score: Option, /// Short summary of the vulnerability pub summary: Option, /// Link to vulnerability details pub url: Option, /// Notification lifecycle status pub status: NotificationStatus, /// When the CVE was first detected for this dependency #[serde(with = "super::serde_helpers::bson_datetime")] pub created_at: DateTime, /// When the user last interacted with this notification pub read_at: Option>, } impl CveNotification { pub fn new( cve_id: String, repo_id: String, repo_name: String, package_name: String, package_version: String, severity: NotificationSeverity, ) -> Self { Self { id: None, cve_id, repo_id, repo_name, package_name, package_version, severity, cvss_score: None, summary: None, url: None, status: NotificationStatus::New, created_at: Utc::now(), read_at: None, } } } /// Map an OSV/NVD severity string to our notification severity pub fn parse_severity(s: Option<&str>, cvss: Option) -> NotificationSeverity { // Prefer CVSS score if available if let Some(score) = cvss { return match score { s if s >= 9.0 => NotificationSeverity::Critical, s if s >= 7.0 => NotificationSeverity::High, s if s >= 4.0 => NotificationSeverity::Medium, _ => NotificationSeverity::Low, }; } // Fall back to string severity match s.map(|s| s.to_uppercase()).as_deref() { Some("CRITICAL") => NotificationSeverity::Critical, Some("HIGH") => NotificationSeverity::High, Some("MODERATE" | "MEDIUM") => NotificationSeverity::Medium, _ => NotificationSeverity::Low, } }