use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
#[path = "../support/json_report.rs"]
mod json_report_support;
use json_report_support::parse_json_array;
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(["--backend", "simd"])
.args(args)
.arg("--format")
.arg("json")
.arg("--daemon=off")
.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_negative_twin_is_subset_of_default() {
let fixture = concat!(
"aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n",
"ABUSEIPDB_API_KEY=Kp4Qx7Rm2Sn5Tb8Vw3YzKp4Qx7Rm2Sn5Tb8Vw3YzKp4Qx7Rm2Sn5Tb8Vw3YzKp4Qx7Rm2Sn5Tb8Vw3Yz\n",
);
let (def_out, _, _) = scan_with_args(fixture, &[]);
let (prec_out, _, _) = scan_with_args(fixture, &["--precision"]);
let def: Vec<String> = parse_json_array(&def_out, "default precision negative-twin scan")
.iter()
.filter_map(|finding| {
finding
.get("detector_id")
.and_then(|value| value.as_str())
.map(String::from)
})
.collect();
let prec: Vec<String> = parse_json_array(&prec_out, "explicit precision negative-twin scan")
.iter()
.filter_map(|finding| {
finding
.get("detector_id")
.and_then(|value| value.as_str())
.map(String::from)
})
.collect();
for det in &prec {
assert!(
def.contains(det),
"precision found detector {det:?} but default didn't; \
this violates the subset property. default={def:?}, precision={prec:?}"
);
}
assert!(
prec.len() < def.len(),
"precision must be strictly tighter than default; \
default={def:?}, precision={prec:?}"
);
}
#[test]
fn precision_mode_composes_with_no_suppress_test_fixtures() {
let stripe_key = concat!("sk_", "live_", "4eC39HqLyjWDarjtT1zdp7dc");
let fixture = format!("STRIPE_KEY = \"{stripe_key}\"\n");
let (def_suppressed, _, def_code) = scan_with_args(&fixture, &[]);
assert_eq!(
def_code,
Some(0),
"default mode suppresses the Stripe demo key"
);
let def: Vec<String> =
parse_json_array(&def_suppressed, "default suppressed Stripe precision scan")
.iter()
.filter_map(|finding| {
finding
.get("service")
.and_then(|value| value.as_str())
.map(String::from)
})
.collect();
assert!(
def.is_empty(),
"default suppresses Stripe and should emit no findings for this fixture; got {def:?}"
);
let (prec_nosuppress, _, prec_code) =
scan_with_args(&fixture, &["--precision", "--no-suppress-test-fixtures"]);
assert_eq!(
prec_code,
Some(1),
"precision with --no-suppress-test-fixtures must find the Stripe key"
);
let prec: Vec<String> = parse_json_array(&prec_nosuppress, "precision no-suppress Stripe scan")
.iter()
.filter_map(|finding| {
finding
.get("service")
.and_then(|value| value.as_str())
.map(String::from)
})
.collect();
assert!(
prec.contains(&"stripe".to_string()),
"precision --no-suppress-test-fixtures must find Stripe; got {prec:?}"
);
}
#[test]
fn precision_mode_respects_min_confidence_when_higher() {
let fixture = "aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n";
let (out, _, code) = scan_with_args(fixture, &["--precision", "--min-confidence", "0.9"]);
assert_eq!(
code,
Some(1),
"--precision --min-confidence 0.9 must find the high-confidence AWS secret"
);
let findings: serde_json::Value = serde_json::from_str(&out).expect("JSON");
let arr = findings.as_array().expect("array");
assert!(
!arr.is_empty(),
"the AWS secret (conf > 0.9) must survive max(0.85, 0.9)"
);
}
#[test]
fn precision_mode_empty_file_exits_thirteen() {
let fixture = "";
let (_out, _, code) = scan_with_args(fixture, &["--precision"]);
assert_eq!(
code,
Some(13),
"empty file scans zero bytes and must exit 13"
);
}
#[test]
fn precision_mode_whitespace_only_exits_zero() {
let fixture = " \n\t\n # just comments\n";
let (out, _, code) = scan_with_args(fixture, &["--precision"]);
assert_eq!(code, Some(0), "whitespace-only file must exit 0");
let findings: serde_json::Value = serde_json::from_str(&out).expect("JSON");
let arr = findings.as_array().expect("array");
assert!(arr.is_empty(), "whitespace-only file must have no findings");
}
#[test]
fn precision_mode_composes_with_verify_flag() {
let fixture = "aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n";
let (_out, err, code) = scan_with_args(fixture, &["--precision", "--verify"]);
assert!(
code.is_some_and(|c| c == 0 || c == 1),
"precision --verify must succeed (exit 0 or 1 depending on findings/verify result); \
got {code:?}, stderr={err}"
);
}
#[test]
fn precision_mode_composes_with_scan_comments() {
let fixture =
"// TODO: rotate aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n";
let (out, err, code) = scan_with_args(fixture, &["--precision", "--scan-comments"]);
assert!(
code.is_some_and(|c| c == 1),
"precision --scan-comments must find the AWS secret in comment; \
got {code:?}, stderr={err}"
);
let findings: serde_json::Value = serde_json::from_str(&out).expect("JSON");
let arr = findings.as_array().expect("array");
assert!(
!arr.is_empty(),
"precision --scan-comments must find the AWS secret; got {out}"
);
}
#[test]
fn precision_mode_ignores_min_confidence_when_lower_than_0_85() {
let fixture = concat!(
"ABUSEIPDB_API_KEY=",
"Kp4Qx7Rm2Sn5Tb8Vw3YzKp4Qx7Rm2Sn5Tb8Vw3Yz",
"Kp4Qx7Rm2Sn5Tb8Vw3YzKp4Qx7Rm2Sn5Tb8Vw3Yz\n",
);
let (default_out, _, default_code) = scan_with_args(fixture, &[]);
assert_eq!(default_code, Some(1));
let default = parse_json_array(&default_out, "default lower-override twin");
assert!(default.iter().any(|finding| {
finding.get("detector_id").and_then(|value| value.as_str()) == Some("abuseipdb-api-key")
&& finding.get("confidence").and_then(|value| value.as_f64()) == Some(0.795)
}));
let (out, _, code) = scan_with_args(fixture, &["--precision", "--min-confidence", "0.3"]);
assert_eq!(
code,
Some(0),
"precision must enforce 0.85 even with --min-confidence 0.3"
);
let findings: serde_json::Value = serde_json::from_str(&out).expect("JSON");
let arr = findings.as_array().expect("array");
assert!(
arr.is_empty(),
"precision must drop the 0.795 credential; got {arr:?}"
);
}
#[test]
fn precision_mode_tightens_large_mixed_corpus() {
let fixture = concat!(
"# Real credentials\n",
"aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n",
"GH_TOKEN = \"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\"\n",
"# Weak default-only generic finding\n",
"DATABASE_PASSWORD = \"admin123\"\n",
);
let (def_out, _, _) = scan_with_args(fixture, &[]);
let (prec_out, _, _) = scan_with_args(fixture, &["--precision"]);
let def_count = parse_json_array(&def_out, "default large mixed precision scan").len();
let prec_count = parse_json_array(&prec_out, "explicit large mixed precision scan").len();
assert!(
def_count > prec_count,
"precision must reduce the finding count on a large mixed corpus; \
default={def_count}, precision={prec_count}"
);
assert!(
prec_count > 0,
"precision must still find the high-confidence credentials; got count={prec_count}"
);
}