rustsec 0.17.1

Client library for the RustSec security advisory database
Documentation
//! 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::{package_scope::PackageScope, Database, Query},
    error::Error,
    lockfile::Lockfile,
    platforms::target::{Arch, OS},
    registry,
    vulnerability::Vulnerability,
    warning::{self, Warning},
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Vulnerability report for a given lockfile
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Report {
    /// Information about the advisory database
    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: Vec<Warning>,
}

impl Report {
    /// Generate a report for the given advisory database and lockfile
    pub fn generate(db: &Database, lockfile: &Lockfile, settings: &Settings) -> Self {
        let mut package_scope = &PackageScope::default();
        if let Some(scope) = &settings.package_scope {
            package_scope = scope;
        }

        let vulnerabilities = db
            .query_vulnerabilities(lockfile, &settings.query(), package_scope)
            .into_iter()
            .filter(|vuln| !settings.ignore.contains(&vuln.advisory.id))
            .collect();

        let mut warnings = find_warnings(db, lockfile, settings);

        // TODO(tarcieri): logging for registry index errors
        if let Ok(mut yanked) = find_yanked_crates(lockfile) {
            warnings.append(&mut yanked);
        }

        Self {
            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>,

    /// Scope of packages which should be considered for audit
    pub package_scope: Option<PackageScope>,
}

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
#[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: String,

    /// Date when the advisory database was last committed to
    #[serde(rename = "last-updated")]
    pub last_updated: DateTime<Utc>,
}

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().commit_id.clone(),
            last_updated: db.latest_commit().time,
        }
    }
}

/// 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,
        }
    }
}

/// Find warnings from the given advisory [`Database`] and [`Lockfile`]
pub fn find_warnings(db: &Database, lockfile: &Lockfile, settings: &Settings) -> Vec<Warning> {
    let query = settings.query().informational(true);
    let mut result = vec![];

    let mut package_scope = &PackageScope::default();
    if let Some(scope) = &settings.package_scope {
        package_scope = scope;
    }

    // TODO(tarcieri): abstract `Cargo.lock` query logic between vulnerabilities/warnings
    for advisory_vuln in db.query_vulnerabilities(lockfile, &query, package_scope) {
        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 kind = match advisory.informational.as_ref().unwrap() {
                advisory::Informational::Notice => warning::Kind::Informational {
                    advisory: advisory.clone(),
                    versions: advisory_vuln.versions.clone(),
                },
                advisory::Informational::Unmaintained => warning::Kind::Unmaintained {
                    advisory: advisory.clone(),
                    versions: advisory_vuln.versions.clone(),
                },
                advisory::Informational::Other(_) => continue,
            };

            result.push(Warning::new(kind, &advisory_vuln.package))
        }
    }

    result
}

/// If the index is available, check for yanked crates in the lockfile
fn find_yanked_crates(lockfile: &Lockfile) -> Result<Vec<Warning>, Error> {
    let index = registry::Index::open()?;

    let mut result = vec![];

    for package in &lockfile.packages {
        if index.find(&package.name, &package.version)?.is_yanked {
            result.push(Warning::new(warning::Kind::Yanked, package));
        }
    }

    Ok(result)
}