keyhog_scanner/lib.rs
1//! KeyHog Scanner: A high-performance, multi-layered secret detection engine.
2//!
3//! This crate implements the core scanning logic, combining SIMD pre-filtering,
4//! Aho-Corasick literal matching, regex fallback, and ML-based confidence scoring.
5//!
6//! # Module map (by pipeline stage)
7//!
8//! The modules below are declared in dependency order, but they READ in pipeline
9//! order, the same bytes→finding flow as [`docs/src/architecture.md`] and the
10//! method-level map in [`engine`] (`engine::mod` "# The one flow"). To find a
11//! responsibility, locate its stage:
12//!
13//! - **Config / state / shared types**: [`scanner_config`], [`scan_state`],
14//! [`types`], [`hw_probe`] (hardware routing), [`error`].
15//! - **Phase 1 · prefilter** (cheap "could a detector fire here?")
16//! [`alphabet_filter`], [`bigram_bloom`], [`prefix_trie`], `ascii_ci`,
17//! `simd` / `simdsieve_prefilter` (feature-gated), `prefilter_degrade`
18//! (loud Law-10 fallback).
19//! - **Compile and lifecycle** (detectors → matchers): `compiled_scanner/`,
20//! [`compiler`], `shared_regexes`, [`static_intern`].
21//! - **Scan engine** (phase 1 triggers + phase 2 extraction; CPU or GPU):
22//! [`engine`] (start at its header doc), [`pipeline`], [`gpu`]. Public scan
23//! entry methods live in `compiled_scanner/runtime.rs` and dispatch here.
24//! - **Decode-through** (nested base64/hex/url/unicode, recursive)
25//! [`decode`], [`decode_structure`].
26//! - **Entropy**: [`entropy`] is now the single home for all of it: the
27//! keyword/scanner detection logic plus the fast Shannon-entropy primitive
28//! `entropy::fast` (+ `entropy::avx512` / `entropy::fast_x86` /
29//! `entropy::fast_neon` SIMD impls, arch-gated).
30//! - **Confidence / ML**: [`ml_scorer`] (serves the embedded `weights.bin`;
31//! trained out-of-band by the repo's `ml/`), [`confidence`],
32//! `probabilistic_gate`.
33//! - **Context, fragment reassembly, multiline, suppression, resolution**
34//! [`context`], `fragment_cache`, [`multiline`], `suppression`,
35//! [`resolution`], `structured`.
36//! - **Specialized validators**: [`checksum`], [`jwt`], [`aws`],
37//! `homoglyph`, [`unicode_hardening`].
38//! - **Cross-cutting**: `platform_compat`, `placeholder_words`,
39//! `process_exit`, [`telemetry`], `util_hash`.
40//!
41//! Most single-file modules are one responsibility each; the multi-file engine
42//! is the exception and carries its own internal map in `engine::mod`.
43
44#![deny(unsafe_op_in_unsafe_fn)]
45#![allow(clippy::too_many_arguments)]
46
47use std::borrow::Cow;
48
49#[cfg(test)]
50extern crate self as keyhog_scanner;
51
52#[cfg(test)]
53#[path = "../tests/adversarial/mod.rs"]
54mod adversarial;
55#[cfg(test)]
56#[path = "../tests/gap/mod.rs"]
57mod gap;
58#[cfg(test)]
59#[path = "../tests/gate/mod.rs"]
60mod gate;
61#[cfg(test)]
62#[path = "../tests/property/mod.rs"]
63mod property;
64#[cfg(test)]
65#[path = "../tests/support/mod.rs"]
66mod support;
67#[cfg(test)]
68#[path = "../tests/unit/mod.rs"]
69mod unit;
70
71// ── Public API ──────────────────────────────────────────────────────
72pub(crate) mod api;
73/// Compiled detector-owned assignment-key admission index.
74mod assignment_keyword_matcher;
75/// Tier-B generic credential-assignment keyword vocabulary (phase-2 prefilter).
76pub(crate) mod assignment_keywords;
77/// Offline AWS account-ID recovery from an access-key ID (no network/verify).
78pub mod aws;
79/// Service-specific credential checksum validation (GitHub, npm, Slack, etc.).
80pub mod checksum;
81/// Compiled scanner construction and lifecycle implementation.
82mod compiled_scanner;
83/// Detector compilation into high-performance matching structures.
84pub(crate) mod compiler;
85/// Heuristic and ML-based confidence scoring for candidate matches.
86pub(crate) mod confidence;
87/// Code context analysis (comments, assignments, test files).
88pub mod context;
89pub(crate) mod credential_context_keywords;
90pub(crate) mod credential_shapes;
91pub(crate) mod deadline;
92/// Decode-through pipeline for nested encodings (base64, hex, URL, etc.).
93pub mod decode;
94/// Decode-structure analysis: classify what a candidate base64/hex-decodes to
95/// (binary asset magic bytes, protobuf wire) so decode-through feeds scoring.
96pub(crate) mod decode_structure;
97pub(crate) mod detector_catalog;
98/// Cache-local detector facts used by candidate execution and emission.
99pub(crate) mod detector_execution_policy;
100/// Canonical detector-id strings and scanner-side detector-family predicates.
101pub(crate) mod detector_ids;
102/// Compiled canonical/decoded key-material policy from detector TOMLs.
103pub(crate) mod detector_key_material_policy;
104/// Compiled cache-local form of detector-owned model policy.
105#[cfg(feature = "ml")]
106pub(crate) mod detector_ml_policy;
107/// Unified detector-indexed runtime plan compiled from detector TOMLs.
108pub(crate) mod detector_plan;
109/// Core scan execution engine.
110pub(crate) mod engine;
111/// Shannon entropy analysis for secret detection.
112pub mod entropy;
113/// Tier-B per-family generic-detector entropy-floor calibration table.
114/// Specialized error types for the scanner.
115pub(crate) mod error;
116/// Cross-chunk fragment reassembly cache.
117pub(crate) mod fragment_cache;
118/// Detector-owned generic assignment value-shape adjudication.
119mod generic_assignment_shape;
120/// Named-detector ownership for assignment-key fallback suppression.
121pub(crate) mod generic_keyword_owner;
122/// GPU-accelerated matching via wgpu.
123pub mod gpu;
124/// Scanner GPU batch input policy.
125pub(crate) mod gpu_input_budget;
126/// GPU literal artifact compilation from the typed detector plan.
127pub(crate) mod gpu_literal_artifacts;
128/// Persistent GPU matcher artifact cache.
129pub(crate) mod gpu_matcher_cache;
130/// Hardware capability detection and backend selection.
131pub mod hw_probe;
132/// Machine learning inference for secret scoring.
133pub mod ml_scorer;
134/// Multiline secret reassembly logic.
135pub(crate) mod multiline;
136/// Pure phase-two regex truncation and UTF-8 focus boundaries.
137pub(crate) mod phase2_truncate;
138pub(crate) mod placeholder_words;
139pub(crate) mod platform_compat;
140pub(crate) mod process_exit;
141/// Match resolution and deduplication.
142pub mod resolution;
143/// Process-wide scan profiling and diagnostics.
144pub(crate) mod scan_profile;
145/// Runtime match heap, interners, and ML pending queue for one scan.
146pub(crate) mod scan_state;
147/// Scanner configuration and state.
148pub(crate) mod scanner_config;
149/// Tier-B distinctive vendor secret-prefix vocabulary for the multiline no-hit gate.
150pub(crate) mod secret_prefixes;
151/// Coalesced match-to-input attribution primitive.
152pub(crate) mod segment_attribution;
153/// Static-string interner backed by a single-hash `ahash` map.
154/// Used by `CompiledScanner` to pre-intern detector metadata strings
155/// so the per-scan `ScanState` interner is hit only by dynamic
156/// strings (file paths, commit SHAs).
157pub(crate) mod static_intern;
158/// Shared types for the scanner engine.
159pub(crate) mod types;
160
161// Internal modules.
162pub(crate) mod adjudicate;
163/// SIMD-accelerated alphabet pre-filtering.
164pub(crate) mod alphabet_filter;
165pub(crate) mod anchored_regex;
166/// ASCII case-insensitive byte-search primitives shared by every hot path
167/// that needs to skim text without lowering the haystack first.
168pub(crate) mod ascii_ci;
169/// Bigram bloom filter for fast chunk gating.
170pub(crate) mod bigram_bloom;
171// The fast Shannon-entropy primitives (scalar dispatcher + AVX-512 / AVX2-SSE2 /
172// NEON SIMD impls) now live UNDER `entropy/` (entropy::fast / ::avx512 /
173// ::fast_x86 / ::fast_neon) (one home for all entropy code. See `entropy/mod.rs`).
174pub(crate) mod homoglyph;
175/// JWT structural validation and anomaly detection.
176pub mod jwt;
177/// Internal scan pipeline orchestration.
178pub(crate) mod pipeline;
179/// Prefix trie for efficient keyword propagation.
180pub(crate) mod prefix_trie;
181pub(crate) mod probabilistic_gate;
182pub(crate) mod structured;
183pub(crate) mod suppression;
184/// Per-scan telemetry: always-on counters + opt-in `--dogfood` events.
185pub mod telemetry;
186/// Shared parse + validate primitive for Tier-B single-column token lists
187/// (assignment keywords, multiline secret prefixes) (one owner, no drift).
188pub(crate) mod tier_b_list;
189pub(crate) mod tuning;
190/// Unicode normalization and homoglyph defense.
191pub(crate) mod unicode_hardening;
192/// Shared FNV-1a hash + content-keyed memoization primitives. Single home for
193/// the seed every per-scan cache keys on, plus the bounded thread-local cache
194/// helper they all share, so a hash change can never re-key only some caches.
195pub mod util_hash;
196
197/// Loud, recall-preserving degradation for static prefilter automata (Law 10).
198pub(crate) mod prefilter_degrade;
199
200pub(crate) use engine::floor_char_boundary;
201/// SHA-256 of a credential as the `CredentialHash` domain type. Re-exported
202/// from the single canonical implementation in `keyhog_core` so the scanner,
203/// core dedup, and telemetry all hash credentials identically (no second copy
204/// to drift). Hex encoding is a separate step at the serde/reporter boundary
205/// (`keyhog_core::hex_encode`), keeping the pre-dedup hot path zero-heap.
206pub(crate) use keyhog_core::sha256_hash;
207pub(crate) use pipeline::compute_line_offsets;
208
209#[cfg(feature = "simd")]
210pub(crate) mod simd;
211#[cfg(feature = "simdsieve")]
212mod simdsieve_prefilter;
213
214pub(crate) mod shared_regexes;
215
216pub use api::*;
217/// Install a pre-exit hook for scanner hard-stops (`process::exit` paths).
218///
219/// The CLI uses this to dump rate-limited WARN summaries that Drop would skip.
220pub use process_exit::set_pre_exit_hook;
221
222/// Configure the Hyperscan compiled-database cache directory for this process.
223///
224/// Call before compiling a scanner. `None` restores the platform default
225/// (`dirs::cache_dir()/keyhog`, with a per-user temp fallback). The SIMD backend
226/// still validates the final directory: explicit paths must live under the
227/// user's home or the per-uid keyhog temp cache root, must be user-owned, and
228/// must not be symlinks.
229#[cfg(feature = "simd")]
230pub fn set_hyperscan_cache_dir(path: Option<std::path::PathBuf>) {
231 simd::backend::set_configured_cache_dir(path);
232}
233
234/// Validate an explicit Hyperscan cache directory without compiling a scanner.
235#[cfg(feature = "simd")]
236pub fn validate_hyperscan_cache_dir(path: &std::path::Path) -> std::result::Result<(), String> {
237 simd::backend::validate_configured_cache_dir(path)
238}
239
240/// True when `detector_id` names the pure-entropy fallback family (`"entropy"`
241/// or any `"entropy-*"` id such as `entropy-token`).
242///
243/// Pure-entropy detectors fire on the Shannon entropy of the matched character
244/// run rather than on a distinctive prefix/shape, so whether one fires for a
245/// given secret is *context-dependent*: the same bytes embedded in a longer
246/// token run (a connection-string URL, a `key=` assignment) can dilute below the
247/// entropy gate even though they fire in isolation. Consumers that categorize a
248/// finding by detector family, and the contract test harness, which must not
249/// gate context-dependent firings all-or-nothing, use this to distinguish the
250/// entropy fallback from service-anchored detectors without re-encoding the
251/// naming contract owned by [`detector_ids`].
252#[inline]
253pub fn is_entropy_detector(detector_id: &str) -> bool {
254 detector_ids::is_entropy_detector(detector_id)
255}
256
257/// True for a detector that fires via the entropy / phase2-generic path (the
258/// `generic-*` family + entropy fallback), carrying ZERO patterns by design.
259#[inline]
260pub fn is_generic_or_entropy_detector(detector_id: &str) -> bool {
261 detector_ids::is_generic_or_entropy_detector(detector_id)
262}
263
264/// Strip invisible-reorder evasion characters (zero-width + RTL override, per
265/// [`unicode_hardening::is_evasion_char`]) from context-window text. Deliberately
266/// narrower than [`unicode_hardening::normalize_homoglyphs`]: this feeds the
267/// surrounding-context features, where collapsing homoglyphs/fullwidth/combining
268/// marks in ordinary prose would distort keyword and comment context; homoglyph
269/// folding stays on the credential-value scan path.
270pub(crate) fn normalize_chunk_data(data: &str) -> Cow<'_, str> {
271 if data.is_ascii() {
272 return Cow::Borrowed(data);
273 }
274 let mut normalized: Option<String> = None;
275 for (byte_pos, ch) in data.char_indices() {
276 if unicode_hardening::is_evasion_char(ch) {
277 normalized.get_or_insert_with(|| {
278 let mut out = String::with_capacity(data.len());
279 out.push_str(&data[..byte_pos]);
280 out
281 });
282 } else if let Some(out) = &mut normalized {
283 out.push(ch);
284 }
285 }
286 normalized.map(Cow::Owned).unwrap_or(Cow::Borrowed(data)) // LAW10: no evasion chars means the original scan text is byte-preserved.
287}
288
289#[doc(hidden)]
290pub mod testing;