use serde::Serialize;
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Level {
Must,
Should,
May,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(tag = "kind", content = "condition", rename_all = "lowercase")]
pub enum Applicability {
Universal,
Conditional(&'static str),
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ExceptionCategory {
HumanTui,
FileTraversal,
PosixUtility,
DiagnosticOnly,
}
impl ExceptionCategory {
pub fn as_kebab_case(&self) -> &'static str {
match self {
ExceptionCategory::HumanTui => "human-tui",
ExceptionCategory::FileTraversal => "file-traversal",
ExceptionCategory::PosixUtility => "posix-utility",
ExceptionCategory::DiagnosticOnly => "diagnostic-only",
}
}
pub fn description(&self) -> &'static str {
match self {
ExceptionCategory::HumanTui => {
"TUI-by-design tools (lazygit, k9s, btop). Interactive-prompt MUSTs \
suppressed; the TTY-driving contract is out of scope for verification."
}
ExceptionCategory::FileTraversal => {
"File-traversal utilities (fd, find). Subcommand-structure SHOULDs \
relaxed; these tools have no subcommands by design."
}
ExceptionCategory::PosixUtility => {
"POSIX utilities (cat, sed, awk). Stdin-as-primary-input is their \
contract; P1 interactive-prompt MUSTs satisfied vacuously."
}
ExceptionCategory::DiagnosticOnly => {
"Diagnostic tools (nvidia-smi, vmstat). No write operations, so the \
P5 mutation-boundary MUSTs do not apply."
}
}
}
}
pub const ALL_EXCEPTION_CATEGORIES: &[ExceptionCategory] = &[
ExceptionCategory::HumanTui,
ExceptionCategory::FileTraversal,
ExceptionCategory::PosixUtility,
ExceptionCategory::DiagnosticOnly,
];
#[allow(dead_code)]
const fn _all_categories_covers_every_variant(c: ExceptionCategory) -> bool {
match c {
ExceptionCategory::HumanTui
| ExceptionCategory::FileTraversal
| ExceptionCategory::PosixUtility
| ExceptionCategory::DiagnosticOnly => true,
}
}
pub const SUPPRESSION_EVIDENCE_PREFIX: &str = "suppressed by audit_profile: ";
pub static SUPPRESSION_TABLE: &[(ExceptionCategory, &[&str])] = &[
(
ExceptionCategory::HumanTui,
&[
"p1-non-interactive",
"p1-flag-existence",
"p1-non-interactive-source",
"p1-tty-detection-source",
"p6-sigpipe",
],
),
(
ExceptionCategory::FileTraversal,
&[
],
),
(
ExceptionCategory::PosixUtility,
&[
"p1-non-interactive",
"p1-flag-existence",
"p1-non-interactive-source",
],
),
(
ExceptionCategory::DiagnosticOnly,
&[
"p5-dry-run",
],
),
];
pub fn suppresses(check_id: &str, category: ExceptionCategory) -> bool {
SUPPRESSION_TABLE
.iter()
.find(|(cat, _)| *cat == category)
.is_some_and(|(_, ids)| ids.contains(&check_id))
}
#[derive(Debug, Clone, Serialize)]
pub struct Requirement {
pub id: &'static str,
pub principle: u8,
pub level: Level,
pub summary: &'static str,
pub applicability: Applicability,
}
include!(concat!(env!("OUT_DIR"), "/generated_requirements.rs"));
pub fn find(id: &str) -> Option<&'static Requirement> {
REQUIREMENTS.iter().find(|r| r.id == id)
}
#[allow(dead_code)]
pub fn count_at_level(level: Level) -> usize {
REQUIREMENTS.iter().filter(|r| r.level == level).count()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn ids_are_unique() {
let mut seen = HashSet::new();
for r in REQUIREMENTS {
assert!(seen.insert(r.id), "duplicate requirement ID: {}", r.id);
}
}
#[test]
fn ids_follow_naming_convention() {
for r in REQUIREMENTS {
let prefix = format!("p{}-", r.principle);
assert!(
r.id.starts_with(&prefix),
"requirement {} does not start with {}",
r.id,
prefix
);
let level_token = match r.level {
Level::Must => "-must-",
Level::Should => "-should-",
Level::May => "-may-",
};
assert!(
r.id.contains(level_token),
"requirement {} level token {} missing",
r.id,
level_token
);
}
}
#[test]
fn principle_range_is_valid() {
for r in REQUIREMENTS {
assert!(
(1..=7).contains(&r.principle),
"requirement {} has invalid principle {}",
r.id,
r.principle
);
}
}
#[test]
fn summary_is_non_empty() {
for r in REQUIREMENTS {
assert!(
!r.summary.trim().is_empty(),
"requirement {} has empty summary",
r.id
);
}
}
#[test]
fn find_returns_registered_ids() {
assert!(find("p1-must-no-interactive").is_some());
assert!(find("p6-must-sigpipe").is_some());
assert!(find("nonexistent-id").is_none());
}
#[test]
fn registry_size_matches_spec() {
assert_eq!(REQUIREMENTS.len(), 46);
}
#[test]
fn level_counts_match_spec() {
assert_eq!(count_at_level(Level::Must), 23);
assert_eq!(count_at_level(Level::Should), 16);
assert_eq!(count_at_level(Level::May), 7);
}
#[test]
fn exception_category_as_kebab_case_matches_serde() {
for cat in [
ExceptionCategory::HumanTui,
ExceptionCategory::FileTraversal,
ExceptionCategory::PosixUtility,
ExceptionCategory::DiagnosticOnly,
] {
let via_serde = serde_json::to_value(cat)
.ok()
.and_then(|v| v.as_str().map(|s| s.to_string()))
.expect("serde renders category as string");
assert_eq!(via_serde, cat.as_kebab_case(), "mismatch for {cat:?}");
}
}
#[test]
fn suppresses_positive_cases() {
assert!(suppresses(
"p1-non-interactive",
ExceptionCategory::HumanTui
));
assert!(suppresses("p6-sigpipe", ExceptionCategory::HumanTui));
assert!(suppresses(
"p1-non-interactive",
ExceptionCategory::PosixUtility
));
assert!(suppresses("p5-dry-run", ExceptionCategory::DiagnosticOnly));
}
#[test]
fn suppresses_negative_cases() {
assert!(!suppresses("p2-json-output", ExceptionCategory::HumanTui));
assert!(!suppresses("p6-sigpipe", ExceptionCategory::PosixUtility));
assert!(!suppresses("p6-sigpipe", ExceptionCategory::DiagnosticOnly));
assert!(!suppresses(
"totally-fake-check-id",
ExceptionCategory::HumanTui
));
assert!(!suppresses(
"totally-fake-check-id",
ExceptionCategory::DiagnosticOnly
));
}
#[test]
fn suppression_table_covers_every_category() {
for cat in [
ExceptionCategory::HumanTui,
ExceptionCategory::FileTraversal,
ExceptionCategory::PosixUtility,
ExceptionCategory::DiagnosticOnly,
] {
assert!(
SUPPRESSION_TABLE.iter().any(|(c, _)| *c == cat),
"SUPPRESSION_TABLE missing category {cat:?} — a variant was \
added to ExceptionCategory without a corresponding table \
entry. Add a row (empty slice is fine) and document why.",
);
}
}
#[test]
fn suppression_table_check_ids_exist_in_catalog() {
use crate::check::Check;
use crate::checks::all_checks_catalog;
let catalog: Vec<Box<dyn Check>> = all_checks_catalog();
let catalog_ids: Vec<&str> = catalog.iter().map(|c| c.id()).collect();
for (cat, ids) in SUPPRESSION_TABLE {
for id in *ids {
assert!(
catalog_ids.contains(id),
"SUPPRESSION_TABLE entry for {cat:?} references unknown \
check ID `{id}` — either the check was renamed/removed \
or the table has a typo. Fix the table, not the \
catalog.",
);
}
}
}
}