use std::collections::HashMap;
use std::ops::Deref;
use std::panic::panic_any;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{fs::File, io, panic, path::PathBuf};
use cargo_metadata::{Metadata, Package, Version};
use clap::{CommandFactory, Parser};
use console::Term;
use errors::{
AuditAsError, AuditAsErrors, CacheAcquireError, CertifyError, NeedsAuditAsError,
NeedsAuditAsErrors, ShouldntBeAuditAsError, ShouldntBeAuditAsErrors, UserInfoError,
};
use format::{CriteriaName, CriteriaStr, PackageName, PolicyEntry};
use futures_util::future::join_all;
use indicatif::ProgressDrawTarget;
use lazy_static::lazy_static;
use miette::{miette, Context, Diagnostic, IntoDiagnostic};
use network::Network;
use reqwest::Url;
use serde::de::Deserialize;
use thiserror::Error;
use tracing::{error, info, trace, warn};
use crate::cli::*;
use crate::errors::{CommandError, DownloadError, RegenerateExemptionsError};
use crate::format::{
AuditEntry, AuditKind, AuditsFile, ConfigFile, CriteriaEntry, DependencyCriteria,
ExemptedDependency, FetchCommand, ImportsFile, MetaConfig, MetaConfigInstance, PackageStr,
SortedMap, StoreInfo,
};
use crate::out::{indeterminate_spinner, Out, StderrLogWriter, MULTIPROGRESS};
use crate::resolver::{CriteriaMapper, CriteriaNamespace, DepGraph, ResolveDepth};
use crate::storage::{Cache, Store};
mod cli;
mod editor;
pub mod errors;
mod flock;
pub mod format;
pub mod network;
mod out;
pub mod resolver;
mod serialization;
pub mod storage;
#[cfg(test)]
mod tests;
pub struct Config {
pub metacfg: MetaConfig,
pub metadata: Metadata,
_rest: PartialConfig,
}
pub struct PartialConfig {
pub cli: Cli,
pub cache_dir: PathBuf,
pub mock_cache: bool,
}
impl Deref for Config {
type Target = PartialConfig;
fn deref(&self) -> &Self::Target {
&self._rest
}
}
pub trait PackageExt {
fn is_third_party(&self, policy: &SortedMap<PackageName, PolicyEntry>) -> bool;
}
impl PackageExt for Package {
fn is_third_party(&self, policy: &SortedMap<PackageName, PolicyEntry>) -> bool {
let forced_third_party = policy
.get(&self.name)
.and_then(|policy| policy.audit_as_crates_io)
.unwrap_or(false);
let is_crates_io = self
.source
.as_ref()
.map(|s| s.is_crates_io())
.unwrap_or(false);
forced_third_party || is_crates_io
}
}
const CACHE_DIR_SUFFIX: &str = "cargo-vet";
const CARGO_ENV: &str = "CARGO";
const PACKAGE_VET_CONFIG: &str = "vet";
const WORKSPACE_VET_CONFIG: &str = "vet";
const DURATION_DAY: Duration = Duration::from_secs(60 * 60 * 24);
struct ExitPanic(i32);
type ReportErrorFunc = dyn Fn(&miette::Report) + Send + Sync + 'static;
lazy_static! {
static ref REPORT_ERROR: Mutex<Option<Box<ReportErrorFunc>>> = Mutex::new(None);
}
fn set_report_errors_as_json(out: Arc<dyn Out>) {
*REPORT_ERROR.lock().unwrap() = Some(Box::new(move |error| {
let mut report = String::new();
miette::JSONReportHandler::new()
.render_report(&mut report, error.as_ref())
.unwrap();
writeln!(out, r#"{{"error": {}}}"#, report);
}));
}
fn report_error(error: &miette::Report) {
{
let guard = REPORT_ERROR.lock().unwrap();
if let Some(do_report) = &*guard {
do_report(error);
return;
}
}
error!("{:?}", error);
}
fn main() -> Result<(), ()> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.max_blocking_threads(128)
.enable_all()
.build()
.unwrap();
let _guard = runtime.enter();
let panic_result = std::panic::catch_unwind(real_main);
let main_result = match panic_result {
Ok(main_result) => main_result,
Err(e) => {
if let Some(ExitPanic(code)) = e.downcast_ref::<ExitPanic>() {
std::process::exit(*code);
} else {
std::panic::resume_unwind(e);
}
}
};
main_result.map_err(|e| {
report_error(&e);
std::process::exit(-1);
})
}
fn real_main() -> Result<(), miette::Report> {
use cli::Commands::*;
let fake_cli = cli::FakeCli::parse();
let cli::FakeCli::Vet(cli) = fake_cli;
if let Some(log_path) = &cli.log_file {
let log_file = File::create(log_path).unwrap();
tracing_subscriber::fmt::fmt()
.with_max_level(cli.verbose)
.with_target(false)
.without_time()
.with_ansi(false)
.with_writer(log_file)
.init();
} else {
tracing_subscriber::fmt::fmt()
.with_max_level(cli.verbose)
.with_target(false)
.without_time()
.with_ansi(console::colors_enabled_stderr())
.with_writer(StderrLogWriter::new)
.init();
}
let using_log_file = cli.log_file.is_some();
miette::set_hook(Box::new(move |_| {
let graphical_theme = if console::colors_enabled_stderr() && !using_log_file {
miette::GraphicalTheme::unicode()
} else {
miette::GraphicalTheme::unicode_nocolor()
};
Box::new(
miette::MietteHandlerOpts::new()
.graphical_theme(graphical_theme)
.build(),
)
}))
.expect("failed to initialize error handler");
panic::set_hook(Box::new(move |panic_info| {
if panic_info.payload().is::<ExitPanic>() {
return;
}
let payload = panic_info.payload();
let message = if let Some(msg) = payload.downcast_ref::<&str>() {
msg
} else if let Some(msg) = payload.downcast_ref::<String>() {
&msg[..]
} else {
"something went wrong"
};
#[derive(Debug, Error, Diagnostic)]
#[error("{message}")]
pub struct PanicError {
pub message: String,
#[help]
pub help: Option<String>,
}
report_error(
&miette::Report::from(PanicError {
message: message.to_owned(),
help: panic_info
.location()
.map(|loc| format!("at {}:{}:{}", loc.file(), loc.line(), loc.column())),
})
.wrap_err("cargo vet panicked"),
);
}));
MULTIPROGRESS.set_draw_target(ProgressDrawTarget::stderr());
let out: Arc<dyn Out> = if let Some(output_path) = &cli.output_file {
Arc::new(File::create(output_path).unwrap())
} else {
Arc::new(Term::stdout())
};
if cli.output_format == OutputFormat::Json {
set_report_errors_as_json(out.clone());
}
let cache_dir = dirs::cache_dir()
.unwrap_or_else(std::env::temp_dir)
.join(CACHE_DIR_SUFFIX);
let partial_cfg = PartialConfig {
cli,
cache_dir,
mock_cache: false,
};
match &partial_cfg.cli.command {
Some(HelpMarkdown(sub_args)) => return cmd_help_md(&out, &partial_cfg, sub_args),
Some(Gc(sub_args)) => return cmd_gc(&out, &partial_cfg, sub_args),
_ => {
}
}
let cli = &partial_cfg.cli;
let cargo_path = std::env::var_os(CARGO_ENV).expect("Cargo failed to set $CARGO, how?");
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.cargo_path(cargo_path);
if let Some(manifest_path) = &cli.manifest_path {
cmd.manifest_path(manifest_path);
}
if !cli.no_all_features {
cmd.features(cargo_metadata::CargoOpt::AllFeatures);
}
if cli.no_default_features {
cmd.features(cargo_metadata::CargoOpt::NoDefaultFeatures);
}
if !cli.features.is_empty() {
cmd.features(cargo_metadata::CargoOpt::SomeFeatures(cli.features.clone()));
}
let mut other_options = Vec::new();
if cli.frozen {
other_options.push("--frozen".to_string());
} else {
other_options.push("--locked".to_string());
}
cmd.other_options(other_options);
info!("Running: {:#?}", cmd.cargo_command());
let metadata = {
let _spinner = indeterminate_spinner("Running", "`cargo metadata`");
cmd.exec()
.into_diagnostic()
.wrap_err("'cargo metadata' exited unsuccessfully")?
};
trace!("Got Metadata!");
let default_config = MetaConfigInstance {
version: Some(1),
store: Some(StoreInfo {
path: Some(
metadata
.workspace_root
.join(storage::DEFAULT_STORE)
.into_std_path_buf(),
),
}),
};
let workspace_metacfg = metadata
.workspace_metadata
.get(WORKSPACE_VET_CONFIG)
.map(|cfg| {
MetaConfigInstance::deserialize(cfg)
.into_diagnostic()
.wrap_err("Workspace had [{WORKSPACE_VET_CONFIG}] but it was malformed")
})
.transpose()?;
let package_metacfg = metadata
.root_package()
.and_then(|r| r.metadata.get(PACKAGE_VET_CONFIG))
.map(|cfg| {
MetaConfigInstance::deserialize(cfg)
.into_diagnostic()
.wrap_err("Root package had [{PACKAGE_VET_CONFIG}] but it was malformed")
})
.transpose()?;
if workspace_metacfg.is_some() && package_metacfg.is_some() {
return Err(miette!("Both a workspace and a package defined [metadata.vet]! We don't know what that means, if you do, let us know!"));
}
let mut metacfgs = vec![default_config];
if let Some(metacfg) = workspace_metacfg {
metacfgs.push(metacfg);
}
if let Some(metacfg) = package_metacfg {
metacfgs.push(metacfg);
}
let metacfg = MetaConfig(metacfgs);
info!("Final Metadata Config: ");
info!(" - version: {}", metacfg.version());
info!(" - store.path: {:#?}", metacfg.store_path());
let init = Store::is_init(&metacfg);
if matches!(cli.command, Some(Commands::Init { .. })) {
if init {
return Err(miette!(
"'cargo vet' already initialized (store found at {})",
metacfg.store_path().display()
));
}
} else if !init {
return Err(miette!(
"You must run 'cargo vet init' (store not found at {})",
metacfg.store_path().display()
));
}
let cfg = Config {
metacfg,
metadata,
_rest: partial_cfg,
};
use RegenerateSubcommands::*;
match &cfg.cli.command {
None => cmd_check(&out, &cfg, &cfg.cli.check_args),
Some(Check(sub_args)) => cmd_check(&out, &cfg, sub_args),
Some(Init(sub_args)) => cmd_init(&out, &cfg, sub_args),
Some(Certify(sub_args)) => cmd_certify(&out, &cfg, sub_args),
Some(AddExemption(sub_args)) => cmd_add_exemption(&out, &cfg, sub_args),
Some(RecordViolation(sub_args)) => cmd_record_violation(&out, &cfg, sub_args),
Some(Suggest(sub_args)) => cmd_suggest(&out, &cfg, sub_args),
Some(Fmt(sub_args)) => cmd_fmt(&out, &cfg, sub_args),
Some(FetchImports(sub_args)) => cmd_fetch_imports(&out, &cfg, sub_args),
Some(DumpGraph(sub_args)) => cmd_dump_graph(&out, &cfg, sub_args),
Some(Inspect(sub_args)) => cmd_inspect(&out, &cfg, sub_args),
Some(Diff(sub_args)) => cmd_diff(&out, &cfg, sub_args),
Some(Regenerate(Imports(sub_args))) => cmd_regenerate_imports(&out, &cfg, sub_args),
Some(Regenerate(Exemptions(sub_args))) => cmd_regenerate_exemptions(&out, &cfg, sub_args),
Some(Regenerate(AuditAsCratesIo(sub_args))) => {
cmd_regenerate_audit_as(&out, &cfg, sub_args)
}
Some(HelpMarkdown(_)) | Some(Gc(_)) => unreachable!("handled earlier"),
}
}
fn cmd_init(_out: &Arc<dyn Out>, cfg: &Config, _sub_args: &InitArgs) -> Result<(), miette::Report> {
trace!("initializing...");
let mut store = Store::create(cfg)?;
let (config, audits, imports) = init_files(&cfg.metadata, cfg.cli.filter_graph.as_ref());
store.config = config;
store.audits = audits;
store.imports = imports;
fix_audit_as(cfg, &mut store)?;
store.commit()?;
Ok(())
}
pub fn init_files(
metadata: &Metadata,
filter_graph: Option<&Vec<GraphFilter>>,
) -> (ConfigFile, AuditsFile, ImportsFile) {
let audits = AuditsFile {
criteria: SortedMap::new(),
audits: SortedMap::new(),
};
let imports = ImportsFile {
audits: SortedMap::new(),
};
let config = {
let mut dependencies = SortedMap::new();
let graph = DepGraph::new(metadata, filter_graph, None);
for package in &graph.nodes {
if !package.is_third_party {
continue;
}
let criteria = if package.is_dev_only {
vec![format::DEFAULT_POLICY_DEV_CRITERIA.to_string().into()]
} else {
vec![format::DEFAULT_POLICY_CRITERIA.to_string().into()]
};
let item = ExemptedDependency {
version: package.version.clone(),
criteria,
dependency_criteria: DependencyCriteria::new(),
notes: None,
suggest: true,
};
dependencies
.entry(package.name.to_string())
.or_insert(vec![])
.push(item);
}
ConfigFile {
default_criteria: format::get_default_criteria(),
imports: SortedMap::new(),
exemptions: dependencies,
policy: SortedMap::new(),
}
};
(config, audits, imports)
}
fn cmd_inspect(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &InspectArgs,
) -> Result<(), miette::Report> {
let version = &sub_args.version;
let package = &*sub_args.package;
let fetched = {
let network = Network::acquire(cfg);
let store = Store::acquire(cfg, network.as_ref(), false)?;
let cache = Cache::acquire(cfg)?;
cache.set_last_fetch(FetchCommand::Inspect {
package: package.to_owned(),
version: version.clone(),
});
if sub_args.mode == FetchMode::Sourcegraph {
let url = format!("https://sourcegraph.com/crates/{package}@v{version}");
tokio::runtime::Handle::current()
.block_on(prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
None,
version,
Some(&url),
))
.into_diagnostic()?;
open::that(&url).into_diagnostic().wrap_err_with(|| {
format!("Couldn't open {url} in your browser, try --mode=local?")
})?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
return Ok(());
}
tokio::runtime::Handle::current().block_on(async {
let (pkg, eulas) = tokio::join!(
cache.fetch_package(network.as_ref(), package, version),
prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
None,
version,
None,
),
);
eulas.into_diagnostic()?;
pkg.into_diagnostic()
})?
};
#[cfg(target_family = "unix")]
if let Some(shell) = std::env::var_os("SHELL") {
writeln!(out, "Opening nested shell in: {:#?}", fetched);
writeln!(out, "Use `exit` or Ctrl-D to finish.",);
let status = std::process::Command::new(shell)
.current_dir(fetched.clone())
.env("PWD", fetched)
.status()
.map_err(CommandError::CommandFailed)
.into_diagnostic()?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
if let Some(code) = status.code() {
panic_any(ExitPanic(code));
}
return Ok(());
}
writeln!(out, " fetched to {:#?}", fetched);
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
Ok(())
}
fn cmd_certify(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &CertifyArgs,
) -> Result<(), miette::Report> {
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
let last_fetch = Cache::acquire(cfg)?.get_last_fetch();
do_cmd_certify(out, cfg, sub_args, &mut store, network.as_ref(), last_fetch)?;
match resolver::regenerate_exemptions(cfg, &mut store, false, false) {
Ok(()) | Err(RegenerateExemptionsError::ViolationConflict) => {}
}
store.commit()?;
Ok(())
}
fn do_cmd_certify(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &CertifyArgs,
store: &mut Store,
network: Option<&Network>,
last_fetch: Option<FetchCommand>,
) -> Result<(), CertifyError> {
let package = if let Some(package) = &sub_args.package {
package.clone()
} else if let Some(last_fetch) = &last_fetch {
last_fetch.package().to_owned()
} else {
return Err(CertifyError::CouldntGuessPackage);
};
if !sub_args.force
&& !foreign_packages(&cfg.metadata, &store.config).any(|pkg| pkg.name == *package)
{
return Err(CertifyError::NotAPackage(package));
}
let dependency_criteria = if sub_args.dependency_criteria.is_empty() {
DependencyCriteria::new()
} else {
let mut dep_criteria = DependencyCriteria::new();
for arg in &sub_args.dependency_criteria {
dep_criteria
.entry(arg.dependency.clone())
.or_insert_with(Vec::new)
.push(arg.criteria.clone().into());
}
dep_criteria
};
let kind = if let Some(v1) = &sub_args.version1 {
if let Some(v2) = &sub_args.version2 {
AuditKind::Delta {
from: v1.clone(),
to: v2.clone(),
dependency_criteria,
}
} else {
AuditKind::Full {
version: v1.clone(),
dependency_criteria,
}
}
} else if let Some(fetch) = last_fetch.filter(|f| f.package() == package) {
match fetch {
FetchCommand::Inspect { version, .. } => AuditKind::Full {
version,
dependency_criteria,
},
FetchCommand::Diff {
version1, version2, ..
} => AuditKind::Delta {
from: version1,
to: version2,
dependency_criteria,
},
}
} else {
return Err(CertifyError::CouldntGuessVersion(package));
};
let (username, who) = if let Some(who) = &sub_args.who {
(who.clone(), Some(who.clone()))
} else {
let user_info = get_user_info()?;
let who = format!("{} <{}>", user_info.username, user_info.email);
(user_info.username, Some(who))
};
let criteria_mapper = CriteriaMapper::new(
&store.audits.criteria,
store.imported_audits(),
&store.config.imports,
);
let criteria_names = if sub_args.criteria.is_empty() {
let (from, to) = match &kind {
AuditKind::Full { version, .. } => (None, version),
AuditKind::Delta { from, to, .. } => (Some(from), to),
_ => unreachable!(),
};
let mut chosen_criteria = guess_audit_criteria(cfg, store, &package, from, to);
loop {
out.clear_screen()?;
write!(out, "choose criteria to certify for {}", package);
match &kind {
AuditKind::Full { version, .. } => write!(out, ":{}", version),
AuditKind::Delta { from, to, .. } => write!(out, ":{} -> {}", from, to),
AuditKind::Violation { .. } => unreachable!(),
}
writeln!(out);
writeln!(out, " 0. <clear selections>");
let implied_criteria = criteria_mapper.criteria_from_list(&chosen_criteria);
let local_criteria = criteria_mapper
.list
.iter()
.enumerate()
.filter(|(_, info)| matches!(info.namespace, CriteriaNamespace::Local));
for (criteria_idx, criteria_info) in local_criteria {
if chosen_criteria.contains(&criteria_info.namespaced_name) {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
out.style().green().apply_to(&criteria_info.namespaced_name)
);
} else if implied_criteria.has_criteria(criteria_idx) {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
out.style()
.yellow()
.apply_to(&criteria_info.namespaced_name)
);
} else {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
&criteria_info.namespaced_name
);
}
}
writeln!(out);
writeln!(
out,
"current selection: {:?}",
criteria_mapper
.criteria_names(&implied_criteria)
.collect::<Vec<_>>()
);
writeln!(out, "(press ENTER to accept the current criteria)");
let input = out.read_line_with_prompt("> ")?;
let input = input.trim();
if input.is_empty() {
if chosen_criteria.is_empty() {
return Err(CertifyError::NoCriteriaChosen);
}
break;
}
let answer = if let Ok(val) = input.parse::<usize>() {
val
} else {
writeln!(out, "error: not a valid integer");
continue;
};
if answer == 0 {
chosen_criteria.clear();
continue;
}
if answer > criteria_mapper.list.len() {
writeln!(out, "error: not a valid criteria");
continue;
}
chosen_criteria.push(criteria_mapper.list[answer - 1].namespaced_name.clone());
}
chosen_criteria
} else {
sub_args.criteria.clone()
};
let criteria_set = criteria_mapper.criteria_from_list(&criteria_names);
let criteria_names = criteria_mapper
.criteria_names(&criteria_set)
.collect::<Vec<_>>();
let what_version = match &kind {
AuditKind::Full { version, .. } => {
format!("version {}", version)
}
AuditKind::Delta { from, to, .. } => {
format!("the changes from version {} to {}", from, to)
}
AuditKind::Violation { .. } => unreachable!(),
};
let statement = format!(
"I, {}, certify that I have audited {} of {} in accordance with the above criteria.",
username, what_version, package,
);
let mut notes = sub_args.notes.clone();
if !sub_args.accept_all {
let eulas = tokio::runtime::Handle::current().block_on(join_all(
criteria_names.iter().map(|criteria| async {
(
*criteria,
eula_for_criteria(network, &store.audits.criteria, criteria).await,
)
}),
));
let mut editor = out.editor("VET_CERTIFY")?;
if let Some(notes) = ¬es {
editor.select_comment_char(notes);
}
editor.add_comments(
"Please read the following criteria and then follow the instructions below:",
)?;
editor.add_text("")?;
for (criteria, eula) in &eulas {
editor.add_comments(&format!("=== BEGIN CRITERIA {:?} ===", criteria))?;
editor.add_comments("")?;
editor.add_comments(eula)?;
editor.add_comments("")?;
editor.add_comments("=== END CRITERIA ===")?;
editor.add_comments("")?;
}
editor.add_comments("Uncomment the following statement:")?;
editor.add_text("")?;
editor.add_comments(&statement)?;
editor.add_text("")?;
editor.add_comments("Add any notes about your audit below this line:")?;
editor.add_text("")?;
if let Some(notes) = ¬es {
editor.add_text(notes)?;
}
let editor_result = editor.edit()?;
let new_notes = match editor_result.trim_start().strip_prefix(&statement) {
Some(notes) => notes.trim_start_matches('\n'),
None => {
return Err(CertifyError::CouldntFindCertifyStatement);
}
};
let new_notes = new_notes
.strip_suffix('\n')
.filter(|s| !s.contains('\n'))
.unwrap_or(new_notes);
notes = if new_notes.is_empty() {
None
} else {
Some(new_notes.to_owned())
};
}
let new_entry = AuditEntry {
kind: kind.clone(),
criteria: criteria_names
.iter()
.map(|s| s.to_string().into())
.collect(),
who,
notes,
is_fresh_import: false,
};
store
.audits
.audits
.entry(package.clone())
.or_insert(vec![])
.push(new_entry);
if let AuditKind::Full { version, .. } = &kind {
if let Some(exemption_list) = store.config.exemptions.get_mut(&package) {
let cur_criteria_set = criteria_mapper.criteria_from_list(criteria_names);
for idx in (0..exemption_list.len()).rev() {
let entry = &exemption_list[idx];
let entry_criteria_set = criteria_mapper.criteria_from_list(&entry.criteria);
if &entry.version == version && cur_criteria_set.contains(&entry_criteria_set) {
exemption_list.remove(idx);
}
}
if exemption_list.is_empty() {
store.config.exemptions.remove(&package);
}
}
}
Ok(())
}
fn guess_audit_criteria(
cfg: &Config,
store: &Store,
package: PackageStr<'_>,
from: Option<&Version>,
to: &Version,
) -> Vec<String> {
let criteria = resolver::resolve(
&cfg.metadata,
cfg.cli.filter_graph.as_ref(),
store,
ResolveDepth::Deep,
)
.compute_suggested_criteria(package, from, to);
if !criteria.is_empty() {
return criteria;
}
resolver::resolve(
&cfg.metadata,
cfg.cli.filter_graph.as_ref(),
&store.clone_for_suggest(),
ResolveDepth::Deep,
)
.compute_suggested_criteria(package, from, to)
}
#[allow(clippy::too_many_arguments)]
async fn prompt_criteria_eulas(
out: &Arc<dyn Out>,
cfg: &Config,
network: Option<&Network>,
store: &Store,
package: PackageStr<'_>,
from: Option<&Version>,
to: &Version,
url: Option<&str>,
) -> Result<(), io::Error> {
let description = if let Some(from) = from {
format!(
"You are about to diff versions {} and {} of '{}'",
from, to, package
)
} else {
format!("You are about to inspect version {} of '{}'", to, package)
};
let criteria_names = guess_audit_criteria(cfg, store, package, from, to);
if criteria_names.is_empty() {
writeln!(out, "{}", out.style().bold().apply_to(description));
warn!("unable to determine likely criteria, this may not be a relevant audit for this project.");
} else {
let eulas = join_all(criteria_names.iter().map(|criteria| async {
(
&criteria[..],
eula_for_criteria(network, &store.audits.criteria, criteria).await,
)
}))
.await;
for (idx, (criteria, eula)) in eulas.into_iter().enumerate() {
let prompt = if idx == 0 {
format!(
"{}, likely to certify it for {:?}, which means:",
description, criteria
)
} else {
format!("... and for {:?}, which means:", criteria)
};
writeln!(
out,
"{}\n\n {}",
out.style().bold().apply_to(prompt),
eula.replace('\n', "\n "),
);
}
writeln!(
out,
"{}",
out.style().bold().apply_to(
"Please read the above criteria and consider them when performing the audit."
)
);
}
writeln!(
out,
"{}",
out.style().bold().apply_to(
"Other software projects may rely on this audit. Ask for help if you're not sure.\n"
)
);
let final_prompt = if let Some(url) = url {
writeln!(
out,
"You can inspect the {} here: {}\n",
if from.is_some() { "diff" } else { "crate" },
url,
);
"(press ENTER to open in your browser, or re-run with --mode=local)"
} else {
"(press ENTER to inspect locally)"
};
let out_ = out.clone();
tokio::task::spawn_blocking(move || out_.read_line_with_prompt(final_prompt)).await??;
Ok(())
}
fn cmd_record_violation(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &RecordViolationArgs,
) -> Result<(), miette::Report> {
let mut store = Store::acquire_offline(cfg)?;
let kind = AuditKind::Violation {
violation: sub_args.versions.clone(),
};
let (_username, who) = if let Some(who) = &sub_args.who {
(who.clone(), Some(who.clone()))
} else {
let user_info = get_user_info()?;
let who = format!("{} <{}>", user_info.username, user_info.email);
(user_info.username, Some(who))
};
let notes = sub_args.notes.clone();
let criteria = if sub_args.criteria.is_empty() {
vec![store.config.default_criteria.clone().into()]
} else {
sub_args
.criteria
.iter()
.map(|s| s.to_owned().into())
.collect()
};
if !sub_args.force
&& !foreign_packages(&cfg.metadata, &store.config).any(|pkg| pkg.name == sub_args.package)
{
return Err(miette!(
"'{}' isn't one of your foreign packages",
sub_args.package
));
}
let new_entry = AuditEntry {
kind,
criteria,
who,
notes,
is_fresh_import: false,
};
store
.audits
.audits
.entry(sub_args.package.clone())
.or_insert(vec![])
.push(new_entry);
store.commit()?;
writeln!(out, "If you've identified a security vulnerability in {} please report it at https://github.com/rustsec/advisory-db#reporting-vulnerabilities", sub_args.package);
Ok(())
}
fn cmd_add_exemption(
_out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &AddExemptionArgs,
) -> Result<(), miette::Report> {
let mut store = Store::acquire_offline(cfg)?;
let dependency_criteria = if sub_args.dependency_criteria.is_empty() {
DependencyCriteria::new()
} else {
let mut dep_criteria = DependencyCriteria::new();
for arg in &sub_args.dependency_criteria {
dep_criteria
.entry(arg.dependency.clone())
.or_insert_with(Vec::new)
.push(arg.criteria.clone().into());
}
dep_criteria
};
let notes = sub_args.notes.clone();
let criteria = if sub_args.criteria.is_empty() {
vec![store.config.default_criteria.clone().into()]
} else {
sub_args
.criteria
.iter()
.map(|s| s.to_owned().into())
.collect()
};
let suggest = !sub_args.no_suggest;
if !sub_args.force
&& !foreign_packages(&cfg.metadata, &store.config).any(|pkg| pkg.name == sub_args.package)
{
return Err(miette!(
"'{}' isn't one of your foreign packages",
sub_args.package
));
}
let new_entry = ExemptedDependency {
criteria,
notes,
version: sub_args.version.clone(),
dependency_criteria,
suggest,
};
store
.config
.exemptions
.entry(sub_args.package.clone())
.or_insert(vec![])
.push(new_entry);
store.commit()?;
Ok(())
}
fn cmd_suggest(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &SuggestArgs,
) -> Result<(), miette::Report> {
trace!("suggesting...");
let network = Network::acquire(cfg);
let suggest_store = Store::acquire(cfg, network.as_ref(), false)?.clone_for_suggest();
let report = resolver::resolve(
&cfg.metadata,
cfg.cli.filter_graph.as_ref(),
&suggest_store,
if sub_args.shallow {
ResolveDepth::Shallow
} else {
ResolveDepth::Deep
},
);
let suggest = report.compute_suggest(cfg, network.as_ref(), true)?;
match cfg.cli.output_format {
OutputFormat::Human => report
.print_suggest_human(out, cfg, suggest.as_ref())
.into_diagnostic()?,
OutputFormat::Json => report.print_json(out, cfg, suggest.as_ref())?,
}
Ok(())
}
fn cmd_regenerate_imports(
out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &RegenerateImportsArgs,
) -> Result<(), miette::Report> {
trace!("regenerating imports...");
if cfg.cli.locked {
writeln!(
out,
"warning: ran `regenerate imports` with --locked, this won't do anything!"
);
return Ok(());
}
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), true)?;
match resolver::regenerate_exemptions(cfg, &mut store, false, true) {
Ok(()) | Err(RegenerateExemptionsError::ViolationConflict) => {}
}
store.commit()?;
Ok(())
}
fn cmd_regenerate_audit_as(
_out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &RegenerateAuditAsCratesIoArgs,
) -> Result<(), miette::Report> {
trace!("regenerating audit-as-crates-io...");
let mut store = Store::acquire_offline(cfg)?;
fix_audit_as(cfg, &mut store)?;
store.commit()?;
Ok(())
}
fn fix_audit_as(cfg: &Config, store: &mut Store) -> Result<(), CacheAcquireError> {
let issues = check_audit_as_crates_io(cfg, store);
if let Err(AuditAsErrors { errors }) = issues {
for error in errors {
match error {
AuditAsError::NeedsAuditAs(needs) => {
for err in needs.errors {
store
.config
.policy
.entry(err.package)
.or_default()
.audit_as_crates_io = Some(false);
}
}
AuditAsError::ShouldntBeAuditAs(shouldnts) => {
for err in shouldnts.errors {
store
.config
.policy
.entry(err.package)
.or_default()
.audit_as_crates_io = Some(false);
}
}
AuditAsError::CacheAcquire(err) => return Err(err),
}
}
}
Ok(())
}
fn cmd_regenerate_exemptions(
_out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &RegenerateExemptionsArgs,
) -> Result<(), miette::Report> {
trace!("regenerating exemptions...");
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
resolver::regenerate_exemptions(cfg, &mut store, true, false)?;
store.commit()?;
Ok(())
}
fn cmd_diff(out: &Arc<dyn Out>, cfg: &Config, sub_args: &DiffArgs) -> Result<(), miette::Report> {
let version1 = &sub_args.version1;
let version2 = &sub_args.version2;
let package = &*sub_args.package;
let (fetched1, fetched2) = {
let network = Network::acquire(cfg);
let store = Store::acquire(cfg, network.as_ref(), false)?;
let cache = Cache::acquire(cfg)?;
cache.set_last_fetch(FetchCommand::Diff {
package: package.to_owned(),
version1: version1.clone(),
version2: version2.clone(),
});
if sub_args.mode == FetchMode::Sourcegraph {
let url = format!(
"https://sourcegraph.com/crates/{package}/-/compare/v{version1}...v{version2}"
);
tokio::runtime::Handle::current()
.block_on(prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
Some(version1),
version2,
Some(&url),
))
.into_diagnostic()?;
open::that(&url).into_diagnostic().wrap_err_with(|| {
format!("Couldn't open {url} in your browser, try --mode=local?")
})?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
return Ok(());
}
tokio::runtime::Handle::current().block_on(async {
let (pkgs, eulas) = tokio::join!(
async {
tokio::try_join!(
cache.fetch_package(network.as_ref(), package, version1),
cache.fetch_package(network.as_ref(), package, version2)
)
},
prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
Some(version1),
version2,
None,
)
);
eulas.into_diagnostic()?;
pkgs.into_diagnostic()
})?
};
writeln!(out);
std::process::Command::new("git")
.arg("diff")
.arg("--no-index")
.arg(&fetched1)
.arg(&fetched2)
.status()
.map_err(CommandError::CommandFailed)
.into_diagnostic()?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
Ok(())
}
fn cmd_check(out: &Arc<dyn Out>, cfg: &Config, sub_args: &CheckArgs) -> Result<(), miette::Report> {
trace!("vetting...");
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
if !cfg.cli.locked {
check_audit_as_crates_io(cfg, &store)?;
}
let report = resolver::resolve(
&cfg.metadata,
cfg.cli.filter_graph.as_ref(),
&store,
if sub_args.shallow {
ResolveDepth::Shallow
} else {
ResolveDepth::Deep
},
);
let suggest = if !cfg.cli.locked {
report.compute_suggest(cfg, network.as_ref(), true)?
} else {
None
};
match cfg.cli.output_format {
OutputFormat::Human => report
.print_human(out, cfg, suggest.as_ref())
.into_diagnostic()?,
OutputFormat::Json => report.print_json(out, cfg, suggest.as_ref())?,
}
if report.has_errors() {
panic_any(ExitPanic(-1));
} else {
if !cfg.cli.locked {
#[allow(clippy::single_match)]
match resolver::regenerate_exemptions(cfg, &mut store, false, false) {
Err(RegenerateExemptionsError::ViolationConflict) => {
unreachable!("unexpeced violation conflict regenerating exemptions?")
}
Ok(()) => {}
}
}
store.commit()?;
}
Ok(())
}
fn cmd_fetch_imports(
out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &FetchImportsArgs,
) -> Result<(), miette::Report> {
trace!("fetching imports...");
if cfg.cli.locked {
writeln!(
out,
"warning: ran fetch-imports with --locked, this won't do anything!"
);
return Ok(());
}
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
match resolver::regenerate_exemptions(cfg, &mut store, false, true) {
Ok(()) | Err(RegenerateExemptionsError::ViolationConflict) => {}
}
store.commit()?;
Ok(())
}
fn cmd_dump_graph(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &DumpGraphArgs,
) -> Result<(), miette::Report> {
trace!("dumping...");
let graph = resolver::DepGraph::new(&cfg.metadata, cfg.cli.filter_graph.as_ref(), None);
match cfg.cli.output_format {
OutputFormat::Human => graph.print_mermaid(out, sub_args).into_diagnostic()?,
OutputFormat::Json => {
serde_json::to_writer_pretty(&**out, &graph.nodes).into_diagnostic()?
}
}
Ok(())
}
fn cmd_fmt(_out: &Arc<dyn Out>, cfg: &Config, _sub_args: &FmtArgs) -> Result<(), miette::Report> {
trace!("formatting...");
let store = Store::acquire_offline(cfg)?;
store.commit()?;
Ok(())
}
fn cmd_help_md(
out: &Arc<dyn Out>,
_cfg: &PartialConfig,
_sub_args: &HelpMarkdownArgs,
) -> Result<(), miette::Report> {
let app_name = "cargo-vet";
let pretty_app_name = "cargo vet";
writeln!(out, "# {pretty_app_name} CLI manual");
writeln!(out);
writeln!(
out,
"> This manual can be regenerated with `{pretty_app_name} help-markdown`"
);
writeln!(out);
let mut fake_cli = FakeCli::command().term_width(0);
let full_command = fake_cli.get_subcommands_mut().next().unwrap();
full_command.build();
let mut todo = vec![full_command];
let mut is_full_command = true;
while let Some(command) = todo.pop() {
let mut help_buf = Vec::new();
command.write_long_help(&mut help_buf).unwrap();
let help = String::from_utf8(help_buf).unwrap();
let mut lines = help.lines();
let version_line = lines.next().unwrap();
let subcommand_name = command.get_name();
if is_full_command {
writeln!(out, "Version: `{version_line}`");
writeln!(out);
} else {
writeln!(out, "<br><br><br>");
writeln!(out, "## {pretty_app_name} {subcommand_name}");
}
let mut in_subcommands_listing = false;
let mut in_usage = false;
let mut in_global_options = false;
for line in lines {
if let Some(heading) = line.strip_suffix(':') {
if !line.starts_with(' ') {
if heading.to_ascii_uppercase() == heading {
in_subcommands_listing = heading == "SUBCOMMANDS";
in_usage = heading == "USAGE";
in_global_options = heading == "GLOBAL OPTIONS";
writeln!(out, "### {heading}");
if in_global_options && !is_full_command {
writeln!(
out,
"This subcommand accepts all the [global options](#global-options)"
);
}
} else {
writeln!(out, "### {heading}");
}
continue;
}
}
if in_global_options && !is_full_command {
continue;
}
if in_subcommands_listing && !line.starts_with(" ") {
let own_subcommand_name = line.trim();
write!(
out,
"* [{own_subcommand_name}](#{app_name}-{own_subcommand_name}): "
);
continue;
}
let line = line.trim();
if in_usage && line.starts_with(pretty_app_name) {
writeln!(out, "```");
writeln!(out, "{line}");
writeln!(out, "```");
continue;
}
if line.starts_with('-') || line.starts_with('<') {
writeln!(out, "#### `{line}`");
continue;
}
if line.starts_with('[') {
writeln!(out, "\\{line} ");
continue;
}
writeln!(out, "{line}");
}
writeln!(out);
todo.extend(
command
.get_subcommands_mut()
.filter(|cmd| !cmd.is_hide_set())
.collect::<Vec<_>>()
.into_iter()
.rev(),
);
is_full_command = false;
}
Ok(())
}
fn cmd_gc(
out: &Arc<dyn Out>,
cfg: &PartialConfig,
sub_args: &GcArgs,
) -> Result<(), miette::Report> {
let cache = Cache::acquire(cfg)?;
if sub_args.clean {
writeln!(
out,
"cleaning entire contents of cache directory: {}",
cfg.cache_dir.display()
);
cache.clean_sync().into_diagnostic()?;
return Ok(());
}
if sub_args.max_package_age_days.is_nan() {
return Err(miette!("max package age cannot be NaN"));
}
if sub_args.max_package_age_days < 0.0 {
return Err(miette!("max package age cannot be negative"));
}
cache.gc_sync(DURATION_DAY.mul_f64(sub_args.max_package_age_days));
Ok(())
}
struct UserInfo {
username: String,
email: String,
}
fn get_user_info() -> Result<UserInfo, UserInfoError> {
fn get_git_config(value_name: &str) -> Result<String, CommandError> {
let out = std::process::Command::new("git")
.arg("config")
.arg("--get")
.arg(value_name)
.output()
.map_err(CommandError::CommandFailed)?;
if !out.status.success() {
return Err(CommandError::BadStatus(out.status.code().unwrap()));
}
String::from_utf8(out.stdout)
.map(|s| s.trim().to_string())
.map_err(CommandError::BadOutput)
}
let username = get_git_config("user.name").map_err(UserInfoError::UserCommandFailed)?;
let email = get_git_config("user.email").map_err(UserInfoError::EmailCommandFailed)?;
Ok(UserInfo { username, email })
}
async fn eula_for_criteria(
network: Option<&Network>,
criteria_map: &SortedMap<CriteriaName, CriteriaEntry>,
criteria: CriteriaStr<'_>,
) -> String {
let builtin_eulas = [
(
format::SAFE_TO_DEPLOY,
include_str!("criteria/safe-to-deploy.txt"),
),
(
format::SAFE_TO_RUN,
include_str!("criteria/safe-to-run.txt"),
),
]
.into_iter()
.collect::<HashMap<_, _>>();
let builtin = builtin_eulas.get(criteria).map(|s| s.to_string());
if let Some(eula) = builtin {
return eula;
}
let criteria_entry = criteria_map
.get(criteria)
.unwrap_or_else(|| panic!("no entry for the criteria {}", criteria));
assert!(
criteria_entry.description.is_some() || criteria_entry.description_url.is_some(),
"entry for criteria {} is corrupt!",
criteria
);
if let Some(eula) = criteria_entry.description.clone() {
return eula;
}
let url = Url::parse(criteria_entry.description_url.as_ref().unwrap()).unwrap();
if let Some(network) = network {
if let Ok(eula) = network.download(url.clone()).await.and_then(|bytes| {
String::from_utf8(bytes).map_err(|error| DownloadError::InvalidText {
url: url.clone(),
error,
})
}) {
return eula;
}
}
format!("Could not download criteria description, it should be available at {url}")
}
fn foreign_packages<'a>(
metadata: &'a Metadata,
config: &'a ConfigFile,
) -> impl Iterator<Item = &'a Package> + 'a {
metadata
.packages
.iter()
.filter(|package| package.is_third_party(&config.policy))
}
fn first_party_packages_strict<'a>(
metadata: &'a Metadata,
_config: &'a ConfigFile,
) -> impl Iterator<Item = &'a Package> + 'a {
let empty_policy = SortedMap::new();
metadata
.packages
.iter()
.filter(move |package| !package.is_third_party(&empty_policy))
}
fn check_audit_as_crates_io(cfg: &Config, store: &Store) -> Result<(), AuditAsErrors> {
let cache = Cache::acquire(cfg).map_err(|e| AuditAsErrors {
errors: vec![AuditAsError::CacheAcquire(e)],
})?;
let mut needs_audit_as_entry = vec![];
let mut shouldnt_be_audit_as = vec![];
'packages: for package in first_party_packages_strict(&cfg.metadata, &store.config) {
let audit_policy = store
.config
.policy
.get(&package.name)
.and_then(|policy| policy.audit_as_crates_io);
if audit_policy == Some(false) {
continue;
}
if let Some(index_entry) = cache.query_package_from_index(&package.name) {
if storage::exact_version(&index_entry, &package.version).is_some() {
if audit_policy == None {
needs_audit_as_entry.push(NeedsAuditAsError {
package: package.name.clone(),
version: package.version.clone(),
});
}
continue 'packages;
}
}
if audit_policy == Some(true) {
shouldnt_be_audit_as.push(ShouldntBeAuditAsError {
package: package.name.clone(),
version: package.version.clone(),
});
}
}
if !needs_audit_as_entry.is_empty() || !shouldnt_be_audit_as.is_empty() {
let mut errors = vec![];
if !needs_audit_as_entry.is_empty() {
errors.push(AuditAsError::NeedsAuditAs(NeedsAuditAsErrors {
errors: needs_audit_as_entry,
}));
}
if !shouldnt_be_audit_as.is_empty() {
errors.push(AuditAsError::ShouldntBeAuditAs(ShouldntBeAuditAsErrors {
errors: shouldnt_be_audit_as,
}));
}
return Err(AuditAsErrors { errors });
}
Ok(())
}