use std::fs;
use std::path::Path;
use std::process::{Command, Output};
use assert_cmd::cargo::CommandCargoExt;
use tempfile::TempDir;
fn write(path: &Path, content: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, content).unwrap();
}
fn run(root: &Path, args: &[&str]) -> (Output, String, String) {
let mut full = vec!["-C", root.to_str().unwrap()];
full.extend_from_slice(args);
let output = Command::cargo_bin("secunit")
.unwrap()
.args(&full)
.output()
.expect("run secunit");
let stdout = String::from_utf8(output.stdout.clone()).unwrap();
let stderr = String::from_utf8(output.stderr.clone()).unwrap();
(output, stdout, stderr)
}
fn staged_root() -> TempDir {
let dir = TempDir::new().unwrap();
let root = dir.path();
fs::create_dir_all(root.join("controls")).unwrap();
fs::create_dir_all(root.join("skills")).unwrap();
write(
&root.join("inventory.yaml"),
"source_repos:\n - name: app-api\n in_scope_since: 2026-01-01\n tags: [production]\n",
);
write(
&root.join("controls/ra-vuln-audit.yaml"),
"id: ra-vuln-audit\n\
title: Vulnerability audit\n\
policy: security/ra.md\n\
nist: [RA-5]\n\
owner: cto\n\
cadence: annual\n\
skill: ra-vuln-audit\n\
remediation_thresholds:\n high: 30\n critical: 14\n\
evidence_required:\n - kind: summary\n description: Findings\n",
);
write(&root.join("skills/ra-vuln-audit.md"), "# stub runbook\n");
let manifest = serde_json::json!({
"schema_version": 1,
"control_id": "ra-vuln-audit",
"run_id": "2026-05-25-run-001",
"started_at": "2026-05-25T14:00:00Z",
"completed_at": "2026-05-25T14:30:00Z",
"agent": {
"model": "m",
"skill": "s",
"skill_sha256": "a".repeat(64),
"control_sha256": "b".repeat(64)
},
"registry_git_sha": "abcdef0",
"scope_layout": "flat",
"resolved_scope": [],
"artifacts": [],
"status": "complete",
"draft_risks": [
{
"finding_id": "S032",
"title": "S032 — pickle deserialization RCE (CWE-502)",
"severity": "critical",
"impact": 3,
"likelihood": 3,
"affected_systems": ["app-api"],
"body_path": "findings.md#risk-1"
}
]
});
let run_dir = root.join("evidence/2026/q2/ra-vuln-audit/2026-05-25-run-001");
write(
&run_dir.join("manifest.json"),
&serde_json::to_string_pretty(&manifest).unwrap(),
);
dir
}
fn run_dir_arg(root: &Path) -> String {
root.join("evidence/2026/q2/ra-vuln-audit/2026-05-25-run-001")
.to_str()
.unwrap()
.to_string()
}
#[test]
fn open_list_show_round_trip() {
let dir = staged_root();
let root = dir.path();
let rd = run_dir_arg(root);
let (out, stdout, stderr) = run(
root,
&[
"risks",
"open",
"ra-vuln-audit",
"--from",
&rd,
"--finding",
"S032",
"--owner",
"cto",
],
);
assert!(out.status.success(), "open failed: {stderr}");
assert!(stdout.contains("R-0001"), "stdout: {stdout}");
assert!(stdout.contains("2026-06-08"), "due date wrong: {stdout}");
let (out, stdout, _) = run(root, &["--today", "2026-05-26", "risks", "list"]);
assert!(out.status.success());
assert!(stdout.contains("R-0001"));
assert!(stdout.contains("critical"));
assert!(stdout.contains("open"));
assert!(stdout.contains("cto"));
assert!(stdout.contains("ra-vuln-audit"));
let (out, stdout, _) = run(root, &["--json", "risks", "list"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v["risks"]["R-0001"]["severity"], "critical");
assert_eq!(v["risks"]["R-0001"]["owner"], "cto");
assert_eq!(v["risks"]["R-0001"]["source_control"], "ra-vuln-audit");
let (out, stdout, _) = run(root, &["risks", "show", "R-0001"]);
assert!(out.status.success());
assert!(stdout.contains("R-0001"));
assert!(stdout.contains("pickle deserialization"));
assert!(stdout.contains("opened"));
assert!(stdout.contains("owner-assigned"));
assert!(stdout.contains("S032"));
let (out, stdout, _) = run(root, &["--json", "risks", "show", "R-0001"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v["state"]["status"], "open");
assert_eq!(v["state"]["severity"], "critical");
assert_eq!(v["events"].as_array().unwrap().len(), 2);
}
#[test]
fn status_filter_and_past_sla() {
let dir = staged_root();
let root = dir.path();
let rd = run_dir_arg(root);
run(
root,
&[
"risks",
"open",
"ra-vuln-audit",
"--from",
&rd,
"--finding",
"S032",
],
);
let (_o, stdout, _) = run(
root,
&["--today", "2026-07-01", "risks", "list", "--past-sla"],
);
assert!(stdout.contains("R-0001"), "expected past-sla hit: {stdout}");
let (_o, stdout, _) = run(
root,
&["--today", "2026-05-26", "risks", "list", "--past-sla"],
);
assert!(stdout.contains("No risks match"), "stdout: {stdout}");
let (_o, stdout, _) = run(root, &["risks", "list", "--severity", "low"]);
assert!(stdout.contains("No risks match"), "stdout: {stdout}");
let (_o, stdout, _) = run(root, &["risks", "list", "--status", "open"]);
assert!(stdout.contains("R-0001"));
}
fn staged_root_legacy_drafts() -> TempDir {
let dir = TempDir::new().unwrap();
let root = dir.path();
fs::create_dir_all(root.join("controls")).unwrap();
fs::create_dir_all(root.join("skills")).unwrap();
write(
&root.join("inventory.yaml"),
"source_repos:\n - name: mdpdf\n in_scope_since: 2026-01-01\n tags: [production]\n - name: brdg\n in_scope_since: 2026-01-01\n tags: [production]\n",
);
write(
&root.join("controls/ra-vuln-audit.yaml"),
"id: ra-vuln-audit\n\
title: Vulnerability audit\n\
policy: security/ra.md\n\
nist: [RA-5]\n\
owner: cto\n\
cadence: annual\n\
skill: ra-vuln-audit\n\
remediation_thresholds:\n high: 30\n critical: 14\n\
evidence_required:\n - kind: summary\n description: Findings\n",
);
write(&root.join("skills/ra-vuln-audit.md"), "# stub runbook\n");
let manifest = serde_json::json!({
"schema_version": 1,
"control_id": "ra-vuln-audit",
"run_id": "2026-05-25-run-001",
"started_at": "2026-05-25T14:00:00Z",
"completed_at": "2026-05-25T14:30:00Z",
"agent": {
"model": "m",
"skill": "s",
"skill_sha256": "a".repeat(64),
"control_sha256": "b".repeat(64)
},
"registry_git_sha": "abcdef0",
"scope_layout": "flat",
"resolved_scope": [],
"artifacts": [],
"status": "complete",
"draft_risks": [
{
"affected_systems": ["mdpdf"],
"evidence": "by-system/mdpdf/raw/dependabot-alerts.json",
"id": "GHSA-xphw-cqx3-667j",
"remediation": "Upgrade thin-vec to >=0.2.16",
"severity": "high",
"sla_due": "2026-07-22",
"subject": "thin-vec use-after-free / double-free"
},
{
"affected": ["brdg"],
"evidence": "by-system/brdg/raw/dependabot-alerts.json",
"ghsa": [
"GHSA-752w-5fwx-jx9f",
"GHSA-v92g-xgxw-vvmm",
"GHSA-2h4p-vjrc-8xpq",
"GHSA-qccp-gfcp-xxvc",
"GHSA-mf9v-mfxr-j63j"
],
"severity": "High",
"sla_days": 60,
"subject": "brdg — 5 high-severity dependency vulnerabilities (SCA)"
}
]
});
let run_dir = root.join("evidence/2026/q2/ra-vuln-audit/2026-05-25-run-001");
write(
&run_dir.join("manifest.json"),
&serde_json::to_string_pretty(&manifest).unwrap(),
);
dir
}
#[test]
fn open_promotes_legacy_draft_a() {
let dir = staged_root_legacy_drafts();
let root = dir.path();
let rd = run_dir_arg(root);
let (out, stdout, stderr) = run(
root,
&[
"risks",
"open",
"ra-vuln-audit",
"--from",
&rd,
"--finding",
"GHSA-xphw-cqx3-667j",
],
);
assert!(out.status.success(), "open A failed: {stderr}");
assert!(stdout.contains("R-0001"), "stdout: {stdout}");
assert!(stdout.contains("2026-06-24"), "due date wrong: {stdout}");
assert!(
stderr.contains("derived (3, 3) from severity"),
"expected score note: {stderr}"
);
let (out, stdout, _) = run(root, &["--json", "risks", "show", "R-0001"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v["state"]["status"], "open");
assert_eq!(v["state"]["severity"], "high");
assert_eq!(v["state"]["impact"], 3);
assert_eq!(v["state"]["likelihood"], 3);
assert_eq!(v["state"]["title"], "thin-vec use-after-free / double-free");
assert_eq!(v["state"]["affected_systems"], serde_json::json!(["mdpdf"]));
let fr = &v["events"][0]["data"]["finding_ref"];
assert_eq!(fr["finding_id"], "GHSA-xphw-cqx3-667j");
}
#[test]
fn open_promotes_legacy_draft_b() {
let dir = staged_root_legacy_drafts();
let root = dir.path();
let rd = run_dir_arg(root);
let (out, stdout, stderr) = run(
root,
&[
"risks",
"open",
"ra-vuln-audit",
"--from",
&rd,
"--finding",
"GHSA-752w-5fwx-jx9f",
],
);
assert!(out.status.success(), "open B failed: {stderr}");
assert!(stdout.contains("R-0001"), "stdout: {stdout}");
assert!(stdout.contains("2026-06-24"), "due date wrong: {stdout}");
assert!(
stderr.contains("derived (3, 3) from severity"),
"expected score note: {stderr}"
);
let (out, stdout, _) = run(root, &["--json", "risks", "show", "R-0001"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v["state"]["status"], "open");
assert_eq!(v["state"]["severity"], "high");
assert_eq!(v["state"]["impact"], 3);
assert_eq!(v["state"]["likelihood"], 3);
assert_eq!(
v["state"]["title"],
"brdg — 5 high-severity dependency vulnerabilities (SCA)"
);
assert_eq!(v["state"]["affected_systems"], serde_json::json!(["brdg"]));
let fr = &v["events"][0]["data"]["finding_ref"];
assert_eq!(fr["finding_id"], "GHSA-752w-5fwx-jx9f");
assert_eq!(fr["control_id"], "ra-vuln-audit");
assert_eq!(
v["state"]["finding_refs"][0]["finding_id"],
"GHSA-752w-5fwx-jx9f"
);
}
#[test]
fn rebuild_regenerates_index() {
let dir = staged_root();
let root = dir.path();
let rd = run_dir_arg(root);
run(
root,
&[
"risks",
"open",
"ra-vuln-audit",
"--from",
&rd,
"--finding",
"S032",
],
);
fs::remove_file(root.join("risks/index.json")).unwrap();
let (out, stdout, stderr) = run(root, &["risks", "rebuild"]);
assert!(out.status.success(), "rebuild failed: {stderr}");
assert!(stdout.contains("1 risk"));
assert!(root.join("risks/index.json").exists());
}
fn write_run(
root: &Path,
control_id: &str,
run_id: &str,
draft_risks: serde_json::Value,
) -> String {
let manifest = serde_json::json!({
"schema_version": 1,
"control_id": control_id,
"run_id": run_id,
"started_at": "2026-06-20T14:00:00Z",
"completed_at": "2026-06-20T14:30:00Z",
"agent": {
"model": "m",
"skill": "s",
"skill_sha256": "a".repeat(64),
"control_sha256": "b".repeat(64)
},
"registry_git_sha": "abcdef0",
"scope_layout": "flat",
"resolved_scope": [],
"artifacts": [],
"status": "complete",
"draft_risks": draft_risks
});
let run_dir = root.join(format!("evidence/2026/q2/{control_id}/{run_id}"));
write(
&run_dir.join("manifest.json"),
&serde_json::to_string_pretty(&manifest).unwrap(),
);
run_dir.to_str().unwrap().to_string()
}
fn opened_in_progress(root: &Path) -> &'static str {
let rd = run_dir_arg(root);
let (out, _, stderr) = run(
root,
&[
"risks",
"open",
"ra-vuln-audit",
"--from",
&rd,
"--finding",
"S032",
],
);
assert!(out.status.success(), "open failed: {stderr}");
let (out, _, stderr) = run(
root,
&[
"risks",
"status",
"R-0001",
"--to",
"in-progress",
"--reason",
"working it",
],
);
assert!(out.status.success(), "status failed: {stderr}");
"R-0001"
}
#[test]
fn remediate_accepts_clean_evidence_run() {
let dir = staged_root();
let root = dir.path();
let id = opened_in_progress(root);
let clean = write_run(
root,
"ra-vuln-audit",
"2026-06-20-run-001",
serde_json::json!([]),
);
let (out, stdout, stderr) = run(
root,
&[
"risks",
"remediate",
id,
"--evidence",
&clean,
"--note",
"patched; clean rescan",
],
);
assert!(out.status.success(), "remediate failed: {stderr}");
assert!(stdout.contains("remediated"), "stdout: {stdout}");
let (out, stdout, _) = run(root, &["--json", "risks", "show", id]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v["state"]["status"], "remediated");
let ev = v["events"].as_array().unwrap().last().unwrap().clone();
assert_eq!(ev["type"], "remediated");
assert_eq!(
ev["data"]["resolved_run_ref"]["run_id"],
"2026-06-20-run-001"
);
assert_eq!(ev["data"]["resolved_run_ref"]["finding_id"], "S032");
}
#[test]
fn remediate_rejects_run_still_reporting_finding() {
let dir = staged_root();
let root = dir.path();
let id = opened_in_progress(root);
let rd = run_dir_arg(root);
let (out, _, stderr) = run(
root,
&[
"risks",
"remediate",
id,
"--evidence",
&rd,
"--note",
"should not land",
],
);
assert!(!out.status.success(), "expected rejection");
assert!(stderr.contains("disproves the fix"), "stderr: {stderr}");
}
#[test]
fn remediate_rejects_run_from_other_control() {
let dir = staged_root();
let root = dir.path();
let id = opened_in_progress(root);
let other = write_run(
root,
"other-control",
"2026-06-20-run-001",
serde_json::json!([]),
);
let (out, _, stderr) = run(
root,
&[
"risks",
"remediate",
id,
"--evidence",
&other,
"--note",
"should not land",
],
);
assert!(!out.status.success(), "expected rejection");
assert!(
stderr.contains("must come from the same control"),
"stderr: {stderr}"
);
}
#[test]
fn retitle_round_trip() {
let dir = staged_root();
let root = dir.path();
let rd = run_dir_arg(root);
let (out, _, stderr) = run(
root,
&[
"risks",
"open",
"ra-vuln-audit",
"--from",
&rd,
"--finding",
"S032",
],
);
assert!(out.status.success(), "open failed: {stderr}");
let (out, stdout, stderr) = run(
root,
&[
"risks",
"retitle",
"R-0001",
"--title",
"Pickle RCE in app-api ingest (CWE-502)",
"--reason",
"scope narrowed",
],
);
assert!(out.status.success(), "retitle failed: {stderr}");
assert!(stdout.contains("retitled"), "stdout: {stdout}");
let (out, stdout, _) = run(root, &["--json", "risks", "show", "R-0001"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(
v["state"]["title"],
"Pickle RCE in app-api ingest (CWE-502)"
);
let ev = v["events"].as_array().unwrap().last().unwrap().clone();
assert_eq!(ev["type"], "retitled");
assert_eq!(ev["data"]["reason"], "scope narrowed");
let (out, stdout, _) = run(root, &["--json", "risks", "list"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(
v["risks"]["R-0001"]["title"],
"Pickle RCE in app-api ingest (CWE-502)"
);
let (out, stdout, _) = run(root, &["risks", "show", "R-0001"]);
assert!(out.status.success());
assert!(stdout.contains("Pickle RCE in app-api ingest"));
}
#[test]
fn retitle_rejects_empty_title() {
let dir = staged_root();
let root = dir.path();
let rd = run_dir_arg(root);
run(
root,
&[
"risks",
"open",
"ra-vuln-audit",
"--from",
&rd,
"--finding",
"S032",
],
);
let (out, _, stderr) = run(root, &["risks", "retitle", "R-0001", "--title", " "]);
assert!(!out.status.success(), "expected rejection");
assert!(stderr.contains("must not be empty"), "stderr: {stderr}");
}