use crate::{
config::{OutputConfig, OutputFormat},
prelude::*,
};
use abscissa_core::terminal::{
self,
Color::{self, Red, Yellow},
};
use rustsec::{
cargo_lock::{
dependency::{self, graph::EdgeDirection, Dependency},
Lockfile, Package,
},
warning, Vulnerability, Warning,
};
use std::{
collections::BTreeSet as Set,
io::{self, Write},
path::Path,
process::exit,
};
#[derive(Clone, Debug)]
pub struct Presenter {
displayed_packages: Set<Dependency>,
config: OutputConfig,
}
impl Presenter {
pub fn new(config: &OutputConfig) -> Self {
Self {
displayed_packages: Set::new(),
config: config.clone(),
}
}
pub fn before_report(&mut self, lockfile_path: &Path, lockfile: &Lockfile) {
if !self.config.is_quiet() {
status_ok!(
"Scanning",
"{} for vulnerabilities ({} crate dependencies)",
lockfile_path.display(),
lockfile.packages.len(),
);
}
}
pub fn print_report(
&mut self,
report: &rustsec::Report,
self_advisories: &[rustsec::Advisory],
lockfile: &Lockfile,
) {
if self.config.format == OutputFormat::Json {
serde_json::to_writer(io::stdout(), &report).unwrap();
io::stdout().flush().unwrap();
return;
}
if report.vulnerabilities.found {
status_err!("Vulnerable crates found!");
} else {
status_ok!("Success", "No vulnerable packages found");
}
let tree = lockfile
.dependency_tree()
.expect("invalid Cargo.lock dependency tree");
for vulnerability in &report.vulnerabilities.list {
self.print_vulnerability(vulnerability, &tree);
}
if !report.warnings.is_empty() {
println!();
let warning_word = if report.warnings.len() != 1 {
"warnings"
} else {
"warning"
};
if self.config.deny_warnings {
status_err!("{} {} found", report.warnings.len(), warning_word);
} else {
status_warn!("{} {} found", report.warnings.len(), warning_word);
}
for warning in &report.warnings {
self.print_warning(warning, &tree)
}
}
if !self_advisories.is_empty() {
println!();
let msg = "this copy of cargo-audit has known advisories!";
if self.config.deny_warnings {
status_err!(msg);
} else {
status_warn!(msg);
}
for advisory in self_advisories {
self.print_advisory_warning(&advisory.metadata);
}
}
if report.vulnerabilities.found {
println!();
if report.vulnerabilities.count == 1 {
status_err!("1 vulnerability found!");
} else {
status_err!("{} vulnerabilities found!", report.vulnerabilities.count);
}
}
if !report.warnings.is_empty() {
if !report.vulnerabilities.found {
println!();
}
let warnings_word = if report.warnings.len() != 1 {
"warnings"
} else {
"warning"
};
if self.config.deny_warnings {
status_err!(
"{} {} found and `--deny-warnings` enabled!",
report.warnings.len(),
warnings_word
);
exit(1);
} else {
status_warn!("{} {} found!", report.warnings.len(), warnings_word);
}
}
let upgrade_msg = "upgrade cargo-audit to the latest version: \
cargo install --force cargo-audit";
if !self_advisories.is_empty() {
if self.config.deny_warnings {
status_err!(upgrade_msg);
exit(1);
} else {
status_warn!(upgrade_msg);
}
}
}
fn print_vulnerability(&mut self, vulnerability: &Vulnerability, tree: &dependency::Tree) {
let advisory = &vulnerability.advisory;
println!();
self.print_attr(Red, "ID: ", &advisory.id);
self.print_attr(Red, "Crate: ", &vulnerability.package.name);
self.print_attr(Red, "Version: ", &vulnerability.package.version.to_string());
self.print_attr(Red, "Date: ", &advisory.date);
if let Some(url) = advisory.id.url() {
self.print_attr(Red, "URL: ", &url);
} else if let Some(url) = &advisory.url {
self.print_attr(Red, "URL: ", url);
}
self.print_attr(Red, "Title: ", &advisory.title);
if vulnerability.versions.patched.is_empty() {
self.print_attr(
Red,
"Solution:",
String::from(" No safe upgrade is available!"),
);
} else {
self.print_attr(
Red,
"Solution:",
String::from(" upgrade to ")
+ &vulnerability
.versions
.patched
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.as_slice()
.join(" OR "),
);
}
self.print_tree(Red, &vulnerability.package, tree);
}
fn print_warning(&mut self, warning: &Warning, tree: &dependency::Tree) {
match &warning.kind {
warning::Kind::Informational { advisory, .. }
| warning::Kind::Unmaintained { advisory, .. } => self.print_advisory_warning(advisory),
warning::Kind::Yanked => self.print_yanked_warning(&warning.package),
}
self.print_tree(self.warning_color(), &warning.package, tree);
}
fn warning_color(&self) -> Color {
if self.config.deny_warnings {
Red
} else {
Yellow
}
}
fn print_advisory_warning(&self, metadata: &rustsec::advisory::Metadata) {
let color = self.warning_color();
println!();
self.print_attr(color, "Crate: ", &metadata.package);
self.print_attr(color, "Title: ", &metadata.title);
self.print_attr(color, "Date: ", &metadata.date);
if let Some(url) = metadata.id.url() {
self.print_attr(color, "URL: ", &url);
} else if let Some(url) = &metadata.url {
self.print_attr(color, "URL: ", url);
}
}
fn print_yanked_warning(&self, package: &Package) {
let color = self.warning_color();
println!();
self.print_attr(color, "Crate: ", &package.name);
self.print_attr(color, "Version: ", package.version.to_string());
self.print_attr(color, "Warning: ", "package has been yanked!");
}
fn print_attr(&self, color: Color, attr: &str, content: impl AsRef<str>) {
terminal::status::Status::new()
.bold()
.color(color)
.status(attr)
.print_stdout(content.as_ref())
.unwrap();
}
fn print_tree(&mut self, color: Color, package: &Package, tree: &dependency::Tree) {
if !self.displayed_packages.insert(Dependency::from(package)) {
return;
}
if !self.config.show_tree.unwrap_or(true) {
return;
}
terminal::status::Status::new()
.bold()
.color(color)
.status("Dependency tree:")
.print_stdout("")
.unwrap();
let package_node = tree.nodes()[&Dependency::from(package)];
tree.render(&mut io::stdout(), package_node, EdgeDirection::Incoming)
.unwrap();
}
}