fix/audit-findings #5

Merged
trevon merged 14 commits from fix/audit-findings into main 2026-09-25 02:39:30 +00:00
2 changed files with 58 additions and 1 deletions
Showing only changes of commit 4c630ffeb1 - Show all commits

fix(check): gate on recomputed metrics, not the stored copy

`brick check` read `metrics` from the model JSON and gated on them, so
setting `cycle_count` to 0 and `score` to 100 in the file turned a
failing `--max-cycles 0` gate into "passed" without changing the graph.

check now recomputes the metrics from the loaded nodes and edges and
gates on those; if the stored copy differs, that is itself a failure
("stored metrics do not match the graph"). Honest models recompute to
identical values, so existing gates are unchanged.

Test: diff_cli::check_recomputes_metrics_instead_of_trusting_the_file
fails before this change (the tampered model passes).
trvon 2026-09-24 20:02:04 -06:00
Signed by: trevon
GPG key ID: 38686EA797D7A8A7

View file

@ -227,8 +227,19 @@ fn run(cli: Cli) -> Result<()> {
bail!("--min-score must be a finite number");
}
let building = read_json(&input)?;
let metrics = &building.metrics;
// Gate on metrics recomputed from the graph, never on the stored copy:
// editing `metrics` in the JSON must not be able to pass a gate.
let mut recomputed = building.clone();
brick::metrics::calculate(&mut recomputed);
let mut failures = Vec::new();
if serde_json::to_value(&recomputed.metrics)?
!= serde_json::to_value(&building.metrics)?
{
failures.push(
"stored metrics do not match the graph; gating on recomputed metrics".into(),
);
}
let metrics = &recomputed.metrics;
if metrics.structural_health.score < min_score {
failures.push(format!(
"score {:.1} is below {:.1}",

View file

@ -259,3 +259,49 @@ fn relocated_artifacts_compare_after_checkouts_are_deleted() {
);
}
}
#[test]
fn check_recomputes_metrics_instead_of_trusting_the_file() {
// `check` gated on the metrics stored in the JSON, so editing `cycle_count` and
// `score` turned a failing gate into a pass without changing the graph.
let directory = tempfile::tempdir().unwrap();
let project = directory.path().join("project");
fs::create_dir(&project).unwrap();
fs::write(
project.join("lib.rs"),
"pub fn alpha() { beta(); }\nfn beta() { alpha(); }\n",
)
.unwrap();
let model = scan::scan(&project).unwrap();
assert!(model.metrics.cycle_count > 0);
let honest = directory.path().join("honest.json");
write_model(&honest, &model);
let gate = |path: &Path| {
Command::new(env!("CARGO_BIN_EXE_brick"))
.args([
"check",
path.to_str().unwrap(),
"--max-cycles",
"0",
"--min-score",
"0",
])
.output()
.unwrap()
};
assert!(!gate(&honest).status.success());
let mut value: Value = serde_json::to_value(&model).unwrap();
value["metrics"]["cycle_count"] = 0.into();
value["metrics"]["structural_health"]["score"] = 100.0.into();
let tampered = directory.path().join("tampered.json");
fs::write(&tampered, serde_json::to_vec_pretty(&value).unwrap()).unwrap();
let output = gate(&tampered);
assert!(!output.status.success(), "tampered metrics passed the gate");
let text = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(text.contains("stored metrics"), "{text}");
}