use serde::Serialize;
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Level {
Must,
Should,
May,
}
impl Level {
pub fn as_str(self) -> &'static str {
match self {
Level::Must => "must",
Level::Should => "should",
Level::May => "may",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Applicability {
Universal,
Conditional {
#[serde(default, skip_serializing_if = "Option::is_none")]
condition: Option<&'static str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
antecedent: Option<Antecedent>,
},
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
pub struct Antecedent {
pub audit_id: &'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",
"p6-sigterm",
],
),
(
ExceptionCategory::FileTraversal,
&[
],
),
(
ExceptionCategory::PosixUtility,
&[
"p1-non-interactive",
"p1-flag-existence",
"p1-non-interactive-source",
],
),
(
ExceptionCategory::DiagnosticOnly,
&[
"p5-dry-run",
],
),
];
pub fn suppresses(audit_id: &str, category: ExceptionCategory) -> bool {
SUPPRESSION_TABLE
.iter()
.find(|(cat, _)| *cat == category)
.is_some_and(|(_, ids)| ids.contains(&audit_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..=8).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(), 59);
}
#[test]
fn level_counts_match_spec() {
assert_eq!(count_at_level(Level::Must), 28);
assert_eq!(count_at_level(Level::Should), 21);
assert_eq!(count_at_level(Level::May), 10);
}
#[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-audit-id",
ExceptionCategory::HumanTui
));
assert!(!suppresses(
"totally-fake-audit-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_audit_ids_exist_in_catalog() {
use crate::audit::Audit;
use crate::audits::all_audits_catalog;
let catalog: Vec<Box<dyn Audit>> = all_audits_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 \
audit ID `{id}` — either the audit was renamed/removed \
or the table has a typo. Fix the table, not the \
catalog.",
);
}
}
}
#[test]
fn every_conditional_antecedent_resolves_to_a_real_audit() {
use crate::audit::Audit;
use crate::audits::all_audits_catalog;
let catalog: Vec<Box<dyn Audit>> = all_audits_catalog();
let catalog_ids: Vec<&str> = catalog.iter().map(|c| c.id()).collect();
let mut dangling: Vec<(&str, &str)> = Vec::new();
for req in REQUIREMENTS {
if let Applicability::Conditional {
antecedent: Some(ante),
..
} = req.applicability
&& !catalog_ids.contains(&ante.audit_id)
{
dangling.push((req.id, ante.audit_id));
}
}
assert!(
dangling.is_empty(),
"conditional requirements with dangling antecedent audit_ids:\n{}\n\
Fix the spec's `antecedent.audit_id` or add the missing audit to the catalog.",
dangling
.iter()
.map(|(req, ante)| format!(
" - row `{req}` → antecedent `{ante}` (not in catalog)"
))
.collect::<Vec<_>>()
.join("\n"),
);
}
#[test]
fn no_conditional_row_names_itself_as_antecedent() {
use crate::audit::Audit;
use crate::audits::all_audits_catalog;
let catalog: Vec<Box<dyn Audit>> = all_audits_catalog();
let mut covers_by_audit: std::collections::HashMap<&'static str, &'static [&'static str]> =
std::collections::HashMap::new();
for c in &catalog {
covers_by_audit.insert(Box::leak(c.id().to_string().into_boxed_str()), c.covers());
}
for req in REQUIREMENTS {
let Applicability::Conditional {
antecedent: Some(ante),
..
} = req.applicability
else {
continue;
};
if let Some(covers) = covers_by_audit.get(ante.audit_id) {
assert!(
!covers.contains(&req.id),
"conditional row `{}` declares antecedent `{}`, but that \
audit already covers `{}` directly — the row would gate \
its own status against itself.",
req.id,
ante.audit_id,
req.id,
);
}
}
}
}