From 5ef1ac48e4f5e6772bb5901f7f1dd3ea4040c20b Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:03:23 -0500 Subject: [PATCH 1/8] fix(standards): update gitleaks job to use GITLEAKS_CHECKSUM and require .gitleaks.toml --- standards/push-protection.md | 54 ++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/standards/push-protection.md b/standards/push-protection.md index 05d3fe9f..a1210d45 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,21 @@ 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}" + wget -q "${url}" -O /tmp/gitleaks.tar.gz + echo "${GITLEAKS_CHECKSUM} /tmp/gitleaks.tar.gz" | sha256sum -c + tar -xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks + + - name: Run gitleaks + run: gitleaks detect --source . --config .gitleaks.toml --redact --verbose --exit-code 1 ``` The job MUST: @@ -242,9 +247,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) From 1de307b0f11123df7cabc27981ba4f3f282b0b5c Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:03:28 -0500 Subject: [PATCH 2/8] fix(standards): add .gitleaks.toml requirement and GITLEAKS_CHECKSUM note to secret-scan section --- standards/ci-standards.md | 46 +++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/standards/ci-standards.md b/standards/ci-standards.md index 4b560eb6..aedd47fe 100644 --- a/standards/ci-standards.md +++ b/standards/ci-standards.md @@ -275,12 +275,31 @@ require a valid license. Obtain a free license at [gitleaks.io](https://gitleaks **For personal/user repos:** The `GITLEAKS_LICENSE` environment variable is optional. If omitted, gitleaks runs in open-source mode (free, no license needed). +**Required repo artifact — `.gitleaks.toml`:** + +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. + +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`. + +> **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 | +| `missing gitleaks license` | License not passed to job | Ensure `GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}` is set in the job's `env:` block | +| `config file not found` | `.gitleaks.toml` missing at repo root | Copy `standards/gitleaks.toml` and commit it | +| Secrets found | Legitimate secrets in the code | Add the path to `.gitleaks.toml` `allowlist.paths`, or remove the secret | ### 5. Claude Code (`claude.yml`) @@ -969,16 +988,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 --- From cc8aab468572128659612b5210d9deaa3f49fff0 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:03:32 -0500 Subject: [PATCH 3/8] fix(standards): add canonical .gitleaks.toml template with _bmad/ allowlist --- standards/gitleaks.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 standards/gitleaks.toml 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/''', +] From fe9c2e950133ecf8ce12d9cc0574640d0a989642 Mon Sep 17 00:00:00 2001 From: don-petry Date: Tue, 5 May 2026 20:37:55 -0500 Subject: [PATCH 4/8] ci: trigger CI with clean check-suite preferences From f924c95c15f9be37223becc294cea3a53ba81100 Mon Sep 17 00:00:00 2001 From: don-petry Date: Tue, 5 May 2026 21:36:54 -0500 Subject: [PATCH 5/8] test: trigger re-review to validate Claude bot-comment workflow From bc94343161ba06958be139fbea7ef4e76e239ebc Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 02:55:09 +0000 Subject: [PATCH 6/8] fix: address Copilot and CodeRabbit review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use $RUNNER_TEMP/gitleaks-bin instead of /usr/local/bin for the gitleaks install step (Copilot: /usr/local/bin is root-owned on GitHub-hosted runners and requires sudo) - Update ci-standards.md §4 to reflect binary install: remove "gitleaks action" wording, drop the GITHUB_TOKEN env snippet, add a no-license-required note, and remove the stale GITLEAKS_LICENSE entry from the org secrets table and failure-cause table - Tighten "Secrets found" failure table row: require remove+rotate immediately; restrict allowlist.paths to confirmed false positives only (CodeRabbit) - Update compliance audit (scripts/lib/push-protection.sh) to accept either gitleaks/gitleaks-action OR the new canonical binary-install pattern (gitleaks detect --config .gitleaks.toml) (CodeRabbit) - Update compliance table description in push-protection.md to match - Replace .gitleaksignore references with .gitleaks.toml allowlist.paths in "Writing tests and fixtures" and push-protection bypass guidance (CodeRabbit) Co-authored-by: Don Petry --- scripts/lib/push-protection.sh | 11 ++++++-- standards/ci-standards.md | 50 ++++++++-------------------------- standards/push-protection.md | 18 +++++++----- 3 files changed, 30 insertions(+), 49 deletions(-) diff --git a/scripts/lib/push-protection.sh b/scripts/lib/push-protection.sh index 9f1ea5da..ef1dfe83 100644 --- a/scripts/lib/push-protection.sh +++ b/scripts/lib/push-protection.sh @@ -200,10 +200,15 @@ 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) + has_binary=$(echo "$ci_content" | grep -cE 'gitleaks[[:space:]]+detect[[:space:]].*--config[[:space:]].*\.gitleaks\.toml' || true) + + 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 090a35b1..4d1906f9 100644 --- a/standards/ci-standards.md +++ b/standards/ci-standards.md @@ -232,48 +232,22 @@ 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: - -```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:** - -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`: - - ```bash - gh secret set GITLEAKS_LICENSE --org petry-projects --body "" - ``` - -**For personal/user repos:** The `GITLEAKS_LICENSE` environment variable is optional. -If omitted, gitleaks runs in open-source mode (free, no license needed). +**Required secrets:** None for the binary install pattern. **Required repo artifact — `.gitleaks.toml`:** @@ -297,9 +271,8 @@ rule fires on knowledge file paths such as `api-request.md` and | Failure | Root cause | Fix | |---------|-----------|-----| -| `missing gitleaks license` | License not passed to job | Ensure `GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}` is set in the job's `env:` block | | `config file not found` | `.gitleaks.toml` missing at repo root | Copy `standards/gitleaks.toml` and commit it | -| Secrets found | Legitimate secrets in the code | Add the path to `.gitleaks.toml` `allowlist.paths`, or remove the secret | +| 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`) @@ -907,7 +880,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 | diff --git a/standards/push-protection.md b/standards/push-protection.md index a1210d45..9344aee4 100644 --- a/standards/push-protection.md +++ b/standards/push-protection.md @@ -233,9 +233,13 @@ secret-scan: 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 /usr/local/bin gitleaks + 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 @@ -344,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 @@ -381,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. @@ -453,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 | From 48fc9d80efa2d042f0a6ff73441bd3140cc9ce3e Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 11:03:51 +0000 Subject: [PATCH 7/8] fix(push-protection): broaden has_binary detection to handle multiline run blocks Use two separate greps so `gitleaks detect` and `--config .gitleaks.toml` can appear on different lines in a YAML run: block without being flagged as non-compliant. Co-authored-by: Don Petry --- scripts/lib/push-protection.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/lib/push-protection.sh b/scripts/lib/push-protection.sh index ef1dfe83..d8173cf3 100644 --- a/scripts/lib/push-protection.sh +++ b/scripts/lib/push-protection.sh @@ -204,7 +204,12 @@ pp_check_secret_scan_ci_job() { # (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) - has_binary=$(echo "$ci_content" | grep -cE 'gitleaks[[:space:]]+detect[[:space:]].*--config[[:space:]].*\.gitleaks\.toml' || true) + if echo "$ci_content" | grep -qE 'gitleaks[[:space:]]+detect' \ + && echo "$ci_content" | grep -qE -- '--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" \ From 6f2fe9656bc2281261bbd3b1dac0b69887fdfabf Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 19:05:59 +0000 Subject: [PATCH 8/8] fix(push-protection): collapse newlines before binary-pattern grep Two independent grep -qE calls on ci_content could match `gitleaks detect` in one step and `--config .gitleaks.toml` in an unrelated step/comment, producing a false-negative compliance pass. Collapsing newlines to spaces first ensures both tokens must appear in the same contiguous run block. Co-authored-by: Don Petry --- scripts/lib/push-protection.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/lib/push-protection.sh b/scripts/lib/push-protection.sh index d8173cf3..59fa4e7e 100644 --- a/scripts/lib/push-protection.sh +++ b/scripts/lib/push-protection.sh @@ -204,8 +204,9 @@ pp_check_secret_scan_ci_job() { # (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) - if echo "$ci_content" | grep -qE 'gitleaks[[:space:]]+detect' \ - && echo "$ci_content" | grep -qE -- '--config([[:space:]]|=).*\.gitleaks\.toml'; then + 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