use std::collections::HashMap;
use std::io::{BufRead, BufReader};
use std::ops::Deref;
use std::panic::panic_any;
use std::process::Stdio;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use std::{fs::File, io, panic, path::PathBuf};
use cargo_metadata::{Metadata, Package};
use clap::{CommandFactory, Parser};
use console::Term;
use errors::{
AggregateCriteriaDescription, AggregateCriteriaDescriptionMismatchError,
AggregateCriteriaImplies, AggregateError, AggregateErrors, AggregateImpliesMismatchError,
AuditAsError, AuditAsErrors, CacheAcquireError, CertifyError, CratePolicyError,
CratePolicyErrors, DependencyCriteriaNeedsPolicyVersionErrors, FetchAuditError, LoadTomlError,
NeedsAuditAsErrors, PackageError, ShouldntBeAuditAsErrors, ThirdPartyNeedsPolicyVersionErrors,
UnusedAuditAsErrors, UnusedPolicyVersionErrors, UserInfoError, VersionedPackageError,
};
use format::{CriteriaName, CriteriaStr, PackageName, Policy, PolicyEntry, SortedSet, VetVersion};
use futures_util::future::{join_all, try_join_all};
use indicatif::ProgressDrawTarget;
use lazy_static::lazy_static;
use miette::{miette, Context, Diagnostic, IntoDiagnostic};
use network::Network;
use out::{progress_bar, IncProgressOnDrop};
use reqwest::Url;
use resolver::AuditGraph;
use serde::de::Deserialize;
use serialization::spanned::Spanned;
use serialization::Tidyable;
use storage::fetch_registry;
use thiserror::Error;
use tracing::{error, info, trace, warn};
use crate::cli::*;
use crate::criteria::CriteriaMapper;
use crate::errors::{
CommandError, DownloadError, FetchAndDiffError, FetchError, MetadataAcquireError, SourceFile,
};
use crate::format::{
AuditEntry, AuditKind, AuditsFile, ConfigFile, CratesPublisherSource, CratesSourceId,
CriteriaEntry, ExemptedDependency, FetchCommand, MetaConfig, MetaConfigInstance, PackageStr,
SortedMap, StoreInfo, TrustEntry, WildcardEntry,
};
use crate::git_tool::Pager;
use crate::out::{indeterminate_spinner, Out, StderrLogWriter, MULTIPROGRESS};
use crate::storage::{Cache, FetchModeCommand, Store};
mod cli;
mod credentials;
mod criteria;
pub mod errors;
mod flock;
pub mod format;
mod git_tool;
pub mod network;
mod out;
pub mod resolver;
mod serialization;
pub mod storage;
mod string_format;
#[cfg(test)]
mod tests;
pub struct Config {
pub metacfg: MetaConfig,
pub metadata: Metadata,
_rest: PartialConfig,
}
pub struct PartialConfig {
pub cli: Cli,
pub now: chrono::DateTime<chrono::Utc>,
pub cache_dir: PathBuf,
pub mock_cache: bool,
}
impl PartialConfig {
pub fn today(&self) -> chrono::NaiveDate {
self.now.date_naive()
}
}
impl Deref for Config {
type Target = PartialConfig;
fn deref(&self) -> &Self::Target {
&self._rest
}
}
pub trait PackageExt {
fn is_third_party(&self, policy: &Policy) -> bool;
fn is_crates_io(&self) -> bool;
fn policy_entry<'a>(&self, policy: &'a Policy) -> Option<&'a PolicyEntry>;
fn git_rev(&self) -> Option<String>;
fn vet_version(&self) -> VetVersion;
}
impl PackageExt for Package {
fn is_third_party(&self, policy: &Policy) -> bool {
let forced_third_party = self
.policy_entry(policy)
.and_then(|policy| policy.audit_as_crates_io)
.unwrap_or(false);
forced_third_party || self.is_crates_io()
}
fn is_crates_io(&self) -> bool {
self.source
.as_ref()
.map(|s| s.is_crates_io())
.unwrap_or(false)
}
fn policy_entry<'a>(&self, policy: &'a Policy) -> Option<&'a PolicyEntry> {
policy.get(&self.name, &self.vet_version())
}
fn git_rev(&self) -> Option<String> {
self.source.as_ref().and_then(|s| {
let git_source = s.repr.strip_prefix("git+")?;
let source_url = Url::parse(git_source).ok()?;
Some(source_url.fragment()?.to_owned())
})
}
fn vet_version(&self) -> VetVersion {
VetVersion {
semver: self.version.clone(),
git_rev: self.git_rev(),
}
}
}
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);
lazy_static! {
static ref WILDCARD_AUDIT_EXPIRATION_DURATION: chrono::Duration = chrono::Duration::weeks(6);
static ref WILDCARD_AUDIT_INACTIVE_CRATE_DURATION: chrono::Duration =
chrono::Duration::weeks(16);
}
const WILDCARD_AUDIT_EXPIRATION_STRING: &str = "six weeks";
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"
};
#[allow(unused_assignments)]
{
#[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 = cli.cache_dir.clone().unwrap_or_else(|| {
dirs::cache_dir()
.unwrap_or_else(std::env::temp_dir)
.join(CACHE_DIR_SUFFIX)
});
let now = cli
.current_time
.unwrap_or_else(|| chrono::DateTime::from(SystemTime::now()));
let partial_cfg = PartialConfig {
cli,
now,
cache_dir,
mock_cache: false,
};
match &partial_cfg.cli.command {
Some(Aggregate(sub_args)) => return cmd_aggregate(&out, &partial_cfg, sub_args),
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());
}
if !using_log_file
&& cli.output_format == OutputFormat::Human
&& console::colors_enabled_stderr()
{
other_options.push("--color=always".to_string());
}
other_options.extend(cli.cargo_arg.iter().cloned());
cmd.other_options(other_options);
info!("Running: {:#?}", cmd.cargo_command());
let metadata = {
let _spinner = indeterminate_spinner("Running", "`cargo metadata`");
cmd.exec().map_err(MetadataAcquireError::from)?
};
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()?;
let cli_metacfg = cli.store_path.as_ref().map(|path| MetaConfigInstance {
version: Some(1),
store: Some(StoreInfo {
path: Some(path.clone()),
}),
});
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);
}
if let Some(metacfg) = cli_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(Import(sub_args)) => cmd_import(&out, &cfg, sub_args),
Some(Trust(sub_args)) => cmd_trust(&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(Prune(sub_args)) => cmd_prune(&out, &cfg, sub_args),
Some(DumpGraph(sub_args)) => cmd_dump_graph(&out, &cfg, sub_args),
Some(ExplainAudit(sub_args)) => cmd_explain_audit(&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(Regenerate(Unpublished(sub_args))) => cmd_regenerate_unpublished(&out, &cfg, sub_args),
Some(Renew(sub_args)) => cmd_renew(&out, &cfg, sub_args),
Some(Aggregate(_)) | 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 network = Network::acquire(cfg);
let mut store = Store::create(cfg)?;
check_crate_policies(cfg, &store)?;
tokio::runtime::Handle::current().block_on(fix_audit_as(cfg, network.as_ref(), &mut store))?;
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::RegenerateExemptions,
prune_exemptions: true,
prune_non_importable_audits: true,
prune_imports: true,
});
store.commit()?;
Ok(())
}
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(),
});
let mode = cache.select_fetch_mode(
FetchModeCommand::Inspect,
&sub_args.mode,
version.git_rev.is_some(),
);
if let Some(url) = mode.inspect_url(package, &version.to_string()) {
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!(
async {
if let Some(git_rev) = &version.git_rev {
storage::locate_local_checkout(&cfg.metadata, package, version).ok_or_else(
|| FetchError::UnknownGitRevision {
package: package.to_owned(),
git_rev: git_rev.to_owned(),
},
)
} else {
cache
.fetch_package(&cfg.metadata, network.as_ref(), package, version)
.await
}
},
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)?;
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));
}
#[derive(Debug)]
enum CertifyKind {
Delta {
from: VetVersion,
to: VetVersion,
},
Full {
version: VetVersion,
},
Wildcard {
source: CratesPublisherSource,
start: chrono::NaiveDate,
end: chrono::NaiveDate,
set_renew_false: bool,
},
}
let kind = if let Some(identifier) = &sub_args.wildcard {
let publishers = store.ensure_publisher_versions(cfg, network, &package)?;
let published_versions = publishers
.iter()
.filter(|publisher| publisher.source.as_identifier() == identifier);
let earliest = published_versions.min_by_key(|p| p.when).ok_or_else(|| {
CertifyError::NotAPublisher(identifier.to_owned(), package.to_owned())
})?;
let start = sub_args.start_date.unwrap_or(earliest.when);
let max_end = cfg.today() + chrono::Months::new(12);
let end = sub_args.end_date.unwrap_or(max_end);
let set_renew_false = sub_args.end_date.is_some();
if end > max_end {
return Err(CertifyError::BadWildcardEndDate(end));
}
CertifyKind::Wildcard {
source: earliest.source.clone(),
start,
end,
set_renew_false,
}
} else if let Some(v1) = &sub_args.version1 {
if let Some(v2) = &sub_args.version2 {
CertifyKind::Delta {
from: v1.clone(),
to: v2.clone(),
}
} else {
CertifyKind::Full {
version: v1.clone(),
}
}
} else if let Some(fetch) = last_fetch.filter(|f| f.package() == package) {
match fetch {
FetchCommand::Inspect { version, .. } => CertifyKind::Full { version },
FetchCommand::Diff {
version1, version2, ..
} => CertifyKind::Delta {
from: version1,
to: version2,
},
}
} else {
return Err(CertifyError::CouldntGuessVersion(package));
};
let (username, who) = if sub_args.who.is_empty() {
let user_info = get_user_info()?;
let who = format!("{} <{}>", user_info.username, user_info.email);
(user_info.username, vec![Spanned::from(who)])
} else {
(
sub_args.who.join(", "),
sub_args
.who
.iter()
.map(|w| Spanned::from(w.clone()))
.collect(),
)
};
let (criteria_guess, prompt) = if sub_args.criteria.is_empty() {
match &kind {
CertifyKind::Full { version } => (
guess_audit_criteria(cfg, store, &package, None, version),
Some(format!(
"choose criteria to certify for {package}:{version}"
)),
),
CertifyKind::Delta { from, to } => (
guess_audit_criteria(cfg, store, &package, Some(from), to),
Some(format!(
"choose criteria to certify for {package}:{from} -> {to}"
)),
),
CertifyKind::Wildcard { .. } => {
(
vec![format::SAFE_TO_DEPLOY.to_owned()],
Some(format!("choose criteria to certify for {package}:*")),
)
}
}
} else {
(sub_args.criteria.clone(), None)
};
let criteria_names =
criteria_picker(out, &store.audits.criteria, criteria_guess, prompt.as_ref())?;
let statement = match &kind {
CertifyKind::Full { version } => {
format!(
"I, {username}, certify that I have audited version {version} of {package} in accordance with the above criteria.",
)
}
CertifyKind::Delta { from, to } => {
format!(
"I, {username}, certify that I have audited the changes from version {from} to {to} of {package} in accordance with the above criteria.",
)
}
CertifyKind::Wildcard {
source, start, end, ..
} => {
let identifier = source.as_identifier();
format!(
"I, {username}, certify that any version of {package} published by '{identifier}' between {start} and {end} will satisfy the above criteria.",
)
}
};
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 criteria = criteria_names.into_iter().map(|s| s.into()).collect();
match kind {
CertifyKind::Full { version } => {
let kind = AuditKind::Full { version };
let importable = kind.default_importable();
store
.audits
.audits
.entry(package.clone())
.or_default()
.push(AuditEntry {
kind,
criteria,
who,
importable,
notes,
aggregated_from: vec![],
is_fresh_import: false,
});
}
CertifyKind::Delta { from, to } => {
let from_is_git_version = from.git_rev.is_some();
let kind = AuditKind::Delta { from, to };
let importable = kind.default_importable();
let mut entry = AuditEntry {
kind,
criteria,
who,
importable,
notes,
aggregated_from: vec![],
is_fresh_import: false,
};
if from_is_git_version && !sub_args.no_collapse {
let is_rooted_for_criteria = {
let mapper = CriteriaMapper::new(&store.audits.criteria);
let criteria = mapper.criteria_from_list(&entry.criteria);
let audit_graph = match resolver::AuditGraph::build(
store, &mapper, &package, None,
) {
Ok(graph) => Some(graph),
Err(_) => {
warn!(
"failed to build audit graph to determine audit collapse validity, so not collapsing any audits"
);
None
}
};
move |audit: &AuditEntry| {
let Some(audit_graph) = &audit_graph else {
return false;
};
let version = match &audit.kind {
AuditKind::Delta { from, .. } => from,
AuditKind::Full { .. } => return true,
AuditKind::Violation { .. } => return false,
};
mapper.minimal_indices(&criteria).all(|idx| {
audit_graph
.search(idx, version, resolver::SearchMode::PreferExemptions)
.is_ok()
})
}
};
for audit in store
.audits
.audits
.get(&package)
.into_iter()
.flatten()
.filter(|a| !a.importable && is_rooted_for_criteria(a))
{
if let Some(new_entry) = entry.try_collapse_with_prior(audit) {
entry = new_entry;
break;
}
}
}
store
.audits
.audits
.entry(package.clone())
.or_default()
.push(entry);
}
CertifyKind::Wildcard {
source,
start,
end,
set_renew_false,
..
} => {
store
.audits
.wildcard_audits
.entry(package.clone())
.or_default()
.push(WildcardEntry {
who,
criteria,
source: source.as_wildcard_source(),
start: start.into(),
end: end.into(),
renew: set_renew_false.then_some(false),
notes,
aggregated_from: vec![],
is_fresh_import: false,
});
}
};
store
.validate(cfg.today(), false)
.expect("the new audit entry made the store invalid?");
resolver::update_store(cfg, store, |name| resolver::UpdateMode {
search_mode: if name == &package[..] {
resolver::SearchMode::PreferFreshImports
} else {
resolver::SearchMode::PreferExemptions
},
prune_exemptions: name == &package[..],
prune_non_importable_audits: name == &package[..],
prune_imports: false,
});
Ok(())
}
fn criteria_picker(
out: &Arc<dyn Out>,
store_criteria: &SortedMap<CriteriaName, CriteriaEntry>,
criteria_guess: Vec<CriteriaName>,
prompt: Option<&impl AsRef<str>>,
) -> Result<Vec<CriteriaName>, CertifyError> {
let criteria_mapper = CriteriaMapper::new(store_criteria);
let mut chosen_criteria = criteria_guess;
if let Some(prompt) = prompt {
loop {
out.clear_screen()?;
writeln!(out, "{}", prompt.as_ref());
for (criteria_idx, criteria_name) in criteria_mapper.all_criteria_names().enumerate() {
if chosen_criteria.iter().any(|s| s == criteria_name) {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
out.style().green().bold().apply_to(criteria_name)
);
} else {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
out.style().bold().dim().apply_to(criteria_name)
);
}
}
writeln!(out);
writeln!(out, "current selection: {:?}", chosen_criteria);
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 || answer > criteria_mapper.len() {
writeln!(out, "error: not a valid criteria");
continue;
}
let selection = criteria_mapper.criteria_name(answer - 1).to_owned();
if chosen_criteria.contains(&selection) {
chosen_criteria.retain(|x| x != &selection);
} else {
chosen_criteria.push(selection);
}
}
}
let criteria_set = criteria_mapper.criteria_from_list(&chosen_criteria);
Ok(criteria_mapper
.criteria_names(&criteria_set)
.map(|s| s.to_owned())
.collect::<Vec<_>>())
}
fn guess_audit_criteria(
cfg: &Config,
store: &Store,
package: PackageStr<'_>,
from: Option<&VetVersion>,
to: &VetVersion,
) -> Vec<String> {
let criteria = resolver::resolve(&cfg.metadata, cfg.cli.filter_graph.as_ref(), store)
.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(true),
)
.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<&VetVersion>,
to: &VetVersion,
url: Option<&str>,
) -> Result<(), io::Error> {
let description = if let Some(from) = from {
format!("You are about to diff versions {from} and {to} of '{package}'")
} else {
format!("You are about to inspect version {to} of '{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!("{description}, likely to certify it for {criteria:?}, which means:")
} else {
format!("... and for {criteria:?}, which means:")
};
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_import(
_out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &ImportArgs,
) -> Result<(), miette::Report> {
let Some(network) = Network::acquire(cfg) else {
return Err(miette!("`cargo vet import` cannot be run while frozen"));
};
let registry_file;
let import_urls = if sub_args.url.is_empty() {
registry_file = tokio::runtime::Handle::current().block_on(fetch_registry(&network))?;
registry_file
.registry
.get(&sub_args.name)
.ok_or_else(|| miette!("no peer named {} found in the registry", &sub_args.name))
.map(|entry| entry.url.clone())?
} else {
sub_args.url.clone()
};
let mut store = Store::acquire_offline(cfg)?;
store
.config
.imports
.entry(sub_args.name.clone())
.or_default()
.url = import_urls;
let cache = Cache::acquire(cfg)?;
tokio::runtime::Handle::current().block_on(store.go_online(cfg, &network, &cache, false))?;
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::PreferFreshImports,
prune_exemptions: true,
prune_non_importable_audits: true,
prune_imports: true,
});
store.commit()?;
Ok(())
}
fn cmd_trust(out: &Arc<dyn Out>, cfg: &Config, sub_args: &TrustArgs) -> Result<(), miette::Report> {
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
do_cmd_trust(out, cfg, sub_args, &mut store, network.as_ref())?;
store.commit()?;
Ok(())
}
fn do_cmd_trust(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &TrustArgs,
store: &mut Store,
network: Option<&Network>,
) -> Result<(), miette::Report> {
if let Some(package) = &sub_args.package {
let publishers = store.ensure_publisher_versions(cfg, network, package)?;
let publisher_identifier = if let Some(login) = &sub_args.publisher_identifier {
login.clone()
} else if let Some(first) = publishers.first() {
if publishers
.iter()
.all(|publisher| publisher.source == first.source)
{
first.source.as_identifier().to_owned()
} else {
return Err(miette!(
"The package '{}' has multiple known publishers, \
please explicitly specify which publisher to trust",
package
));
}
} else {
return Err(miette!(
"The package '{}' has no known publishers, so cannot be trusted",
package
));
};
apply_cmd_trust(
out,
cfg,
store,
network,
package,
&publisher_identifier,
sub_args.start_date,
sub_args.end_date,
&sub_args.criteria,
sub_args.notes.as_ref(),
)
} else if let Some(publisher_identifier) = &sub_args.all {
let suggest_store = store.clone_for_suggest(true);
let report =
resolver::resolve(&cfg.metadata, cfg.cli.filter_graph.as_ref(), &suggest_store);
let resolver::Conclusion::FailForVet(fail) = &report.conclusion else {
return Err(miette!(
"No failing or exempted crates, trust --all will do nothing"
));
};
let mut failed_criteria = report.criteria_mapper.no_criteria();
let mut trust = Vec::new();
let mut skipped = Vec::new();
for (failure_idx, audit_failure) in &fail.failures {
let package = &report.graph.nodes[*failure_idx];
let publishers = store.ensure_publisher_versions(cfg, network, package.name)?;
let by_user = publishers
.iter()
.filter(|p| p.source.as_identifier() == publisher_identifier)
.count();
if by_user == 0 {
continue; }
if by_user != publishers.len() && !sub_args.allow_multiple_publishers {
skipped.push(package.name);
} else {
trust.push(package.name);
failed_criteria.unioned_with(&audit_failure.criteria_failures);
}
}
trust.sort();
trust.dedup();
let maybe_warn_skipped = || {
if !skipped.is_empty() {
skipped.sort();
skipped.dedup();
warn!(
"Skipped {} due to multiple publishers",
string_format::FormatShortList::new(skipped)
);
warn!(" Run with --allow-multiple-publishers to also trust these packages");
}
};
if trust.is_empty() {
maybe_warn_skipped();
return Err(miette!(
"No failing or exempted packages published by {publisher_identifier}"
));
}
let criteria_names = criteria_picker(
out,
&store.audits.criteria,
if sub_args.criteria.is_empty() {
report
.criteria_mapper
.criteria_names(&failed_criteria)
.map(|s| s.to_owned())
.collect()
} else {
sub_args.criteria.clone()
},
if sub_args.criteria.is_empty() {
Some(format!(
"choose trusted criteria for packages published by {publisher_identifier} ({})",
string_format::FormatShortList::new(trust.clone())
))
} else {
None
}
.as_ref(),
)?;
maybe_warn_skipped();
for package in &trust {
apply_cmd_trust(
out,
cfg,
store,
network,
package,
publisher_identifier,
sub_args.start_date,
sub_args.end_date,
&criteria_names,
sub_args.notes.as_ref(),
)?;
}
Ok(())
} else {
Err(miette!("Please specify either a package to trust or --all"))
}
}
#[allow(clippy::too_many_arguments)]
fn apply_cmd_trust(
out: &Arc<dyn Out>,
cfg: &Config,
store: &mut Store,
network: Option<&Network>,
package: &str,
publisher_identifier: &str,
start_date: Option<chrono::NaiveDate>,
end_date: Option<chrono::NaiveDate>,
criteria: &[CriteriaName],
notes: Option<&String>,
) -> Result<(), miette::Report> {
let publishers = store.ensure_publisher_versions(cfg, network, package)?;
let published_versions = publishers
.iter()
.filter(|publisher| publisher.source.as_identifier() == publisher_identifier);
let earliest = published_versions.min_by_key(|p| p.when).ok_or_else(|| {
CertifyError::NotAPublisher(publisher_identifier.to_owned(), package.to_owned())
})?;
let source = earliest.source.as_wildcard_source();
let start = start_date.unwrap_or(earliest.when);
let end = end_date.unwrap_or(cfg.today() + chrono::Months::new(12));
let criteria_names = criteria_picker(
out,
&store.audits.criteria,
if criteria.is_empty() {
vec![format::SAFE_TO_DEPLOY.to_owned()]
} else {
criteria.to_owned()
},
if criteria.is_empty() {
Some(format!(
"choose trusted criteria for {package}:* published by {publisher_identifier}"
))
} else {
None
}
.as_ref(),
)?;
let criteria = criteria_names.into_iter().map(Spanned::from).collect();
let trust_entries = store.audits.trusted.entry(package.to_owned()).or_default();
if let Some(trust_entry) = trust_entries.iter_mut().find(|trust_entry| {
trust_entry.criteria == criteria
&& trust_entry.source == source
&& start <= *trust_entry.start
&& *trust_entry.end <= end
&& notes.is_none()
}) {
trust_entry.start = start.into();
trust_entry.end = end.into();
} else {
trust_entries.push(TrustEntry {
criteria,
source,
start: start.into(),
end: end.into(),
notes: notes.cloned(),
aggregated_from: vec![],
});
}
store
.validate(cfg.today(), false)
.expect("the new trusted entry made the store invalid?");
resolver::update_store(cfg, store, |name| resolver::UpdateMode {
search_mode: if name == package {
resolver::SearchMode::PreferFreshImports
} else {
resolver::SearchMode::PreferExemptions
},
prune_exemptions: name == package,
prune_non_importable_audits: name == package,
prune_imports: false,
});
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 sub_args.who.is_empty() {
let user_info = get_user_info()?;
let who = format!("{} <{}>", user_info.username, user_info.email);
(user_info.username, vec![Spanned::from(who)])
} else {
(
sub_args.who.join(", "),
sub_args
.who
.iter()
.map(|w| Spanned::from(w.clone()))
.collect(),
)
};
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,
importable: true,
notes,
aggregated_from: vec![],
is_fresh_import: false,
};
store
.audits
.audits
.entry(sub_args.package.clone())
.or_default()
.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 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(),
suggest,
};
store
.config
.exemptions
.entry(sub_args.package.clone())
.or_default()
.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(true);
let report = resolver::resolve(&cfg.metadata, cfg.cli.filter_graph.as_ref(), &suggest_store);
let suggest = report.compute_suggest(cfg, &suggest_store, network.as_ref())?;
match cfg.cli.output_format {
OutputFormat::Human => report
.print_suggest_human(out, cfg, suggest.as_ref())
.into_diagnostic()?,
OutputFormat::Json => report.print_json(out, 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)?;
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::PreferFreshImports,
prune_exemptions: true,
prune_non_importable_audits: true,
prune_imports: true,
});
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 network = Network::acquire(cfg);
let mut store = Store::acquire_offline(cfg)?;
tokio::runtime::Handle::current().block_on(fix_audit_as(cfg, network.as_ref(), &mut store))?;
store.commit()?;
Ok(())
}
fn cmd_regenerate_unpublished(
out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &RegenerateUnpublishedArgs,
) -> Result<(), miette::Report> {
trace!("regenerating unpublished entries...");
if cfg.cli.locked {
writeln!(
out,
"warning: ran `regenerate unpublished` with --locked, this won't do anything!"
);
return Ok(());
}
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
if let Some(live_imports) = &mut store.live_imports {
for unpublished in live_imports.unpublished.values_mut() {
unpublished.retain_mut(|u| std::mem::replace(&mut u.is_fresh_import, false));
}
}
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::PreferExemptions,
prune_exemptions: false,
prune_non_importable_audits: false,
prune_imports: false,
});
store.commit()?;
Ok(())
}
fn cmd_renew(out: &Arc<dyn Out>, cfg: &Config, sub_args: &RenewArgs) -> Result<(), miette::Report> {
trace!("renewing wildcard audits");
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
do_cmd_renew(out, cfg, &mut store, sub_args);
store.commit()?;
Ok(())
}
fn do_cmd_renew(out: &Arc<dyn Out>, cfg: &Config, store: &mut Store, sub_args: &RenewArgs) {
assert!(sub_args.expiring ^ sub_args.crate_name.is_some());
let cache = Cache::acquire(cfg).ok();
let new_end_date = cfg.today() + chrono::Months::new(12);
let mut renewing: WildcardAuditRenewal;
if let Some(name) = &sub_args.crate_name {
match WildcardAuditRenewal::single_crate(name, store) {
Some(renewal) => {
renewing = renewal;
if renewing.is_empty() {
info!("no wildcard audits for {name} are eligible for renewal (all have `renew = false`)");
return;
}
}
None => {
warn!("ran `renew {name}`, but there are no wildcard audits for the crate");
return;
}
}
} else {
assert!(sub_args.expiring);
renewing = WildcardAuditRenewal::expiring(cfg, store, !sub_args.include_inactive);
if renewing.is_empty() {
info!("no wildcard audits that are eligible for renewal have expired or are expiring in the next {WILDCARD_AUDIT_EXPIRATION_STRING}");
return;
}
}
renewing.renew(new_end_date);
writeln!(
out,
"Updated wildcard audits for the following crates and publishers to expire on {new_end_date}:"
);
let user_string = |source: &CratesSourceId| -> String {
match source {
CratesSourceId::User { user_id } => cache
.as_ref()
.and_then(|c| c.get_crates_user_info(*user_id))
.map(|n| n.to_string())
.unwrap_or_else(|| format!("id={}", user_id)),
CratesSourceId::TrustedPublisher { trusted_publisher } => trusted_publisher.clone(),
}
};
for (name, entries) in renewing.crates {
writeln!(
out,
" {}: {:80}",
name,
string_format::FormatShortList::new(
entries
.iter()
.map(|(entry, _)| user_string(&entry.source))
.collect()
)
);
}
}
async fn fix_audit_as(
cfg: &Config,
network: Option<&Network>,
store: &mut Store,
) -> Result<(), CacheAcquireError> {
let _spinner = indeterminate_spinner("Fetching", "crate metadata");
let mut cache = Cache::acquire(cfg)?;
let third_party_packages = foreign_packages_strict(&cfg.metadata, &store.config)
.map(|p| &*p.name)
.collect::<SortedSet<_>>();
let issues = check_audit_as_crates_io(cfg, store, network, &mut cache).await;
if let Err(AuditAsErrors { errors }) = issues {
fn get_policy_entry<'a>(
store: &'a mut Store,
cfg: &Config,
third_party_packages: &SortedSet<&String>,
error: &VersionedPackageError,
) -> &'a mut PolicyEntry {
let is_third_party = third_party_packages.contains(&error.package);
let require_versions = is_third_party.then(|| {
cfg.metadata
.packages
.iter()
.filter(|&p| *p.name == error.package)
.map(|p| p.vet_version())
.collect()
});
store.config.policy.get_mut_or_default(
error.package.clone(),
&error.version,
require_versions,
)
}
for error in errors {
match error {
AuditAsError::NeedsAuditAs(NeedsAuditAsErrors { errors }) => {
for err in errors {
let default_audit_as =
match cache.crates_io_info(network, &err.package).await {
Ok(entry) => cfg.metadata.packages.iter().any(|p| {
*p.name == err.package && entry.metadata.consider_as_same(p)
}),
Err(e) => {
warn!("crate metadata error for {}: {e}", &err.package);
false
}
};
get_policy_entry(store, cfg, &third_party_packages, &err)
.audit_as_crates_io = Some(default_audit_as);
}
}
AuditAsError::ShouldntBeAuditAs(ShouldntBeAuditAsErrors { errors }) => {
for err in errors {
get_policy_entry(store, cfg, &third_party_packages, &err)
.audit_as_crates_io = Some(false);
}
}
AuditAsError::UnusedAuditAs(unuseds) => {
for err in unuseds.errors {
if let Some(policy) = store
.config
.policy
.get_mut(&err.package, err.version.as_ref())
{
policy.audit_as_crates_io = None;
}
}
}
}
}
}
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::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::RegenerateExemptions,
prune_exemptions: true,
prune_non_importable_audits: true,
prune_imports: true,
});
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 to_compare = {
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(),
});
let mode = cache.select_fetch_mode(
FetchModeCommand::Diff,
&sub_args.mode,
version1.git_rev.is_some() || version2.git_rev.is_some(),
);
if let Some(url) = mode.diff_url(package, &version1.to_string(), &version2.to_string()) {
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 (to_compare, eulas) = tokio::join!(
async {
let (pkg1, pkg2) = tokio::try_join!(
cache.fetch_package(&cfg.metadata, network.as_ref(), package, version1),
cache.fetch_package(&cfg.metadata, network.as_ref(), package, version2)
)?;
let (_, to_compare) = cache
.diffstat_package(
&pkg1,
&pkg2,
version1.git_rev.is_some() || version2.git_rev.is_some(),
)
.await?;
Ok::<_, FetchAndDiffError>(to_compare)
},
prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
Some(version1),
version2,
None,
)
);
eulas.into_diagnostic()?;
to_compare.into_diagnostic()
})?
};
writeln!(out);
let mut pager = Pager::new(&**out).into_diagnostic()?;
for (from, to) in to_compare {
let output = std::process::Command::new("git")
.arg("-c")
.arg("core.safecrlf=false")
.arg("diff")
.arg(if pager.use_color() {
"--color=always"
} else {
"--color=never"
})
.arg("--no-index")
.arg("--ignore-cr-at-eol")
.arg(&from)
.arg(&to)
.stdout(Stdio::piped())
.output()
.map_err(CommandError::CommandFailed)
.into_diagnostic()?;
io::Write::write_all(&mut pager, &output.stdout).into_diagnostic()?;
}
pager.wait().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)?;
check_crate_policies(cfg, &store)?;
if !cfg.cli.locked {
let mut cache = Cache::acquire(cfg).into_diagnostic()?;
tokio::runtime::Handle::current().block_on(check_audit_as_crates_io(
cfg,
&store,
network.as_ref(),
&mut cache,
))?;
}
let report = resolver::resolve(&cfg.metadata, cfg.cli.filter_graph.as_ref(), &store);
let suggest = if !cfg.cli.locked {
report.compute_suggest(cfg, &store, network.as_ref())?
} 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, suggest.as_ref())?,
}
if report.has_errors() {
panic_any(ExitPanic(-1));
} else {
if !cfg.cli.locked {
let updates = resolver::get_store_updates(cfg, &store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::PreferFreshImports,
prune_exemptions: true,
prune_non_importable_audits: true,
prune_imports: true,
});
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::PreferExemptions,
prune_exemptions: false,
prune_non_importable_audits: false,
prune_imports: false,
});
if store.config.exemptions != updates.exemptions {
warn!("Your supply-chain has unnecessary exemptions which could be relaxed or pruned.");
warn!(" Consider running `cargo vet prune` to prune unnecessary exemptions and imports.");
} else if store.imports != updates.imports {
warn!("Your supply-chain has unnecessary imports which could be pruned.");
warn!(" Consider running `cargo vet prune` to prune unnecessary imports.");
} else if store.audits.audits != updates.audits {
warn!("Your supply-chain has unnecessary audits which could be pruned.");
warn!(" Consider running `cargo vet prune` to prune unnecessary imports.");
}
let since_published: Vec<_> = updates
.imports
.unpublished
.iter()
.filter(|(_, unpublished)| unpublished.iter().any(|u| !u.still_unpublished))
.map(|(package, _)| package)
.collect();
if !since_published.is_empty() {
let published = string_format::FormatShortList::new(since_published);
warn!("Your supply-chain depends on previously unpublished versions of {published} which have since been published.");
warn!(" Consider running `cargo vet regenerate unpublished` to remove these entries.");
}
let expiry = WildcardAuditRenewal::expiring(cfg, &mut store, true);
if !expiry.is_empty() {
let expired = expiry.expired_crates();
let expiring_soon = expiry.expiring_crates();
if !expired.is_empty() {
let expired = string_format::FormatShortList::new(expired);
warn!(
"Your audit set contains wildcard audits for {expired} which have expired."
);
}
if !expiring_soon.is_empty() {
let expiring = string_format::FormatShortList::new(expiring_soon);
warn!("Your audit set contains wildcard audits for {expiring} which expire within the next {WILDCARD_AUDIT_EXPIRATION_STRING}.");
}
warn!(" Consider running `cargo vet renew --expiring` or adding `renew = false` to the wildcard entries in audits.toml.");
}
}
store.commit()?;
}
Ok(())
}
#[derive(Default)]
struct WildcardAuditRenewal<'a> {
pub crates: SortedMap<PackageStr<'a>, Vec<(&'a mut WildcardEntry, bool)>>,
}
impl<'a> WildcardAuditRenewal<'a> {
pub fn expiring(cfg: &Config, store: &'a mut Store, ignore_inactive: bool) -> Self {
let expire_date = cfg.today() + *WILDCARD_AUDIT_EXPIRATION_DURATION;
let mut crates: SortedMap<PackageStr<'a>, Vec<(&'a mut WildcardEntry, bool)>> =
Default::default();
for (name, audits) in store.audits.wildcard_audits.iter_mut() {
let last_publish_date = store
.live_imports
.as_ref()
.and_then(|imports| imports.publisher.get(name))
.map(|publishers| &publishers[..])
.unwrap_or(&[])
.iter()
.map(|p| p.when)
.max()
.unwrap_or(cfg.today());
for entry in audits.iter_mut().filter(|e| e.should_renew(expire_date)) {
let expired = entry.should_renew(cfg.today());
if ignore_inactive
&& last_publish_date < *entry.end
&& last_publish_date < cfg.today() - *WILDCARD_AUDIT_INACTIVE_CRATE_DURATION
{
continue;
}
crates.entry(name).or_default().push((entry, expired));
}
}
WildcardAuditRenewal { crates }
}
pub fn single_crate(name: PackageStr<'a>, store: &'a mut Store) -> Option<Self> {
let mut crates: SortedMap<PackageStr<'a>, Vec<(&'a mut WildcardEntry, bool)>> =
Default::default();
let audits = store.audits.wildcard_audits.get_mut(name)?;
for entry in audits {
if entry.renew.unwrap_or(true) {
crates.entry(name).or_default().push((entry, false));
}
}
Some(WildcardAuditRenewal { crates })
}
pub fn is_empty(&self) -> bool {
self.crates.is_empty()
}
pub fn expired_crates(&'a self) -> Vec<PackageStr<'a>> {
self.crates
.iter()
.filter_map(|(name, ids)| ids.iter().any(|(_, expired)| *expired).then_some(*name))
.collect()
}
pub fn expiring_crates(&'a self) -> Vec<PackageStr<'a>> {
self.crates
.iter()
.filter_map(|(name, ids)| ids.iter().any(|(_, expired)| !*expired).then_some(*name))
.collect()
}
pub fn renew(&mut self, new_end_date: chrono::NaiveDate) {
for entry in self
.crates
.values_mut()
.flat_map(|v| v.iter_mut().map(|t| &mut t.0))
{
entry.end = new_end_date.into();
}
}
}
fn cmd_prune(
_out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &PruneArgs,
) -> Result<(), miette::Report> {
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
let _spinner = indeterminate_spinner("Pruning", "unnecessary imports and exemptions");
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: if sub_args.no_exemptions {
resolver::SearchMode::PreferExemptions
} else {
resolver::SearchMode::PreferFreshImports
},
prune_exemptions: !sub_args.no_exemptions,
prune_non_importable_audits: !sub_args.no_audits,
prune_imports: !sub_args.no_imports,
});
store.commit()?;
Ok(())
}
fn cmd_aggregate(
out: &Arc<dyn Out>,
cfg: &PartialConfig,
sub_args: &AggregateArgs,
) -> Result<(), miette::Report> {
let network =
Network::acquire(cfg).ok_or_else(|| miette!("cannot aggregate imports when --frozen"))?;
let mut urls = Vec::new();
{
let sources_file = BufReader::new(
File::open(&sub_args.sources)
.into_diagnostic()
.wrap_err("failed to open sources file")?,
);
for line_result in sources_file.lines() {
let line = line_result
.into_diagnostic()
.wrap_err("failed to read sources file")?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
urls.push(
Url::parse(trimmed)
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse url: {trimmed:?}"))?,
);
}
}
let progress_bar = progress_bar("Fetching", "source audits", urls.len() as u64);
let sources = tokio::runtime::Handle::current()
.block_on(try_join_all(urls.into_iter().map(|url| async {
let _guard = IncProgressOnDrop(&progress_bar, 1);
let url_string = url.to_string();
let audit_bytes = network.download(url).await?;
let audit_string = String::from_utf8(audit_bytes).map_err(LoadTomlError::from)?;
let audit_source = SourceFile::new(&url_string, audit_string);
let audit_file =
storage::foreign_audit_source_to_local_warn(&url_string, audit_source)?;
Ok::<_, FetchAuditError>((url_string, audit_file))
})))
.into_diagnostic()?;
let merged_audits = do_aggregate_audits(sources).into_diagnostic()?;
let document = serialization::to_formatted_toml(merged_audits, None).into_diagnostic()?;
write!(out, "{document}");
Ok(())
}
fn do_aggregate_audits(sources: Vec<(String, AuditsFile)>) -> Result<AuditsFile, AggregateErrors> {
let mut errors = Vec::new();
let mut aggregate = AuditsFile {
criteria: SortedMap::new(),
wildcard_audits: SortedMap::new(),
audits: SortedMap::new(),
trusted: SortedMap::new(),
};
for (source, audit_file) in sources {
for (criteria_name, mut criteria_entry) in audit_file.criteria {
match aggregate.criteria.entry(criteria_name) {
std::collections::btree_map::Entry::Vacant(vacant) => {
criteria_entry.aggregated_from.push(source.clone().into());
vacant.insert(criteria_entry);
}
std::collections::btree_map::Entry::Occupied(occupied) => {
let prev_source = occupied
.get()
.aggregated_from
.last()
.map(|s| s.to_string())
.unwrap_or_default();
if occupied.get().description != criteria_entry.description
|| occupied.get().description_url != criteria_entry.description_url
{
errors.push(AggregateError::CriteriaDescriptionMismatch(
AggregateCriteriaDescriptionMismatchError {
criteria_name: occupied.key().to_owned(),
first: AggregateCriteriaDescription {
source: prev_source.clone(),
description: occupied.get().description.clone(),
description_url: occupied.get().description_url.clone(),
},
second: AggregateCriteriaDescription {
source: source.clone(),
description: criteria_entry.description.clone(),
description_url: criteria_entry.description_url.clone(),
},
},
))
}
if occupied.get().implies != criteria_entry.implies {
errors.push(AggregateError::ImpliesMismatch(
AggregateImpliesMismatchError {
criteria_name: occupied.key().to_owned(),
first: AggregateCriteriaImplies {
source: prev_source.clone(),
implies: occupied
.get()
.implies
.iter()
.map(|c| c.to_string())
.collect(),
},
second: AggregateCriteriaImplies {
source: source.clone(),
implies: criteria_entry
.implies
.iter()
.map(|c| c.to_string())
.collect(),
},
},
));
}
}
}
}
for (package_name, entries) in audit_file.audits {
aggregate.audits.entry(package_name).or_default().extend(
entries
.into_iter()
.filter(|audit_entry| audit_entry.importable)
.map(|mut audit_entry| {
audit_entry.aggregated_from.push(source.clone().into());
audit_entry
}),
);
}
for (package_name, entries) in audit_file.wildcard_audits {
aggregate
.wildcard_audits
.entry(package_name)
.or_default()
.extend(entries.into_iter().map(|mut wildcard_entry| {
wildcard_entry.aggregated_from.push(source.clone().into());
wildcard_entry
}));
}
for (package_name, entries) in audit_file.trusted {
aggregate
.trusted
.entry(package_name)
.or_default()
.extend(entries.into_iter().map(|mut trusted_entry| {
trusted_entry.aggregated_from.push(source.clone().into());
trusted_entry
}));
}
}
aggregate.tidy();
if errors.is_empty() {
Ok(aggregate)
} else {
Err(AggregateErrors { errors })
}
}
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 explain_write_edge(
out: &Arc<dyn Out>,
store: &Store,
package: PackageStr<'_>,
idx: usize,
edge: &resolver::DeltaEdgeOrigin,
) {
fn format_who(who: &[Spanned<String>]) -> String {
if who.is_empty() {
"<unspecified>".to_owned()
} else {
string_format::FormatShortList::new(who.to_owned()).to_string()
}
}
fn format_freshness(is_fresh_import: bool) -> &'static str {
if is_fresh_import {
" (uncached)"
} else {
""
}
}
use resolver::DeltaEdgeOrigin::*;
match *edge {
StoredLocalAudit { audit_index, .. } => {
let audit = &store.audits.audits[package][audit_index];
let who = format_who(&audit.who);
match &audit.kind {
AuditKind::Full { version } => {
writeln!(out, "{idx}) [local] full audit for {version} by {who}");
}
AuditKind::Delta { from, to } => {
writeln!(out, "{idx}) [local] delta audit for {from}->{to} by {who}");
}
_ => unreachable!(),
}
}
ImportedAudit {
import_index,
audit_index,
} => {
let (import_name, audits_file) =
store.imported_audits().iter().nth(import_index).unwrap();
let audit = &audits_file.audits[package][audit_index];
let freshness = format_freshness(audit.is_fresh_import);
let who = format_who(&audit.who);
match &audit.kind {
AuditKind::Full { version } => {
writeln!(
out,
"{idx}) [{import_name}{freshness}] full audit for {version} by {who}"
);
}
AuditKind::Delta { from, to } => {
writeln!(
out,
"{idx}) [{import_name}{freshness}] delta audit for {from}->{to} by {who}"
);
}
_ => unreachable!(),
}
}
WildcardAudit {
import_index,
audit_index,
publisher_index,
} => {
let (import_name, audits_file) = match import_index {
Some(import_index) => {
let (import_name, audits_file) =
store.imported_audits().iter().nth(import_index).unwrap();
(&import_name[..], audits_file)
}
None => ("local", &store.audits),
};
let audit = &audits_file.wildcard_audits[package][audit_index];
let who = format_who(&audit.who);
let publisher = &store.publishers()[package][publisher_index];
let version = &publisher.version;
let identifier = publisher.source.as_identifier();
let freshness = format_freshness(audit.is_fresh_import || publisher.is_fresh_import);
writeln!(out, "{idx}) [{import_name}{freshness}] wildcard audit for {version} (published by: {identifier}) by {who}");
}
Trusted { publisher_index } => {
let publisher = &store.publishers()[package][publisher_index];
let version = &publisher.version;
let identifier = publisher.source.as_identifier();
let freshness = format_freshness(publisher.is_fresh_import);
writeln!(
out,
"{idx}) [local{freshness}] trusted entry for {version} (published by: {identifier})"
);
}
Exemption { exemption_index } => {
let exemption = &store.config.exemptions[package][exemption_index];
let version = &exemption.version;
writeln!(out, "{idx}) [local] exemption for {version}");
}
Unpublished { unpublished_index } => {
let unpublished = &store.unpublished()[package][unpublished_index];
let version = &unpublished.version;
let audited_as = &unpublished.audited_as;
let freshness = format_freshness(unpublished.is_fresh_import);
writeln!(
out,
"{idx}) [local{freshness}] auditing unpublished version {version} as {audited_as}"
);
}
FreshExemption { .. } => {
unreachable!("Should not observe FreshExemption edge with PreferExemptions mode")
}
}
}
fn do_cmd_explain_audit(
out: &Arc<dyn Out>,
store: &Store,
package: PackageStr<'_>,
version: &VetVersion,
criteria_name: CriteriaStr<'_>,
) -> Result<(), miette::Report> {
let criteria_mapper = CriteriaMapper::new(&store.audits.criteria);
let audit_graph = AuditGraph::build(store, &criteria_mapper, package, None)
.map_err(|_| miette!("This package has violation conflicts"))?;
match audit_graph.search(
criteria_mapper.criteria_index(criteria_name),
version,
resolver::SearchMode::PreferExemptions,
) {
Ok(path) => {
writeln!(
out,
"The package {package} {version} certifies for {criteria_name}"
);
for (idx, edge) in path.iter().rev().enumerate() {
explain_write_edge(out, store, package, idx + 1, edge);
}
}
Err(failure) => {
writeln!(
out,
"The package {package} {version} does not certify for {criteria_name}"
);
if failure.reachable_from_root.len() > 1 {
writeln!(out, "The following versions would certify:");
for version in failure.reachable_from_root.iter().flatten() {
writeln!(out, " - {version}");
}
}
}
}
Ok(())
}
fn cmd_explain_audit(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &ExplainAuditArgs,
) -> Result<(), miette::Report> {
let network = Network::acquire(cfg);
let store = Store::acquire(cfg, network.as_ref(), false)?;
let version = if let Some(version) = &sub_args.version {
version.clone()
} else {
let matching_packages = cfg
.metadata
.packages
.iter()
.filter(|pkg| *pkg.name == sub_args.package)
.collect::<Vec<_>>();
miette::ensure!(matching_packages.len() == 1, "Ambiguous package version");
matching_packages[0].vet_version()
};
do_cmd_explain_audit(out, &store, &sub_args.package, &version, &sub_args.criteria)
}
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> {
writeln!(out, "# cargo vet CLI manual");
writeln!(out);
writeln!(
out,
"> This manual can be regenerated with `cargo vet help-markdown`"
);
writeln!(out);
let mut fake_cli = FakeCli::command()
.term_width(0)
.disable_help_subcommand(true);
fake_cli.build();
let full_command = fake_cli.get_subcommands_mut().next().unwrap();
let mut todo = vec![("cargo vet".to_owned(), full_command)];
let mut is_full_command = true;
while let Some((name, 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();
if !is_full_command {
writeln!(out, "<br><br><br>");
}
let name_anchor = name.replace(' ', "-");
writeln!(out, "## {name}");
enum Section {
None,
Usage,
Commands,
Arguments,
Options,
GlobalOptions,
}
let mut section = Section::None;
for mut line in help.lines() {
if let Some((heading, rest)) = line.split_once(':') {
let new_section = match heading {
"Usage" => Section::Usage,
"Commands" => Section::Commands,
"Arguments" => Section::Arguments,
"Options" => Section::Options,
"Global Options" => Section::GlobalOptions,
_ => Section::None,
};
if !matches!(new_section, Section::None) {
writeln!(out, "### {heading}");
section = new_section;
line = rest;
if matches!(section, Section::GlobalOptions) && !is_full_command {
writeln!(
out,
"This subcommand accepts all the [global options](#global-options)"
);
continue;
}
}
}
let line = line.trim();
if matches!(section, Section::GlobalOptions) && !is_full_command {
continue;
}
if matches!(section, Section::Commands) {
if let Some((sub_name, description)) = line.trim().split_once(' ') {
let description = description.trim();
writeln!(
out,
"* [{sub_name}](#{name_anchor}-{sub_name}): {description}"
);
continue;
}
}
if matches!(section, Section::Usage) && line.starts_with(&name) {
writeln!(out, "```");
writeln!(out, "{line}");
writeln!(out, "```");
continue;
}
if matches!(section, Section::Options | Section::GlobalOptions)
&& line.starts_with('-')
&& !line.starts_with("- ")
{
writeln!(out, "#### `{line}`");
continue;
}
if matches!(section, Section::Arguments)
&& (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())
.map(|cmd| (format!("{} {}", name, cmd.get_name()), cmd))
.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 {criteria} is corrupt!"
);
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: Box::new(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 {
metadata
.packages
.iter()
.filter(move |package| !package.is_crates_io())
}
fn foreign_packages_strict<'a>(
metadata: &'a Metadata,
_config: &ConfigFile,
) -> impl Iterator<Item = &'a Package> + 'a {
metadata
.packages
.iter()
.filter(move |package| package.is_crates_io())
}
async fn check_audit_as_crates_io(
cfg: &Config,
store: &Store,
network: Option<&Network>,
cache: &mut Cache,
) -> Result<(), AuditAsErrors> {
let first_party_packages: Vec<_> =
first_party_packages_strict(&cfg.metadata, &store.config).collect();
let mut errors = vec![];
{
let mut unused_audit_as: SortedSet<(PackageName, Option<VetVersion>)> = store
.config
.policy
.iter()
.filter(|(_, _, policy)| policy.audit_as_crates_io.is_some())
.map(|(name, version, _)| (name.clone(), version.cloned()))
.collect();
for package in &first_party_packages {
unused_audit_as.remove(&(package.name.to_string(), Some(package.vet_version())));
unused_audit_as.remove(&(package.name.to_string(), None));
}
if !unused_audit_as.is_empty() {
errors.push(AuditAsError::UnusedAuditAs(UnusedAuditAsErrors {
errors: unused_audit_as
.into_iter()
.map(|(package, version)| PackageError { package, version })
.collect(),
}))
}
}
let progress = progress_bar(
"Validating",
"audit-as-crates-io specifications",
first_party_packages.len() as u64,
);
enum CheckAction {
NeedAuditAs,
ShouldntBeAuditAs,
}
let actions: Vec<_> = join_all(first_party_packages.into_iter().map(|package| {
let progress = &progress;
let cache = &cache;
async move {
let _inc_progress = IncProgressOnDrop(progress, 1);
let audit_policy = package
.policy_entry(&store.config.policy)
.and_then(|policy| policy.audit_as_crates_io);
if audit_policy == Some(false) {
return None;
}
let package_name = package.name.as_str();
let has_existing_audits = || {
std::iter::once(&store.audits)
.chain(store.imports.audits.values())
.any(|audits_file| {
audits_file.audits.contains_key(package_name)
|| audits_file.wildcard_audits.contains_key(package_name)
|| audits_file.trusted.contains_key(package_name)
})
|| store.config.exemptions.contains_key(package_name)
};
let matches_crates_io_package = async {
cache
.crates_io_info(network, &package.name)
.await
.is_ok_and(|entry| entry.metadata.consider_as_same(package))
};
let crate_is_on_crates_io = has_existing_audits() || matches_crates_io_package.await;
if crate_is_on_crates_io && audit_policy.is_none() {
return Some((CheckAction::NeedAuditAs, package));
}
if network.is_some() && !crate_is_on_crates_io && audit_policy == Some(true) {
return Some((CheckAction::ShouldntBeAuditAs, package));
}
None
}
}))
.await
.into_iter()
.flatten()
.collect();
let mut needs_audit_as_entry = vec![];
let mut shouldnt_be_audit_as = vec![];
for (action, package) in actions {
match action {
CheckAction::NeedAuditAs => {
needs_audit_as_entry.push(VersionedPackageError {
package: package.name.to_string(),
version: package.vet_version(),
});
}
CheckAction::ShouldntBeAuditAs => {
shouldnt_be_audit_as.push(VersionedPackageError {
package: package.name.to_string(),
version: package.vet_version(),
});
}
}
}
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,
}));
}
if !errors.is_empty() {
Err(AuditAsErrors { errors })
} else {
Ok(())
}
}
fn check_crate_policies(cfg: &Config, store: &Store) -> Result<(), CratePolicyErrors> {
let mut policy_crates: SortedSet<&PackageName> = store.config.policy.package.keys().collect();
let mut versioned_policy_crates: SortedSet<(PackageName, VetVersion)> = store
.config
.policy
.iter()
.filter_map(|(name, version, _)| version.map(|version| (name.clone(), version.clone())))
.collect();
let unversioned_policy_crates: SortedSet<&str> = store
.config
.policy
.iter()
.filter_map(|(name, version, _)| version.is_none().then_some(name.as_str()))
.collect();
let third_party_packages = foreign_packages_strict(&cfg.metadata, &store.config)
.map(|p| p.name.as_str())
.collect::<SortedSet<_>>();
let dependency_criteria_packages = store
.config
.policy
.iter()
.filter_map(|(name, _, entry)| (!entry.dependency_criteria.is_empty()).then_some(name))
.collect::<SortedSet<_>>();
let mut must_be_versioned_errors = Vec::new();
let mut needs_policy_version_errors = Vec::new();
for package in &cfg.metadata.packages {
policy_crates.remove(&*package.name);
let has_third_party = third_party_packages.contains(package.name.as_str());
let unversioned_policy_exists = unversioned_policy_crates.contains(package.name.as_str());
let versioned_policy_exists =
versioned_policy_crates.remove(&(package.name.to_string(), package.vet_version()));
if has_third_party
&& dependency_criteria_packages.contains(&*package.name)
&& !versioned_policy_exists
{
needs_policy_version_errors.push(VersionedPackageError {
package: package.name.to_string(),
version: package.vet_version(),
});
} else if has_third_party && unversioned_policy_exists {
must_be_versioned_errors.push(VersionedPackageError {
package: package.name.to_string(),
version: package.vet_version(),
});
}
}
let unused_policy_version_errors: Vec<_> = policy_crates
.into_iter()
.map(|name| PackageError {
package: name.clone(),
version: None,
})
.chain(
versioned_policy_crates
.into_iter()
.map(|(package, version)| PackageError {
package,
version: Some(version),
}),
)
.collect();
let mut errors = Vec::new();
if !needs_policy_version_errors.is_empty() {
errors.push(CratePolicyError::DependencyCriteriaNeedsVersion(
DependencyCriteriaNeedsPolicyVersionErrors {
errors: needs_policy_version_errors,
},
));
}
if !unused_policy_version_errors.is_empty() {
errors.push(CratePolicyError::UnusedVersion(UnusedPolicyVersionErrors {
errors: unused_policy_version_errors,
}));
}
if !must_be_versioned_errors.is_empty() {
errors.push(CratePolicyError::ThirdPartyNeedsVersion(
ThirdPartyNeedsPolicyVersionErrors {
errors: must_be_versioned_errors,
},
));
}
if !errors.is_empty() {
Err(CratePolicyErrors { errors })
} else {
Ok(())
}
}