104 lines
3.2 KiB
Rust
104 lines
3.2 KiB
Rust
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<bson::oid::ObjectId>,
|
|
/// 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<f64>,
|
|
/// Short summary of the vulnerability
|
|
pub summary: Option<String>,
|
|
/// Link to vulnerability details
|
|
pub url: Option<String>,
|
|
/// 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<Utc>,
|
|
/// When the user last interacted with this notification
|
|
pub read_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
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<f64>) -> 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,
|
|
}
|
|
}
|