1use chrono::{DateTime, Utc};
5use modgunn::core::Severity;
6use modgunn::scan::{Advisory, AdvisoryDb};
7use modgunn::versions::RangeEvent;
8use serde::{Deserialize, Serialize};
9
10pub const SET_SCHEMA: &str = "holger.intel-advisories/v1";
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct IntelAdvisory {
17 pub id: String,
18 pub ecosystem: String,
19 pub package: String,
20 #[serde(default, skip_serializing_if = "Vec::is_empty")]
21 pub versions: Vec<String>,
22 #[serde(default, skip_serializing_if = "Vec::is_empty")]
23 pub ranges: Vec<Vec<RangeEvent>>,
24 pub severity: Severity,
25 #[serde(default, skip_serializing_if = "String::is_empty")]
26 pub summary: String,
27 #[serde(default, skip_serializing_if = "Vec::is_empty")]
28 pub aliases: Vec<String>,
29 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
31 pub kev: bool,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub epss: Option<f64>,
34 #[serde(default, skip_serializing_if = "String::is_empty")]
35 pub cvss: String,
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub cwe: Vec<String>,
38 #[serde(default, skip_serializing_if = "String::is_empty")]
39 pub source: String,
40}
41
42impl IntelAdvisory {
43 pub fn from_advisory(a: &Advisory) -> Self {
44 Self {
45 id: a.id.clone(),
46 ecosystem: a.ecosystem.clone(),
47 package: a.package.clone(),
48 versions: a.affected_versions.clone(),
49 ranges: a.ranges.clone(),
50 severity: a.severity,
51 summary: a.summary.clone(),
52 aliases: a.aliases.clone(),
53 kev: a.kev,
54 epss: a.epss,
55 cvss: a.cvss_vector.clone(),
56 cwe: a.cwe.clone(),
57 source: a.source.clone(),
58 }
59 }
60
61 pub fn to_advisory(&self) -> Advisory {
62 let mut a = Advisory::basic(
63 self.id.clone(),
64 self.ecosystem.clone(),
65 self.package.clone(),
66 self.versions.clone(),
67 self.severity,
68 self.summary.clone(),
69 )
70 .with_source(self.source.clone())
71 .with_range_sets(self.ranges.clone());
72 a.aliases = self.aliases.clone();
73 a.kev = self.kev;
74 a.epss = self.epss;
75 a.cvss_vector = self.cvss.clone();
76 a.cwe = self.cwe.clone();
77 a
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct SourceRecord {
84 pub feed: String,
86 pub uri: String,
87 pub sha256: String,
88 pub bytes: u64,
89 pub fetched_at: DateTime<Utc>,
90 pub records: u64,
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct AdvisorySet {
97 pub schema: String,
98 pub generated_at: DateTime<Utc>,
99 pub sources: Vec<SourceRecord>,
100 pub advisories: Vec<IntelAdvisory>,
101}
102
103impl AdvisorySet {
104 pub fn from_advisories(
107 generated_at: DateTime<Utc>,
108 sources: Vec<SourceRecord>,
109 advisories: &[Advisory],
110 ) -> Self {
111 let mut out: Vec<IntelAdvisory> = advisories
112 .iter()
113 .filter(|a| !a.withdrawn)
114 .map(IntelAdvisory::from_advisory)
115 .collect();
116 out.sort_by(|a, b| {
117 (&a.ecosystem, &a.package, &a.id).cmp(&(&b.ecosystem, &b.package, &b.id))
118 });
119 Self { schema: SET_SCHEMA.to_string(), generated_at, sources, advisories: out }
120 }
121
122 pub fn to_json_bytes(&self) -> serde_json::Result<Vec<u8>> {
123 serde_json::to_vec(self)
124 }
125
126 pub fn from_json_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
128 let set: Self = serde_json::from_slice(bytes)
129 .map_err(|e| anyhow::anyhow!("the advisory set is not valid JSON: {e}"))?;
130 anyhow::ensure!(
131 set.schema == SET_SCHEMA,
132 "the advisory set has schema {:?}; this build reads {SET_SCHEMA:?}",
133 set.schema
134 );
135 Ok(set)
136 }
137
138 pub fn to_advisory_db(&self, rev: impl Into<String>) -> AdvisoryDb {
140 AdvisoryDb::new(rev, self.advisories.iter().map(IntelAdvisory::to_advisory).collect())
141 }
142}