Repository: mozilla/cargo-vet
Pull request #230 (2022-06-27 03:29:47)

Introduce Spanned toml deserialization, add some validation/tests
Spanning is done with the Spanned<T> hack from toml-rs. A bunch of serialization code had to be rejigged
to play nice with that hack. In particular things like flatten and untagged_enum make it freak out, so
now we deserialize all the fields and then pick which variant it is.

Store now records the source files it imported, and we now check that criteria names are valid everywhere.
New tests allow us to quickly check different inputs parse/fail-parse/fail-validate.

Gankra: I have been struggling in a mental labyrinth trying to get the Spanned stuff to work with all the weird parsing hacks we already had for like the last week, and ended up obsessively working on it this sunday, so I'll take off some other day this week to make up.
Gankra: ![image](https://user-images.githubusercontent.com/1136864/175855295-3fc72b77-6b56-40ab-b380-608b457cb38f.png)

beautiful ;-;

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 8ef8399..3bf08a9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -136,6 +136,7 @@ dependencies = [
  "textwrap",
  "thiserror",
  "tokio",
+ "toml",
  "toml_edit",
  "tracing",
  "tracing-subscriber",
@@ -1383,6 +1384,15 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "toml"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "toml_edit"
 version = "0.14.4"
diff --git a/Cargo.toml b/Cargo.toml
index 734ead0..0763c2a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,6 +42,7 @@ tracing-subscriber = "0.3.11"
 miette = { version = "5.0.0", features = ["fancy"] }
 thiserror = "1.0.31"
 url = "2.2.2"
+toml = "0.5.9"
 
 [target.'cfg(windows)'.dependencies.winapi]
 version = "0.3"
diff --git a/src/errors.rs b/src/errors.rs
index 3c46c35..c74f255 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -1,7 +1,7 @@
 use std::{ffi::OsString, fmt::Display, path::PathBuf, string::FromUtf8Error, sync::Arc};
 
 use cargo_metadata::Version;
-use miette::{Diagnostic, NamedSource};
+use miette::{Diagnostic, NamedSource, SourceOffset, SourceSpan};
 use thiserror::Error;
 
 use crate::format::{ImportName, PackageName};
@@ -114,11 +114,11 @@ pub enum CertifyError {
 #[non_exhaustive]
 pub enum EditError {
     #[error("Failed to launch editor")]
-    CouldntLaunch(#[diagnostic(cause)] std::io::Error),
+    CouldntLaunch(#[source] std::io::Error),
     #[error("Failed to open result of editor")]
-    CouldntOpen(#[diagnostic(cause)] std::io::Error),
+    CouldntOpen(#[source] std::io::Error),
     #[error("Failed to read result of editor")]
-    CouldntRead(#[diagnostic(cause)] std::io::Error),
+    CouldntRead(#[source] std::io::Error),
 }
 
 ///////////////////////////////////////////////////////////
@@ -129,8 +129,8 @@ pub enum EditError {
 #[error("Failed to initialize the cargo-vet store (supply-chain)")]
 #[non_exhaustive]
 pub enum InitError {
-    StoreCreate(#[diagnostic(cause)] StoreCreateError),
-    StoreCommit(#[diagnostic(cause)] StoreCommitError),
+    StoreCreate(#[source] StoreCreateError),
+    StoreCommit(#[source] StoreCommitError),
 }
 
 //////////////////////////////////////////////////////////
@@ -155,10 +155,10 @@ pub enum MinimizeUnauditedError {
 #[non_exhaustive]
 #[error("Couldn't create the store (supply-chain)")]
 pub enum StoreCreateError {
-    CouldntCreate(#[diagnostic(cause)] std::io::Error),
+    CouldntCreate(#[source] std::io::Error),
     CouldntAcquire(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         FlockError,
     ),
 }
@@ -169,19 +169,16 @@ pub enum StoreAcquireError {
     #[error("Couldn't acquire the store's (supply-chain's) lock")]
     CouldntLock(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         FlockError,
     ),
     #[error(transparent)]
-    LoadToml(
-        #[from]
-        #[diagnostic(cause)]
-        LoadTomlError,
-    ),
-    #[error(transparent)]
+    #[diagnostic(transparent)]
+    LoadToml(#[from] LoadTomlError),
+    #[error("Couldn't acquire the store")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
     #[error(transparent)]
@@ -189,33 +186,53 @@ pub enum StoreAcquireError {
     Validate(#[from] StoreValidateErrors),
 }
 
-#[derive(Debug, Error, Diagnostic)]
-#[error("Your cargo-vet store (supply-chain) has consistency errors")]
-pub struct StoreValidateErrors {
-    #[related]
-    errors: Vec<StoreValidateError>,
-}
-
-#[derive(Debug, Error, Diagnostic)]
-#[non_exhaustive]
-pub enum StoreValidateError {}
-
 #[derive(Debug, Error, Diagnostic)]
 #[non_exhaustive]
 #[error("Failed to commit store")]
 pub enum StoreCommitError {
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
     StoreToml(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         StoreTomlError,
     ),
 }
 
+////////////////////////////////////////////////////////////
+// StoreValidateErrors
+////////////////////////////////////////////////////////////
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("Your cargo-vet store (supply-chain) has consistency errors")]
+pub struct StoreValidateErrors {
+    #[related]
+    pub errors: Vec<StoreValidateError>,
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[non_exhaustive]
+pub enum StoreValidateError {
+    #[diagnostic(transparent)]
+    #[error(transparent)]
+    InvalidCriteria(InvalidCriteriaError),
+}
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("'{invalid}' is not a valid criteria name")]
+#[diagnostic(help("the possible criteria are {:?}", valid_names))]
+pub struct InvalidCriteriaError {
+    #[source_code]
+    pub source_code: SourceFile,
+    #[label]
+    pub span: SourceSpan,
+    pub invalid: String,
+    pub valid_names: Arc<Vec<String>>,
+}
+
 //////////////////////////////////////////////////////////
 // CacheErrors
 /////////////////////////////////////////////////////////
@@ -223,40 +240,40 @@ pub enum StoreCommitError {
 #[derive(Debug, Error, Diagnostic)]
 #[non_exhaustive]
 pub enum CacheAcquireError {
-    #[error(transparent)]
+    #[error("Couldn't acquire cache")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
     #[error("Failed to create cache root dir: {}", target.display())]
     Root {
         target: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Failed to create cache src dir: {}", target.display())]
     Src {
         target: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Failed to create cache empty dir: {}", target.display())]
     Empty {
         target: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Failed to create cache package dir: {}", target.display())]
     Cache {
         target: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Couldn't acquire the cache's lock")]
     CouldntLock(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         FlockError,
     ),
 }
@@ -267,17 +284,17 @@ pub enum CacheAcquireError {
 pub enum CacheCommitError {
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
     StoreToml(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         StoreTomlError,
     ),
     StoreJson(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         StoreJsonError,
     ),
 }
@@ -290,11 +307,11 @@ pub enum CacheCommitError {
 #[non_exhaustive]
 pub enum CommandError {
     #[error("Command failed")]
-    CommandFailed(#[diagnostic(cause)] std::io::Error),
+    CommandFailed(#[source] std::io::Error),
     #[error("Bad status {0}")]
     BadStatus(i32),
     #[error("Wasn't UTF-8")]
-    BadOutput(#[diagnostic(cause)] FromUtf8Error),
+    BadOutput(#[source] FromUtf8Error),
 }
 
 //////////////////////////////////////////////////////////
@@ -322,7 +339,7 @@ pub enum DiffError {
     #[error("Failed to diff package")]
     CommandError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         CommandError,
     ),
 }
@@ -335,9 +352,9 @@ pub enum DiffError {
 #[non_exhaustive]
 pub enum UserInfoError {
     #[error("Failed to get user.name")]
-    UserCommandFailed(#[diagnostic(cause)] CommandError),
+    UserCommandFailed(#[source] CommandError),
     #[error("Failed to get user.email")]
-    EmailCommandFailed(#[diagnostic(cause)] CommandError),
+    EmailCommandFailed(#[source] CommandError),
 }
 
 //////////////////////////////////////////////////////////
@@ -350,7 +367,7 @@ pub enum FetchError {
     #[error("Invalid URL for package: {url}")]
     InvalidUrl {
         url: String,
-        #[diagnostic(cause)]
+        #[source]
         error: url::ParseError,
     },
     #[error("Running as --frozen but needed to fetch {package}:{version}")]
@@ -361,13 +378,13 @@ pub enum FetchError {
     #[error("Failed to unpack .crate at {}", src.display())]
     Unpack {
         src: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: UnpackError,
     },
     #[error("failed to open cached .crate at {}", target.display())]
     OpenCached {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error(transparent)]
@@ -379,9 +396,9 @@ pub enum FetchError {
 #[non_exhaustive]
 pub enum UnpackError {
     #[error("Failed to iterate archive")]
-    ArchiveIterate(#[diagnostic(cause)] std::io::Error),
+    ArchiveIterate(#[source] std::io::Error),
     #[error("Failed to read archive entry")]
-    ArchiveEntry(#[diagnostic(cause)] std::io::Error),
+    ArchiveEntry(#[source] std::io::Error),
     #[error("Invalid archive, {} wasn't under {}", entry_path.display(), prefix.to_string_lossy())]
     InvalidPaths {
         entry_path: PathBuf,
@@ -390,13 +407,13 @@ pub enum UnpackError {
     #[error("Failed to unpack archive entry {}", entry_path.display())]
     Unpack {
         entry_path: PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Failed to finalize unpack to {}", target.display())]
     LockCreate {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error(transparent)]
@@ -415,7 +432,7 @@ pub enum FetchAuditError {
     InvalidUrl {
         import_name: ImportName,
         import_url: String,
-        #[diagnostic(cause)]
+        #[source]
         error: url::ParseError,
     },
     #[diagnostic(transparent)]
@@ -423,7 +440,7 @@ pub enum FetchAuditError {
     Download(#[from] DownloadError),
     #[diagnostic(transparent)]
     #[error(transparent)]
-    Toml(#[from] TomlParseError),
+    Toml(#[from] LoadTomlError),
     #[diagnostic(transparent)]
     #[error(transparent)]
     Validate(#[from] StoreValidateErrors),
@@ -439,37 +456,37 @@ pub enum DownloadError {
     #[error("failed to start download of {url}")]
     FailedToStartDownload {
         url: reqwest::Url,
-        #[diagnostic(cause)]
+        #[source]
         error: reqwest::Error,
     },
     #[error("failed to create file for download to {}", target.display())]
     FailedToCreateDownload {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("failed to read download from {url}")]
     FailedToReadDownload {
         url: reqwest::Url,
-        #[diagnostic(cause)]
+        #[source]
         error: reqwest::Error,
     },
     #[error("failed to write download to {}", target.display())]
     FailedToWriteDownload {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("failed to rename download to final location {}", target.display())]
     FailedToFinalizeDownload {
         target: std::path::PathBuf,
-        #[diagnostic(cause)]
+        #[source]
         error: std::io::Error,
     },
     #[error("Download wasn't valid utf8: {url}")]
     InvalidText {
         url: reqwest::Url,
-        #[diagnostic(cause)]
+        #[source]
         error: FromUtf8Error,
     },
 }
@@ -493,10 +510,10 @@ pub enum SuggestError {
 #[derive(Debug, Error, Diagnostic)]
 #[non_exhaustive]
 pub enum FlockError {
-    #[error(transparent)]
+    #[error("couldn't acquire file lock")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
@@ -508,12 +525,12 @@ pub enum FlockError {
 #[derive(Debug, Error, Diagnostic)]
 #[error("Failed to parse toml file")]
 pub struct TomlParseError {
-    // #[source_code]
-    // input: SourceFile,
-    // #[label("here")]
-    // span: SourceOffset,
-    #[diagnostic(cause)]
-    pub error: toml_edit::de::Error,
+    #[source_code]
+    pub source_code: SourceFile,
+    #[label("here")]
+    pub span: SourceOffset,
+    #[source]
+    pub error: toml::de::Error,
 }
 
 #[derive(Debug, Error, Diagnostic)]
@@ -523,7 +540,7 @@ pub struct JsonParseError {
     // input: SourceFile,
     // #[label("here")]
     // span: SourceOffset,
-    #[diagnostic(cause)]
+    #[source]
     pub error: serde_json::Error,
 }
 
@@ -533,10 +550,18 @@ pub enum LoadTomlError {
     #[error(transparent)]
     #[diagnostic(transparent)]
     TomlParse(#[from] TomlParseError),
-    #[error(transparent)]
+
+    #[error("TOML wasn't valid utf8")]
+    InvalidText {
+        #[source]
+        #[from]
+        error: FromUtf8Error,
+    },
+
+    #[error("couldn't load toml")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
@@ -547,10 +572,10 @@ pub enum LoadJsonError {
     #[error(transparent)]
     #[diagnostic(transparent)]
     JsonParse(#[from] JsonParseError),
-    #[error(transparent)]
+    #[error("couldn't load json")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
@@ -560,10 +585,10 @@ pub enum LoadJsonError {
 pub enum StoreJsonError {
     #[error(transparent)]
     JsonSerialize(#[from] serde_json::Error),
-    #[error(transparent)]
+    #[error("couldn't store json")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
@@ -573,10 +598,10 @@ pub enum StoreJsonError {
 pub enum StoreTomlError {
     #[error(transparent)]
     TomlSerialize(#[from] toml_edit::ser::Error),
-    #[error(transparent)]
+    #[error("couldn't store toml")]
     IoError(
         #[from]
-        #[diagnostic(cause)]
+        #[source]
         std::io::Error,
     ),
 }
diff --git a/src/format.rs b/src/format.rs
index 86fec02..2ae2276 100644
--- a/src/format.rs
+++ b/src/format.rs
@@ -1,5 +1,6 @@
 //! Details of the file formats used by cargo vet
 
+use crate::serialization::spanned::Spanned;
 use crate::{flock::Filesystem, serialization};
 use core::{cmp, fmt};
 use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
@@ -149,17 +150,16 @@ pub struct CriteriaEntry {
     #[serde(skip_serializing_if = "Vec::is_empty")]
     #[serde(default)]
     #[serde(with = "serialization::string_or_vec")]
-    pub implies: Vec<CriteriaName>,
+    pub implies: Vec<Spanned<CriteriaName>>,
 }
 
 /// This is conceptually an enum
 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
+#[serde(try_from = "serialization::audit::AuditEntryAll")]
+#[serde(into = "serialization::audit::AuditEntryAll")]
 pub struct AuditEntry {
     pub who: Option<String>,
-    #[serde(default)]
-    #[serde(with = "serialization::string_or_vec")]
-    pub criteria: Vec<CriteriaName>,
-    #[serde(flatten)]
+    pub criteria: Vec<Spanned<CriteriaName>>,
     pub kind: AuditKind,
     pub notes: Option<String>,
 }
@@ -179,23 +179,14 @@ impl cmp::Ord for AuditEntry {
     }
 }
 
-#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)]
-#[serde(untagged)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
 pub enum AuditKind {
     Full {
         version: Version,
-        #[serde(rename = "dependency-criteria")]
-        #[serde(skip_serializing_if = "DependencyCriteria::is_empty")]
-        #[serde(with = "serialization::dependency_criteria")]
-        #[serde(default)]
         dependency_criteria: DependencyCriteria,
     },
     Delta {
         delta: Delta,
-        #[serde(rename = "dependency-criteria")]
-        #[serde(skip_serializing_if = "DependencyCriteria::is_empty")]
-        #[serde(with = "serialization::dependency_criteria")]
-        #[serde(default)]
         dependency_criteria: DependencyCriteria,
     },
     Violation {
@@ -211,7 +202,7 @@ pub enum AuditKind {
 /// ```toml
 /// dependency_criteria = { hmac: ['secure', 'crypto_reviewed'] }
 /// ```
-pub type DependencyCriteria = SortedMap<PackageName, Vec<CriteriaName>>;
+pub type DependencyCriteria = SortedMap<PackageName, Vec<Spanned<CriteriaName>>>;
 
 /// A "VERSION -> VERSION"
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -355,14 +346,14 @@ pub struct PolicyEntry {
     /// If not present, this defaults to the default criteria in the audits table.
     #[serde(default)]
     #[serde(with = "serialization::string_or_vec_or_none")]
-    pub criteria: Option<Vec<CriteriaName>>,
+    pub criteria: Option<Vec<Spanned<CriteriaName>>>,
 
     /// Same as `criteria`, but for first-party(?) crates/dependencies that are only
     /// used as dev-dependencies.
     #[serde(rename = "dev-criteria")]
     #[serde(default)]
     #[serde(with = "serialization::string_or_vec_or_none")]
-    pub dev_criteria: Option<Vec<CriteriaName>>,
+    pub dev_criteria: Option<Vec<Spanned<CriteriaName>>>,
 
     /// TODO: figure this out
     pub targets: Option<Vec<String>>,
@@ -404,7 +395,7 @@ pub struct CriteriaMapping {
     pub ours: CriteriaName,
     /// If all of these foreign criteria apply
     #[serde(with = "serialization::string_or_vec")]
-    pub theirs: Vec<ForeignCriteriaName>,
+    pub theirs: Vec<Spanned<ForeignCriteriaName>>,
 }
 
 /// Semantically identical to a 'full audit' entry, but private to our project
@@ -418,7 +409,7 @@ pub struct UnauditedDependency {
     /// pick a "good" initial value.
     #[serde(default)]
     #[serde(with = "serialization::string_or_vec")]
-    pub criteria: Vec<CriteriaName>,
+    pub criteria: Vec<Spanned<CriteriaName>>,
     /// Whether 'suggest' should bother mentioning this (defaults true).
     #[serde(default = "get_default_unaudited_suggest")]
     #[serde(skip_serializing_if = "is_default_unaudited_suggest")]
diff --git a/src/main.rs b/src/main.rs
index 2580bdd..ee5c1d0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -468,9 +468,9 @@ pub fn init_files(
                 continue;
             }
             let criteria = if package.is_dev_only {
-                vec![format::DEFAULT_POLICY_DEV_CRITERIA.to_string()]
+                vec![format::DEFAULT_POLICY_DEV_CRITERIA.to_string().into()]
             } else {
-                vec![format::DEFAULT_POLICY_CRITERIA.to_string()]
+                vec![format::DEFAULT_POLICY_CRITERIA.to_string().into()]
             };
             // NOTE: May have multiple copies of a package!
             let item = UnauditedDependency {
@@ -584,7 +584,7 @@ fn do_cmd_certify(
             dep_criteria
                 .entry(arg.dependency.clone())
                 .or_insert_with(Vec::new)
-                .push(arg.criteria.clone());
+                .push(arg.criteria.clone().into());
         }
         dep_criteria
     };
@@ -816,7 +816,10 @@ fn do_cmd_certify(
 
     let new_entry = AuditEntry {
         kind: kind.clone(),
-        criteria: criteria_names.iter().map(|s| s.to_string()).collect(),
+        criteria: criteria_names
+            .iter()
+            .map(|s| s.to_string().into())
+            .collect(),
         who,
         notes,
     };
@@ -925,9 +928,13 @@ fn cmd_record_violation(
 
     let criteria = if sub_args.criteria.is_empty() {
         // TODO: provide an interactive prompt for this
-        vec![store.config.default_criteria.clone()]
+        vec![store.config.default_criteria.clone().into()]
     } else {
-        sub_args.criteria.clone()
+        sub_args
+            .criteria
+            .iter()
+            .map(|s| s.to_owned().into())
+            .collect()
     };
 
     // FIXME: can/should we check if the version makes sense..?
@@ -981,7 +988,7 @@ fn cmd_add_unaudited(
             dep_criteria
                 .entry(arg.dependency.clone())
                 .or_insert_with(Vec::new)
-                .push(arg.criteria.clone());
+                .push(arg.criteria.clone().into());
         }
         dep_criteria
     };
@@ -990,9 +997,13 @@ fn cmd_add_unaudited(
 
     let criteria = if sub_args.criteria.is_empty() {
         // TODO: provide an interactive prompt for this
-        vec![store.config.default_criteria.clone()]
+        vec![store.config.default_criteria.clone().into()]
     } else {
-        sub_args.criteria.clone()
+        sub_args
+            .criteria
+            .iter()
+            .map(|s| s.to_owned().into())
+            .collect()
     };
 
     let suggest = !sub_args.no_suggest;
@@ -1161,7 +1172,7 @@ pub fn minimize_unaudited(
                     .or_insert(Vec::new())
                     .push(UnauditedDependency {
                         version: item.suggested_diff.to.clone(),
-                        criteria: criteria_names,
+                        criteria: criteria_names.iter().map(|s| s.to_owned().into()).collect(),
                         dependency_criteria: DependencyCriteria::new(),
                         notes: None,
                         suggest: true,
diff --git a/src/resolver.rs b/src/resolver.rs
index 4e04592..6119476 100644
--- a/src/resolver.rs
+++ b/src/resolver.rs
@@ -77,6 +77,7 @@ use crate::format::{
 use crate::format::{FastMap, FastSet, SortedMap, SortedSet};
 use crate::network::Network;
 use crate::out::Out;
+use crate::serialization::spanned::Spanned;
 use crate::{
     AuditEntry, Cache, Config, CriteriaEntry, DumpGraphArgs, GraphFilter, GraphFilterProperty,
     GraphFilterQuery, PackageExt, Store,
@@ -362,7 +363,7 @@ fn builtin_criteria() -> SortedMap<CriteriaName, CriteriaEntry> {
             CriteriaEntry {
                 description: Some("safe to deploy to production".to_string()),
                 description_url: None,
-                implies: vec!["safe-to-run".to_string()],
+                implies: vec!["safe-to-run".to_string().into()],
             },
         ),
     ]
@@ -395,7 +396,7 @@ impl CriteriaMapper {
 
             fn recursive_implies(
                 result: &mut CriteriaSet,
-                implies: &[CriteriaName],
+                implies: &[Spanned<CriteriaName>],
                 index: &FastMap<CriteriaName, usize>,
                 list: &[(CriteriaName, CriteriaEntry)],
             ) {
diff --git a/src/serialization.rs b/src/serialization.rs
index 5e97f7d..64c874e 100644
--- a/src/serialization.rs
+++ b/src/serialization.rs
@@ -6,13 +6,83 @@ use serde::{
     de::{self, value, SeqAccess, Visitor},
     Deserialize, Deserializer, Serialize, Serializer,
 };
+use spanned::Spanned;
 
-/// Serde handler to allow specifying any of [], "foo",
-/// ["foo"], or ["foo", "bar"].
+/// Serde handler to allow specifying any of [], "foo", ["foo"], or ["foo", "bar"],
+/// with the strings getting proper toml spans from the original source. Specifically,
+/// we deserialize `Vec<Spanned<String>>`.
+///
+/// This needs to solve three problems:
+///
+/// 1. Getting spans from toml deserialization, in general
+/// 2. Being able to take two very different inputs and squish them into one form
+/// 3. Wiring the span machinery into that process
+///
+/// Problem 1 is solved by toml-rs having a [Spanned] type for this exact thing.
+/// It's genuinely magic in that the deserializer just checks if it's deserializing
+/// a struct with its exact magic field names and then emits two extra values for the span.
+/// We fork the actual Spanned type here to make it more ergonomic/transparent
+///
+/// Problem 2 is solved by [string_or_vec_inner], which is based on an example in serde's
+/// own docs. We create a custom visitor that can be visited with either a sequence or
+/// a string. In the sequence case we just do a normal deserialization of our target.
+/// In the string case we create a new Vec to wrap our string.
+///
+/// Problem 3 is where things get messy. Our solution to Problem 2 naturally gets spanned
+/// for the sequence case, but in the string case we're "too late" to request spanning,
+/// as we no longer have a deserializer, just a `str`. All we can do is emit a dummy Spanned.
+///
+/// Our solution to this is to recognize that in this case we have an array of one element,
+/// and so a span over the entire array is just as good as a span over the element.
+/// So we deserialize a `Spanned<Vec<Spanned<String>>>` and if the Vec has `len == 1`,
+/// we edit the dummy inner span to equal the outer span. We then just discard the outer span
+/// (or we could keep it if we decide that's useful later).
+///
+/// To make this a bit easier to do, we wrap Problem 2's solution in a `StringOrVec` struct,
+/// deserialize `Spanned<StringOrVec>`, and then transform that into `Vec<Spanned<String>>`
 pub mod string_or_vec {
     use super::*;
 
-    pub fn serialize<S>(v: &Vec<String>, s: S) -> Result<S::Ok, S::Error>
+    /// A type using string_or_vec_inner to make it easy to wrap in Spanned.
+    #[derive(Serialize, Deserialize)]
+    pub struct StringOrVec(
+        #[serde(default)]
+        #[serde(with = "string_or_vec_inner")]
+        pub Vec<Spanned<String>>,
+    );
+
+    pub fn serialize<S>(v: &Vec<Spanned<String>>, s: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        if v.len() == 1 {
+            s.serialize_str(&v[0])
+        } else {
+            v.serialize(s)
+        }
+    }
+
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Spanned<String>>, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        // Get a Spanned<StringOrVec> and then use it to fixup a dummy span for len == 1
+        let spanned_vec = Spanned::<StringOrVec>::deserialize(deserializer)?;
+        let start = Spanned::start(&spanned_vec);
+        let end = Spanned::end(&spanned_vec);
+        let mut vec = Spanned::into_inner(spanned_vec).0;
+        if vec.len() == 1 {
+            Spanned::update_span(&mut vec[0], start, end);
+        }
+        Ok(vec)
+    }
+}
+
+/// See [string_or_vec]
+pub mod string_or_vec_inner {
+    use super::*;
+
+    pub fn serialize<S>(v: &Vec<Spanned<String>>, s: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
@@ -23,14 +93,14 @@ pub mod string_or_vec {
         }
     }
 
-    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Spanned<String>>, D::Error>
     where
         D: Deserializer<'de>,
     {
         struct StringOrVec;
 
         impl<'de> Visitor<'de> for StringOrVec {
-            type Value = Vec<String>;
+            type Value = Vec<Spanned<String>>;
 
             fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                 formatter.write_str("string or list of strings")
@@ -40,7 +110,7 @@ pub mod string_or_vec {
             where
                 E: de::Error,
             {
-                Ok(vec![s.to_owned()])
+                Ok(vec![Spanned::from(s.to_owned())])
             }
 
             fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
@@ -61,7 +131,7 @@ pub mod string_or_vec {
 pub mod string_or_vec_or_none {
     use super::*;
 
-    pub fn serialize<S>(maybe_v: &Option<Vec<String>>, s: S) -> Result<S::Ok, S::Error>
+    pub fn serialize<S>(maybe_v: &Option<Vec<Spanned<String>>>, s: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
@@ -72,7 +142,7 @@ pub mod string_or_vec_or_none {
         }
     }
 
-    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<Spanned<String>>>, D::Error>
     where
         D: Deserializer<'de>,
     {
@@ -88,7 +158,7 @@ pub mod dependency_criteria {
 
     use super::*;
     #[derive(Serialize, Deserialize)]
-    struct Wrapper(#[serde(with = "string_or_vec")] Vec<CriteriaName>);
+    struct Wrapper(#[serde(with = "string_or_vec")] Vec<Spanned<CriteriaName>>);
 
     pub fn serialize<S>(c: &DependencyCriteria, s: S) -> Result<S::Ok, S::Error>
     where
@@ -105,8 +175,94 @@ pub mod dependency_criteria {
     where
         D: Deserializer<'de>,
     {
-        let m: SortedMap<PackageName, Wrapper> = SortedMap::deserialize(deserializer)?;
-        Ok(m.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())
+        let m = SortedMap::<PackageName, Wrapper>::deserialize(deserializer)?;
+        Ok(m.into_iter().map(|(k, v)| (k, v.0)).collect())
+    }
+}
+
+pub mod audit {
+    use super::*;
+
+    use crate::format::{
+        AuditEntry, AuditKind, CriteriaName, Delta, DependencyCriteria, VersionReq,
+    };
+    use cargo_metadata::Version;
+
+    #[derive(Serialize, Deserialize)]
+    pub struct AuditEntryAll {
+        who: Option<String>,
+        #[serde(default)]
+        #[serde(with = "string_or_vec")]
+        criteria: Vec<Spanned<CriteriaName>>,
+        version: Option<Version>,
+        delta: Option<Delta>,
+        violation: Option<VersionReq>,
+        #[serde(rename = "dependency-criteria")]
+        #[serde(skip_serializing_if = "DependencyCriteria::is_empty")]
+        #[serde(with = "dependency_criteria")]
+        #[serde(default)]
+        dependency_criteria: DependencyCriteria,
+        notes: Option<String>,
+    }
+
+    impl TryFrom<AuditEntryAll> for AuditEntry {
+        type Error = String;
+        fn try_from(val: AuditEntryAll) -> Result<AuditEntry, Self::Error> {
+            let kind = match (val.version, val.delta, val.violation) {
+                (Some(version), None, None) => Ok(AuditKind::Full {
+                    version,
+                    dependency_criteria: val.dependency_criteria,
+                }),
+                (None, Some(delta), None) => Ok(AuditKind::Delta {
+                    delta,
+                    dependency_criteria: val.dependency_criteria,
+                }),
+                (None, None, Some(violation)) => {
+                    if val.dependency_criteria.is_empty() {
+                        Ok(AuditKind::Violation { violation })
+                    } else {
+                        Err("'violation' can't have dependency_criteria".to_string())
+                    }
+                }
+                _ => Err(
+                    "audit entires must have exactly one of 'version', 'delta', and 'violation'"
+                        .to_string(),
+                ),
+            };
+            Ok(AuditEntry {
+                who: val.who,
+                notes: val.notes,
+                criteria: val.criteria,
+                kind: kind?,
+            })
+        }
+    }
+
+    impl From<AuditEntry> for AuditEntryAll {
+        fn from(val: AuditEntry) -> AuditEntryAll {
+            let (version, delta, violation, dependency_criteria) = match val.kind {
+                AuditKind::Full {
+                    version,
+                    dependency_criteria,
+                } => (Some(version), None, None, dependency_criteria),
+                AuditKind::Delta {
+                    delta,
+                    dependency_criteria,
+                } => (None, Some(delta), None, dependency_criteria),
+                AuditKind::Violation { violation } => {
+                    (None, None, Some(violation), DependencyCriteria::new())
+                }
+            };
+            AuditEntryAll {
+                who: val.who,
+                notes: val.notes,
+                criteria: val.criteria,
+                version,
+                delta,
+                violation,
+                dependency_criteria,
+            }
+        }
     }
 }
 
@@ -201,6 +357,202 @@ where
     Ok(toml_document)
 }
 
+pub mod spanned {
+    use std::{
+        borrow::Borrow,
+        cmp::Ordering,
+        fmt::{self, Display},
+        hash::{Hash, Hasher},
+        ops::{Deref, DerefMut},
+    };
+
+    use miette::SourceSpan;
+    use serde::{ser, Deserialize};
+
+    /// A spanned value, indicating the range at which it is defined in the source.
+    #[derive(Clone, Default, Deserialize)]
+    #[serde(from = "toml::Spanned<T>")]
+    pub struct Spanned<T> {
+        start: usize,
+        end: usize,
+        value: T,
+    }
+
+    impl<T> Spanned<T> {
+        /// Access the start of the span of the contained value.
+        pub fn start(this: &Self) -> usize {
+            this.start
+        }
+
+        /// Access the end of the span of the contained value.
+        pub fn end(this: &Self) -> usize {
+            this.end
+        }
+
+        /// Update the span
+        pub fn update_span(this: &mut Self, start: usize, end: usize) {
+            this.start = start;
+            this.end = end;
+        }
+
+        /// Get the span of the contained value.
+        pub fn span(this: &Self) -> SourceSpan {
+            (Self::start(this)..Self::end(this)).into()
+        }
+
+        /// Consumes the spanned value and returns the contained value.
+        pub fn into_inner(this: Self) -> T {
+            this.value
+        }
+    }
+
+    impl<T> IntoIterator for Spanned<T>
+    where
+        T: IntoIterator,
+    {
+        type IntoIter = T::IntoIter;
+        type Item = T::Item;
+        fn into_iter(self) -> Self::IntoIter {
+            self.value.into_iter()
+        }
+    }
+
+    impl<'a, T> IntoIterator for &'a Spanned<T>
+    where
+        &'a T: IntoIterator,
+    {
+        type IntoIter = <&'a T as IntoIterator>::IntoIter;
+        type Item = <&'a T as IntoIterator>::Item;
+        fn into_iter(self) -> Self::IntoIter {
+            self.value.into_iter()
+        }
+    }
+
+    impl<'a, T> IntoIterator for &'a mut Spanned<T>
+    where
+        &'a mut T: IntoIterator,
+    {
+        type IntoIter = <&'a mut T as IntoIterator>::IntoIter;
+        type Item = <&'a mut T as IntoIterator>::Item;
+        fn into_iter(self) -> Self::IntoIter {
+            self.value.into_iter()
+        }
+    }
+
+    impl<T> fmt::Debug for Spanned<T>
+    where
+        T: fmt::Debug,
+    {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            self.value.fmt(f)
+        }
+    }
+
+    impl<T> Display for Spanned<T>
+    where
+        T: Display,
+    {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            self.value.fmt(f)
+        }
+    }
+
+    impl<T> Deref for Spanned<T> {
+        type Target = T;
+        fn deref(&self) -> &Self::Target {
+            &self.value
+        }
+    }
+
+    impl<T> DerefMut for Spanned<T> {
+        fn deref_mut(&mut self) -> &mut Self::Target {
+            &mut self.value
+        }
+    }
+
+    impl Borrow<str> for Spanned<String> {
+        fn borrow(&self) -> &str {
+            self
+        }
+    }
+
+    impl<T, U: ?Sized> AsRef<U> for Spanned<T>
+    where
+        T: AsRef<U>,
+    {
+        fn as_ref(&self) -> &U {
+            self.value.as_ref()
+        }
+    }
+
+    impl<T: PartialEq> PartialEq for Spanned<T> {
+        fn eq(&self, other: &Self) -> bool {
+            self.value.eq(&other.value)
+        }
+    }
+
+    impl<T: PartialEq<T>> PartialEq<T> for Spanned<T> {
+        fn eq(&self, other: &T) -> bool {
+            self.value.eq(other)
+        }
+    }
+
+    impl<T: Eq> Eq for Spanned<T> {}
+
+    impl<T: Hash> Hash for Spanned<T> {
+        fn hash<H: Hasher>(&self, state: &mut H) {
+            self.value.hash(state);
+        }
+    }
+
+    impl<T: PartialOrd> PartialOrd for Spanned<T> {
+        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+            self.value.partial_cmp(&other.value)
+        }
+    }
+
+    impl<T: PartialOrd<T>> PartialOrd<T> for Spanned<T> {
+        fn partial_cmp(&self, other: &T) -> Option<Ordering> {
+            self.value.partial_cmp(other)
+        }
+    }
+
+    impl<T: Ord> Ord for Spanned<T> {
+        fn cmp(&self, other: &Self) -> Ordering {
+            self.value.cmp(&other.value)
+        }
+    }
+
+    impl<T> From<T> for Spanned<T> {
+        fn from(value: T) -> Self {
+            Self {
+                start: 0,
+                end: 0,
+                value,
+            }
+        }
+    }
+
+    impl<T> From<toml::Spanned<T>> for Spanned<T> {
+        fn from(value: toml::Spanned<T>) -> Self {
+            Self {
+                start: value.start(),
+                end: value.end(),
+                value: value.into_inner(),
+            }
+        }
+    }
+
+    impl<T: ser::Serialize> ser::Serialize for Spanned<T> {
+        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: ser::Serializer,
+        {
+            self.value.serialize(serializer)
+        }
+    }
+}
+
 #[cfg(test)]
 mod test {
     use crate::format::*;
@@ -211,8 +563,8 @@ mod test {
         dc_long.insert(
             "example-crate-1".to_owned(),
             vec![
-                "criteria-one-very-long".to_owned(),
-                "criteria-two-very-long".to_owned(),
+                "criteria-one-very-long".to_owned().into(),
+                "criteria-two-very-long".to_owned().into(),
             ],
         );
         dc_long.insert(
@@ -220,23 +572,25 @@ mod test {
             vec![
                 // This array would wrap over multiple lines if byte length was
                 // used rather than utf-8 character length.
-                "criteria-one-✨✨✨✨✨✨✨✨✨✨".to_owned(),
-                "criteria-two-✨✨✨✨✨✨✨✨✨✨".to_owned(),
+                "criteria-one-✨✨✨✨✨✨✨✨✨✨".to_owned().into(),
+                "criteria-two-✨✨✨✨✨✨✨✨✨✨".to_owned().into(),
             ],
         );
         dc_long.insert(
             "example-crate-3".to_owned(),
             vec![
-                "criteria-one-very-long".to_owned(),
-                "criteria-two-very-long".to_owned(),
-                "criteria-three-extremely-long-this-array-should-wrap".to_owned(),
+                "criteria-one-very-long".to_owned().into(),
+                "criteria-two-very-long".to_owned().into(),
+                "criteria-three-extremely-long-this-array-should-wrap"
+                    .to_owned()
+                    .into(),
             ],
         );
 
         let mut dc_short = SortedMap::new();
         dc_short.insert(
             "example-crate-1".to_owned(),
-            vec!["criteria-one".to_owned()],
+            vec!["criteria-one".to_owned().into()],
         );
 
         let mut audits = SortedMap::new();
@@ -245,7 +599,7 @@ mod test {
             vec![
                 AuditEntry {
                     who: None,
-                    criteria: vec!["long-criteria".to_owned()],
+                    criteria: vec!["long-criteria".to_owned().into()],
                     kind: AuditKind::Full {
                         version: "1.0.0".parse().unwrap(),
                         dependency_criteria: dc_long,
@@ -254,7 +608,7 @@ mod test {
                 },
                 AuditEntry {
                     who: None,
-                    criteria: vec!["short-criteria".to_owned()],
+                    criteria: vec!["short-criteria".to_owned().into()],
                     kind: AuditKind::Full {
                         version: "1.0.0".parse().unwrap(),
                         dependency_criteria: dc_short,
diff --git a/src/storage.rs b/src/storage.rs
index deae943..1464671 100644
--- a/src/storage.rs
+++ b/src/storage.rs
@@ -12,6 +12,7 @@ use cargo_metadata::Version;
 use crates_index::Index;
 use flate2::read::GzDecoder;
 use futures_util::future::try_join_all;
+use miette::{NamedSource, SourceOffset};
 use reqwest::Url;
 use serde::{Deserialize, Serialize};
 use tar::Archive;
@@ -20,18 +21,19 @@ use tracing::{error, info, log::warn, trace};
 use crate::{
     errors::{
         CacheAcquireError, CacheCommitError, CommandError, DiffError, FetchAndDiffError,
-        FetchAuditError, FetchError, FlockError, JsonParseError, LoadJsonError, LoadTomlError,
-        StoreAcquireError, StoreCommitError, StoreCreateError, StoreJsonError, StoreTomlError,
-        StoreValidateErrors, TomlParseError, UnpackError,
+        FetchAuditError, FetchError, FlockError, InvalidCriteriaError, JsonParseError,
+        LoadJsonError, LoadTomlError, SourceFile, StoreAcquireError, StoreCommitError,
+        StoreCreateError, StoreJsonError, StoreTomlError, StoreValidateError, StoreValidateErrors,
+        TomlParseError, UnpackError,
     },
     flock::{FileLock, Filesystem},
     format::{
-        AuditsFile, CommandHistory, ConfigFile, Delta, DiffCache, DiffStat, FastMap, FetchCommand,
-        ImportsFile, MetaConfig, PackageStr, SortedMap,
+        AuditsFile, CommandHistory, ConfigFile, CriteriaName, Delta, DiffCache, DiffStat, FastMap,
+        FetchCommand, ImportsFile, MetaConfig, PackageStr, SortedMap, SAFE_TO_DEPLOY, SAFE_TO_RUN,
     },
     network::Network,
     resolver,
-    serialization::to_formatted_toml,
+    serialization::{spanned::Spanned, to_formatted_toml},
     Config, PartialConfig,
 };
 
@@ -120,6 +122,10 @@ pub struct Store {
     pub config: ConfigFile,
     pub imports: ImportsFile,
     pub audits: AuditsFile,
+
+    pub config_src: SourceFile,
+    pub imports_src: SourceFile,
+    pub audits_src: SourceFile,
 }
 
 impl Store {
@@ -145,6 +151,9 @@ impl Store {
                 criteria: SortedMap::new(),
                 audits: SortedMap::new(),
             },
+            config_src: Arc::new(NamedSource::new(CONFIG_TOML, "")),
+            audits_src: Arc::new(NamedSource::new(AUDITS_TOML, "")),
+            imports_src: Arc::new(NamedSource::new(IMPORTS_LOCK, "")),
         })
     }
 
@@ -163,15 +172,19 @@ impl Store {
         // exclusive one isn't needed.
         let lock = StoreLock::new(&root)?;
 
-        let config: ConfigFile = load_toml(lock.read_config()?)?;
-        let audits: AuditsFile = load_toml(lock.read_audits()?)?;
-        let imports: ImportsFile = load_toml(lock.read_imports()?)?;
+        let (config_src, config): (_, ConfigFile) = load_toml(CONFIG_TOML, lock.read_config()?)?;
+        let (audits_src, audits): (_, AuditsFile) = load_toml(AUDITS_TOML, lock.read_audits()?)?;
+        let (imports_src, imports): (_, ImportsFile) =
+            load_toml(IMPORTS_LOCK, lock.read_imports()?)?;
 
         let store = Self {
             lock: Some(lock),
             config,
             audits,
             imports,
+            config_src,
+            audits_src,
+            imports_src,
         };
 
         // Check that the store isn't corrupt
@@ -188,9 +201,37 @@ impl Store {
             config,
             imports,
             audits,
+            config_src: Arc::new(NamedSource::new(CONFIG_TOML, "")),
+            audits_src: Arc::new(NamedSource::new(AUDITS_TOML, "")),
+            imports_src: Arc::new(NamedSource::new(IMPORTS_LOCK, "")),
         }
     }
 
+    #[cfg(test)]
+    pub fn mock_acquire(
+        config: &str,
+        audits: &str,
+        imports: &str,
+    ) -> Result<Self, StoreAcquireError> {
+        let (config_src, config): (_, ConfigFile) = load_toml(CONFIG_TOML, config.as_bytes())?;
+        let (audits_src, audits): (_, AuditsFile) = load_toml(AUDITS_TOML, audits.as_bytes())?;
+        let (imports_src, imports): (_, ImportsFile) = load_toml(IMPORTS_LOCK, imports.as_bytes())?;
+
+        let store = Self {
+            lock: None,
+            config,
+            imports,
+            audits,
+            config_src,
+            audits_src,
+            imports_src,
+        };
+
+        store.validate()?;
+
+        Ok(store)
+    }
+
     /// Create a clone of the store for use to resolve `suggest`.
     ///
     /// This cloned store will not contain `unaudited` entries from the config,
@@ -206,6 +247,9 @@ impl Store {
             config: self.config.clone(),
             imports: self.imports.clone(),
             audits: self.audits.clone(),
+            config_src: self.config_src.clone(),
+            audits_src: self.audits_src.clone(),
+            imports_src: self.imports_src.clone(),
         };
         // Delete all unaudited entries except those that are suggest=false
         for versions in &mut clone.config.unaudited.values_mut() {
@@ -230,26 +274,155 @@ impl Store {
     }
 
     /// Validate the store's integrity
+    #[allow(clippy::for_kv_map)]
     pub fn validate(&self) -> Result<(), StoreValidateErrors> {
         // ERRORS: ideally these are all gathered diagnostics, want to report as many errors
         // at once as possible!
 
         // TODO(#66): implement validation
         //
-        // * check that policy entries are only first-party
+        // * check that policy entries are only first-party?
+        //   * (we currently allow policy.criteria on third-parties for audit-as-crates-io)
         // * check that unaudited entries are for things that exist?
         // * check that lockfile and imports aren't desync'd (catch new/removed import urls)
         //
         // * check that each CriteriaEntry has 'description' or 'description_url'
         // * check that no one is trying to shadow builtin criteria (safe-to-run, safe-to-deploy)
-        // * check that all criteria are valid in:
-        //   * CriteriaEntry::implies
-        //   * AuditEntry::criteria
-        //   * DependencyCriteria
         // * check that all 'audits' entries are well-formed
         // * check that all package names are valid (with crates.io...?)
         // * check that all reviews have a 'who' (currently an Option to stub it out)
         // * catch no-op deltas?
+        // * nested check imports, complicated because of namespaces
+
+        fn check_criteria(
+            source_code: &SourceFile,
+            valid: &Arc<Vec<CriteriaName>>,
+            errors: &mut Vec<InvalidCriteriaError>,
+            criteria: &[Spanned<CriteriaName>],
+        ) {
+            for criteria in criteria {
+                if !valid.contains(criteria) {
+                    errors.push(InvalidCriteriaError {
+                        source_code: source_code.clone(),
+                        span: Spanned::span(criteria),
+                        invalid: criteria.to_string(),
+                        valid_names: valid.clone(),
+                    })
+                }
+            }
+        }
+
+        // Fixme: this should probably be a Map...? Sorted? Stable?
+        let valid_criteria = Arc::new(
+            self.audits
+                .criteria
+                .iter()
+                .map(|(c, _)| &**c)
+                .chain([SAFE_TO_RUN, SAFE_TO_DEPLOY])
+                .map(|name| name.to_string())
+                .collect::<Vec<_>>(),
+        );
+        let no_criteria = vec![];
+        let mut invalid_criteria_errors = vec![];
+
+        for (_package, entries) in &self.config.unaudited {
+            for entry in entries {
+                check_criteria(
+                    &self.config_src,
+                    &valid_criteria,
+                    &mut invalid_criteria_errors,
+                    &entry.criteria,
+                );
+                for (_dep_package, dep_criteria) in &entry.dependency_criteria {
+                    check_criteria(
+                        &self.config_src,
+                        &valid_criteria,
+                        &mut invalid_criteria_errors,
+                        dep_criteria,
+                    );
+                }
+            }
+        }
+        for (_package, policy) in &self.config.policy {
+            check_criteria(
+                &self.config_src,
+                &valid_criteria,
+                &mut invalid_criteria_errors,
+                policy.criteria.as_ref().unwrap_or(&no_criteria),
+            );
+            check_criteria(
+                &self.config_src,
+                &valid_criteria,
+                &mut invalid_criteria_errors,
+                policy.dev_criteria.as_ref().unwrap_or(&no_criteria),
+            );
+            for (_dep_package, dep_criteria) in &policy.dependency_criteria {
+                check_criteria(
+                    &self.config_src,
+                    &valid_criteria,
+                    &mut invalid_criteria_errors,
+                    dep_criteria,
+                );
+            }
+        }
+        for (_new_criteria, entry) in &self.audits.criteria {
+            // TODO: check that new_criteria isn't shadowing a builtin criteria
+            check_criteria(
+                &self.audits_src,
+                &valid_criteria,
+                &mut invalid_criteria_errors,
+                &entry.implies,
+            );
+        }
+        for (_package, entries) in &self.audits.audits {
+            for entry in entries {
+                // TODO: check that new_criteria isn't shadowing a builtin criteria
+                check_criteria(
+                    &self.audits_src,
+                    &valid_criteria,
+                    &mut invalid_criteria_errors,
+                    &entry.criteria,
+                );
+                match &entry.kind {
+                    crate::format::AuditKind::Full {
+                        dependency_criteria,
+                        ..
+                    } => {
+                        for (_dep_package, dep_criteria) in dependency_criteria {
+                            check_criteria(
+                                &self.audits_src,
+                                &valid_criteria,
+                                &mut invalid_criteria_errors,
+                                dep_criteria,
+                            );
+                        }
+                    }
+                    crate::format::AuditKind::Delta {
+                        dependency_criteria,
+                        ..
+                    } => {
+                        for (_dep_package, dep_criteria) in dependency_criteria {
+                            check_criteria(
+                                &self.audits_src,
+                                &valid_criteria,
+                                &mut invalid_criteria_errors,
+                                dep_criteria,
+                            );
+                        }
+                    }
+                    crate::format::AuditKind::Violation { .. } => {}
+                }
+            }
+        }
+
+        let errors = invalid_criteria_errors
+            .into_iter()
+            .map(StoreValidateError::InvalidCriteria)
+            .collect::<Vec<_>>();
+        if !errors.is_empty() {
+            return Err(StoreValidateErrors { errors });
+        }
+
         Ok(())
     }
 
@@ -287,8 +460,18 @@ async fn fetch_foreign_audit(
         error,
     })?;
     let audit_bytes = network.download(parsed_url).await?;
-    let audit_file: AuditsFile =
-        toml_edit::de::from_slice(&audit_bytes).map_err(|error| TomlParseError { error })?;
+    let audit_string = String::from_utf8(audit_bytes).map_err(LoadTomlError::from)?;
+    let audit_source = Arc::new(NamedSource::new(name, audit_string.clone()));
+    let audit_file: AuditsFile = toml::de::from_str(&audit_string)
+        .map_err(|error| {
+            let (line, col) = error.line_col().unwrap_or((0, 0));
+            TomlParseError {
+                source_code: audit_source,
+                span: SourceOffset::from_location(&audit_string, line + 1, col + 1),
+                error,
+            }
+        })
+        .map_err(LoadTomlError::from)?;
     Ok(audit_file)
 }
 
@@ -435,7 +618,7 @@ impl Cache {
             .unwrap_or_else(|| root.join(CACHE_DIFF_CACHE));
         let diff_cache: DiffCache = File::open(&diff_cache_path)
             .ok()
-            .and_then(|f| load_toml(f).ok())
+            .and_then(|f| load_toml(CACHE_DIFF_CACHE, f).map(|v| v.1).ok())
             .unwrap_or_default();
 
         // Setup the command_history.
@@ -1032,15 +1215,28 @@ fn find_cargo_registry() -> Result<CargoRegistry, crates_index::Error> {
     })
 }
 
-fn load_toml<T>(reader: impl Read) -> Result<T, LoadTomlError>
+fn load_toml<T>(file_name: &str, reader: impl Read) -> Result<(SourceFile, T), LoadTomlError>
 where
     T: for<'a> Deserialize<'a>,
 {
     let mut reader = BufReader::new(reader);
     let mut string = String::new();
     reader.read_to_string(&mut string)?;
-    let toml = toml_edit::de::from_str(&string).map_err(|error| TomlParseError { error })?;
-    Ok(toml)
+    let result = toml::de::from_str(&string);
+    // We defer the NamedSource creation into each branch because the error path wants to
+    // have access to the input string, but NamedSource consumes and erases it.
+    match result {
+        Ok(toml) => Ok((Arc::new(NamedSource::new(file_name, string)), toml)),
+        Err(error) => {
+            let (line, col) = error.line_col().unwrap_or((0, 0));
+            let span = SourceOffset::from_location(&string, line + 1, col);
+            Err(TomlParseError {
+                source_code: Arc::new(NamedSource::new(file_name, string)),
+                span,
+                error,
+            })?
+        }
+    }
 }
 fn store_toml<T>(mut writer: impl Write, heading: &str, val: T) -> Result<(), StoreTomlError>
 where


Test patch:
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index 8b820ab..63830c8 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -26,6 +26,7 @@ use crate::{
 mod audit_as_crates_io;
 mod certify;
 mod regenerate_unaudited;
+mod store_parsing;
 mod vet;
 mod violations;
 
@@ -239,7 +240,7 @@ fn dep_ver(name: &'static str, version: u64) -> MockDependency {
 fn default_unaudited(version: Version, config: &ConfigFile) -> UnauditedDependency {
     UnauditedDependency {
         version,
-        criteria: vec![config.default_criteria.clone()],
+        criteria: vec![config.default_criteria.clone().into()],
         dependency_criteria: DependencyCriteria::new(),
         notes: None,
         suggest: true,
@@ -248,7 +249,7 @@ fn default_unaudited(version: Version, config: &ConfigFile) -> UnauditedDependen
 fn unaudited(version: Version, criteria: CriteriaStr) -> UnauditedDependency {
     UnauditedDependency {
         version,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         dependency_criteria: DependencyCriteria::new(),
         notes: None,
         suggest: true,
@@ -267,7 +268,7 @@ fn unaudited_dep(
 ) -> UnauditedDependency {
     UnauditedDependency {
         version,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         notes: None,
         suggest: true,
         dependency_criteria: dependency_criteria
@@ -275,7 +276,7 @@ fn unaudited_dep(
             .map(|(k, v)| {
                 (
                     k.into(),
-                    v.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
+                    v.into_iter().map(|s| s.into().into()).collect::<Vec<_>>(),
                 )
             })
             .collect(),
@@ -287,7 +288,7 @@ fn delta_audit(from: Version, to: Version, criteria: CriteriaStr) -> AuditEntry
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Delta {
             delta,
             dependency_criteria: DependencyCriteria::default(),
@@ -311,7 +312,7 @@ fn delta_audit_dep(
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Delta {
             delta,
             dependency_criteria: dependency_criteria
@@ -319,7 +320,7 @@ fn delta_audit_dep(
                 .map(|(k, v)| {
                     (
                         k.into(),
-                        v.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
+                        v.into_iter().map(|s| s.into().into()).collect::<Vec<_>>(),
                     )
                 })
                 .collect(),
@@ -331,7 +332,7 @@ fn full_audit(version: Version, criteria: CriteriaStr) -> AuditEntry {
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Full {
             version,
             dependency_criteria: DependencyCriteria::default(),
@@ -346,7 +347,7 @@ fn full_audit_m(
     AuditEntry {
         who: None,
         notes: None,
-        criteria: criteria.into_iter().map(|s| s.into()).collect(),
+        criteria: criteria.into_iter().map(|s| s.into().into()).collect(),
         kind: AuditKind::Full {
             version,
             dependency_criteria: DependencyCriteria::default(),
@@ -367,7 +368,7 @@ fn full_audit_dep(
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Full {
             version,
             dependency_criteria: dependency_criteria
@@ -375,7 +376,7 @@ fn full_audit_dep(
                 .map(|(k, v)| {
                     (
                         k.into(),
-                        v.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
+                        v.into_iter().map(|s| s.into().into()).collect::<Vec<_>>(),
                     )
                 })
                 .collect(),
@@ -387,7 +388,7 @@ fn violation_hard(version: VersionReq) -> AuditEntry {
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![SAFE_TO_RUN.to_string()],
+        criteria: vec![SAFE_TO_RUN.to_string().into()],
         kind: AuditKind::Violation { violation: version },
     }
 }
@@ -396,7 +397,7 @@ fn violation(version: VersionReq, criteria: CriteriaStr) -> AuditEntry {
     AuditEntry {
         who: None,
         notes: None,
-        criteria: vec![criteria.to_string()],
+        criteria: vec![criteria.to_string().into()],
         kind: AuditKind::Violation { violation: version },
     }
 }
@@ -408,7 +409,7 @@ fn violation_m(
     AuditEntry {
         who: None,
         notes: None,
-        criteria: criteria.into_iter().map(|s| s.into()).collect(),
+        criteria: criteria.into_iter().map(|s| s.into().into()).collect(),
         kind: AuditKind::Violation { violation: version },
     }
 }
@@ -434,7 +435,7 @@ fn audit_as_policy(audit_as_crates_io: Option<bool>) -> PolicyEntry {
 
 fn self_policy(criteria: impl IntoIterator<Item = impl Into<CriteriaName>>) -> PolicyEntry {
     PolicyEntry {
-        criteria: Some(criteria.into_iter().map(|s| s.into()).collect()),
+        criteria: Some(criteria.into_iter().map(|s| s.into().into()).collect()),
         ..default_policy()
     }
 }
@@ -453,7 +454,7 @@ fn dep_policy(
             .map(|(k, v)| {
                 (
                     k.into(),
-                    v.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
+                    v.into_iter().map(|s| s.into().into()).collect::<Vec<_>>(),
                 )
             })
             .collect(),
@@ -889,7 +890,7 @@ fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
         (
             "strong-reviewed".to_string(),
             CriteriaEntry {
-                implies: vec!["reviewed".to_string()],
+                implies: vec!["reviewed".to_string().into()],
                 description: Some("strongly reviewed".to_string()),
                 description_url: None,
             },
@@ -897,7 +898,7 @@ fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
         (
             "reviewed".to_string(),
             CriteriaEntry {
-                implies: vec!["weak-reviewed".to_string()],
+                implies: vec!["weak-reviewed".to_string().into()],
                 description: Some("reviewed".to_string()),
                 description_url: None,
             },
@@ -928,8 +929,8 @@ fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
                     package.name.clone(),
                     PolicyEntry {
                         audit_as_crates_io: None,
-                        criteria: Some(vec![DEFAULT_CRIT.to_string()]),
-                        dev_criteria: Some(vec![DEFAULT_CRIT.to_string()]),
+                        criteria: Some(vec![DEFAULT_CRIT.to_string().into()]),
+                        dev_criteria: Some(vec![DEFAULT_CRIT.to_string().into()]),
                         dependency_criteria: DependencyCriteria::new(),
                         targets: None,
                         dev_targets: None,
@@ -944,8 +945,8 @@ fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
     // Rewrite the default used by init
     for unaudited in &mut config.unaudited {
         for entry in unaudited.1 {
-            assert_eq!(&*entry.criteria, &["safe-to-deploy"]);
-            entry.criteria = vec![DEFAULT_CRIT.to_string()];
+            assert_eq!(&*entry.criteria, &["safe-to-deploy".to_string()]);
+            entry.criteria = vec![DEFAULT_CRIT.to_string().into()];
         }
     }
 
@@ -1015,7 +1016,10 @@ fn builtin_files_minimal_audited(metadata: &Metadata) -> (ConfigFile, AuditsFile
             audited
                 .entry(name.clone())
                 .or_insert(vec![])
-                .push(full_audit_m(entry.version, &entry.criteria));
+                .push(full_audit_m(
+                    entry.version,
+                    entry.criteria.iter().map(|s| &**s).collect::<Vec<_>>(),
+                ));
         }
     }
     audits.audits = audited;
diff --git a/src/tests/regenerate_unaudited.rs b/src/tests/regenerate_unaudited.rs
index 0fa1f5e..d1c3c60 100644
--- a/src/tests/regenerate_unaudited.rs
+++ b/src/tests/regenerate_unaudited.rs
@@ -474,7 +474,7 @@ fn builtin_simple_audit_as_weaker_root_regenerate() {
     config.policy.insert(
         "root-package".to_string(),
         PolicyEntry {
-            criteria: Some(vec![SAFE_TO_RUN.to_string()]),
+            criteria: Some(vec![SAFE_TO_RUN.to_string().into()]),
             ..audit_as_policy(Some(true))
         },
     );
diff --git a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong-json.snap b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong-json.snap
index a100fcf..dcfcb24 100644
--- a/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong-json.snap
+++ b/src/tests/snapshots/cargo_vet__tests__audit_as_crates_io__complex-audit-as-crates-io-max-wrong-json.snap
@@ -2,4 +2,4 @@
 source: src/tests/audit_as_crates_io.rs
 expression: output
 ---
-{"message": "There are some issues with your policy.audit-as-crates-io entries","severity": "error","labels": [],"related": [{"message": "Some first-party packages match published crates.io versions\n  firstA:10.0.0\n  firstAB:10.0.0\n  firstB:10.0.0\n  firstB-nodeps:10.0.0","severity": "error","help": "Add a `policy.*.audit-as-crates-io` entry for them","labels": [],"related": []},{"message": "some audit-as-crates-io packages don't match published crates.io versions\n  rootA:10.0.0\n  rootB:10.0.0","severity": "error","help": "Remove the audit-as-crates-io entries or make them `false`","labels": [],"related": []}]}
+{"message": "There are some issues with your policy.audit-as-crates-io entries","severity": "error","labels": [],"related": [{"message": "Some non-crates.io-fetched packages match published crates.io versions\n  firstA:10.0.0\n  firstAB:10.0.0\n  firstB:10.0.0\n  firstB-nodeps:10.0.0","severity": "error","help": "Add a `policy.*.audit-as-crates-io` entry for them","labels": [],"related": []},{"message": "some audit-as-crates-io packages don't match published crates.io versions\n  rootA:10.0.0\n  rootB:10.0.0","severity": "error","help": "Remove the audit-as-crates-io entries or make them `false`","labels": [],"related": []}]}
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__all_empty.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__all_empty.snap
new file mode 100644
index 0000000..6f0b2b0
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__all_empty.snap
@@ -0,0 +1,13 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Failed to parse toml file
+  ╰─▶ missing field `audits`
+   ╭─[audits.toml:1:1]
+ 1 │ 
+   · ▲
+   · ╰── here
+   ╰────
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__all_min.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__all_min.snap
new file mode 100644
index 0000000..802d949
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__all_min.snap
@@ -0,0 +1,5 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap
new file mode 100644
index 0000000..9f3eb7a
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap
@@ -0,0 +1,106 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Your cargo-vet store (supply-chain) has consistency errors
+
+Error: 
+  × 'bad-imply' is not a valid criteria name
+   ╭─[audits.toml:3:1]
+ 3 │ description = "great"
+ 4 │ implies = ["safe-to-deploy", "bad-imply"]
+   ·                              ───────────
+ 5 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad' is not a valid criteria name
+   ╭─[audits.toml:7:1]
+ 7 │ version = "1.0.0"
+ 8 │ criteria = "bad"
+   ·            ─────
+ 9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad1' is not a valid criteria name
+    ╭─[audits.toml:8:1]
+  8 │ criteria = "bad"
+  9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+    ·                                                           ──────
+ 10 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad2' is not a valid criteria name
+    ╭─[audits.toml:8:1]
+  8 │ criteria = "bad"
+  9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+    ·                                                                           ──────
+ 10 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad-dep' is not a valid criteria name
+    ╭─[audits.toml:8:1]
+  8 │ criteria = "bad"
+  9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+    ·                                ─────────
+ 10 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'dang' is not a valid criteria name
+    ╭─[audits.toml:12:1]
+ 12 │ delta = "1.0.0 -> 1.1.0"
+ 13 │ criteria = ["safe-to-run", "dang"]
+    ·                            ──────
+ 14 │ dependency-criteria = {}
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'oops' is not a valid criteria name
+    ╭─[audits.toml:17:1]
+ 17 │ delta = "1.0.0 -> 1.1.0"
+ 18 │ criteria = "oops"
+    ·            ──────
+ 19 │ dependency-criteria = { "nope" = "nah" }
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nah' is not a valid criteria name
+    ╭─[audits.toml:18:1]
+ 18 │ criteria = "oops"
+ 19 │ dependency-criteria = { "nope" = "nah" }
+    ·                                  ─────
+ 20 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'safe-to-jog' is not a valid criteria name
+    ╭─[audits.toml:22:1]
+ 22 │ version = "2.0.0"
+ 23 │ criteria = "safe-to-jog"
+    ·            ─────────────
+ 24 │ dependency-criteria = { toml = ["unsafe-to-destroy"] }
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'unsafe-to-destroy' is not a valid criteria name
+    ╭─[audits.toml:23:1]
+ 23 │ criteria = "safe-to-jog"
+ 24 │ dependency-criteria = { toml = ["unsafe-to-destroy"] }
+    ·                                ─────────────────────
+ 25 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'no-good-bad-bad' is not a valid criteria name
+    ╭─[audits.toml:27:1]
+ 27 │ violation = "5.0.0 "
+ 28 │ criteria = "no-good-bad-bad"
+    ·            ─────────────────
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap
new file mode 100644
index 0000000..96d018f
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap
@@ -0,0 +1,71 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Your cargo-vet store (supply-chain) has consistency errors
+
+Error: 
+  × 'oops' is not a valid criteria name
+    ╭─[config.toml:16:1]
+ 16 │ version = "1.0.0"
+ 17 │ criteria = "oops"
+    ·            ──────
+ 18 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nah' is not a valid criteria name
+    ╭─[config.toml:17:1]
+ 17 │ criteria = "oops"
+ 18 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+    ·                                       ─────
+ 19 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'no' is not a valid criteria name
+    ╭─[config.toml:17:1]
+ 17 │ criteria = "oops"
+ 18 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+    ·                                                      ────
+ 19 │ 
+    ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'bad' is not a valid criteria name
+   ╭─[config.toml:2:1]
+ 2 │ [policy.serde]
+ 3 │ criteria = "bad"
+   ·            ─────
+ 4 │ dev-criteria = "nope"
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nope' is not a valid criteria name
+   ╭─[config.toml:3:1]
+ 3 │ criteria = "bad"
+ 4 │ dev-criteria = "nope"
+   ·                ──────
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'unsafe-for-all' is not a valid criteria name
+   ╭─[config.toml:4:1]
+ 4 │ dev-criteria = "nope"
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ·                                                                       ────────────────
+ 6 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nada' is not a valid criteria name
+   ╭─[config.toml:4:1]
+ 4 │ dev-criteria = "nope"
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ·                                        ──────
+ 6 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_policies.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_policies.snap
new file mode 100644
index 0000000..77357fe
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_policies.snap
@@ -0,0 +1,44 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Your cargo-vet store (supply-chain) has consistency errors
+
+Error: 
+  × 'bad' is not a valid criteria name
+   ╭─[config.toml:2:1]
+ 2 │ [policy.serde]
+ 3 │ criteria = "bad"
+   ·            ─────
+ 4 │ dev-criteria = "nope"
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nope' is not a valid criteria name
+   ╭─[config.toml:3:1]
+ 3 │ criteria = "bad"
+ 4 │ dev-criteria = "nope"
+   ·                ──────
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'unsafe-for-all' is not a valid criteria name
+   ╭─[config.toml:4:1]
+ 4 │ dev-criteria = "nope"
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ·                                                                       ────────────────
+ 6 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+Error: 
+  × 'nada' is not a valid criteria name
+   ╭─[config.toml:4:1]
+ 4 │ dev-criteria = "nope"
+ 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+   ·                                        ──────
+ 6 │ 
+   ╰────
+  help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap
new file mode 100644
index 0000000..a3ba998
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap
@@ -0,0 +1,16 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Your cargo-vet store (supply-chain) has consistency errors
+
+Error: 
+  × 'bad' is not a valid criteria name
+   ╭─[audits.toml:3:1]
+ 3 │ version = "1.0.0"
+ 4 │ criteria = "bad"
+   ·            ─────
+   ╰────
+  help: the possible criteria are ["safe-to-run", "safe-to-deploy"]
+
diff --git a/src/tests/store_parsing.rs b/src/tests/store_parsing.rs
new file mode 100644
index 0000000..59515b9
--- /dev/null
+++ b/src/tests/store_parsing.rs
@@ -0,0 +1,110 @@
+const EMPTY_CONFIG: &str = "\n";
+const EMPTY_AUDITS: &str = "[audits]\n";
+const EMPTY_IMPORTS: &str = "[audits]\n";
+
+fn get_valid_store(config: &str, audits: &str, imports: &str) -> String {
+    let res = crate::Store::mock_acquire(config, audits, imports);
+    match res {
+        Ok(_) => String::new(),
+        Err(e) => format!("{:?}", miette::Report::new(e)),
+    }
+}
+
+#[test]
+fn test_all_empty() {
+    let acquire_errors = get_valid_store("\n", "\n", "\n");
+    insta::assert_snapshot!(acquire_errors);
+}
+
+#[test]
+fn test_all_min() {
+    let acquire_errors = get_valid_store(EMPTY_CONFIG, EMPTY_AUDITS, EMPTY_IMPORTS);
+    insta::assert_snapshot!(acquire_errors);
+}
+
+#[test]
+fn test_simple_bad_audit() {
+    let audits = r##"
+[[audits.serde]]
+version = "1.0.0"
+criteria = "bad"
+"##;
+
+    let acquire_errors = get_valid_store(EMPTY_CONFIG, audits, EMPTY_IMPORTS);
+    insta::assert_snapshot!(acquire_errors);
+}
+
+#[test]
+fn test_many_bad_audits() {
+    let audits = r##"
+[criteria.good]
+description = "great"
+implies = ["safe-to-deploy", "bad-imply"]
+
+[[audits.serde]]
+version = "1.0.0"
+criteria = "bad"
+dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+
+[[audits.serde]]
+delta = "1.0.0 -> 1.1.0"
+criteria = ["safe-to-run", "dang"]
+dependency-criteria = {}
+
+[[audits.serde]]
+delta = "1.0.0 -> 1.1.0"
+criteria = "oops"
+dependency-criteria = { "nope" = "nah" }
+
+[[audits.serde]]
+version = "2.0.0"
+criteria = "safe-to-jog"
+dependency-criteria = { toml = ["unsafe-to-destroy"] }
+
+[[audits.serde]]
+violation = "5.0.0 "
+criteria = "no-good-bad-bad"
+"##;
+
+    let acquire_errors = get_valid_store(EMPTY_CONFIG, audits, EMPTY_IMPORTS);
+    insta::assert_snapshot!(acquire_errors);
+}
+
+#[test]
+fn test_many_bad_config() {
+    let config = r##"
+[policy.serde]
+criteria = "bad"
+dev-criteria = "nope"
+dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+
+[policy.clap]
+criteria = "safe-to-deploy"
+dev-criteria = "safe-to-run"
+dependency-criteria = { clap_derive = "good" }
+
+[policy.boring]
+audit-as-crates-io = true
+
+[[unaudited.clap]]
+version = "1.0.0"
+criteria = "oops"
+dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+
+[[unaudited.clap_derive]]
+version = "1.0.0"
+criteria = "safe-to-run"
+
+"##;
+
+    let audits = r##"
+[criteria.good]
+description = "great"
+implies = ["safe-to-deploy"]
+
+[audits]
+"##;
+
+    let acquire_errors = get_valid_store(config, audits, EMPTY_IMPORTS);
+    insta::assert_snapshot!(acquire_errors);
+}
diff --git a/src/tests/vet.rs b/src/tests/vet.rs
index fbf3860..cc70443 100644
--- a/src/tests/vet.rs
+++ b/src/tests/vet.rs
@@ -2474,7 +2474,7 @@ fn builtin_simple_audit_as_weaker_root() {
     config.policy.insert(
         "root-package".to_string(),
         PolicyEntry {
-            criteria: Some(vec![SAFE_TO_RUN.to_string()]),
+            criteria: Some(vec![SAFE_TO_RUN.to_string().into()]),
             ..audit_as_policy(Some(true))
         },
     );