Skip to content

Chore/detect secrets perf optimizations - #294

Merged
Saarett merged 11 commits into
masterfrom
chore/detect-secrets-perf-optimizations
Aug 12, 2026
Merged

Chore/detect secrets perf optimizations#294
Saarett merged 11 commits into
masterfrom
chore/detect-secrets-perf-optimizations

Conversation

@GillSami

@GillSami GillSami commented Aug 11, 2026

Copy link
Copy Markdown
  • Please check if the PR fulfills these requirements
  • Tests for the changes have been added
  • Docs have been added / updated
  • All CI checks are green
  • What kind of change does this PR introduce?

Summary

Reduces full production-scale secrets scan time by ~3.6x (≈40 minutes → 11m21s) via six targeted, low-risk optimizations to the scanning hot path, with zero correctness regressions verified at every step.

Performance Result

Measured with the exact production command against a 21,339-file synthetic monorepo:

checkov --framework secrets --enable-secret-scan-all-files -d synthetic-code-repo/ --compact --skip-results-upload -o json
Stage Time
Unoptimized baseline ~40m47s
+ DI cache, pre-gate, filter cache, entropy short-circuit ~12m16s–12m31s
+ PrivateKeyDetector file-size cache 11m21s

Net: ~3.6x faster, with identical scan findings before and after (12/12 on the synthetic repo, 343/343 on the correctness fixture) — the speedup comes from doing measurably less CPU work (confirmed via user/sys/%CPU profiling, not from OS or disk caching effects).

What Changed (one sentence each)

  1. DI plan cache (c6509ad) — caches the reflection-based dependency-injection plan per callable instead of recomputing it with inspect on every single invocation.
  2. Pre-gate (72ba48f) — skips full plugin analysis on lines that structurally cannot contain a secret, using a cheap superset check before the expensive per-plugin work.
  3. Filter list cache (3503f9f) — caches the result of get_filters_with_parameter() instead of rebuilding the filtered plugin list from scratch on every line.
  4. Entropy short-circuit (80971af) — returns early from calculate_shannon_entropy() for strings that are too short or insufficiently varied to ever cross any configured entropy threshold.
  5. Lazy logging (eaffec1) — switches hot-path log calls to lazy %-style formatting so log messages aren't string-built when the log level would discard them anyway.
  6. PrivateKeyDetector file-size cache (0bce612) — looks up each file's size once instead of once per line, matching the scope of upstream PR perf(private_key): look up file size once per file instead of per line #293.

(Two additional commits, 3168e33 and 0f08efe, are correctness-only fixes that pin filter configuration and restore/lock in the 343-finding parity baseline used to validate every change above — no performance impact themselves, but they were the safety net that caught a real regression during development, see below.)

Correctness Verification

  • Parity fixture (secrets-examples/, 225 files with known seeded secrets): 343/343 findings (detect-secrets CLI) and 204/204 failed checks (checkov) — identical before and after every commit, verified both by an automated parity-oracle test and an independent manual diff outside that test harness.
  • Full existing test suite: 1116 passed / 1 skipped / 6 xfailed — unchanged throughout.
  • Caught in the wild: an early version of the pre-gate (commit history, since fixed before this PR was opened) under-matched real detector patterns and silently dropped 12 legitimate findings; the parity oracle caught this immediately, which is exactly the scenario this safety net exists for.
  • Ruled out false explanations: independently verified via controlled, cache-equalized back-to-back runs that the ~3.6x speedup is not an artifact of OS filesystem or checkov-internal caching — both the "optimizations off" and "optimizations on" runs show ~100% CPU utilization (i.e., CPU-bound, not I/O-bound), and the optimized run does proportionally less actual CPU work (user time), not just faster I/O.

Feature Flags

Four of the six changes are individually toggleable via environment variables (all default enabled) for easy A/B verification or rollback without a code change:

  • DETECT_SECRETS_PERF_DI_CACHE
  • DETECT_SECRETS_PERF_PREGATE
  • DETECT_SECRETS_PERF_FILTER_CACHE
  • DETECT_SECRETS_PERF_ENTROPY_SC

The lazy-logging and PrivateKeyDetector changes are unconditional — they're behavior-preserving by construction (same log content, same detection outcome) and don't warrant a flag.

Risk Assessment

All changes are additive caching/short-circuit optimizations with no change to detection semantics — none alter what is matched, only how much redundant work is done to get there. The riskiest of the six (the pre-gate, since it's the only one capable of skipping data before any plugin sees it) is the one with the most test coverage: dedicated unit tests asserting it's a strict superset of every plugin's trigger patterns, plus the parity oracle as a continuous regression guard.

GillSami and others added 10 commits August 9, 2026 14:35
The call_function_with_arguments() function is called O(lines x plugins) times
during scanning. Previously, make_function_self_aware() ran on every call because
bound methods are new objects on each attribute access.

This change adds a _plan_cache dict keyed by id(func.__func__) for bound methods
(stable across calls). The injectable parameter set is computed once per distinct
callable and reused on subsequent calls.

Feature flag: DETECT_SECRETS_PERF_DI_CACHE=0 disables the cache for debugging.
The golden snapshot was silently regenerated from 343 to 306 findings when the
optional gibberish-detector package's ML filter auto-activated after dev
dependencies finished installing mid-session, masking 37 real findings.

Fix: explicitly disable this filter in all scan invocations via --disable-filter.
Added test_parity_oracle_filter_config_is_pinned regression guard.
Fixed a bug in the golden_findings fixture that read the wrong JSON keys
(filename/line_number instead of file/line) for the flat snapshot format.
Re-validated Task 3's DI cache: 343 findings both enabled and disabled.
…v timing

Root cause: golden snapshot was silently regenerated from 343 to 306 findings
when the optional gibberish-detector ML filter auto-activated mid-session.
Fixed by pinning --disable-filter detect_secrets.filters.gibberish.should_exclude_secret
in all scan invocations. Restored 343/225 baseline matching original Task 0 capture.

Also fixed checkov wiring: checkov's Python (pyenv 3.13.1) was silently using a
stale PyPI copy of bc-detect-secrets instead of our local fork, making all prior
checkov-based DI-cache comparisons meaningless. Installed our fork editable into
that environment.

Real full-repo (21,339 files) checkov timing, using the exact customer-facing
command: checkov --framework seccommand: checkov --framework seccommand: che
------------------------------------------------------------------------------------nd---------------------------------44---------------------------------------------------------------er, zero finding-count regression
Add _could_contain_secret() pre-filter in _process_line_based_plugins().
Lines that do not match the trigger pattern skip code_snippet construction
and all ~21 detector regex calls entirely.

The trigger pattern is a superset of all detector patterns, verified
empirically by the parity oracle (343 findings unchanged, zero regressions).

Iteration note: initial version used prefix-only keyword patterns
(api_key, db_pass, etc.) and length thresholds of 32-40 for entropy
candidates, modeled on assumptions rather than the real detector code.
This missed 12 real findings caught by the parity oracle: the actual
KeywordDetector DENYLIST matches bare/compound forms (key, pass, _pass,
client_key, service_key) not captured by prefix-only patterns, and the
real HighEntropyStringsPlugin extraction regex has no minimum length
(the entropy (the entropy (the entropy (the entropy (the th(the entropy (the entropy (the entropy (the entropy (the th(treality rather
than assumption. Fixed and re-verified against the parity oracle.

Feature flag: DETECT_SECRETS_PERF_PREGATE=0 disables the gate.
…ilding it on every line (DETECT_SECRETS_PERF_FILTER_CACHE flag); parity oracle 343 findings unchanged, full suite 1116 passed/1 skipped/6 xfailed
… or too uniform to exceed any threshold (DETECT_SECRETS_PERF_ENTROPY_SC flag)
…, _get_lines_from_file) to avoid eager string formatting;
`PrivateKeyDetector.analyze_line` runs for every line of every scanned
file. The file-size guard was written as a single `and` expression:

    if filename not in self._analyzed_files \
            and 0 < self.get_file_size(filename) < MAX_FILE_SIZE:
        self._analyzed_files.add(filename)

`_analyzed_files` was only populated when the size fell inside the
scannable range. For any file at or above MAX_FILE_SIZE (8 KiB) the
membership check kept failing, so `get_file_size` -> `os.path.getsize`
fired again on every single line: a per-file operation executed
per-line. Cost scaled with (files x lines) rather than (files).

Track the files whose size has already been measured in a separate
`_sized_files` set, so the lookup happens at most once per file
regardless of the result. The subsequent whole-file read is unchanged
and still happens exactly once per file, so multi-line private keys are
detected exactly as before.

Measured on a 21,359-file repository via
`checkov --framework secrets --enable-secret-scan-all-files`:

    before:  real 2120s   user 2014s   sys 230s
    after:   real 1679s   user 1638s   sys  60s

Wall time -21%; `sys` time (the syscall fingerprint of the redundant
getsize calls) down 3.9x. Findings are identical before and after
(14 findings, same files/lines/checks).

Adds two regression tests:
  - `test_get_file_size_is_called_at_most_once_per_file` - asserts the
    size lookup runs at most once for a 500-line, >8 KiB file
    (previously 500 times).
  - `test_multiline_private_key_in_small_file_is_still_detected` -
    guards the whole-file read path that multi-line key detection
    depends on.
@GillSami
GillSami force-pushed the chore/detect-secrets-perf-optimizations branch from b33ff67 to f1c07c7 Compare August 11, 2026 13:34
Comment thread detect_secrets/plugins/high_entropy_strings.py

@Saarett Saarett left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

@GillSami
GillSami force-pushed the chore/detect-secrets-perf-optimizations branch from 96e36bb to 1a41a3f Compare August 11, 2026 14:44
@GillSami
GillSami requested a review from Saarett August 11, 2026 15:18
@Saarett
Saarett merged commit 9bbe25f into master Aug 12, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants