use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_keyhog"))
}
fn scan_with_args(fixture: &str, args: &[&str]) -> (String, String, Option<i32>) {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("config.txt");
std::fs::write(&path, fixture).expect("write fixture");
let output = Command::new(binary())
.arg("scan")
.args(args)
.arg("--no-daemon")
.arg(&path)
.output()
.expect("spawn keyhog scan");
(
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
output.status.code(),
)
}
#[test]
fn precision_mode_disables_entropy_scoring() {
let fixture = concat!(
"base64_data = \"SGVsbG8gV29ybGQgSXMgQSBUZXN0Ig==\"\n",
"GH_TOKEN = \"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\"\n",
);
let (prec_out, _e, prec_code) = scan_with_args(fixture, &["--precision", "--format", "json"]);
assert_eq!(
prec_code,
Some(1),
"precision mode must find the GitHub token (entropy is disabled)"
);
let findings: serde_json::Value = serde_json::from_str(&prec_out).expect("precision JSON");
let arr = findings.as_array().expect("array");
assert!(
arr.iter()
.any(|f| f.get("service").and_then(|v| v.as_str()) == Some("github")),
"precision must find the GitHub token; got {arr:?}"
);
}
#[test]
fn precision_mode_single_layer_decode_depth_one() {
const ENCODED: &str =
"YXdzX3NlY3JldF9hY2Nlc3Nfa2V5PWtQOHhRMm1OdlI3dFo0d0w5YllzSDNqRDZmRzFjQTBlWHVWaUs1b1QK";
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("secret.yaml");
std::fs::write(
&path,
format!(
"apiVersion: v1\nkind: Secret\nmetadata:\n name: aws\ndata:\n cred.env: {ENCODED}\n"
),
)
.expect("write fixture");
let output = Command::new(binary())
.args(["scan", "--precision", "--format", "json", "--no-daemon"])
.arg(&path)
.output()
.expect("spawn keyhog scan");
let prec_out = String::from_utf8_lossy(&output.stdout);
let findings: serde_json::Value = serde_json::from_str(&prec_out).expect("precision JSON");
let arr = findings.as_array().expect("array");
assert!(
arr.iter()
.any(|f| f.get("detector_id").and_then(|v| v.as_str())
.is_some_and(|id| id.contains("aws"))),
"precision with decode_depth=1 must find the single-layer base64-encoded AWS secret; got {arr:?}"
);
}
#[test]
fn precision_mode_effective_config_shows_0_85_floor() {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("fixture.txt");
std::fs::write(&path, "ordinary content\n").expect("write fixture");
let output = Command::new(binary())
.arg("scan")
.arg("--precision")
.arg("--no-daemon")
.env("KEYHOG_PRINT_EFFECTIVE_CONFIG", "1")
.arg(&path)
.output()
.expect("spawn keyhog scan");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("min_confidence = 0.85"),
"effective config must show min_confidence = 0.85; got:\n{stdout}"
);
assert!(
stdout.contains("entropy_enabled = false"),
"effective config must show entropy_enabled = false; got:\n{stdout}"
);
assert!(
stdout.contains("max_decode_depth = 1"),
"effective config must show max_decode_depth = 1; got:\n{stdout}"
);
}
#[test]
fn precision_mode_rejects_entropy_threshold_override_at_clap_level() {
let (_out, err, code) =
scan_with_args("content\n", &["--precision", "--entropy-threshold", "5.0"]);
if code == Some(2) {
assert!(
err.to_lowercase().contains("conflict")
|| err.to_lowercase().contains("cannot be used"),
"clap error must name the conflict; stderr={err}"
);
} else {
assert_eq!(
code,
Some(0),
"scan must succeed (entropy_threshold has no effect)"
);
}
}
#[test]
fn precision_mode_global_floor_0_85_on_unspecified_detectors() {
let fixture = "PASSWORD = \"admin123\"\n";
let (prec_out, _e, prec_code) = scan_with_args(fixture, &["--precision", "--format", "json"]);
assert_eq!(
prec_code,
Some(0),
"precision mode must drop weak generic passwords (below 0.85 floor)"
);
let findings: serde_json::Value = serde_json::from_str(&prec_out).expect("JSON");
let arr = findings.as_array().expect("array");
assert!(
arr.is_empty(),
"precision must reject the weak generic password; got {arr:?}"
);
}
#[test]
fn precision_mode_uses_highest_floor_global_vs_per_detector() {
let dir = TempDir::new().expect("tempdir");
let aws_secret =
concat!("aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n");
std::fs::write(dir.path().join("planted.txt"), aws_secret).expect("write fixture");
let config = "[detector.aws-secret-access-key]\nmin_confidence = 0.90\n";
std::fs::write(dir.path().join(".keyhog.toml"), config).expect("write config");
let output = Command::new(binary())
.args(["scan", "--precision", "--format", "json", "--no-daemon"])
.arg(dir.path())
.output()
.expect("spawn precision scan with high per-detector floor");
assert_eq!(
output.status.code(),
Some(1),
"precision with per-detector floor 0.90 must find the AWS secret (conf >= 0.95)"
);
let findings: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout is JSON");
let arr = findings.as_array().expect("array");
assert!(
arr.iter()
.any(|f| f.get("detector_id").and_then(|v| v.as_str()) == Some("aws-secret-access-key")),
"precision must find the AWS secret; got {arr:?}"
);
}