use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
use serde_json::Value;
fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_keyhog"))
}
const GHP: &str = "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";
const GHP_DETECTOR: &str = "github-classic-pat";
const AKIA: &str = "AKIAQYLPMN5HFIQR7XYA";
const AKIA_DETECTOR: &str = "aws-access-key";
fn cache_home() -> TempDir {
TempDir::new().expect("tempdir")
}
fn fixture(name: &str, content: &str) -> (TempDir, PathBuf) {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join(name);
std::fs::write(&path, content).expect("write fixture");
(dir, path)
}
fn scan(
home: &Path,
path: &Path,
backend: Option<&str>,
extra: &[&str],
) -> (Option<i32>, String, String) {
let mut cmd = Command::new(binary());
cmd.env("HOME", home)
.env("XDG_CACHE_HOME", home)
.env("NO_COLOR", "1");
cmd.args(["scan", "--daemon=off", "--format", "json"]);
if let Some(b) = backend {
cmd.args(["--backend", b]);
}
for a in extra {
cmd.arg(a);
}
cmd.arg(path);
let out = cmd.output().expect("spawn keyhog scan");
(
out.status.code(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn findings(stdout: &str) -> Vec<Value> {
let v: Value = serde_json::from_str(stdout)
.unwrap_or_else(|e| panic!("scan stdout must be a JSON array ({e}); stdout={stdout:?}"));
v.as_array()
.expect("JSON report is a top-level array")
.clone()
}
fn detector_ids(stdout: &str) -> Vec<String> {
let mut ids: Vec<String> = findings(stdout)
.iter()
.map(|f| {
f.get("detector_id")
.and_then(Value::as_str)
.expect("every finding carries a detector_id string")
.to_string()
})
.collect();
ids.sort();
ids
}
fn cred_hashes(stdout: &str) -> BTreeSet<String> {
findings(stdout)
.iter()
.filter_map(|f| {
f.get("credential_hash")
.and_then(Value::as_str)
.map(String::from)
})
.collect()
}
fn ordered_findings(stdout: &str) -> Vec<Value> {
let mut fs = findings(stdout);
fs.sort_by(|a, b| {
let da = a.get("detector_id").and_then(Value::as_str).unwrap_or("");
let db = b.get("detector_id").and_then(Value::as_str).unwrap_or("");
let oa = a
.pointer("/location/offset")
.and_then(Value::as_u64)
.unwrap_or(0);
let ob = b
.pointer("/location/offset")
.and_then(Value::as_u64)
.unwrap_or(0);
(da, oa).cmp(&(db, ob))
});
fs
}
const SIMD_FAIL_CLOSED_MSG: &str = "silent cpu-fallback execution is forbidden";
const EXIT_BACKEND_UNAVAILABLE: i32 = 3;
fn assert_accel_matches_cpu_or_fails_closed(home: &Path, path: &Path, b: &str) -> bool {
let (code_cpu, out_cpu, _) = scan(home, path, Some("cpu"), &[]);
let (code_accel, out_accel, err_accel) = scan(home, path, Some(b), &[]);
if code_accel == Some(EXIT_BACKEND_UNAVAILABLE) {
assert!(
err_accel.contains(SIMD_FAIL_CLOSED_MSG),
"`--backend {b}` unavailable must fail closed (exit 3) with the \
no-silent-fallback message; stderr={err_accel}"
);
false
} else {
assert_eq!(
code_accel, code_cpu,
"`--backend {b}` (available) must share cpu's exit code; stderr={err_accel}"
);
assert_eq!(
ordered_findings(&out_accel),
ordered_findings(&out_cpu),
"`--backend {b}` (available) must produce byte-identical findings to cpu"
);
true
}
}
#[test]
fn cpu_backend_surfaces_exactly_the_planted_github_pat() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (code, out, err) = scan(home.path(), &path, Some("cpu"), &[]);
assert_eq!(
code,
Some(1),
"findings present, none verified -> exit 1; stderr={err}"
);
assert_eq!(
detector_ids(&out),
vec![GHP_DETECTOR.to_string()],
"the cpu backend must surface exactly one github-classic-pat finding; stdout={out}"
);
}
#[test]
fn simd_backend_surfaces_the_planted_github_pat_or_fails_closed() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let available = assert_accel_matches_cpu_or_fails_closed(home.path(), &path, "simd");
if available {
let (code, out, _) = scan(home.path(), &path, Some("simd"), &[]);
assert_eq!(
code,
Some(1),
"simd (available) on a planted secret -> exit 1"
);
assert_eq!(
detector_ids(&out),
vec![GHP_DETECTOR.to_string()],
"the simd backend must surface exactly one github-classic-pat finding; stdout={out}"
);
}
}
#[test]
fn cpu_and_simd_agree_on_detector_ids_count_and_exit_code() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (code_cpu, out_cpu, _) = scan(home.path(), &path, Some("cpu"), &[]);
assert_eq!(code_cpu, Some(1), "cpu exits 1 on the planted secret");
assert_eq!(detector_ids(&out_cpu), vec![GHP_DETECTOR.to_string()]);
assert_accel_matches_cpu_or_fails_closed(home.path(), &path, "simd");
}
#[test]
fn cpu_and_simd_agree_on_the_credential_hash_set() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (_c, out_cpu, _) = scan(home.path(), &path, Some("cpu"), &[]);
let cpu = cred_hashes(&out_cpu);
assert_eq!(
cpu.len(),
1,
"one planted credential -> one hash; cpu={cpu:?}"
);
if assert_accel_matches_cpu_or_fails_closed(home.path(), &path, "simd") {
let (_s, out_simd, _) = scan(home.path(), &path, Some("simd"), &[]);
assert_eq!(
cpu,
cred_hashes(&out_simd),
"cpu and simd must derive the SAME credential hash for the same bytes"
);
}
}
#[test]
fn cpu_and_simd_agree_value_for_value_including_location() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (_c, out_cpu, _) = scan(home.path(), &path, Some("cpu"), &[]);
let cpu = ordered_findings(&out_cpu);
assert_eq!(
cpu.len(),
1,
"single planted secret yields a single finding; cpu={cpu:?}"
);
let offset = cpu[0]
.pointer("/location/offset")
.and_then(Value::as_u64)
.expect("finding carries a numeric location.offset");
assert_eq!(
offset, 13,
"the token starts after `GITHUB_TOKEN=` (13 bytes); got offset {offset}"
);
assert_accel_matches_cpu_or_fails_closed(home.path(), &path, "simd");
}
#[test]
fn cpu_fallback_alias_matches_cpu() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (code_cpu, out_cpu, _) = scan(home.path(), &path, Some("cpu"), &[]);
let (code_alias, out_alias, _) = scan(home.path(), &path, Some("cpu-fallback"), &[]);
assert_eq!(code_cpu, Some(1));
assert_eq!(
code_alias, code_cpu,
"`cpu-fallback` is an alias of `cpu` and must share its exit code"
);
assert_eq!(
ordered_findings(&out_alias),
ordered_findings(&out_cpu),
"`--backend cpu-fallback` must be identical to `--backend cpu`"
);
}
#[test]
fn simd_regex_alias_matches_simd() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (code_simd, out_simd, _) = scan(home.path(), &path, Some("simd"), &[]);
let (code_alias, out_alias, _) = scan(home.path(), &path, Some("simd-regex"), &[]);
assert_eq!(
code_alias, code_simd,
"`simd-regex` is an alias of `simd` and must share its exit code"
);
if code_simd != Some(EXIT_BACKEND_UNAVAILABLE) {
assert_eq!(
ordered_findings(&out_alias),
ordered_findings(&out_simd),
"`--backend simd-regex` must be identical to `--backend simd`"
);
} else {
assert_eq!(
out_alias, out_simd,
"both aliases must fail closed with identical (empty) output"
);
}
}
#[test]
fn two_distinct_secrets_surface_identically_across_cpu_and_simd() {
let home = cache_home();
let content = format!("GITHUB_TOKEN={GHP}\nAWS_ACCESS_KEY_ID={AKIA}\n");
let (_d, path) = fixture("multi.env", &content);
let (code_cpu, out_cpu, _) = scan(home.path(), &path, Some("cpu"), &[]);
assert_eq!(code_cpu, Some(1));
let ids_cpu = detector_ids(&out_cpu);
assert_eq!(
ids_cpu,
vec![AKIA_DETECTOR.to_string(), GHP_DETECTOR.to_string()],
"cpu must surface BOTH planted detectors; got {ids_cpu:?}\nstdout={out_cpu}"
);
assert_accel_matches_cpu_or_fails_closed(home.path(), &path, "simd");
}
#[test]
fn clean_file_yields_zero_findings_and_exit_zero_on_cpu() {
let home = cache_home();
let (_d, path) = fixture("clean.txt", "the quick brown fox jumps over the lazy dog\n");
let (code, out, err) = scan(home.path(), &path, Some("cpu"), &[]);
assert_eq!(
code,
Some(0),
"a clean file exits 0 (no secrets); stderr={err}"
);
assert_eq!(
detector_ids(&out),
Vec::<String>::new(),
"the cpu backend must report ZERO findings for clean text; stdout={out}"
);
}
#[test]
fn clean_file_yields_the_same_empty_set_on_cpu_and_simd() {
let home = cache_home();
let (_d, path) = fixture("clean.txt", "the quick brown fox jumps over the lazy dog\n");
let (code_cpu, out_cpu, _) = scan(home.path(), &path, Some("cpu"), &[]);
assert_eq!(code_cpu, Some(0), "cpu clean-file exit 0");
assert_eq!(detector_ids(&out_cpu), Vec::<String>::new());
assert_accel_matches_cpu_or_fails_closed(home.path(), &path, "simd");
}
#[test]
fn scalar_alias_is_rejected_by_the_cli_parser_exit_2() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (code, _out, err) = scan(home.path(), &path, Some("scalar"), &[]);
assert_eq!(
code,
Some(2),
"clap rejects the unadvertised `scalar` value -> exit 2 (user error); stderr={err}"
);
assert!(
err.contains("scalar"),
"the parser error must name the rejected value; stderr={err}"
);
assert!(
err.contains("possible values") || err.contains("invalid value"),
"clap must explain the valid set / invalid value; stderr={err}"
);
}
#[test]
fn unknown_backend_value_is_rejected_by_the_cli_parser_exit_2() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (code, _out, err) = scan(home.path(), &path, Some("turbo"), &[]);
assert_eq!(
code,
Some(2),
"an unknown backend value must fail closed at parse time -> exit 2; stderr={err}"
);
assert!(
err.contains("turbo"),
"the parser error must name the rejected `turbo` value; stderr={err}"
);
}
#[test]
fn auto_backend_without_calibration_leaves_input_unscanned() {
let home = cache_home();
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (code, out, err) = scan(
home.path(),
&path,
Some("auto"),
&["--autoroute-cache", "off"],
);
let (_c, out_cpu, _) = scan(home.path(), &path, Some("cpu"), &[]);
assert_eq!(
code,
Some(13),
"uncalibrated auto must report incomplete coverage; stdout={out} stderr={err}"
);
assert!(
err.contains("autoroute calibration required")
&& err.contains("No backend was selected")
&& err.contains("batch was not scanned")
&& !err.contains("scalar correctness recovery"),
"failure must be operator-visible with repair context; stderr={err}"
);
assert!(
ordered_findings(&out).is_empty(),
"unrouted input must not produce findings"
);
assert!(
!ordered_findings(&out_cpu).is_empty(),
"explicit CPU control must prove the secret-bearing fixture is detectable"
);
}
#[test]
fn calibrated_auto_backend_surfaces_the_same_finding_set_as_cpu() {
let home = cache_home();
let calibrate = Command::new(binary())
.env("HOME", home.path())
.env("XDG_CACHE_HOME", home.path())
.env("NO_COLOR", "1")
.env("RAYON_NUM_THREADS", "4")
.env(
"KEYHOG_CI_AUTOROUTE_TIMING_FIXTURE",
"confidence-separated-v1",
)
.env(
"KEYHOG_CI_AUTOROUTE_FIXTURE_AUTH",
"bench-backend-parity-v1",
)
.env("KEYHOG_CI_AUTOROUTE_WORKLOAD_FIXTURE", "bounded-e2e-v1")
.env(
"KEYHOG_CI_AUTOROUTE_WORKLOAD_FIXTURE_AUTH",
"core-workload-plan-v1",
)
.args(["calibrate-autoroute", "--quiet"])
.output()
.expect("spawn keyhog calibrate-autoroute");
assert_eq!(
calibrate.status.code(),
Some(0),
"calibrate-autoroute must prime the cache (exit 0); stderr={}",
String::from_utf8_lossy(&calibrate.stderr)
);
let (_d, path) = fixture("leak.env", &format!("GITHUB_TOKEN={GHP}\n"));
let (code_auto, out_auto, err_auto) = scan(home.path(), &path, Some("auto"), &[]);
let (_code_cpu, out_cpu, _) = scan(home.path(), &path, Some("cpu"), &[]);
assert_eq!(
code_auto,
Some(1),
"calibrated autoroute must complete the scan and find the plant (exit 1); \
stdout={out_auto} stderr={err_auto}"
);
assert_eq!(
detector_ids(&out_auto),
vec![GHP_DETECTOR.to_string()],
"calibrated auto must surface exactly github-classic-pat; stdout={out_auto}"
);
assert_eq!(
cred_hashes(&out_auto),
cred_hashes(&out_cpu),
"calibrated auto and explicit cpu must agree on the credential hash"
);
}