blob: 213ff6ecbd10b0de7903f80e9e67bfe5d7d9b523 [file] [log] [blame]
//! Vulnerability report generator
//!
//! These types map directly to the JSON report generated by `cargo-audit`,
//! but also provide the core reporting functionality used in general.
use crate::{
advisory,
database::{Database, Query},
map,
platforms::target::{Arch, OS},
vulnerability::Vulnerability,
warning::{self, Warning},
Lockfile, Map,
};
use serde::{Deserialize, Serialize};
/// Vulnerability report for a given lockfile
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Report {
/// Information about the advisory database
#[cfg(feature = "git")]
#[cfg_attr(docsrs, doc(cfg(feature = "git")))]
pub database: DatabaseInfo,
/// Information about the audited lockfile
pub lockfile: LockfileInfo,
/// Settings used when generating report
pub settings: Settings,
/// Vulnerabilities detected in project
pub vulnerabilities: VulnerabilityInfo,
/// Warnings about dependencies (from e.g. informational advisories)
pub warnings: WarningInfo,
}
impl Report {
/// Generate a report for the given advisory database and lockfile
pub fn generate(db: &Database, lockfile: &Lockfile, settings: &Settings) -> Self {
let vulnerabilities = db
.query_vulnerabilities(lockfile, &settings.query())
.into_iter()
.filter(|vuln| !settings.ignore.contains(&vuln.advisory.id))
.collect();
let warnings = find_warnings(db, lockfile, settings);
Self {
#[cfg(feature = "git")]
database: DatabaseInfo::new(db),
lockfile: LockfileInfo::new(lockfile),
settings: settings.clone(),
vulnerabilities: VulnerabilityInfo::new(vulnerabilities),
warnings,
}
}
}
/// Options to use when generating the report
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Settings {
/// CPU architecture
pub target_arch: Option<Arch>,
/// Operating system
pub target_os: Option<OS>,
/// Severity threshold to alert at
pub severity: Option<advisory::Severity>,
/// List of advisory IDs to ignore
pub ignore: Vec<advisory::Id>,
/// Types of informational advisories to generate warnings for
pub informational_warnings: Vec<advisory::Informational>,
}
impl Settings {
/// Get a query which corresponds to the configured report settings.
/// Note that queries can't filter ignored advisories, so this happens in
/// a separate pass
pub fn query(&self) -> Query {
let mut query = Query::crate_scope();
if let Some(target_arch) = self.target_arch {
query = query.target_arch(target_arch);
}
if let Some(target_os) = self.target_os {
query = query.target_os(target_os);
}
if let Some(severity) = self.severity {
query = query.severity(severity);
}
query
}
}
/// Information about the advisory database
#[cfg(feature = "git")]
#[cfg_attr(docsrs, doc(cfg(feature = "git")))]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DatabaseInfo {
/// Number of advisories in the database
#[serde(rename = "advisory-count")]
pub advisory_count: usize,
/// Git commit hash for the last commit to the database
#[serde(rename = "last-commit")]
pub last_commit: Option<String>,
/// Date when the advisory database was last committed to
#[serde(rename = "last-updated", with = "time::serde::rfc3339::option")]
pub last_updated: Option<time::OffsetDateTime>,
}
#[cfg(feature = "git")]
impl DatabaseInfo {
/// Create database information from the advisory db
pub fn new(db: &Database) -> Self {
Self {
advisory_count: db.iter().count(),
last_commit: db.latest_commit().map(|c| c.commit_id.to_hex()),
last_updated: db.latest_commit().map(|c| c.timestamp),
}
}
}
/// Information about `Cargo.lock`
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LockfileInfo {
/// Number of dependencies in the lock file
#[serde(rename = "dependency-count")]
dependency_count: usize,
}
impl LockfileInfo {
/// Create lockfile information from the given lockfile
pub fn new(lockfile: &Lockfile) -> Self {
Self {
dependency_count: lockfile.packages.len(),
}
}
}
/// Information about detected vulnerabilities
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct VulnerabilityInfo {
/// Were any vulnerabilities found?
pub found: bool,
/// Number of vulnerabilities found
pub count: usize,
/// List of detected vulnerabilities
pub list: Vec<Vulnerability>,
}
impl VulnerabilityInfo {
/// Create new vulnerability info
pub fn new(list: Vec<Vulnerability>) -> Self {
Self {
found: !list.is_empty(),
count: list.len(),
list,
}
}
}
/// Information about warnings
pub type WarningInfo = Map<warning::WarningKind, Vec<Warning>>;
/// Find warnings from the given advisory [`Database`] and [`Lockfile`]
pub fn find_warnings(db: &Database, lockfile: &Lockfile, settings: &Settings) -> WarningInfo {
let query = settings.query().informational(true);
let mut warnings = WarningInfo::default();
// TODO(tarcieri): abstract `Cargo.lock` query logic between vulnerabilities/warnings
for advisory_vuln in db.query_vulnerabilities(lockfile, &query) {
let advisory = &advisory_vuln.advisory;
if settings.ignore.contains(&advisory.id) {
continue;
}
if settings
.informational_warnings
.iter()
.any(|info| Some(info) == advisory.informational.as_ref())
{
let warning_kind = match advisory
.informational
.as_ref()
.expect("informational advisory")
.warning_kind()
{
Some(kind) => kind,
None => continue,
};
let warning = Warning::new(
warning_kind,
&advisory_vuln.package,
Some(advisory.clone()),
advisory_vuln.affected.clone(),
Some(advisory_vuln.versions.clone()),
);
match warnings.entry(warning.kind) {
map::Entry::Occupied(entry) => (*entry.into_mut()).push(warning),
map::Entry::Vacant(entry) => {
entry.insert(vec![warning]);
}
}
}
}
warnings
}