Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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: {}

Expand Down Expand Up @@ -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/
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
7 changes: 7 additions & 0 deletions standards/gitleaks.toml
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
15 changes: 15 additions & 0 deletions standards/push-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
106 changes: 104 additions & 2 deletions test/workflows/ci/secret-scan-config.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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" ]]
Expand All @@ -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"
}
Loading