use chrono::{DateTime, Utc};
use modgunn::core::Severity;
use modgunn::scan::{Advisory, AdvisoryDb};
use modgunn::versions::RangeEvent;
use serde::{Deserialize, Serialize};
pub const SET_SCHEMA: &str = "holger.intel-advisories/v1";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IntelAdvisory {
pub id: String,
pub ecosystem: String,
pub package: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub versions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ranges: Vec<Vec<RangeEvent>>,
pub severity: Severity,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub summary: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub kev: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub epss: Option<f64>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub cvss: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cwe: Vec<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub source: String,
}
impl IntelAdvisory {
pub fn from_advisory(a: &Advisory) -> Self {
Self {
id: a.id.clone(),
ecosystem: a.ecosystem.clone(),
package: a.package.clone(),
versions: a.affected_versions.clone(),
ranges: a.ranges.clone(),
severity: a.severity,
summary: a.summary.clone(),
aliases: a.aliases.clone(),
kev: a.kev,
epss: a.epss,
cvss: a.cvss_vector.clone(),
cwe: a.cwe.clone(),
source: a.source.clone(),
}
}
pub fn to_advisory(&self) -> Advisory {
let mut a = Advisory::basic(
self.id.clone(),
self.ecosystem.clone(),
self.package.clone(),
self.versions.clone(),
self.severity,
self.summary.clone(),
)
.with_source(self.source.clone())
.with_range_sets(self.ranges.clone());
a.aliases = self.aliases.clone();
a.kev = self.kev;
a.epss = self.epss;
a.cvss_vector = self.cvss.clone();
a.cwe = self.cwe.clone();
a
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SourceRecord {
pub feed: String,
pub uri: String,
pub sha256: String,
pub bytes: u64,
pub fetched_at: DateTime<Utc>,
pub records: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdvisorySet {
pub schema: String,
pub generated_at: DateTime<Utc>,
pub sources: Vec<SourceRecord>,
pub advisories: Vec<IntelAdvisory>,
}
impl AdvisorySet {
pub fn from_advisories(
generated_at: DateTime<Utc>,
sources: Vec<SourceRecord>,
advisories: &[Advisory],
) -> Self {
let mut out: Vec<IntelAdvisory> = advisories
.iter()
.filter(|a| !a.withdrawn)
.map(IntelAdvisory::from_advisory)
.collect();
out.sort_by(|a, b| {
(&a.ecosystem, &a.package, &a.id).cmp(&(&b.ecosystem, &b.package, &b.id))
});
Self { schema: SET_SCHEMA.to_string(), generated_at, sources, advisories: out }
}
pub fn to_json_bytes(&self) -> serde_json::Result<Vec<u8>> {
serde_json::to_vec(self)
}
pub fn from_json_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
let set: Self = serde_json::from_slice(bytes)
.map_err(|e| anyhow::anyhow!("the advisory set is not valid JSON: {e}"))?;
anyhow::ensure!(
set.schema == SET_SCHEMA,
"the advisory set has schema {:?}; this build reads {SET_SCHEMA:?}",
set.schema
);
Ok(set)
}
pub fn to_advisory_db(&self, rev: impl Into<String>) -> AdvisoryDb {
AdvisoryDb::new(rev, self.advisories.iter().map(IntelAdvisory::to_advisory).collect())
}
}