#[cfg(feature = "fix")]
mod fix;
#[cfg(feature = "binary-scanning")]
mod binary_scanning;
use crate::{
auditor::Auditor,
config::{AuditConfig, DenyOption, FilterList, OutputFormat},
error::display_err_with_source,
lockfile,
prelude::*,
};
use abscissa_core::{
FrameworkError, FrameworkErrorKind, config::Override, error::Context, terminal::ColorChoice,
};
use clap::{Parser, ValueEnum};
use rustsec::platforms::target::{Arch, OS};
use std::{
fmt,
io::{self, IsTerminal},
path::PathBuf,
process::exit,
};
#[cfg(feature = "binary-scanning")]
use self::binary_scanning::BinCommand;
#[cfg(feature = "fix")]
use self::fix::FixCommand;
#[cfg(any(feature = "fix", feature = "binary-scanning"))]
use clap::Subcommand;
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
#[value(rename_all = "kebab-case")] enum Color {
Always,
#[default]
Auto,
Never,
}
impl From<Color> for ColorChoice {
fn from(value: Color) -> Self {
match value {
Color::Always => ColorChoice::Always,
Color::Auto => ColorChoice::Auto,
Color::Never => ColorChoice::Never,
}
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Color::Always => f.write_str("always"),
Color::Auto => f.write_str("auto"),
Color::Never => f.write_str("never"),
}
}
}
#[derive(Command, Clone, Default, Debug, Parser)]
#[command(version)]
pub struct AuditCommand {
#[cfg(any(feature = "fix", feature = "binary-scanning"))]
#[command(subcommand)]
subcommand: Option<AuditSubcommand>,
#[arg(
short = 'c',
long = "color",
help = "color configuration (default: auto)"
)]
color: Option<Color>,
#[arg(
short,
long = "db",
help = "advisory database git repo path (default: ~/.cargo/advisory-db)"
)]
db: Option<PathBuf>,
#[arg(
short = 'D',
long = "deny",
help = "exit with an error on: warnings (any), unmaintained, unsound, yanked"
)]
deny: Vec<DenyOption>,
#[arg(
short = 'f',
long = "file",
help = "Cargo lockfile to inspect (or `-` for STDIN, default: Cargo.lock)"
)]
file: Option<PathBuf>,
#[arg(
long = "ignore",
value_name = "ADVISORY_ID",
help = "Advisory id to ignore (can be specified multiple times)"
)]
ignore: Vec<String>,
#[arg(
short = 'n',
long = "no-fetch",
help = "do not perform a git fetch on the advisory DB"
)]
no_fetch: bool,
#[arg(long = "stale", help = "allow stale database")]
stale: bool,
#[arg(
long = "target-arch",
help = "filter vulnerabilities by CPU (default: no filter). Can be specified multiple times"
)]
target_arch: Vec<Arch>,
#[arg(
long = "target-os",
help = "filter vulnerabilities by OS (default: no filter). Can be specified multiple times"
)]
target_os: Vec<OS>,
#[arg(short = 'u', long = "url", help = "URL for advisory database git repo")]
url: Option<String>,
#[arg(
short = 'q',
long = "quiet",
help = "Avoid printing unnecessary information"
)]
quiet: bool,
#[arg(
long = "format",
value_name = "FORMAT",
help = "Output format: terminal, json, or sarif"
)]
output_format: Option<OutputFormat>,
#[arg(long = "json", help = "Output report in JSON format")]
output_json: bool,
}
#[cfg(any(feature = "fix", feature = "binary-scanning"))]
#[derive(Subcommand, Clone, Debug, Runnable)]
pub enum AuditSubcommand {
#[cfg(feature = "fix")]
#[command(about = "automatically upgrade vulnerable dependencies")]
Fix(FixCommand),
#[cfg(feature = "binary-scanning")]
#[command(
about = "scan compiled binaries",
long_about = "Scan compiled binaries for known vulnerabilities.
Performs a complete scan if the binary is built with 'cargo auditable'.
If not, recovers a part of the dependency list from panic messages."
)]
Bin(BinCommand),
}
impl AuditCommand {
pub fn term_colors(&self) -> ColorChoice {
if let Some(color) = self.color {
return color.into();
}
match std::env::var("CARGO_TERM_COLOR") {
Ok(e) if e == "always" => ColorChoice::Always,
Ok(e) if e == "never" => ColorChoice::Never,
Ok(e) if e == "auto" => ColorChoice::Auto,
_ => match io::stdout().is_terminal() {
true => ColorChoice::Auto,
false => ColorChoice::Never,
},
}
}
}
impl Override<AuditConfig> for AuditCommand {
fn override_config(&self, config: AuditConfig) -> Result<AuditConfig, FrameworkError> {
let mut config = config;
if let Some(db) = &self.db {
config.database.path = Some(db.into());
}
for advisory_id in &self.ignore {
config.advisories.ignore.push(
advisory_id
.parse()
.map_err(|e| Context::new(FrameworkErrorKind::ParseError, Some(Box::new(e))))?,
);
}
config.database.fetch &= !self.no_fetch;
config.database.stale |= self.stale;
if !self.target_arch.is_empty() {
config.target.arch = Some(FilterList::Many(self.target_arch.clone()));
}
if !self.target_os.is_empty() {
config.target.os = Some(FilterList::Many(self.target_os.clone()));
}
if let Some(url) = &self.url {
config.database.url = Some(url.clone())
}
for kind in &self.deny {
if *kind == DenyOption::Warnings {
config.output.deny = DenyOption::all();
} else {
config.output.deny.push(*kind);
}
}
config.output.quiet |= self.quiet;
if self.output_json {
config.output.format = OutputFormat::Json;
} else if let Some(format) = self.output_format {
config.output.format = format;
}
Ok(config)
}
}
impl Runnable for AuditCommand {
fn run(&self) {
#[cfg(feature = "fix")]
if let Some(AuditSubcommand::Fix(fix)) = &self.subcommand {
fix.run();
exit(0)
}
#[cfg(feature = "binary-scanning")]
if let Some(AuditSubcommand::Bin(bin)) = &self.subcommand {
bin.run();
exit(0)
}
let maybe_path = self.file.as_deref();
let path = lockfile::locate_or_generate(maybe_path).unwrap_or_else(|e| {
status_err!("{}", display_err_with_source(&e));
exit(2);
});
let mut auditor = self.auditor();
let report = auditor.audit_lockfile(&path);
match report {
Ok(report) => {
if auditor.should_exit_with_failure(&report) {
exit(1);
}
exit(0);
}
Err(e) => {
status_err!("{}", display_err_with_source(&e));
exit(2);
}
};
}
}
impl AuditCommand {
pub fn auditor(&self) -> Auditor {
Auditor::new(&APP.config())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn override_default_fetch_option() {
let mut config: AuditConfig = AuditConfig::default();
assert!(config.database.fetch);
let mut audit_command = AuditCommand::default();
let overridden_config = audit_command.override_config(config.clone()).unwrap();
assert!(overridden_config.database.fetch);
config.database.fetch = false;
let overridden_config = audit_command.override_config(config.clone()).unwrap();
assert!(!overridden_config.database.fetch);
config.database.fetch = true;
audit_command.no_fetch = true;
let overridden_config = audit_command.override_config(config.clone()).unwrap();
assert!(!overridden_config.database.fetch);
}
}