Skip to content

feat(secrets): gitleaks subprocess backend with regex fallback (closes #159)#164

Merged
Wolfvin merged 1 commit into
mainfrom
feat/issue-159-secrets-gitleaks-backend
Jul 3, 2026
Merged

feat(secrets): gitleaks subprocess backend with regex fallback (closes #159)#164
Wolfvin merged 1 commit into
mainfrom
feat/issue-159-secrets-gitleaks-backend

Conversation

@Wolfvin

@Wolfvin Wolfvin commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Closes #159

Summary

Upgrades codelens secrets to use gitleaks as the primary backend when available (600+ maintained rules, entropy scoring, structured JSON output). Falls back to the existing regex scanner when gitleaks is not installed or --no-gitleaks is set. Gitleaks is an opt-in upgrade, never a hard dependency.

What changed

New files (2)

  • scripts/gitleaks_backend.py — self-contained gitleaks integration (454 lines)
    • _gitleaks_available() — detects binary via gitleaks version
    • _run_gitleaks() — invokes gitleaks detect --no-git --report-format json, writes to temp file, parses JSON, handles both list and {Results:[...]} schemas (older gitleaks versions), cleans up temp file
    • _normalize_gitleaks_findings() — maps gitleaks JSON fields to CodeLens finding schema (RuleID→type, File→file, StartLine→line, Secret→masked match/value, Tags→tags, Fingerprint→fingerprint)
    • _infer_severity() — heuristic severity mapping (aws-access-key/private-key/github-pat/stripe-secret/etc → critical; 'critical'/'high'/'medium'/'low' in rule ID or tags → matching severity; default → high)
    • _mask_secret() — first 4 chars + *** (matches secrets_engine._mask_value convention)
    • scan_with_gitleaks() — public entry point; returns CodeLens-shaped result dict with backend='gitleaks', or None if gitleaks unavailable
    • GitleaksError — caught by caller, falls back to regex with warning
  • tests/test_secrets_gitleaks.py — 56 tests (543 lines)

Modified files (2)

  • scripts/commands/secrets.py (73 lines added):
    • Add --no-gitleaks flag (force regex backend)
    • execute() tries gitleaks first (unless --no-gitleaks); on gitleaks failure, falls back to regex with stderr warning (never crashes)
    • Tags result with backend='regex' or 'gitleaks'
    • When gitleaks unavailable, adds gitleaks_hint field with install URL
    • stats.backend also set so compact/ai formatters pick it up
  • SKILL.md — add "Gitleaks-Backed Secrets Scanner" to v8.2 capability pillars with install instructions

Acceptance criteria (from issue body)

  • _gitleaks_available() detection in scripts/commands/secrets.py — in gitleaks_backend.py (called from commands/secrets.py)
  • Gitleaks subprocess invocation with JSON output mode — _run_gitleaks() uses --report-format json --report-path <tempfile>
  • Result normalizer: gitleaks JSON → CodeLens findings schema — _normalize_gitleaks_findings()
  • Graceful fallback to existing regex scanner with warning message — execute() catches GitleaksError, prints stderr warning, runs detect_secrets()
  • --no-gitleaks flag to force regex fallback (useful for testing) — added to add_args()
  • stats.backend field in output: "gitleaks" or "regex" — set in both scan_with_gitleaks() and execute() regex path
  • Installation hint in output when gitleaks not found — gitleaks_hint field with install URL
  • SKILL.md updated: note that gitleaks improves accuracy, link to install docs — added as capability pillar [ARCH] Replace flat registry with true graph data model (nodes + edges) #8
  • Existing tests remain green (mock gitleaks subprocess in tests) — 56 new tests, 0 regressions
  • sync_command_count.py --apply run (count unchanged — same command, upgraded backend) — verified count = 70

Design decisions

  1. gitleaks logic in NEW module (gitleaks_backend.py), not in secrets_engine.py — avoids collision with PR feat(agent-ux): confidence scores per finding + schema_version in JSON output (closes #5) #148 (which touches secrets_engine.py for confidence scores). commands/secrets.py is the wiring point; secrets_engine.py is untouched.

  2. Gitleaks is OPT-IN — if not installed, regex runs unchanged with a hint. Never a hard dependency. This matches the issue's explicit requirement: "Some environments cannot install binaries (locked-down CI, read-only filesystems). The regex fallback ensures codelens secrets always works, everywhere, without setup."

  3. --no-git flag passed to gitleaks — scans the working tree only (not git history) to match the regex backend's behavior. Git history scanning can be added later via a --git-history flag if needed. This keeps the two backends semantically equivalent.

  4. Normalized findings include backend field — so consumers can tell which scanner produced each finding (useful for debugging false positives/negatives, and for future mixed-backend scenarios).

  5. Severity inference is heuristic — gitleaks doesn't have a native severity field. The heuristic maps high-value secret types (aws-access-key, private-key, github-pat, etc.) to critical, checks for severity keywords in rule ID/tags, and defaults to high (gitleaks rules are curated, so default-high is reasonable). Documented in the _infer_severity docstring.

  6. GitleaksError caught, never crashes — the command always returns a result. Gitleaks failure produces a stderr warning and falls back to regex. This matches the issue's "graceful fallback" requirement.

Tests

  • New: 56 passed in tests/test_secrets_gitleaks.py
    • TestGitleaksAvailable (5) — detection: available, not-found, timeout, nonzero, exception
    • TestGitleaksVersion (2) — version string / None
    • TestMaskSecret (5) — long, short, empty, 4-char, 5-char
    • TestInferSeverity (12) — all high-value markers, tag-based, rule-ID-based, default
    • TestNormalizeFindings (7) — basic, masking, abs→rel paths, tags-as-string, empty, non-dict, missing fields
    • TestStatsRiskRecs (9) — counts, files_with_findings, risk cascade, recommendation content
    • TestRunGitleaks (7) — JSON parse, empty file, missing file, nonzero exit, timeout, invalid JSON, {Results:[...]} schema
    • TestScanWithGitleaks (4) — returns None when unavailable, full result dict, severity filter, nonexistent workspace
    • TestCliIntegration (4) — --no-gitleaks in help, regex backend + hint when gitleaks absent, --no-gitleaks suppresses hint, stats.backend set
  • Regression check: test_secrets_engine + test_cli + test_command_count + test_command_registry → 53 passed, 0 failed (no regressions)
  • Command count: unchanged at 70 (same command, upgraded backend)

End-to-end verification

$ python3 scripts/codelens.py secrets --help | grep no-gitleaks
                           [--max-files MAX_FILES] [--no-gitleaks]

$ python3 scripts/codelens.py secrets tests/fixtures  # gitleaks not installed
{
  "status": "ok",
  "backend": "regex",
  "gitleaks_hint": "gitleaks not found — using built-in regex scanner (lower
    accuracy). Install gitleaks for 600+ maintained rules and entropy scoring:
    https://github.com/gitleaks/gitleaks",
  "stats": {"backend": "regex", ...},
  "findings": [...]
}

$ python3 scripts/codelens.py secrets tests/fixtures --no-gitleaks
{
  "status": "ok",
  "backend": "regex",
  # gitleaks_hint NOT present (user explicitly opted out)
  ...
}

Findings (per pre-flight SKILL.md — flag to BOS)

Branch

feat/issue-159-secrets-gitleaks-backend

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Wolfvin Wolfvin force-pushed the feat/issue-159-secrets-gitleaks-backend branch 2 times, most recently from d04a653 to 994158c Compare July 3, 2026 02:12
@Wolfvin Wolfvin merged commit 61ff4ff into main Jul 3, 2026
…#159)

Upgrade `codelens secrets` to use gitleaks as the primary backend when
available (600+ maintained rules, entropy scoring, structured JSON output).
Falls back to the existing regex scanner when gitleaks is not installed
or `--no-gitleaks` is set. Gitleaks is an opt-in upgrade, never a hard
dependency.

New files:
- scripts/gitleaks_backend.py — self-contained gitleaks integration (454 lines)
  - _gitleaks_available() — detects binary via `gitleaks version`
  - _run_gitleaks() — invokes `gitleaks detect --no-git --report-format json`,
    writes to temp file, parses JSON, handles both list and {Results:[...]}
    schemas (older gitleaks versions), cleans up temp file
  - _normalize_gitleaks_findings() — maps gitleaks JSON fields to CodeLens
    finding schema (RuleID→type, File→file, StartLine→line, Secret→masked
    match/value, Tags→tags, Fingerprint→fingerprint)
  - _infer_severity() — heuristic severity mapping (aws-access-key/private-key/
    github-pat/stripe-secret/etc → critical; 'critical'/'high'/'medium'/'low'
    in rule ID or tags → matching severity; default → high)
  - _mask_secret() — first 4 chars + *** (matches secrets_engine convention)
  - scan_with_gitleaks() — public entry point; returns CodeLens-shaped result
    dict with backend='gitleaks', or None if gitleaks unavailable
  - GitleaksError — caught by caller, falls back to regex with warning
- tests/test_secrets_gitleaks.py — 56 tests (543 lines)
  - Detection (mocked subprocess: available, not-found, timeout, nonzero,
    unexpected exception)
  - Masking (long, short, empty, 4-char, 5-char)
  - Severity inference (aws/private-key/github-pat/stripe → critical;
    tags with high/medium/low; default → high)
  - Normalization (basic, masking, abs→rel paths, tags as string, empty,
    non-dict entries, missing fields)
  - Stats/risk/recommendations (counts, files_with_findings, critical→high
    →medium→low risk cascade, recommendation content)
  - _run_gitleaks (mocked: JSON parse, empty file, missing file, nonzero
    exit, timeout, invalid JSON, {Results:[...]} schema)
  - scan_with_gitleaks (full integration mocked: result dict shape,
    severity filter, nonexistent workspace)
  - CLI integration (subprocess): --no-gitleaks in help, regex backend +
    install hint when gitleaks absent, --no-gitleaks suppresses hint,
    stats.backend field set

Modified files:
- scripts/commands/secrets.py (73 lines):
  - Add --no-gitleaks flag (force regex backend)
  - execute() tries gitleaks first (unless --no-gitleaks); on gitleaks
    failure, falls back to regex with stderr warning (never crashes)
  - Tags result with backend='regex' or 'gitleaks'
  - When gitleaks unavailable, adds gitleaks_hint field with install URL
  - stats.backend also set so compact/ai formatters pick it up
- SKILL.md — add 'Gitleaks-Backed Secrets Scanner' to v8.2 capability pillars
  with install instructions

Design decisions:
- gitleaks logic in NEW module (gitleaks_backend.py), not in secrets_engine.py
  — avoids collision with PR #148 (which touches secrets_engine.py for
  confidence scores). commands/secrets.py is the wiring point.
- Gitleaks is OPT-IN: if not installed, regex runs unchanged with a hint.
  Never a hard dependency.
- --no-gitleaks forces regex even if gitleaks is available — useful for
  testing, offline environments, or when gitleaks produces unexpected results.
- --no-git flag passed to gitleaks (scan working tree, not git history) to
  match the regex backend's behavior. Git history scanning can be added
  later via a --git-history flag if needed.
- Normalized findings include 'backend' field so consumers can tell which
  scanner produced each finding (useful when mixing backends in future).

Verified:
- 56 new tests pass (test_secrets_gitleaks.py — all subprocess mocked)
- 109 passed regression check (test_secrets_engine + test_cli +
  test_command_count + test_command_registry — no regressions)
- Command count unchanged at 70 (same command, upgraded backend)
- End-to-end: --no-gitleaks in help, regex backend + hint when gitleaks
  absent, --no-gitleaks suppresses hint, stats.backend set

Findings (per pre-flight SKILL.md — flag to BOS):
- PR #148 (issue #5, open) touches scripts/secrets_engine.py (+16 lines,
  adding confidence scores). This PR touches scripts/commands/secrets.py
  (different file). No collision. Both can merge independently. The
  gitleaks backend's normalized findings include a 'confidence' field
  position in the schema — PR #148's confidence.py module can score
  gitleaks findings the same way as regex findings if desired.
@sonarqubecloud

sonarqubecloud Bot commented Jul 3, 2026

Copy link
Copy Markdown

@Wolfvin Wolfvin deleted the feat/issue-159-secrets-gitleaks-backend branch July 3, 2026 02:15
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.

[FEAT] Upgrade secrets backend to gitleaks subprocess — replace regex-only scanner

1 participant