Repository: mozilla/cargo-vet (rust)

Issue:
Add a flow to `certify` for creating wildcard audits
This flow should allow users to add new wildcard audits without needing to manually access the crates.io API in order to discover the unique user-ids for authors, instead enabling it to be looked up by a username. The command could probably look something like `cargo vet certify $PACKAGE --wildcard $USERNAME` with a flow to suggest and allow selecting start/end dates, as well as criteria, similar to the existing certify flow.

See https://github.com/mozilla/cargo-vet/pull/398#issuecomment-1423344354 for some initial thoughts on what this flow will look like.

Pull request description:
Add an option to create wildcard audits with certify
This adds a --wildcard flag to the certify subcommand which can be used to create wildcard audits. Start and end dates must be specified with --start-date and --end-date, though they have fallback values.

This is a first-draft of the UX, and we may improve it in the future. We expect creating wildcard audits to be less frequent than other types, so the experience is a bit less polished.

Fixes #402

Patch:
diff --git a/book/src/install.md b/book/src/install.md
index 1d8fb68..4bc5a11 100644
--- a/book/src/install.md
+++ b/book/src/install.md
@@ -3,7 +3,7 @@
 Installing `cargo vet` can be done through Cargo:
 
 ```
-$ cargo install --locked cargo-vet
+$ cargo install cargo-vet
 ```
 
 Afterwards you can confirm that it's installed via:
diff --git a/src/cli.rs b/src/cli.rs
index 0bb83d0..9d11966 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -445,6 +445,12 @@ pub struct CertifyArgs {
     /// If present, instead certify a diff from version1->version2
     #[clap(action)]
     pub version2: Option<VetVersion>,
+    /// If present, certify a wildcard audit for the user with the given username.
+    ///
+    /// Use the --start-date and --end-date options to specify the date range to
+    /// certify for.
+    #[clap(long, action, conflicts_with("version1"), requires("package"))]
+    pub wildcard: Option<String>,
     /// The criteria to certify for this audit
     ///
     /// If not provided, we will prompt you for this information.
@@ -460,6 +466,21 @@ pub struct CertifyArgs {
     /// If not provided, there will be no notes.
     #[clap(long, action)]
     pub notes: Option<String>,
+    /// Start date to create a wildcard audit from.
+    ///
+    /// Only valid with `--wildcard`.
+    ///
+    /// If not provided, will be the publication date of the first version
+    /// published by the given user.
+    #[clap(long, action, requires("wildcard"))]
+    pub start_date: Option<chrono::NaiveDate>,
+    /// End date to create a wildcard audit from. May be at most 1 year in the future.
+    ///
+    /// Only valid with `--wildcard`.
+    ///
+    /// If not provided, will be 1 year from the current date.
+    #[clap(long, action, requires("wildcard"))]
+    pub end_date: Option<chrono::NaiveDate>,
     /// Accept all criteria without an interactive prompt
     #[clap(long, action)]
     pub accept_all: bool,
diff --git a/src/errors.rs b/src/errors.rs
index d0c9078..be8fe58 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -245,12 +245,22 @@ pub enum CertifyError {
     #[error("'{0}' isn't one of your foreign packages")]
     #[diagnostic(help("use --force to ignore this error"))]
     NotAPackage(PackageName),
+    #[error("'{0}' has not published any relevant version of '{1}'")]
+    #[diagnostic(help("please specify a user who has published a version of '{1}'"))]
+    NotAPublisher(String, PackageName),
+    #[error("end date of {0} is too far in the future")]
+    #[diagnostic(help("wildcard audit end dates may be at most 1 year in the future"))]
+    BadWildcardEndDate(chrono::NaiveDate),
     #[error(transparent)]
     IoError(#[from] std::io::Error),
     #[error(transparent)]
     EditError(#[from] EditError),
     #[error(transparent)]
     UserInfoError(#[from] UserInfoError),
+    #[error(transparent)]
+    FetchAuditError(#[from] FetchAuditError),
+    #[error(transparent)]
+    CacheAcquire(#[from] CacheAcquireError),
 }
 
 ///////////////////////////////////////////////////////////
diff --git a/src/main.rs b/src/main.rs
index 1a0bd1c..df1516e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,7 +4,7 @@ use std::ops::Deref;
 use std::panic::panic_any;
 use std::process::Stdio;
 use std::sync::{Arc, Mutex};
-use std::time::Duration;
+use std::time::{Duration, SystemTime};
 use std::{fs::File, io, panic, path::PathBuf};
 
 use cargo_metadata::{Metadata, Package};
@@ -37,8 +37,9 @@ use crate::errors::{
     SourceFile,
 };
 use crate::format::{
-    AuditEntry, AuditKind, AuditsFile, ConfigFile, CriteriaEntry, ExemptedDependency, FetchCommand,
-    ImportsFile, MetaConfig, MetaConfigInstance, PackageStr, SortedMap, StoreInfo,
+    AuditEntry, AuditKind, AuditsFile, ConfigFile, CratesUserId, CriteriaEntry, ExemptedDependency,
+    FetchCommand, ImportsFile, MetaConfig, MetaConfigInstance, PackageStr, SortedMap, StoreInfo,
+    WildcardEntry,
 };
 use crate::git_tool::Pager;
 use crate::out::{indeterminate_spinner, Out, StderrLogWriter, MULTIPROGRESS};
@@ -669,7 +670,16 @@ fn cmd_certify(
     // Grab the last fetch and immediately drop the cache
     let last_fetch = Cache::acquire(cfg)?.get_last_fetch();
 
-    do_cmd_certify(out, cfg, sub_args, &mut store, network.as_ref(), last_fetch)?;
+    let today = <chrono::DateTime<chrono::Utc>>::from(SystemTime::now()).date_naive();
+    do_cmd_certify(
+        out,
+        cfg,
+        sub_args,
+        &mut store,
+        network.as_ref(),
+        last_fetch,
+        today,
+    )?;
 
     // Minimize exemptions after adding the new `certify`. This will be used to
     // potentially update imports, and remove now-unnecessary exemptions.
@@ -690,6 +700,7 @@ fn do_cmd_certify(
     store: &mut Store,
     network: Option<&Network>,
     last_fetch: Option<FetchCommand>,
+    today: chrono::NaiveDate,
 ) -> Result<(), CertifyError> {
     // Before setting up magic, we need to agree on a package
     let package = if let Some(package) = &sub_args.package {
@@ -708,27 +719,71 @@ fn do_cmd_certify(
         return Err(CertifyError::NotAPackage(package));
     }
 
-    let kind = if let Some(v1) = &sub_args.version1 {
+    #[derive(Debug)]
+    enum CertifyKind {
+        Delta {
+            from: VetVersion,
+            to: VetVersion,
+        },
+        Full {
+            version: VetVersion,
+        },
+        Wildcard {
+            user_login: String,
+            user_id: CratesUserId,
+            start: chrono::NaiveDate,
+            end: chrono::NaiveDate,
+        },
+    }
+
+    let kind = if let Some(login) = &sub_args.wildcard {
+        // Fetch publisher information for relevant versions of `package`.
+        let publishers = store.ensure_publisher_versions(cfg, network, &package)?;
+        let published_versions = publishers
+            .iter()
+            .filter(|publisher| &publisher.user_login == login);
+
+        let earliest = published_versions
+            .min_by_key(|p| p.when)
+            .ok_or_else(|| CertifyError::NotAPublisher(login.to_owned(), package.to_owned()))?;
+
+        // Get the from and to dates, defaulting to a from date of the earliest
+        // published package by the user, and a to date of 12 months from today.
+        let start = sub_args.start_date.unwrap_or(earliest.when);
+
+        let max_end = today + chrono::Months::new(12);
+        let end = sub_args.end_date.unwrap_or(max_end);
+        if end > max_end {
+            return Err(CertifyError::BadWildcardEndDate(end));
+        }
+
+        CertifyKind::Wildcard {
+            user_login: earliest.user_login.to_owned(),
+            user_id: earliest.user_id,
+            start,
+            end,
+        }
+    } else if let Some(v1) = &sub_args.version1 {
         // If explicit versions were provided, use those
         if let Some(v2) = &sub_args.version2 {
             // This is a delta audit
-            AuditKind::Delta {
+            CertifyKind::Delta {
                 from: v1.clone(),
                 to: v2.clone(),
             }
         } else {
             // This is a full audit
-            AuditKind::Full {
+            CertifyKind::Full {
                 version: v1.clone(),
             }
         }
     } else if let Some(fetch) = last_fetch.filter(|f| f.package() == package) {
         // Otherwise, is we just fetched this package, use the version(s) we fetched
         match fetch {
-            FetchCommand::Inspect { version, .. } => AuditKind::Full { version },
+            FetchCommand::Inspect { version, .. } => CertifyKind::Full { version },
             FetchCommand::Diff {
                 version1, version2, ..
-            } => AuditKind::Delta {
+            } => CertifyKind::Delta {
                 from: version1,
                 to: version2,
             },
@@ -759,12 +814,6 @@ fn do_cmd_certify(
     );
 
     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!(),
-        };
-
         // If we don't have explicit cli criteria, guess the criteria
         //
         // * Check what would cause `cargo vet` to encounter fewer errors
@@ -772,18 +821,29 @@ fn do_cmd_certify(
         // * Otherwise guess nothing
         //
         // Regardless of the guess, prompt the user to confirm (just needs to mash enter)
-        let mut chosen_criteria = guess_audit_criteria(cfg, store, &package, from, to);
+        let (mut chosen_criteria, version_suffix) = match &kind {
+            CertifyKind::Full { version } => (
+                guess_audit_criteria(cfg, store, &package, None, version),
+                version.to_string(),
+            ),
+            CertifyKind::Delta { from, to } => (
+                guess_audit_criteria(cfg, store, &package, Some(from), to),
+                format!("{from} -> {to}"),
+            ),
+            CertifyKind::Wildcard { .. } => {
+                // FIXME: Consider predicting the criteria better for wildcard
+                // audits in the future.
+                (vec![format::SAFE_TO_DEPLOY.to_owned()], "*".to_owned())
+            }
+        };
 
         // Prompt for criteria
         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,
+                "choose criteria to certify for {package}:{version_suffix}"
+            );
             writeln!(out, "  0. <clear selections>");
             let implied_criteria = criteria_mapper.criteria_from_list(&chosen_criteria);
             // Iterate over all the local criteria. Note that it's fine for us to do the enumerate
@@ -871,13 +931,20 @@ fn do_cmd_certify(
         .collect::<Vec<_>>();
 
     let what_version = match &kind {
-        AuditKind::Full { version, .. } => {
+        CertifyKind::Full { version } => {
             format!("version {version}")
         }
-        AuditKind::Delta { from, to, .. } => {
+        CertifyKind::Delta { from, to } => {
             format!("the changes from version {from} to {to}")
         }
-        AuditKind::Violation { .. } => unreachable!(),
+        CertifyKind::Wildcard {
+            user_login,
+            start,
+            end,
+            ..
+        } => {
+            format!("all versions published by user '{user_login}' between {start} and {end}")
+        }
     };
     let statement = format!(
         "I, {username}, certify that I have audited {what_version} of {package} in accordance with the above criteria.",
@@ -952,12 +1019,41 @@ fn do_cmd_certify(
         };
     }
 
+    let criteria = criteria_names
+        .iter()
+        .map(|s| s.to_string().into())
+        .collect();
+    let audit_kind = match kind {
+        CertifyKind::Full { version } => AuditKind::Full { version },
+        CertifyKind::Delta { from, to } => AuditKind::Delta { from, to },
+        CertifyKind::Wildcard {
+            user_id,
+            start,
+            end,
+            ..
+        } => {
+            store
+                .audits
+                .wildcard_audits
+                .entry(package.clone())
+                .or_default()
+                .push(WildcardEntry {
+                    who,
+                    criteria,
+                    user_id,
+                    start: start.into(),
+                    end: end.into(),
+                    notes,
+                    aggregated_from: vec![],
+                    is_fresh_import: false,
+                });
+            return Ok(());
+        }
+    };
+
     let new_entry = AuditEntry {
-        kind: kind.clone(),
-        criteria: criteria_names
-            .iter()
-            .map(|s| s.to_string().into())
-            .collect(),
+        kind: audit_kind,
+        criteria,
         who,
         notes,
         aggregated_from: vec![],
@@ -971,24 +1067,9 @@ fn do_cmd_certify(
         .or_default()
         .push(new_entry);
 
-    // If we're submitting a full audit, look for a matching exemption entry to remove
-    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);
-            // Iterate backwards so that we can delete while iterating
-            // (will only affect indices that we've already visited!)
-            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);
-            }
-        }
-    }
+    store
+        .validate(today, false)
+        .expect("the new audit entry made the store invalid?");
 
     Ok(())
 }
diff --git a/src/storage.rs b/src/storage.rs
index 20916df..4b52346 100644
--- a/src/storage.rs
+++ b/src/storage.rs
@@ -20,12 +20,12 @@ use tracing::{error, info, log::warn, trace};
 
 use crate::{
     errors::{
-        BadFormatError, BadWildcardEndDateError, CacheAcquireError, CacheCommitError, CommandError,
-        CriteriaChangeError, CriteriaChangeErrors, DiffError, DownloadError, FetchAndDiffError,
-        FetchAuditError, FetchError, FlockError, InvalidCriteriaError, JsonParseError,
-        LoadJsonError, LoadTomlError, SourceFile, StoreAcquireError, StoreCommitError,
-        StoreCreateError, StoreJsonError, StoreTomlError, StoreValidateError, StoreValidateErrors,
-        TomlParseError, UnpackCheckoutError, UnpackError,
+        BadFormatError, BadWildcardEndDateError, CacheAcquireError, CacheCommitError, CertifyError,
+        CommandError, CriteriaChangeError, CriteriaChangeErrors, DiffError, DownloadError,
+        FetchAndDiffError, FetchAuditError, FetchError, FlockError, InvalidCriteriaError,
+        JsonParseError, LoadJsonError, LoadTomlError, SourceFile, StoreAcquireError,
+        StoreCommitError, StoreCreateError, StoreJsonError, StoreTomlError, StoreValidateError,
+        StoreValidateErrors, TomlParseError, UnpackCheckoutError, UnpackError,
     },
     flock::{FileLock, Filesystem},
     format::{
@@ -225,7 +225,20 @@ impl Store {
                 .block_on(fetch_imported_audits(network, &config))?;
             let mut live_imports =
                 process_imported_audits(fetched_audits, &config, &imports, allow_criteria_changes)?;
-            import_publisher_versions(cfg, network, &config, &audits, &imports, &mut live_imports)?;
+            // XXX: Allow callers to hold on to this cache so we don't need to
+            // re-acquire it after `Store::acquire`.
+            let cache = Cache::acquire(cfg).map_err(Box::new)?;
+            import_publisher_versions(
+                &cfg.metadata,
+                network,
+                &cache,
+                &wildcard_audits_packages(&audits, &live_imports),
+                &config,
+                &audits,
+                &imports,
+                &mut live_imports,
+            )
+            .map_err(Box::new)?;
             Some(live_imports)
         } else {
             None
@@ -284,7 +297,18 @@ impl Store {
             tokio::runtime::Handle::current().block_on(fetch_imported_audits(network, &config))?;
         let mut live_imports =
             process_imported_audits(fetched_audits, &config, &imports, allow_criteria_changes)?;
-        import_publisher_versions(cfg, network, &config, &audits, &imports, &mut live_imports)?;
+        let cache = Cache::acquire(cfg).map_err(Box::new)?;
+        import_publisher_versions(
+            &cfg.metadata,
+            network,
+            &cache,
+            &wildcard_audits_packages(&audits, &live_imports),
+            &config,
+            &audits,
+            &imports,
+            &mut live_imports,
+        )
+        .map_err(Box::new)?;
 
         let store = Self {
             lock: None,
@@ -798,6 +822,38 @@ impl Store {
 
         new_imports
     }
+
+    /// Called to ensure that there is publisher information in the store's live
+    /// imports for the given crate. This is used when adding new wildcard
+    /// audits from `certify`.
+    pub fn ensure_publisher_versions(
+        &mut self,
+        cfg: &Config,
+        network: Option<&Network>,
+        package: PackageStr<'_>,
+    ) -> Result<&[CratesPublisher], CertifyError> {
+        if let (Some(network), Some(live_imports)) = (network, self.live_imports.as_mut()) {
+            let cache = Cache::acquire(cfg)?;
+            import_publisher_versions(
+                &cfg.metadata,
+                network,
+                &cache,
+                &[package.to_owned()].into_iter().collect(),
+                &self.config,
+                &self.audits,
+                &self.imports,
+                live_imports,
+            )?;
+
+            Ok(live_imports
+                .publisher
+                .get(package)
+                .map(|v| &v[..])
+                .unwrap_or(&[]))
+        } else {
+            Ok(&[])
+        }
+    }
 }
 
 /// Process imported audits from the network, generating a `LiveImports`
@@ -1166,32 +1222,41 @@ fn parse_imported_wildcard_audit(
     Some(audit)
 }
 
-fn import_publisher_versions(
-    cfg: &Config,
-    network: &Network,
-    config_file: &ConfigFile,
+fn wildcard_audits_packages(
     audits_file: &AuditsFile,
-    imports_lock: &ImportsFile,
-    live_imports: &mut ImportsFile,
-) -> Result<(), StoreAcquireError> {
+    imports_file: &ImportsFile,
+) -> FastSet<PackageName> {
     // Determine which versions are relevant for the purposes of wildcard audit
     // checks. We'll only care about crates which have associated wildcard
     // audits.
-    let relevant_packages: FastSet<_> = audits_file
+    audits_file
         .wildcard_audits
         .keys()
         .chain(
-            live_imports
+            imports_file
                 .audits
                 .values()
                 .flat_map(|audits_file| audits_file.wildcard_audits.keys()),
         )
-        .collect();
+        .cloned()
+        .collect()
+}
 
+#[allow(clippy::too_many_arguments)]
+fn import_publisher_versions(
+    metadata: &cargo_metadata::Metadata,
+    network: &Network,
+    cache: &Cache,
+    relevant_packages: &FastSet<PackageName>,
+    config_file: &ConfigFile,
+    audits_file: &AuditsFile,
+    imports_lock: &ImportsFile,
+    live_imports: &mut ImportsFile,
+) -> Result<(), FetchAuditError> {
     // We also only care about versions for third-party packages which are
     // actually used in-tree.
     let mut relevant_versions: FastMap<PackageStr<'_>, FastSet<&semver::Version>> = FastMap::new();
-    for pkg in &cfg.metadata.packages {
+    for pkg in &metadata.packages {
         if relevant_packages.contains(&pkg.name) && pkg.is_third_party(&config_file.policy) {
             relevant_versions
                 .entry(&pkg.name)
@@ -1200,12 +1265,6 @@ fn import_publisher_versions(
         }
     }
 
-    // Acquire the cache so we can use it to access the publisher cache.
-    // FIXME: Consider providing a way to keep this acquired cache after
-    // acquring the store to reduce the number of times we need to acquire the
-    // cache during an execution?
-    let cache = Cache::acquire(cfg).map_err(Box::new)?;
-
     // We may care about other versions for these packages as well if they
     // appear on the `from` side of a delta audit, or if they are named by an
     // exemption.
@@ -1235,9 +1294,7 @@ fn import_publisher_versions(
         // Access the set of publishers. We provide the set of relevant versions
         // to help decide whether or not to fetch new publisher information from
         // crates.io, to reduce API activity.
-        let publishers = cache
-            .get_publishers(network, pkg_name, versions)
-            .map_err(Box::new)?;
+        let publishers = cache.get_publishers(network, pkg_name, versions)?;
         relevant_publishers.push((pkg_name, publishers));
     }
 


Test patch:
diff --git a/src/tests/certify.rs b/src/tests/certify.rs
index fd55334..e616af9 100644
--- a/src/tests/certify.rs
+++ b/src/tests/certify.rs
@@ -1,3 +1,5 @@
+use crate::network::Network;
+
 use super::*;
 use std::fmt::Write;
 
@@ -138,6 +140,7 @@ fn mock_simple_certify_flow() {
         &mut store,
         None,
         None,
+        chrono::NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
     )
     .expect("do_cmd_certify failed");
 
@@ -147,3 +150,96 @@ fn mock_simple_certify_flow() {
 
     insta::assert_snapshot!("mock-simple-certify-flow", result);
 }
+
+#[test]
+fn mock_wildcard_certify_flow() {
+    let mock = MockMetadata::simple();
+
+    let _enter = TEST_RUNTIME.enter();
+    let metadata = mock.metadata();
+
+    let (config, audits, imports) = files_inited(&metadata);
+
+    let output = BasicTestOutput::with_callbacks(
+        |_| Ok("\n".to_owned()),
+        |_| {
+            Ok("\
+            I, testing, certify that I have audited all versions published by user 'testuser' between 2022-12-12 and 2024-01-01 of third-party1 in accordance with the above criteria.\n\
+            \n\
+            These are testing notes. They contain some\n\
+            newlines. Trailing whitespace        \n    \
+            and leading whitespace\n\
+            \n".to_owned())
+        },
+    );
+
+    let cfg = mock_cfg_args(
+        &metadata,
+        [
+            "cargo",
+            "vet",
+            "certify",
+            "third-party1",
+            "--wildcard",
+            "testuser",
+            "--who",
+            "testing",
+        ],
+    );
+    let sub_args = if let Some(crate::cli::Commands::Certify(sub_args)) = &cfg.cli.command {
+        sub_args
+    } else {
+        unreachable!();
+    };
+
+    let mut network = Network::new_mock();
+    network.mock_serve_json(
+        "https://crates.io/api/v1/crates/third-party1",
+        &serde_json::json!({
+            "versions": [
+                {
+                    "crate": "third-party1",
+                    "created_at": "2022-10-12T04:51:37.251648+00:00",
+                    "num": "9.0.0",
+                    "published_by": {
+                        "id": 5,
+                        "login": "otheruser",
+                        "name": "Other User",
+                        "url": "https://github.com/otheruser"
+                    }
+                },
+                {
+                    "crate": "third-party1",
+                    "created_at": "2022-12-12T04:51:37.251648+00:00",
+                    "num": "10.0.0",
+                    "published_by": {
+                        "id": 2,
+                        "login": "testuser",
+                        "name": "Test user",
+                        "url": "https://github.com/testuser"
+                    }
+                },
+            ]
+        }),
+    );
+
+    let mut store = Store::mock_online(&cfg, config, audits, imports, &network, true)
+        .expect("store acquisition failed");
+
+    crate::do_cmd_certify(
+        &output.clone().as_dyn(),
+        &cfg,
+        sub_args,
+        &mut store,
+        Some(&network),
+        None,
+        chrono::NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
+    )
+    .expect("do_cmd_certify failed");
+
+    let audits = crate::serialization::to_formatted_toml(&store.audits).unwrap();
+
+    let result = format!("OUTPUT:\n{output}\nAUDITS:\n{audits}");
+
+    insta::assert_snapshot!(result);
+}
diff --git a/src/tests/snapshots/cargo_vet__tests__certify__mock_wildcard_certify_flow.snap b/src/tests/snapshots/cargo_vet__tests__certify__mock_wildcard_certify_flow.snap
new file mode 100644
index 0000000..c81f0e7
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__certify__mock_wildcard_certify_flow.snap
@@ -0,0 +1,92 @@
+---
+source: src/tests/certify.rs
+expression: result
+---
+OUTPUT:
+<<<CLEAR SCREEN>>>
+choose criteria to certify for third-party1:*
+  0. <clear selections>
+  1. safe-to-run
+  2. safe-to-deploy
+  3. fuzzed
+  4. reviewed
+  5. strong-reviewed
+  6. weak-reviewed
+
+current selection: ["safe-to-deploy"]
+(press ENTER to accept the current criteria)
+> 
+
+<<<EDITING VET_CERTIFY>>>
+# Please read the following criteria and then follow the instructions below:
+
+# === BEGIN CRITERIA "safe-to-deploy" ===
+#
+# This crate will not introduce a serious security vulnerability to production
+# software exposed to untrusted input.
+#
+# Auditors are not required to perform a full logic review of the entire crate.
+# Rather, they must review enough to fully reason about the behavior of all unsafe
+# blocks and usage of powerful imports. For any reasonable usage of the crate in
+# real-world software, an attacker must not be able to manipulate the runtime
+# behavior of these sections in an exploitable or surprising way.
+#
+# Ideally, all unsafe code is fully sound, and ambient capabilities (e.g.
+# filesystem access) are hardened against manipulation and consistent with the
+# advertised behavior of the crate. However, some discretion is permitted. In such
+# cases, the nature of the discretion should be recorded in the `notes` field of
+# the audit record.
+#
+# For crates which generate deployed code (e.g. build dependencies or procedural
+# macros), reasonable usage of the crate should output code which meets the above
+# criteria.
+#
+# === END CRITERIA ===
+#
+# Uncomment the following statement:
+
+# I, testing, certify that I have audited all versions published by user 'testuser' between 2022-12-12 and 2024-01-01 of third-party1 in accordance with the above criteria.
+
+# Add any notes about your audit below this line:
+
+
+<<<EDIT OK>>>
+I, testing, certify that I have audited all versions published by user 'testuser' between 2022-12-12 and 2024-01-01 of third-party1 in accordance with the above criteria.
+
+These are testing notes. They contain some
+newlines. Trailing whitespace        
+    and leading whitespace
+
+
+<<<END EDIT>>>
+
+AUDITS:
+
+[criteria.fuzzed]
+description = "fuzzed"
+
+[criteria.reviewed]
+description = "reviewed"
+implies = "weak-reviewed"
+
+[criteria.strong-reviewed]
+description = "strongly reviewed"
+implies = "reviewed"
+
+[criteria.weak-reviewed]
+description = "weakly reviewed"
+
+[[wildcard-audits.third-party1]]
+who = "testing"
+criteria = "safe-to-deploy"
+user-id = 2
+start = "2022-12-12"
+end = "2024-01-01"
+notes = """
+These are testing notes. They contain some
+newlines. Trailing whitespace
+    and leading whitespace
+"""
+
+[audits]
+
diff --git a/tests/snapshots/test_cli__markdown-help.snap b/tests/snapshots/test_cli__markdown-help.snap
index 262e5b2..ea76e7d 100644
--- a/tests/snapshots/test_cli__markdown-help.snap
+++ b/tests/snapshots/test_cli__markdown-help.snap
@@ -337,6 +337,11 @@ The version to certify as audited
 If present, instead certify a diff from version1->version2
 
 ### OPTIONS
+#### `--wildcard <WILDCARD>`
+If present, certify a wildcard audit for the user with the given username.
+
+Use the --start-date and --end-date options to specify the date range to certify for.
+
 #### `--criteria <CRITERIA>`
 The criteria to certify for this audit
 
@@ -352,6 +357,21 @@ A free-form string to include with the new audit entry
 
 If not provided, there will be no notes.
 
+#### `--start-date <START_DATE>`
+Start date to create a wildcard audit from.
+
+Only valid with `--wildcard`.
+
+If not provided, will be the publication date of the first version published by the
+given user.
+
+#### `--end-date <END_DATE>`
+End date to create a wildcard audit from. May be at most 1 year in the future.
+
+Only valid with `--wildcard`.
+
+If not provided, will be 1 year from the current date.
+
 #### `--accept-all`
 Accept all criteria without an interactive prompt