use std::{
collections::{BTreeMap, HashSet},
io::{Read, Write},
};
use anyhow::{Context, Result, anyhow, ensure};
use serde::{Deserialize, Serialize};
use crate::{Finding, ScanReport, context::ScanContext};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaselineEntry {
pub finding: Finding,
pub disposition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Baseline {
pub schema_version: u32,
pub context: ScanContext,
pub entries: BTreeMap<String, BaselineEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaselineDiff {
pub complete: bool,
pub new: Vec<Finding>,
pub resolved: Vec<BaselineEntry>,
}
impl Baseline {
pub fn from_report(report: &ScanReport, previous: Option<&Self>) -> Result<Self> {
ensure!(
report.complete && report.errors.is_empty(),
"cannot create a baseline from an incomplete scan"
);
let context = report
.context
.as_ref()
.context("cannot create a baseline without scan context")?;
if let Some(previous) = previous {
ensure!(
previous.context == *context,
"baseline scan context mismatch"
);
}
let mut entries = BTreeMap::new();
for finding in &report.findings {
entries
.entry(finding.fingerprint.clone())
.or_insert_with(|| BaselineEntry {
finding: finding.clone(),
disposition: previous
.and_then(|baseline| baseline.entries.get(&finding.fingerprint))
.and_then(|entry| entry.disposition.clone()),
});
}
Ok(Self {
schema_version: 2,
context: context.clone(),
entries,
})
}
pub fn read(reader: impl Read) -> Result<Self> {
let baseline: Self = serde_json::from_reader(reader).map_err(|error| {
anyhow!(
"invalid baseline JSON ({:?}) at line {} column {}",
error.classify(),
error.line(),
error.column()
)
})?;
ensure!(
baseline.schema_version == 2,
"unsupported baseline schema version {}",
baseline.schema_version
);
for (fingerprint, entry) in &baseline.entries {
ensure!(
!fingerprint.is_empty() && fingerprint == &entry.finding.fingerprint,
"baseline entry has an inconsistent fingerprint"
);
}
Ok(baseline)
}
pub fn write(&self, mut writer: impl Write) -> Result<()> {
serde_json::to_writer_pretty(&mut writer, self).context("could not serialize baseline")?;
writer.write_all(b"\n")?;
writer.flush()?;
Ok(())
}
pub fn diff(&self, report: &ScanReport) -> Result<BaselineDiff> {
let context = report
.context
.as_ref()
.context("cannot compare a baseline without scan context")?;
ensure!(self.context == *context, "baseline scan context mismatch");
let complete = report.complete && report.errors.is_empty();
let present: HashSet<&str> = report
.findings
.iter()
.map(|finding| finding.fingerprint.as_str())
.collect();
Ok(BaselineDiff {
complete,
new: report
.findings
.iter()
.filter(|finding| !self.entries.contains_key(&finding.fingerprint))
.cloned()
.collect(),
resolved: if complete {
self.entries
.iter()
.filter(|(fingerprint, _)| !present.contains(fingerprint.as_str()))
.map(|(_, entry)| entry.clone())
.collect()
} else {
vec![]
},
})
}
}