diff --git a/scripts/lib/push-protection.sh b/scripts/lib/push-protection.sh index 9f1ea5da..59fa4e7e 100644 --- a/scripts/lib/push-protection.sh +++ b/scripts/lib/push-protection.sh @@ -200,10 +200,21 @@ pp_check_secret_scan_ci_job() { return fi - # Match actual action references, not bare mentions in comments or docs. - if ! echo "$ci_content" | grep -qE 'uses:[[:space:]]*(gitleaks/gitleaks-action|zricethezav/gitleaks-action)@'; then + # Accept either the legacy action pattern or the canonical binary-install pattern + # (gitleaks detect --config .gitleaks.toml). Both satisfy the standard. + local has_action has_binary + has_action=$(echo "$ci_content" | grep -cE 'uses:[[:space:]]*(gitleaks/gitleaks-action|zricethezav/gitleaks-action)@' || true) + local ci_collapsed + ci_collapsed=$(echo "$ci_content" | tr '\n' ' ') + if echo "$ci_collapsed" | grep -qE 'gitleaks[[:space:]]+detect[[:space:]].*--config([[:space:]]|=).*\.gitleaks\.toml'; then + has_binary=1 + else + has_binary=0 + fi + + if [ "${has_action:-0}" -eq 0 ] && [ "${has_binary:-0}" -eq 0 ]; then add_finding "$repo" "push-protection" "secret_scan_ci_job_present" "error" \ - "\`ci.yml\` does not contain a job using \`gitleaks\` — add the secret-scan job from the standard" \ + "\`ci.yml\` does not contain a \`secret-scan\` gitleaks job (neither \`gitleaks/gitleaks-action\` nor the canonical \`gitleaks detect --config .gitleaks.toml\` binary-install pattern) — add the secret-scan job from the standard" \ "$PP_STANDARD_REF#required-ci-job" fi } diff --git a/standards/ci-standards.md b/standards/ci-standards.md index 4ee4945d..30aee137 100644 --- a/standards/ci-standards.md +++ b/standards/ci-standards.md @@ -246,55 +246,47 @@ Each repo needs a `sonar-project.properties` file at root with project key and o ### 4. Secret Scanning (`ci.yml` — gitleaks job) -Secret detection via the gitleaks action. This job **must be added to the CI pipeline** -for all organization repositories. The job scans commit history for hardcoded secrets, -API keys, and other sensitive data. +Secret detection via gitleaks (manual binary install). This job **must be added to the CI pipeline** +for all organization repositories. The job downloads a pinned gitleaks binary, verifies its +checksum, and scans commit history for hardcoded secrets, API keys, and other sensitive data. -**Why a separate job?** Gitleaks requires a license key when scanning organization -repositories (free for open-source). The job is part of the main `ci.yml` pipeline -but documented separately to clarify the licensing requirement. +**Why a separate job?** Secret scanning is documented separately to clarify the binary +install pattern, checksum verification, and the required `.gitleaks.toml` config artifact. **Standard configuration:** See the canonical job specification in [`push-protection.md` — Layer 3: CI Secret Scanning](push-protection.md#layer-3--ci-secret-scanning-secondary-defense). -**Organization repos only — GITLEAKS_LICENSE requirement:** +> **No license required:** The canonical job downloads and runs the gitleaks binary directly +> — it does **not** use the `gitleaks/gitleaks-action`. The license requirement applied only +> to the old action-based pattern; the binary itself is open-source and requires no +> `GITLEAKS_LICENSE` secret for either personal or organization repositories. -When adding the `secret-scan` job to an organization repository's `ci.yml`, you **must** -pass the `GITLEAKS_LICENSE` secret to the gitleaks action: +**Required secrets:** None for the binary install pattern. -```yaml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} -``` - -Without this environment variable, gitleaks will fail with "missing gitleaks license" -when scanning in an organization context. - -**Required secrets:** `GITLEAKS_LICENSE` (org-level, organization repositories only) - -**License requirement:** Gitleaks is free for open-source, but organization scans -require a valid license. Obtain a free license at [gitleaks.io](https://gitleaks.io). - -**License setup:** +**Required repo artifact — `.gitleaks.toml`:** -1. Create or log into your account at [gitleaks.io](https://gitleaks.io) -2. Generate a free license key for your organization -3. Add the license as the org-level secret `GITLEAKS_LICENSE`: +Every repo using the `secret-scan` job MUST ship a `.gitleaks.toml` at the +repository root. The `Run gitleaks` step passes `--config .gitleaks.toml`; +without the file the job fails immediately with a file-not-found error. - ```bash - gh secret set GITLEAKS_LICENSE --org petry-projects --body "" - ``` +Copy [`standards/gitleaks.toml`](gitleaks.toml) as a starting point and extend +the `paths` allowlist for any repo-specific false-positive paths. BMAD Method +repos **must** include `'''_bmad/'''` in the allowlist — the `generic-api-key` +rule fires on knowledge file paths such as `api-request.md` and +`auth-session.md`. -**For personal/user repos:** The `GITLEAKS_LICENSE` environment variable is optional. -If omitted, gitleaks runs in open-source mode (free, no license needed). +> **Env var naming:** The binary checksum variable MUST be named +> `GITLEAKS_CHECKSUM`, **not** `GITLEAKS_SHA256`. SonarCloud's security gate +> flags env var names matching `*SHA256*` that contain hex strings as Security +> Hotspots (hardcoded credential false positive). The canonical job in +> `push-protection.md` already uses `GITLEAKS_CHECKSUM`. **CI failure — common causes and fixes:** | Failure | Root cause | Fix | |---------|-----------|-----| -| `missing gitleaks license` | License not passed to action | Ensure env includes `GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}` | -| Secrets found | Legitimate secrets in the code | Use `.gitleaksignore` to allowlist false positives, or remove the secret | +| `config file not found` | `.gitleaks.toml` missing at repo root | Copy `standards/gitleaks.toml` and commit it | +| Secrets found | Secret detection triggered | Remove and rotate the secret immediately; use `.gitleaks.toml` `allowlist.paths` only for confirmed false positives | ### 5. Claude Code (`claude.yml`) @@ -936,7 +928,6 @@ All secrets required by the standard CI workflows are configured at the |--------|---------| | `CLAUDE_CODE_OAUTH_TOKEN` | Claude Code Action authentication | | `SONAR_TOKEN` | SonarCloud analysis authentication | -| `GITLEAKS_LICENSE` | Gitleaks secret scanning (organization repositories only) | | `APP_ID` | GitHub App ID for Dependabot auto-merge | | `APP_PRIVATE_KEY` | GitHub App private key for Dependabot auto-merge | @@ -1025,16 +1016,19 @@ autofix: 1. **Determine tech stack** and select the matching workflow patterns above 2. **Create `ci.yml`** with lint, format, typecheck, and test stages -3. **Enable CodeQL default setup** via `apply-repo-settings.sh` (or `gh api -X PATCH repos///code-scanning/default-setup -F state=configured`) — do **not** add a `codeql.yml` workflow file -4. **Add `sonarcloud.yml`** and configure `sonar-project.properties` -5. **Add `claude.yml`** for AI code review -6. **Add `dependabot.yml`** from the appropriate template in [`standards/dependabot/`](dependabot/) -7. **Add `dependabot-automerge.yml`** from [`standards/workflows/`](workflows/) -8. **Add `dependency-audit.yml`** from [`standards/workflows/`](workflows/) -9. **Add `agent-shield.yml`** from [`standards/workflows/`](workflows/) -10. **Configure secrets** in the repository settings -11. **Set required status checks** in branch protection (see [GitHub Settings](github-settings.md)) -12. **Pin all action references** to commit SHAs +3. **Add `.gitleaks.toml`** at the repository root — copy [`standards/gitleaks.toml`](gitleaks.toml) + and extend the `paths` allowlist for any repo-specific false-positive paths. This file is + **required** by the `secret-scan` job; the job fails without it. +4. **Enable CodeQL default setup** via `apply-repo-settings.sh` (or `gh api -X PATCH repos///code-scanning/default-setup -F state=configured`) — do **not** add a `codeql.yml` workflow file +5. **Add `sonarcloud.yml`** and configure `sonar-project.properties` +6. **Add `claude.yml`** for AI code review +7. **Add `dependabot.yml`** from the appropriate template in [`standards/dependabot/`](dependabot/) +8. **Add `dependabot-automerge.yml`** from [`standards/workflows/`](workflows/) +9. **Add `dependency-audit.yml`** from [`standards/workflows/`](workflows/) +10. **Add `agent-shield.yml`** from [`standards/workflows/`](workflows/) +11. **Configure secrets** in the repository settings +12. **Set required status checks** in branch protection (see [GitHub Settings](github-settings.md)) +13. **Pin all action references** to commit SHAs --- diff --git a/standards/gitleaks.toml b/standards/gitleaks.toml new file mode 100644 index 00000000..6548cdb8 --- /dev/null +++ b/standards/gitleaks.toml @@ -0,0 +1,10 @@ +title = "gitleaks config" + +# Add repo-specific allowlists below. +# Common false-positive paths: +# '''_bmad/''' — BMAD knowledge/config files (not application secrets) +[allowlist] +description = "Allowlisted paths" +paths = [ + '''_bmad/''', +] diff --git a/standards/push-protection.md b/standards/push-protection.md index 05d3fe9f..9344aee4 100644 --- a/standards/push-protection.md +++ b/standards/push-protection.md @@ -216,7 +216,6 @@ secret-scan: runs-on: ubuntu-latest permissions: contents: read - security-events: write steps: - name: Checkout (full history) # Pin to SHA per Action Pinning Policy (ci-standards.md#action-pinning-policy). @@ -225,15 +224,25 @@ secret-scan: with: fetch-depth: 0 - - name: Run gitleaks - # Pinned to SHA per Action Pinning Policy (ci-standards.md#action-pinning-policy). - # Refresh with: gh api repos/gitleaks/gitleaks-action/git/refs/tags/v2 --jq '.object.sha' - # then dereference if it points at an annotated tag. - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9 - with: - args: detect --source . --redact --verbose --exit-code 1 + - name: Install gitleaks env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_VERSION: "8.30.1" + # Named GITLEAKS_CHECKSUM (not GITLEAKS_SHA256) — SonarCloud flags env var names + # matching *SHA256* containing hex strings as Security Hotspots (false positive). + GITLEAKS_CHECKSUM: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" + run: | + tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarball}" + install_dir="${RUNNER_TEMP}/gitleaks-bin" + mkdir -p "${install_dir}" + wget -q "${url}" -O /tmp/gitleaks.tar.gz + echo "${GITLEAKS_CHECKSUM} /tmp/gitleaks.tar.gz" | sha256sum -c + tar -xzf /tmp/gitleaks.tar.gz -C "${install_dir}" gitleaks + chmod +x "${install_dir}/gitleaks" + echo "${install_dir}" >> "${GITHUB_PATH}" + + - name: Run gitleaks + run: gitleaks detect --source . --config .gitleaks.toml --redact --verbose --exit-code 1 ``` The job MUST: @@ -242,9 +251,40 @@ The job MUST: PR diff - Pass `--redact` so leaked values are NEVER written to workflow logs - Fail the build (`--exit-code 1`) when any finding is detected +- Pass `--config .gitleaks.toml` — every adopting repo MUST ship a + `.gitleaks.toml` at root (see [`.gitleaks.toml` template](#gitleakstoml-template) below) +- Use `GITLEAKS_CHECKSUM` (not `GITLEAKS_SHA256`) for the binary checksum env var — + SonarCloud's security gate flags env vars matching `*SHA256*` that contain hex + strings as Security Hotspots (hardcoded credential false positive) - Run as a **required check** via the `code-quality` ruleset (see [`github-settings.md`](github-settings.md#code-quality--required-checks-ruleset-all-repositories)) +### `.gitleaks.toml` template + +Every repository adopting the `secret-scan` job MUST ship a `.gitleaks.toml` +at root. Without it, `--config .gitleaks.toml` fails with a file-not-found +error. Copy [`standards/gitleaks.toml`](gitleaks.toml) as your starting point +and extend the `paths` allowlist for any repo-specific false-positive paths. + +**Why a required config file?** The `generic-api-key` rule in gitleaks fires on +BMAD knowledge file paths (e.g. `api-request.md`, `auth-session.md` inside +`_bmad/` directories) because their names contain substrings gitleaks treats as +API-key indicators. The allowlist suppresses these false positives without +disabling the rule org-wide. + +```toml +title = "gitleaks config" + +# Add repo-specific allowlists below. +# Common false-positive paths: +# '''_bmad/''' — BMAD knowledge/config files (not application secrets) +[allowlist] +description = "Allowlisted paths" +paths = [ + '''_bmad/''', +] +``` + ### Coordination with AgentShield For agent-configuration files specifically, [`agent-shield.yml`](workflows/agent-shield.yml) @@ -308,8 +348,8 @@ org baseline verbatim satisfy this automatically. created RSA keypair inside a `beforeAll` hook) rather than committing a fixed test key. - If a fixture MUST contain a realistic-looking value, prefix the filename - with `fixture-` and add a `.gitleaksignore` entry documenting the - justification. + with `fixture-` and add an `allowlist.paths` entry to `.gitleaks.toml` + documenting the justification. ### Working in a branch that may contain a leaked secret @@ -345,9 +385,9 @@ pointing to the blocked secret. The correct response is: above), rotate the credential, and force-push the rewritten branch. Open an incident issue per the [Incident Response](#incident-response) procedure. - - **False positive:** confirm with the org security owner, then add a - `.gitleaksignore` entry (for CI) and request a push protection bypass - with a `used_in_tests` or `false_positive` reason. + - **False positive:** confirm with the org security owner, then add the + path to `.gitleaks.toml` `allowlist.paths` (for CI) and request a push + protection bypass with a `used_in_tests` or `false_positive` reason. 3. **Never** commit a modified version of the secret (e.g., adding a space, splitting across lines, base64-encoding) to work around detection. This is treated as the same severity as committing the original value. @@ -417,7 +457,7 @@ both at once: | `non_provider_patterns_enabled` | warning | `security_and_analysis.secret_scanning_non_provider_patterns.status == "enabled"` | | `dependabot_security_updates_enabled` | warning | `security_and_analysis.dependabot_security_updates.status == "enabled"` | | `open_secret_alerts` | error | `GET /repos/{owner}/{repo}/secret-scanning/alerts?state=open` returns an empty array | -| `secret_scan_ci_job_present` | error | `.github/workflows/ci.yml` contains a job using `gitleaks/gitleaks-action` | +| `secret_scan_ci_job_present` | error | `.github/workflows/ci.yml` contains a `secret-scan` job using either `gitleaks/gitleaks-action` or the canonical `gitleaks detect --config .gitleaks.toml` binary-install pattern | | `gitignore_secrets_block` | warning | `.gitignore` contains `.env`, `*.pem`, `*.key` entries | | `push_protection_bypasses_recent` | warning | No bypasses in the last 30 days without a documented justification |