//! The reviewed-finding list. //! //! An entry silences one finding by the fingerprint the tool prints beside it, //! and must say why. Silencing one fingerprint at a time keeps a tool's //! thresholds honest; loosening a threshold to hide one finding hides every //! finding like it. use std::collections::HashMap; use std::path::Path; #[derive(serde::Deserialize)] struct File { #[serde(default)] allow: Vec, } #[derive(serde::Deserialize)] struct Entry { fingerprint: String, /// Why this finding is acceptable. Required: an entry without one is a /// parse error, not a silent allow. reason: String, } #[derive(Debug, Default)] pub struct Allowlist(HashMap); impl Allowlist { pub fn load(path: &Path) -> Result { let text = match std::fs::read_to_string(path) { Ok(t) => t, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Self::default()), Err(e) => return Err(format!("{}: {e}", path.display())), }; let parsed: File = toml::from_str(&text).map_err(|e| format!("{}: {e}", path.display()))?; Ok(Allowlist( parsed .allow .into_iter() .map(|e| (e.fingerprint, e.reason)) .collect(), )) } /// The reason a finding is allowed, if any of its fingerprints is listed. pub fn reason<'a>(&'a self, fingerprints: impl Iterator) -> Option<&'a str> { fingerprints .filter_map(|f| self.0.get(f)) .next() .map(String::as_str) } pub fn len(&self) -> usize { self.0.len() } pub fn is_empty(&self) -> bool { self.0.is_empty() } } #[cfg(test)] mod tests { use super::*; fn write(text: &str) -> (tempfile::TempDir, std::path::PathBuf) { let dir = tempfile::tempdir().expect("a temp dir"); let path = dir.path().join("allow.toml"); std::fs::write(&path, text).expect("write"); (dir, path) } #[test] fn a_listed_fingerprint_is_silenced() { let (_dir, path) = write( r#"[[allow]] fingerprint = "0123456789abcdef" reason = "Two provider wrappers that delegate the same way.""#, ); let list = Allowlist::load(&path).expect("loads"); assert_eq!( list.reason(["aaaa", "0123456789abcdef"].into_iter()), Some("Two provider wrappers that delegate the same way.") ); assert_eq!(list.reason(["aaaa", "bbbb"].into_iter()), None); } #[test] fn an_entry_without_a_reason_is_refused() { let (_dir, path) = write("[[allow]]\nfingerprint = \"0123456789abcdef\"\n"); let err = Allowlist::load(&path).expect_err("no reason"); assert!(err.contains("reason"), "{err}"); } #[test] fn a_missing_list_allows_nothing() { let list = Allowlist::load(std::path::Path::new("/nowhere/allow.toml")).expect("loads"); assert_eq!(list.len(), 0); } }