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
//! Adversarial audit — VECTOR 6 GENERALIZATION (KEY: generalization)
//!
//! Finding: the generic-secret / generic-detector ENTROPY GATE is a hardcoded
//! magic constant, NOT derived from the operator-configurable Tier-A
//! `entropy_threshold` knob.
//!
//! keyhog documents `--entropy-threshold <BITS>` (and the `.keyhog.toml`
//! `entropy_threshold` field) as the lever that controls entropy-based
//! detection:
//! * `crates/cli/src/config.rs:52` — "Entropy threshold in bits per byte
//! (default: 4.5)."
//! * `.keyhog.toml.example:74-78` — "Entropy threshold in bits per byte
//! (default: 4.5) … 5.5: Conservative (fewer findings, fewer false
//! positives)."
//! * `crates/core/src/config.rs:22,114` — `entropy_threshold: 4.5`.
//!
//! That knob is carried into the engine as `ScannerConfig.entropy_threshold`
//! (`crates/scanner/src/scanner_config.rs:200`) and it DOES drive the
//! entropy-only scanner + confidence tiers
//! (`crates/scanner/src/confidence/mod.rs:60-76`,
//! `crates/scanner/src/entropy/scanner.rs:369`).
//!
//! But the GENERIC detector path uses two SEPARATE, DIVERGENT, HARDCODED floor
//! tables that never read `entropy_threshold`:
//!
//! * `crates/scanner/src/engine/scan_filters.rs:184-202`
//! `generic_entropy_floor(detector_id, credential_len)`:
//! "generic-api-key" len<=24 => 3.0
//! "generic-api-key" len<=40 => 2.8
//! "generic-api-key" => 3.5
//! "generic-password" => 2.5
//! "generic-database-url" => 2.0
//! _ => 3.5
//! (consumed by `crates/scanner/src/engine/process.rs:136`).
//!
//! * `crates/scanner/src/engine/fallback_generic.rs:165-171`
//! (the `SECRET_NAME = "value"` -> `generic-secret` fallback):
//! value.len() <= 24 => 2.8
//! value.len() <= 40 => 3.2
//! _ => 3.5
//!
//! Two consequences, both VECTOR-6 violations ("hardcoded lists / magic
//! constants … hardcoded thresholds … replace hardcoding with data-driven
//! contracts"):
//! (a) The same conceptual decision ("is this generic value high-entropy
//! enough to report?") is encoded TWICE with DIFFERENT numbers
//! (len<=24: 3.0 vs 2.8; len 25..=40: 2.8 vs 3.2) — one constant source
//! is required, not two.
//! (b) NEITHER table reads the operator's `entropy_threshold`. There is no
//! Tier-A value or `.keyhog.toml` setting that can tighten (or loosen)
//! the generic entropy gate; the threshold is baked into the binary.
//!
//! These black-box tests drive the freshly built `keyhog` binary
//! (`env!("CARGO_BIN_EXE_keyhog")`) and prove (b): cranking
//! `--entropy-threshold` from its 4.5 default up to (and past) the documented
//! maximum has ZERO effect on a moderate-entropy generic finding.
//!
//! Expected fix: route the generic entropy gate through the resolved
//! `ScannerConfig.entropy_threshold` (one Tier-A source of truth) instead of
//! the two hardcoded match tables — e.g. a single
//! `generic_entropy_floor(cfg.entropy_threshold, detector_id, len)` that scales
//! with the operator knob. After that fix, a value below the operator's
//! threshold is suppressed and these tests PASS.
use PathBuf;
use Command;
use TempDir;
/// Write `content` to a neutrally named temp file and scan it with the given
/// extra args + `--format json`. Returns (parsed findings, exit code).
/// The fixture below is `api_key = "<24-char mixed-case+digit value>"`.
/// The value `aAbBcCdDeEfFgGhH12345678` has Shannon entropy ~4.585 bits/byte
/// and is reported by the built-in `generic-secret` detector at the default
/// `--entropy-threshold 4.5`. This is a precondition for the two assertions
/// that follow — if this stops firing, the later tests are vacuous.
const GENERIC_FIXTURE: &str = "api_key = \"aAbBcCdDeEfFgGhH12345678\"\n";
/// AUD-generalization-1: raising `--entropy-threshold` from 4.5 to 6.0 must
/// change which entropy-gated findings survive, per the documented semantics
/// ("5.5: Conservative (fewer findings)"). It does NOT, because the
/// generic-secret entropy gate is the hardcoded
/// `fallback_generic.rs:165-171` table (2.8/3.2/3.5), which ignores
/// `entropy_threshold` entirely.
///
/// FAILS NOW: the finding is reported identically at 4.5 and at 6.0.
/// PASSES AFTER FIX: once the generic gate honors `entropy_threshold`, the
/// entropy-4.585 value falls below a 6.0 threshold and is suppressed, so the
/// generic finding count drops to 0 at 6.0 while remaining 1 at 4.5.
/// AUD-generalization-2 (boundary/extreme): set `--entropy-threshold` to 8.0,
/// the documented MAXIMUM ("bits per byte" — byte-level Shannon entropy is
/// bounded above by log2(256) = 8.0, and an ASCII token can never approach it).
/// At this setting NO realistic generic credential should pass an
/// entropy-driven gate. Yet `api_key = "<entropy 4.585>"` is still reported and
/// the process exits 1 (findings present).
///
/// This proves there is NO operator-reachable entropy setting that tightens the
/// generic gate — the threshold is hardcoded in the binary
/// (fallback_generic.rs:165-171 / scan_filters.rs:184-202), the textbook
/// Vector-6 "hardcoded threshold that should be data-driven / overridable"
/// defect.
///
/// FAILS NOW: finding reported + exit 1 at the documented max threshold.
/// PASSES AFTER FIX: once the generic gate honors `entropy_threshold`, an
/// 8.0 threshold suppresses every ASCII generic value, so this file scans
/// clean (no generic finding, exit 0).