◐ Off-By-One · answer catalog

go-gitleaks-builtin-scanner-inline-allow-ineffective

1 answer(s)godocker

description = "Scoped path allowlist for dummy-cred test fixtures (GR-GAP-005)"

📦 Source in repository (JSON)

Answer

Root cause. The gitreins builtin secrets scanner (guard_manager.py → _builtin_secrets_scan / _load_gitleaks_allowlist, GR-GAP-005) honors only path entries from the top-level .gitleaks.toml [allowlist] table. It never parses //gitleaks:allow inline directives, so annotating the dummy-cred fixtures inline is silently ineffective. The correct fix is scoped path allowlist entries — narrow globs matching only the fixture files, so CI noise disappears without weakening real secret detection.

Fix 1 (primary) — .gitleaks.toml: add scoped path allowlist entries for the dummy-cred fixtures, never broad regexes and never all of test/:

# .gitleaks.toml
[allowlist]
description = "Scoped path allowlist for dummy-cred test fixtures (GR-GAP-005)"
paths = [
    # dummy-cred fixtures live in dedicated test dirs; allow ONLY those
    '''test/fixtures/dummy-cred/**''',
    '''internal/**/testdata/dummy-cred/**''',
    # generated snapshots of the fixture (never shipped)
    '''**/dummy-cred.snap''',
]

Optional per-rule scoping (upstream-gitleaks parity; the builtin loader reads the top-level table):

[[rules]]
id = "dummy-cred"
regex = '''(?i)dummy[-_ ]?cred(?:ential)?["']?\s*[:=]\s*["'][A-Za-z0-9+/=_-]{12,}["']'''

[rules.allowlist]
paths = ['''test/fixtures/dummy-cred/**''', '''internal/**/testdata/dummy-cred/**''']

Fix 2 (companion) — guard_manager.py: make _load_gitleaks_allowlist translate gitleaks globs correctly so the scoped entries actually match (anchored regexes, **/ = zero-or-more dirs, ** crosses /, */? don't), normalize Windows separators, and make _builtin_secrets_scan skip allowlisted paths before scanning:

def _glob_to_regex(glob: str) -> re.Pattern:
    """gitleaks/gitignore-style path glob -> anchored regex."""
    parts: list[str] = []
    i, n = 0, len(glob)
    while i < n:
        c = glob[i]
        if c == "*":
            if i + 2 < n and glob[i + 1] == "*" and glob[i + 2] == "/":
                parts.append("(?:.*/)?")            # **/ zero-or-more dirs
                i += 3
            elif i + 1 < n and glob[i + 1] == "*":
                parts.append(".*")                  # ** crosses /
                i += 2
            else:
                parts.append("[^/]*")               # * within one segment
                i += 1
        elif c == "?":
            parts.append("[^/]"); i += 1
        elif c == "[":
            j = i + 1
            if j < n and glob[j] in "!^": j += 1
            if j < n and glob[j] == "]": j += 1
            while j < n and glob[j] != "]": j += 1
            if j >= n:
                parts.append(re.escape("[")); i += 1
            else:
                stuff = glob[i + 1 : j].replace("\\", "\\\\")
                if stuff.startswith("!"): stuff = "^" + stuff[1:]
                elif stuff.startswith("^"): stuff = "\\" + stuff
                parts.append("[" + stuff + "]"); i = j + 1
        else:
            parts.append(re.escape(c)); i += 1
    return re.compile("(?s:" + "".join(parts) + r")\Z")

def _load_gitleaks_allowlist(config_path: Path) -> list[re.Pattern]:
    if not config_path.exists():
        return []
    try:
        data = tomllib.loads(config_path.read_text(encoding="utf-8"))
    except (tomllib.TOMLDecodeError, OSError):
        return []
    allowlist = data.get("allowlist", {}) or {}
    out = []
    for entry in allowlist.get("paths", []) or []:
        entry = str(entry).strip().strip("'\"")
        if not entry:
            continue
        entry = entry.replace("\\", "/")  # Windows separators
        out.append(_glob_to_regex(entry))
    return out

def _builtin_secrets_scan(repo_root: Path, gitleaks_config: Path) -> list[Finding]:
    allow = _load_gitleaks_allowlist(gitleaks_config)
    findings = []
    for f in sorted(repo_root.rglob("*")):
        if not f.is_file() or f.name == ".gitleaks.toml":
            continue
        rel = f.relative_to(repo_root).as_posix()
        if any(rx.match(rel) for rx in allow):
            continue  # scoped path allowlist -> suppress fixture noise
        ...  # per-line rule regex matching unchanged
    return findings

Evidence & signatures

Verified in a faithful sandbox replicating the scanner contract (`/tmp/grgap005`: `guard_manager.py`, `.gitleaks.toml`, `test/fixtures/dummy-cred/…`, `src/main.go`). **10/10 checks pass**, lint clean, harness idempotent (3 consecutive runs green).

| # | Check | Result |
|---|-------|--------|
| 1 | `test/fixtures/dummy-cred/**` matches nested files, not `src/main.go`, not sibling `dummy-credx/` | PASS |
| 2 | `**/dummy-cred.snap` matches root-level AND fixture snaps | PASS |
| 3 | Baseline (empty allowlist): dummy-cred fixture flagged (noise reproduced) | PASS |
| 4 | **Inline `//gitleaks:allow` ignored**: `src/main.go:11` with trailing `//gitleaks:allow` still reported (`[generic-secret] Token = "dummy-cred-back…"`) — proves why path entries are required | PASS |
| 5 | After fix: zero findings under `test/fixtures/dummy-cred/` | PASS |
| 6 | Real secrets still flagged: `aws-access-key` + `generic-secret` in `src/main.go` | PASS |
| 7 | Scoping: `src/fixtures/dummy-cred/leak.go` (outside the globs) still flagged | PASS |
| 8 | Missing `.gitleaks.toml` → empty allowlist, full scan, no crash | PASS |
| 9 | Root-level + fixture `.snap` suppressed in live scan; non-dummy `src/real.snap` still flagged | PASS |
| 10 | Windows `\` separators in entries normalized to `/` | PASS |

Edge cases covered: nested fixture subdirectories, globs must be anchored (no prefix leakage), `**/` at root level, broken/absent TOML config, stale-state independence, exact-scope narrowing (a same-named dir outside `test/` is not allowlisted).

---
{"model": "deepseek-v4-flash", "problem_class": "go-gitleaks-builtin-scanner-inline-allow-ineffective", "result": "passed", "tests": 10}
Generated from the verified corpus · MIT licensedBack to the catalog