diff --git a/.github/workflows/secret-scanner-reusable.yml b/.github/workflows/secret-scanner-reusable.yml index 3a8d73b9..99e493ae 100644 --- a/.github/workflows/secret-scanner-reusable.yml +++ b/.github/workflows/secret-scanner-reusable.yml @@ -112,6 +112,12 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Full history. One of the two gating passes below scans git history, + # and the default depth-1 clone would silently reduce it to a single + # commit. That pass asserts non-shallowness for itself rather than + # trusting this line to have taken effect. + fetch-depth: 0 # Gitleaks runs as a pinned, checksum-verified binary rather than via # gitleaks/gitleaks-action. The action injects gitleaks into the runner @@ -210,10 +216,15 @@ jobs: rm -rf .standards-gitleaks test -s .gitleaks-estate.toml - # Scans the checked-out working tree (--no-git): deterministic and fast. - # A finding exits non-zero and fails the check. Full-history scanning is - # available by removing --no-git and adding `fetch-depth: 0` to the - # checkout above, at the cost of a slower scan on every PR. + # TWO gating passes run below and they are not redundant. Each sees a + # population the other cannot: + # * working tree (--no-git) -- sees untracked and generated files; + # cannot see a blob deleted in an earlier commit + # * git history -- sees committed-then-deleted secrets; + # cannot see a file that was never committed + # Both gate: a finding in either exits non-zero and fails the check. + # Full history costs a slower scan on every PR. That is the accepted + # price of a deleted secret no longer being permanently invisible. # ── Estate baseline sharing ────────────────────────────────────────── # A consumer repo can inherit the estate-wide allowlist instead of # copying it, by writing in its own .gitleaks.toml: @@ -310,6 +321,56 @@ jobs: --config "$CONFIG" \ --exit-code 1 + # Full-history pass — ADDITIVE, not a replacement. Measured on a fixture + # with this exact pinned binary, the same secret in two placements: + # + # committed then deleted working tree: MISSED history: FOUND + # untracked file working tree: FOUND history: MISSED + # + # git mode scans commit diffs, so it cannot see a file that was never + # committed; --no-git walks the checked-out tree, so it cannot see a blob + # that is no longer in it. Dropping either pass REMOVES coverage. Keeping + # both is why this was added as a new step rather than by editing the one + # above, whose name and log lines existing triage keys on. + # + # KNOWN GAP, stated rather than implied: the AsciiDoc mirror pass below + # scans a filesystem copy with --no-git, so fetch-depth: 0 cannot reach + # it. An AsciiDoc secret that was committed and later deleted stays + # invisible. "The gate is full-history" holds for every file class but + # that one. + - name: Gitleaks secret scan — git history (gating) + run: | + set -euo pipefail + + # fetch-depth: 0 on the checkout is load-bearing HERE. If it ever + # regresses to the default depth-1 clone, detect would walk a single + # commit, find nothing and report a pass — a gate that cannot fail. + # Assert completeness from git itself: gitleaks' own "scanned N + # commits" line under-reports and is not proof of depth. + if [ "$(git rev-parse --is-shallow-repository)" != "false" ]; then + echo "::error::checkout is shallow -- a history scan here would be vacuous; refusing to report a pass" + exit 1 + fi + echo "history available to this scan: $(git rev-list --count HEAD) commits" + + # Same config resolution as the working-tree pass, duplicated rather + # than factored out so that step stays byte-identical. + CONFIG=".gitleaks-estate.toml" + if [ -f .gitleaks.toml ]; then + CONFIG=".gitleaks.toml" + fi + # A staging step that failed silently would leave gitleaks running a + # reduced config over almost nothing and calling the result green. + test -s "$CONFIG" + + "$RUNNER_TEMP/gitleaks" detect \ + --source . \ + --redact \ + --no-banner \ + --verbose \ + --config "$CONFIG" \ + --exit-code 1 + # AsciiDoc blind-spot pass — MEASURED, not theoretical. # # gitleaks' DEFAULT global allowlist excludes paths matching