xtask/tests/threat_model.rs
//! **The threat model is in the tree, names its sections, and every place it cites exists**
//! (OpenSSF silver: `assurance_case` and `documentation_security`; lane OSSF-SILVER-CODE).
//!
//! `docs/THREAT-MODEL.md` is Gunnar's assurance case: the scope, the assets, the trust
//! boundaries, the attackers, the mitigations with the code that holds each one, and the
//! residual risks. A mitigation that cites a file or a function that is no longer there is an
//! argument with a hole in it, and nobody notices the hole by reading the page. So this test
//! reads every citation of the form `` `<path>` `<text>` `` (a repository path, a space, then
//! the symbol or phrase the file must contain) and fails when the file is gone or no longer
//! contains the text. A lone `` `<path>` `` must exist. A path is a backticked token whose
//! first component is an entry at the repository root and that has a `/` or an extension;
//! a crate outside this repository is named by its crate name and version instead, so it is
//! not checked here.
//!
//! The page must also be reachable: `SECURITY.md` and both READMEs (the generated root and
//! its `.nornir/README.md` source) link it.
use std::path::{Path, PathBuf};
const DOC: &str = "docs/THREAT-MODEL.md";
/// The sections, in order. Each is an exact `## ` heading line.
const SECTIONS: &[&str] = &[
"## Scope",
"## Assets",
"## Trust boundaries",
"## Attackers",
"## Mitigations",
"## Residual risks and known gaps",
"## Reporting a vulnerability",
];
/// The pages that must link the threat model, and the link text they must carry.
const LINKED_FROM: &[(&str, &str)] = &[
("SECURITY.md", "(docs/THREAT-MODEL.md)"),
("README.md", "(docs/THREAT-MODEL.md)"),
(".nornir/README.md", "(docs/THREAT-MODEL.md)"),
];
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("the workspace root")
.to_owned()
}
/// The backticked spans of `line`, in order, with the byte offset just after each one.
fn spans(line: &str) -> Vec<(&str, usize)> {
let mut out = Vec::new();
let mut rest = 0;
while let Some(open) = line[rest..].find('`') {
let start = rest + open + 1;
let Some(len) = line[start..].find('`') else {
break;
};
out.push((&line[start..start + len], start + len + 1));
rest = start + len + 1;
}
out
}
/// Is `token` a path into this repository (see the module docs)?
fn is_repo_path(root: &Path, token: &str) -> bool {
if token.is_empty() || token.contains(char::is_whitespace) || token.starts_with('/') {
return false;
}
let first = token.split('/').next().unwrap_or("");
let shaped = token.contains('/') || first.contains('.');
shaped && !first.is_empty() && first != "." && first != ".." && root.join(first).exists()
}
/// Every broken citation in `text`, as `line: what is wrong`.
fn broken_citations(root: &Path, text: &str) -> Vec<String> {
let mut found = Vec::new();
let mut fenced = false;
for (i, line) in text.lines().enumerate() {
if line.trim_start().starts_with("```") {
fenced = !fenced;
continue;
}
if fenced {
continue;
}
let spans = spans(line);
for (k, (token, end)) in spans.iter().enumerate() {
if !is_repo_path(root, token) {
continue;
}
let path = root.join(token.trim_end_matches('/'));
if !path.exists() {
found.push(format!("line {}: `{token}` does not exist", i + 1));
continue;
}
// `<path>` `<text>`: the next span starts one space after this one ends.
let Some((symbol, _)) = spans.get(k + 1) else {
continue;
};
if !line[*end..].starts_with(" `") || is_repo_path(root, symbol) {
continue;
}
if path.is_dir() {
found.push(format!(
"line {}: `{token}` is a directory, so it cannot contain `{symbol}`",
i + 1
));
continue;
}
match std::fs::read_to_string(&path) {
Ok(body) if body.contains(symbol) => {}
Ok(_) => found.push(format!(
"line {}: `{token}` no longer contains `{symbol}`",
i + 1
)),
Err(e) => found.push(format!("line {}: `{token}`: {e}", i + 1)),
}
}
}
found
}
fn read(root: &Path, rel: &str) -> String {
std::fs::read_to_string(root.join(rel)).unwrap_or_else(|e| panic!("{rel}: {e}"))
}
#[test]
fn the_threat_model_names_its_sections_in_order() {
let root = root();
let text = read(&root, DOC);
let headings: Vec<&str> = text.lines().filter(|l| l.starts_with("## ")).collect();
let mut at = 0;
for want in SECTIONS {
match headings[at..].iter().position(|h| h == want) {
Some(p) => at += p + 1,
None => panic!("{DOC} has no `{want}` heading after the ones before it; its headings: {headings:#?}"),
}
}
}
#[test]
fn every_place_the_threat_model_cites_exists_and_holds_what_it_names() {
let root = root();
let text = read(&root, DOC);
let cited = text
.lines()
.flat_map(|l| spans(l).into_iter().map(|(t, _)| t))
.filter(|t| is_repo_path(&root, t))
.count();
assert!(
cited >= 40,
"{DOC} cites only {cited} repository paths; the mitigations must name their code"
);
let broken = broken_citations(&root, &text);
assert!(
broken.is_empty(),
"{DOC} cites code that is not there:\n{}",
broken.join("\n")
);
}
#[test]
fn the_security_policy_and_the_readmes_link_the_threat_model() {
let root = root();
for (page, link) in LINKED_FROM {
assert!(
read(&root, page).contains(link),
"{page} does not link {DOC} as `{link}`"
);
}
}
/// The detector must be able to go red: a citation of a missing file, and a citation of a
/// real file with text it does not hold, are both reported; a true citation is not.
#[test]
fn a_stale_citation_is_reported_and_a_true_one_is_not() {
let root = root();
let good =
"the guard: `xtask/src/guard.rs` `FORBIDDEN_ANYWHERE`, and `Cargo.toml` `[workspace]`.";
assert_eq!(broken_citations(&root, good), Vec::<String>::new());
let missing_file = "see `crates/gunnar-nowhere/src/lib.rs` `anything`";
let found = broken_citations(&root, missing_file);
assert!(
found.len() == 1 && found[0].contains("does not exist"),
"{found:?}"
);
let missing_symbol = "see `xtask/src/guard.rs` `fn_that_was_renamed_away_2026`";
let found = broken_citations(&root, missing_symbol);
assert!(
found.len() == 1 && found[0].contains("no longer contains"),
"{found:?}"
);
// Not repository paths: an outside crate, a ref name, a feature word.
let outside = "`gunnar-pack 2.0.0` `IndexOptions`, `refs/heads/main`, `online`";
assert_eq!(broken_citations(&root, outside), Vec::<String>::new());
// A fenced block is a diagram, not a citation.
let fenced = "```\n`crates/gunnar-nowhere/x.rs`\n```";
assert_eq!(broken_citations(&root, fenced), Vec::<String>::new());
}