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};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Report {
pub database: DatabaseInfo,
pub lockfile: LockfileInfo,
pub settings: Settings,
pub vulnerabilities: VulnerabilityInfo,
pub warnings: Vec<Warning>,
}
impl Report {
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);
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,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Settings {
pub target_arch: Option<Arch>,
pub target_os: Option<OS>,
pub severity: Option<advisory::Severity>,
pub ignore: Vec<advisory::Id>,
pub informational_warnings: Vec<advisory::Informational>,
pub package_scope: Option<PackageScope>,
}
impl Settings {
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
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DatabaseInfo {
#[serde(rename = "advisory-count")]
pub advisory_count: usize,
#[serde(rename = "last-commit")]
pub last_commit: String,
#[serde(rename = "last-updated")]
pub last_updated: DateTime<Utc>,
}
impl DatabaseInfo {
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,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LockfileInfo {
#[serde(rename = "dependency-count")]
dependency_count: usize,
}
impl LockfileInfo {
pub fn new(lockfile: &Lockfile) -> Self {
Self {
dependency_count: lockfile.packages.len(),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct VulnerabilityInfo {
pub found: bool,
pub count: usize,
pub list: Vec<Vulnerability>,
}
impl VulnerabilityInfo {
pub fn new(list: Vec<Vulnerability>) -> Self {
Self {
found: !list.is_empty(),
count: list.len(),
list,
}
}
}
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;
}
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
}
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)
}