From f09667ea3f8bd481425004210317d4e36326a050 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:05:42 -0500 Subject: [PATCH] fix(security): enable gitleaks default rules in .gitleaks.toml template (#1395) A config passed via `--config` replaces gitleaks' built-in ruleset. The allowlist-only .gitleaks.toml and standards/gitleaks.toml had no `[extend] useDefault = true`, so the secret-scan job ran with zero rules and reported "no leaks found" on any history. Add the extend block to both files (the _bmad/ allowlist from #996 is kept). Add detection regression tests to test/workflows/ci/secret-scan-config.bats that run the pinned gitleaks against a throwaway repo with synthetic credentials generated at test time, and install the same pinned, checksum-verified gitleaks release in ci-tests.yml (asserted to stay in lockstep with ci.yml). Document the mandatory extend block in standards/push-protection.md. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci-tests.yml | 33 ++++++- .github/workflows/ci.yml | 8 +- .gitleaks.toml | 7 ++ standards/gitleaks.toml | 7 ++ standards/push-protection.md | 15 +++ test/workflows/ci/secret-scan-config.bats | 106 +++++++++++++++++++++- 6 files changed, 170 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index b4574473b..0b06f3f7c 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -1,7 +1,8 @@ # Regression guard for the flaky "Lint GitHub Actions" download in ci.yml # (issue #894): every curl download in ci.yml must carry bounded retries so a # transient GitHub Releases blip retries instead of failing the job. -# See test/workflows/ci/install-resilience.bats. +# See test/workflows/ci/install-resilience.bats. Also runs the gitleaks config +# detection tests (issue #1395, test/workflows/ci/secret-scan-config.bats). name: CI Workflow Tests on: @@ -10,12 +11,16 @@ on: - '.github/workflows/ci.yml' - 'test/workflows/ci/**' - '.github/workflows/ci-tests.yml' + - '.gitleaks.toml' + - 'standards/gitleaks.toml' push: branches: [main] paths: - '.github/workflows/ci.yml' - 'test/workflows/ci/**' - '.github/workflows/ci-tests.yml' + - '.gitleaks.toml' + - 'standards/gitleaks.toml' permissions: {} @@ -51,5 +56,31 @@ jobs: echo "Apt install failed after 3 attempts" >&2 exit 1 + # The secret-scan detection tests (test/workflows/ci/secret-scan-config.bats, + # issue #1395) run the real scanner, so install the exact release ci.yml's + # secret-scan job pins — same version, retry loop and checksum as ci.yml + # (the bats suite asserts the version/checksum stay in lockstep). + - name: Install gitleaks + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + downloaded=0 + for attempt in 1 2 3; do + if timeout 30 gh release download v8.30.1 --repo gitleaks/gitleaks --pattern 'gitleaks_8.30.1_linux_x64.tar.gz' --output /tmp/gitleaks.tar.gz --clobber; then + downloaded=1 + break + fi + echo "gitleaks download attempt ${attempt} failed" >&2 + [ "${attempt}" -lt 3 ] && sleep $((attempt * 5)) + done + if [ "${downloaded}" -ne 1 ]; then + echo "gitleaks download failed after 3 attempts" >&2 + exit 1 + fi + echo "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb /tmp/gitleaks.tar.gz" | sha256sum -c + tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks + sudo mv /tmp/gitleaks /usr/local/bin/gitleaks + - name: bats run: bats --print-output-on-failure test/workflows/ci/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3ec5df32..44b0843be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -216,8 +216,10 @@ jobs: sudo mv /tmp/gitleaks /usr/local/bin/gitleaks - name: Run gitleaks - # --config .gitleaks.toml is required: without it gitleaks uses its - # default ruleset, whose generic-api-key rule false-positives on certain - # filenames and reds this job intermittently (issue #996). The repo ships + # --config .gitleaks.toml is required: it adds the path allowlist that + # stops the default generic-api-key rule false-positiving on certain + # filenames (issue #996). The file MUST keep `[extend] useDefault = true` + # — a --config file replaces the built-in rules, so without it this scan + # runs with zero rules and always passes (issue #1395). The repo ships # .gitleaks.toml at root; see standards/push-protection.md Layer 3. run: gitleaks detect --source . --config .gitleaks.toml --redact --verbose --exit-code 1 diff --git a/.gitleaks.toml b/.gitleaks.toml index 6548cdb86..79acf299d 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,5 +1,12 @@ title = "gitleaks config" +# REQUIRED: inherit gitleaks' built-in ruleset. A custom config passed via +# --config replaces the defaults entirely, so without this block the scan +# runs with ZERO rules and always passes (issue #1395). Only add allowlist +# entries below — never remove this block. +[extend] +useDefault = true + # Add repo-specific allowlists below. # Common false-positive paths: # '''_bmad/''' — BMAD knowledge/config files (not application secrets) diff --git a/standards/gitleaks.toml b/standards/gitleaks.toml index 6548cdb86..79acf299d 100644 --- a/standards/gitleaks.toml +++ b/standards/gitleaks.toml @@ -1,5 +1,12 @@ title = "gitleaks config" +# REQUIRED: inherit gitleaks' built-in ruleset. A custom config passed via +# --config replaces the defaults entirely, so without this block the scan +# runs with ZERO rules and always passes (issue #1395). Only add allowlist +# entries below — never remove this block. +[extend] +useDefault = true + # Add repo-specific allowlists below. # Common false-positive paths: # '''_bmad/''' — BMAD knowledge/config files (not application secrets) diff --git a/standards/push-protection.md b/standards/push-protection.md index f2aa7b35d..1d800b0ff 100644 --- a/standards/push-protection.md +++ b/standards/push-protection.md @@ -275,9 +275,24 @@ BMAD knowledge file paths (e.g. `api-request.md`, `auth-session.md` inside API-key indicators. The allowlist suppresses these false positives without disabling the rule org-wide. +**The `[extend] useDefault = true` block is mandatory.** A file passed via +`--config` *replaces* gitleaks' built-in ruleset rather than adding to it. An +allowlist-only config therefore loads **zero detection rules**, and the scan +reports "no leaks found" on any history — including one that contains real +credentials (issue #1395). Keep the `[extend]` block and only add `[allowlist]` +entries. To verify a config, scan a throwaway repo holding a synthetic key +(see `test/workflows/ci/secret-scan-config.bats`) and confirm it is reported. + ```toml title = "gitleaks config" +# REQUIRED: inherit gitleaks' built-in ruleset. A custom config passed via +# --config replaces the defaults entirely, so without this block the scan +# runs with ZERO rules and always passes (issue #1395). Only add allowlist +# entries below — never remove this block. +[extend] +useDefault = true + # Add repo-specific allowlists below. # Common false-positive paths: # '''_bmad/''' — BMAD knowledge/config files (not application secrets) diff --git a/test/workflows/ci/secret-scan-config.bats b/test/workflows/ci/secret-scan-config.bats index 2316d81a3..4fd52d213 100644 --- a/test/workflows/ci/secret-scan-config.bats +++ b/test/workflows/ci/secret-scan-config.bats @@ -12,6 +12,14 @@ # (standards/push-protection.md:241, 257-258) requires `--config .gitleaks.toml` # and every adopting repo MUST ship that file at root. These tests assert both, # so a future edit can't silently drop the config and reintroduce the flakiness. +# +# Pins issue #1395: a config passed via --config REPLACES gitleaks' built-in +# ruleset. The allowlist-only .gitleaks.toml / standards/gitleaks.toml had no +# `[extend] useDefault = true`, so the scan ran with ZERO rules and passed on +# any history. The detection tests below run the pinned gitleaks binary (the +# same invocation as ci.yml's `Run gitleaks` step) against a throwaway repo +# holding synthetic credentials and assert each shipped config catches them. +# gitleaks MUST be on PATH — ci-tests.yml installs the version pinned in ci.yml. load 'helpers/setup' @@ -45,8 +53,8 @@ step_block_with() { } @test "secret-scan: gitleaks scan passes --config .gitleaks.toml" { - # Without --config, gitleaks uses its default ruleset, whose generic-api-key - # rule false-positives on certain filenames and reds the job intermittently. + # The config extends gitleaks' default ruleset and adds the allowlist that + # suppresses generic-api-key false positives on certain filenames (#996). local block command block="$(step_block_with 'gitleaks detect')" [[ -n "$block" ]] @@ -61,3 +69,97 @@ step_block_with() { # fails with file-not-found (standards/push-protection.md#gitleakstoml-template). [[ -f "${TT_REPO_ROOT}/.gitleaks.toml" ]] } + +# --- Detection: the shipped configs must actually run gitleaks' rules (#1395) --- + +TT_CI_TESTS_WORKFLOW="${TT_REPO_ROOT}/.github/workflows/ci-tests.yml" + +# Fail (never skip — AGENTS.md) when the scanner is missing. +require_gitleaks() { + if ! command -v gitleaks >/dev/null 2>&1; then + echo "gitleaks not on PATH — install the version pinned in .github/workflows/ci.yml" >&2 + return 1 + fi +} + +# Build a throwaway git repo holding synthetic, obviously-fake credentials. +# Each value is assembled at runtime from split pieces so no secret-shaped +# literal is ever committed here (this repo's own secret scan stays clean). +make_secret_fixture() { + local dir="$1" + local aws="AK""IA""QZ7RT2MX""4KLP3WVN" + local ghp="gh""p_""Zx9Kq2Lm8Np4Rt6V""w1Yb3Cd5Fg7Hj0Kl2Mn4P" + mkdir -p "$dir" + git -C "$dir" init -q + printf 'aws_access_key_id = %s\ngithub_token = %s\n' "$aws" "$ghp" > "$dir/fake-creds.txt" + git -C "$dir" add fake-creds.txt + git -C "$dir" -c user.name=bats -c user.email=bats@example.invalid \ + -c commit.gpgsign=false commit -q -m "synthetic secret fixture" +} + +# Scan the fixture's git history exactly as ci.yml's `Run gitleaks` step does, +# plus a JSON report so the test can assert which rules fired. +scan_fixture_with() { + local config="$1" + local fixture="${BATS_TEST_TMPDIR}/fixture" + make_secret_fixture "$fixture" + REPORT="${BATS_TEST_TMPDIR}/report.json" + run gitleaks detect --source "$fixture" --config "$config" --redact --verbose \ + --exit-code 1 --report-format json --report-path "$REPORT" +} + +# Print the value of `useDefault` inside the `[extend]` table of a gitleaks TOML. +extend_use_default() { + awk ' + /^[[:space:]]*\[/ { in_extend = ($0 ~ /^[[:space:]]*\[extend\][[:space:]]*(#.*)?$/); next } + in_extend && /^[[:space:]]*useDefault[[:space:]]*=/ { + sub(/^[^=]*=[[:space:]]*/, ""); sub(/[[:space:]]*(#.*)?$/, ""); print; exit + } + ' "$1" +} + +@test "secret-scan: .gitleaks.toml extends the gitleaks default ruleset" { + [ "$(extend_use_default "${TT_REPO_ROOT}/.gitleaks.toml")" = "true" ] +} + +@test "secret-scan: standards/gitleaks.toml template extends the gitleaks default ruleset" { + [ "$(extend_use_default "${TT_REPO_ROOT}/standards/gitleaks.toml")" = "true" ] +} + +@test "secret-scan: .gitleaks.toml detects a synthetic secret in git history" { + require_gitleaks + scan_fixture_with "${TT_REPO_ROOT}/.gitleaks.toml" + [ "$status" -eq 1 ] + grep -q '"RuleID": "aws-access-token"' "$REPORT" + grep -q '"RuleID": "github-pat"' "$REPORT" +} + +@test "secret-scan: standards/gitleaks.toml template detects a synthetic secret in git history" { + require_gitleaks + scan_fixture_with "${TT_REPO_ROOT}/standards/gitleaks.toml" + [ "$status" -eq 1 ] + grep -q '"RuleID": "aws-access-token"' "$REPORT" + grep -q '"RuleID": "github-pat"' "$REPORT" +} + +@test "secret-scan: control — an allowlist-only config (no [extend]) detects nothing" { + # Documents the #1395 failure mode and proves the detection tests above can + # tell a working config from a zero-rule one. + require_gitleaks + local cfg="${BATS_TEST_TMPDIR}/allowlist-only.toml" + printf '[allowlist]\ndescription = "Allowlisted paths"\npaths = [\n %s,\n]\n' "'''_bmad/'''" > "$cfg" + scan_fixture_with "$cfg" + [ "$status" -eq 0 ] + [[ "$output" == *"no leaks found"* ]] +} + +@test "secret-scan: ci-tests.yml installs the same pinned gitleaks release as ci.yml" { + # The detection tests are only meaningful against the binary CI scans with. + local want_ver want_sum + want_ver="$(grep -oE 'gitleaks_[0-9]+\.[0-9]+\.[0-9]+_linux_x64\.tar\.gz' "$TT_WORKFLOW" | head -n1)" + want_sum="$(grep -oE '[0-9a-f]{64} /tmp/gitleaks\.tar\.gz' "$TT_WORKFLOW" | head -n1)" + [ -n "$want_ver" ] + [ -n "$want_sum" ] + grep -qF "$want_ver" "$TT_CI_TESTS_WORKFLOW" + grep -qF "$want_sum" "$TT_CI_TESTS_WORKFLOW" +}