# An automated secret-scanning gate exists in the CI or pre-commit change path

- **Pattern:** `ab-002635` (`soc2-readiness.vuln-management-and-monitoring.secret-scanning-in-ci`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-07-03
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002635
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002635)

## Why it matters

A one-time secrets sweep tells you what has already leaked; a scanning gate in the change path is what stops the next leak, and the next one is coming: GitGuardian measured 23.8 million new secrets pushed to public GitHub in 2023 alone, and the Toyota incident (an access key sitting in a public repo for five years) shows how long a single miss survives. This evidences SOC 2 CC7.1 (detection and monitoring of configuration changes and vulnerabilities): the auditor wants to see a recurring, automated detection mechanism, not a clean point-in-time grep. AI-generated and vibe-coded apps typically fail here because coding assistants scaffold the app and sometimes even a test workflow, but no tutorial-shaped prompt ever adds gitleaks or trufflehog to CI, so the change path ships with zero secret detection while `.env` values migrate into committed config during rapid iteration. One leg of this check is an evidence-pointer: GitHub's native secret scanning and push protection live in repo settings that are invisible from a clone, so a committed doc stating they are enabled counts as the evidence an auditor will ask for, with the detail noting it is doc-evidenced rather than directly observed.

## Severity rationale

Medium because this is a layered detective control: its absence does not itself expose a secret, it removes the automated gate that would catch the hardcoded credentials other checks score directly, so exposure accrues on the next mistake rather than immediately.

## Remediation

Add a blocking gitleaks step to your CI workflow, e.g. in `.github/workflows/ci.yml`:

```yaml
- uses: gitleaks/gitleaks-action@v2
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

Do not add `continue-on-error: true`; the step must be able to fail the build. Alternatively wire `gitleaks protect --staged` into `.husky/pre-commit` or add the `gitleaks` hook to `.pre-commit-config.yaml`. If you rely on GitHub's built-in secret scanning with push protection instead, enable it in repo settings and commit a short note in `SECURITY.md` stating that it is enabled, so the control is evidenced in the repo an auditor reviews.

## Detection

- **ID:** `secret-scanning-in-ci`
- **Severity:** `medium`
- **What to look for:** First enumerate CI configuration: `.github/workflows/*.yml` and `*.yaml`, `.gitlab-ci.yml`, `.circleci/config.yml`, `bitbucket-pipelines.yml`, `Jenkinsfile`, `azure-pipelines.yml`. Then search four pass legs. (1) CI step invoking a dedicated secret scanner: `uses: gitleaks/gitleaks-action@...`, `gitleaks detect` / `gitleaks git` / `gitleaks protect` in a `run:` step, `uses: trufflesecurity/trufflehog@...` or `trufflehog git|filesystem` in a `run:`, or `detect-secrets scan` / `detect-secrets-hook`. Include indirect invocations: a `package.json` script, `Makefile` target, or `noxfile.py`/`tox.ini` session that runs one of these tools AND is itself called from a CI step. (2) Commit-time hook config: `.pre-commit-config.yaml` containing a gitleaks, trufflehog, or detect-secrets hook (e.g. `repo: https://github.com/gitleaks/gitleaks` with `id: gitleaks`, or `id: detect-secrets`); JS-ecosystem equivalents count the same way: `.husky/pre-commit` or `lefthook.yml` invoking one of the three scanners. (3) Committed scanner config (`.gitleaks.toml` / `gitleaks.toml`, `.secrets.baseline`, `trufflehog.yaml` / `.trufflehog.yaml`) paired with a CI invocation of the matching tool per leg 1. (4) Evidence-pointer doc: a committed document (`SECURITY.md`, `README.md`, `docs/**`, `compliance/**`) explicitly stating that GitHub secret scanning and/or push protection is enabled for this repo.
- **Pass criteria:** Any one of the four legs is satisfied with the invocation live (not commented out, not gated behind `if: false`, exit code not swallowed). Leg 4 passes on the doc alone, since the host-side setting is invisible from a clone, but the receipt must note it is doc-evidenced.
- **Fail criteria:** CI configuration exists and none of the four legs is satisfied. Sneaky variants that still fail: `gitleaks`, `trufflehog`, or `detect-secrets` present in `package.json` devDependencies or `requirements*.txt` with no CI step or hook ever invoking it (vocabulary presence is not a gate); a scanner step that is commented out, behind `if: false`, or in a workflow triggered only by `workflow_dispatch` with no push/PR trigger; a scanner `run:` suffixed `|| true` or a step with `continue-on-error: true` (it can never block a change, so it is not a gate); a committed `.secrets.baseline` or `.gitleaks.toml` with no invocation anywhere (leg 3 requires the pairing); a `.pre-commit-config.yaml` that exists but contains no secret-scanning hook; a doc that merely recommends enabling secret scanning without stating it is enabled.
- **Skip (N/A) when:** No pass leg is satisfied AND no CI configuration exists at all: the CI-absence root cause is already scored by this audit's automated-tests check, so do not double-punish it here. Quote: `"No CI configuration found (.github/workflows/, .gitlab-ci.yml, .circleci/config.yml, bitbucket-pipelines.yml, Jenkinsfile, azure-pipelines.yml all absent) — CI absence is scored by the automated-tests check"`.
- **Before evaluating, quote:** The satisfying evidence verbatim with its file path: the CI step or `run:` line (leg 1), the hook stanza (leg 2), the config filename plus the CI line invoking the tool (leg 3), or the doc sentence claiming secret scanning / push protection is enabled (leg 4). On fail, list the CI files found (proving CI exists) and state that a search for gitleaks, trufflehog, detect-secrets, and a secret-scanning doc across workflows, hook configs, and docs returned nothing live.
- **Report even on pass:** `"Secret-scanning gate: <tool> via <path> (blocking CI step | pre-commit hook | config + CI invocation | doc-evidenced GitHub push protection)"`.
- **Detail on fail:** `"CI exists (.github/workflows/ci.yml, deploy.yml) but no secret-scanning step, hook, or doc found; gitleaks is in devDependencies yet nothing in any workflow, .husky/, or .pre-commit-config.yaml invokes it"` or `"gitleaks step in .github/workflows/ci.yml has continue-on-error: true, so findings can never block a merge — not a gate; no other scanner or push-protection doc found. If the control lives in GitHub repo settings, commit evidence of it (e.g. a SECURITY.md note)"`.
- **Remediation:** Add a blocking `gitleaks/gitleaks-action` step to `.github/workflows/ci.yml` (no `continue-on-error`), or wire `gitleaks protect --staged` into `.husky/pre-commit`, or add the `gitleaks` hook to `.pre-commit-config.yaml`. If you use GitHub's native secret scanning with push protection instead, enable it and commit a note in `SECURITY.md` stating it is enabled. This gate prevents the next leak; already-committed secrets are scored by the `no-hardcoded-secrets` check in the `project-snapshot` Stack Scan.

## External references

- soc2 CC7.1
- iso-27001 A.8.8
- nist RA-5

Taxons: cryptography-and-secrets, change-management

HTML version: https://auditbuffet.com/patterns/ab-002635
