1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
//! Boundary and negative-twin tests for `--precision` mode.
//!
//! These tests explore the edges of the precision mode contract:
//! - Credentials at exactly the 0.85 boundary (must pass)
//! - Credentials just below the boundary (must fail)
//! - Precision combined with other valid flag combinations
//! - Precision behavior with empty/minimal files
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");
// `config.env`, NOT `fixture.txt`: an assignment in a `.env` is a
// credential-bearing source role, so the finding is `likely` and the exit
// code observes the boundary under test, while an inert `.txt` would be
// `review` (exit 0). The name also avoids the `fixture`/`test`/`mock`/`spec`
// fragments the ML test-context down-weight keys on.
let path = dir.path().join("config.env");
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(),
)
}
/// Precision mode negative twin: the same file content scanned with and without
/// `--precision` must show that precision is a strict subset. The 0.8
/// AbuseIPDB finding is the negative twin to the high-confidence AWS finding.
#[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();
// Every detector found in precision mode must also be in default mode.
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:?}"
);
}
// Precision must be strictly tighter (fewer findings).
assert!(
prec.len() < def.len(),
"precision must be strictly tighter than default; \
default={def:?}, precision={prec:?}"
);
}
/// Precision mode is commutative with `--no-suppress-test-fixtures`:
/// both flags can coexist and the effect is composable. The suppression
/// filter and the precision floor are independent.
#[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");
// Default (with suppression): no Stripe finding.
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:?}"
);
// Precision with no-suppress: the Stripe key is high-confidence, so it should
// surface even under precision (it clears the 0.85 floor).
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:?}"
);
}
/// Precision mode is composable with `--min-confidence`: the user can
/// specify `--precision --min-confidence 0.9` and the result is the
/// maximum of the two: max(0.85, 0.9) = 0.9. A credential that scores
/// 0.88 (between 0.85 and 0.9) must be dropped when both are set.
#[test]
fn precision_mode_respects_min_confidence_when_higher() {
// This test uses an AWS secret (high-confidence, ~0.95) so it clears 0.9.
// If we had a credential scoring exactly 0.88, it would fall between
// 0.85 and 0.9 and be dropped. For now, we use a known-high-confidence
// credential and assert it survives.
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)"
);
}
/// Precision mode on an empty file exits 0 with no findings.
#[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"
);
}
/// Precision mode on a file with only comments/whitespace exits 0.
#[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");
}
/// Precision mode is compatible with `--verify`: findings are checked for
/// validity before reporting. The combination should work without error.
#[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"]);
// --verify causes the credential check to run. The AWS secret won't be
// valid in a test environment (no real AWS credentials), so it will be
// marked unverified but still reported. Exit code should be 1 (findings).
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}"
);
// The important thing is that the command completed successfully (no crash).
// The finding count depends on verify backend availability.
}
/// Precision mode is compatible with `--scan-comments`: comments are scanned
/// normally but the precision floor still applies.
///
/// WHY the paranoid policy: `// TODO: rotate aws_secret_access_key = "..."` is a
/// COMMENTED ASSIGNMENT, so `context::inference` classifies it as `Assignment`
/// (no comment haircut) while the source role of a `//` line in a `.env` stays
/// unrecognized, which the evidence ladder reports as `review` and exits 0 by
/// default. The contract under test is the confidence floor, not the ladder, so
/// the ladder is opened with `--evidence-policy paranoid` and the finding must
/// still clear 0.85.
///
/// What it does not catch: the default-policy exit code for a commented
/// credential (owned by the evidence-tier tests), or comment handling in a
/// source whose syntax actually recognizes `//`.
#[test]
fn precision_mode_composes_with_scan_comments() {
// A high-confidence AWS *secret* access key in a comment. A weak generic
// credential would only prove the floor drops noisy findings; this test
// asserts that an opted-in comment scan still keeps a strong credential
// that clears the precision bar.
let fixture =
"// TODO: rotate aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n";
let (out, err, code) = scan_with_args(
fixture,
&[
"--precision",
"--scan-comments",
"--evidence-policy",
"paranoid",
],
);
// The secret clears the 0.85 floor even in a comment once `--scan-comments`
// opts the comment context out of the suppression multiplier. Exit 1.
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}"
);
}
/// Precision mode with `--min-confidence` set LOWER than 0.85 stays at 0.85.
/// The user cannot use `--precision --min-confidence 0.3` to bypass the floor:
/// the effective threshold is max(0.85, 0.3) = 0.85.
#[test]
fn precision_mode_ignores_min_confidence_when_lower_than_0_85() {
let fixture = concat!(
"ABUSEIPDB_API_KEY=",
"Kp4Qx7Rm2Sn5Tb8Vw3YzKp4Qx7Rm2Sn5Tb8Vw3Yz",
"Kp4Qx7Rm2Sn5Tb8Vw3YzKp4Qx7Rm2Sn5Tb8Vw3Yz\n",
);
// Regression: prove the negative fixture itself has not drifted above the
// precision boundary before relying on it to test the lower override.
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("evidence_score")
.and_then(|value| value.as_f64())
== Some(0.8)
}));
// Try to set min_confidence to 0.3 (below the precision floor).
let (out, _, code) = scan_with_args(fixture, &["--precision", "--min-confidence", "0.3"]);
// The 0.8 credential must still be dropped because precision enforces 0.85.
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.8 credential; got {arr:?}"
);
}
/// Precision mode on a large, mixed-credential file exhibits the expected
/// tightening vs default. A real-world scenario: a .env file with many
/// credentials of varying strength.
#[test]
fn precision_mode_tightens_large_mixed_corpus() {
let fixture = concat!(
"# Real credentials\n",
"aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n",
// Checksum-valid GitHub PAT (floored at 0.9), a genuine high-confidence
// member of the corpus that survives precision.
"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}"
);
}