Add gitleaks secret scanning to CI (#62) #65

Merged
james merged 1 commit from 62-gitleaks-ci into main 2026-06-15 13:23:02 +00:00
Owner

Closes #62.

What lands

  • .forgejo/workflows/secrets.yml (new) — gitleaks job on pull_request and push: branches: [main]. PR runs scope to BASE..HEAD; push runs scope to the full history (backfill + ongoing verification).
  • scripts/ci/security-summary.mjs — extended with a gitleaks parser (reads SARIF) and a render path that emits a rule | file | line | commit table instead of severity buckets. The threshold argument is accepted-but-ignored for gitleaks; the gate is binary.
  • .gitleaks.toml (new) — shared by the local pre-commit hook (PR #61) and the CI job. Inherits the upstream default rule pack via [extend] useDefault = true. The [allowlist] block is commented out (gitleaks rejects an empty one) with a template + format-doc for when a real false positive shows up.
  • docs/ci.md — new Secret scanning (gitleaks) section.
  • docs/adr/0011-gitleaks-ci.md (new) + index entry — rationale.

Why a separate workflow file, not a job in pr.yml

pr.yml's concurrency.group references ${{ github.event.pull_request.number }}, which is empty under a push trigger. Mixing both triggers would either break the concurrency group or require an if: github.event_name == 'pull_request' on every existing job — a per-job footgun. A dedicated file isolates the two-trigger concern. ADR-0011 covers this in detail.

Acceptance criteria

  • A throwaway branch with a synthetic GitHub PAT in a committed file will fail the CI gitleaks job — verified locally with the same gitleaks binary on a temp file containing ghp_…; CI uses the identical CLI invocation (gitleaks detect --source . --redact --report-format sarif).
  • Operational verification: a PR with no secrets surfaces the gitleaks job as green in under 30 seconds — verified once this PR's own CI run completes.
  • Findings appear in the PR step summary in the same Markdown-table style as the existing scans. Smoke-tested locally with synthetic SARIF: empty SARIF renders "No secrets detected.", populated SARIF renders the rule/file/line/commit table.
  • The job runs on the actual commits being merged, not the local hook's pass/fail. Triggered by pull_request server-side; --no-verify on the contributor's side has no effect on this scan.
  • Allowlist mechanism documented in docs/ci.md, ADR-0011, and inline in .gitleaks.toml itself. Sample format template lives in .gitleaks.toml as a commented block.

Composes with

  • #39 / PR #61 — local pre-commit hook with gitleaks. The CI scan is the safety net for the three bypass modes (no gitleaks installed, --no-verify, force-push) the local hook can't catch.
  • #14 / ADR-0005 — existing security scanners use the same scripts/ci/security-summary.mjs rendering pipeline. The gitleaks render path is structurally different (no severity scale) but produces the same Markdown-table style.
  • #44 / ADR-0009 — Renovate keeps GITLEAKS_VERSION fresh under the human-review rule once it starts seeing this workflow.
  • #46 / ADR-0010 — rebased over the install-script allowlist merge; my ADR renumbered 0010 → 0011 accordingly.

Test plan

  • CI green on this PR's own gitleaks job.
  • After merge: confirm the push-to-main run of the workflow completes against the full history and stays green.
  • Spot-check the step-summary rendering on the run page — ### gitleaks (secret scan) heading with "No secrets detected." body.
  • On a future intentional false-positive, add a [allowlist] block per the template in .gitleaks.toml and confirm the local hook and CI scan both pick it up from the single file.
Closes #62. ## What lands - `.forgejo/workflows/secrets.yml` (new) — gitleaks job on `pull_request` *and* `push: branches: [main]`. PR runs scope to `BASE..HEAD`; push runs scope to the full history (backfill + ongoing verification). - `scripts/ci/security-summary.mjs` — extended with a `gitleaks` parser (reads SARIF) and a render path that emits a `rule | file | line | commit` table instead of severity buckets. The threshold argument is accepted-but-ignored for gitleaks; the gate is binary. - `.gitleaks.toml` (new) — shared by the local pre-commit hook (PR #61) and the CI job. Inherits the upstream default rule pack via `[extend] useDefault = true`. The `[allowlist]` block is commented out (gitleaks rejects an empty one) with a template + format-doc for when a real false positive shows up. - `docs/ci.md` — new *Secret scanning (gitleaks)* section. - `docs/adr/0011-gitleaks-ci.md` (new) + index entry — rationale. ## Why a separate workflow file, not a job in pr.yml `pr.yml`'s `concurrency.group` references `${{ github.event.pull_request.number }}`, which is empty under a `push` trigger. Mixing both triggers would either break the concurrency group or require an `if: github.event_name == 'pull_request'` on every existing job — a per-job footgun. A dedicated file isolates the two-trigger concern. ADR-0011 covers this in detail. ## Acceptance criteria - [x] A throwaway branch with a synthetic GitHub PAT in a committed file will fail the CI gitleaks job — verified locally with the same gitleaks binary on a temp file containing `ghp_…`; CI uses the identical CLI invocation (`gitleaks detect --source . --redact --report-format sarif`). - [ ] *Operational verification:* a PR with no secrets surfaces the gitleaks job as green in under 30 seconds — verified once this PR's own CI run completes. - [x] Findings appear in the PR step summary in the same Markdown-table style as the existing scans. Smoke-tested locally with synthetic SARIF: empty SARIF renders "_No secrets detected._", populated SARIF renders the rule/file/line/commit table. - [x] The job runs on the actual commits being merged, not the local hook's pass/fail. Triggered by `pull_request` server-side; `--no-verify` on the contributor's side has no effect on this scan. - [x] Allowlist mechanism documented in `docs/ci.md`, ADR-0011, and inline in `.gitleaks.toml` itself. Sample format template lives in `.gitleaks.toml` as a commented block. ## Composes with - #39 / PR #61 — local pre-commit hook with gitleaks. The CI scan is the safety net for the three bypass modes (no gitleaks installed, `--no-verify`, force-push) the local hook can't catch. - #14 / ADR-0005 — existing security scanners use the same `scripts/ci/security-summary.mjs` rendering pipeline. The gitleaks render path is structurally different (no severity scale) but produces the same Markdown-table style. - #44 / ADR-0009 — Renovate keeps `GITLEAKS_VERSION` fresh under the human-review rule once it starts seeing this workflow. - #46 / ADR-0010 — rebased over the install-script allowlist merge; my ADR renumbered 0010 → 0011 accordingly. ## Test plan - [ ] CI green on this PR's own gitleaks job. - [ ] After merge: confirm the push-to-main run of the workflow completes against the full history and stays green. - [ ] Spot-check the step-summary rendering on the run page — `### gitleaks (secret scan)` heading with "_No secrets detected._" body. - [ ] On a future intentional false-positive, add a `[allowlist]` block per the template in `.gitleaks.toml` and confirm the local hook *and* CI scan both pick it up from the single file.
Add gitleaks secret scanning to CI (#62)
All checks were successful
PR / OSV-Scanner (pull_request) Successful in 37s
Secrets / gitleaks (pull_request) Successful in 38s
PR / Typecheck (pull_request) Successful in 43s
PR / npm audit (pull_request) Successful in 45s
PR / Lint (pull_request) Successful in 47s
PR / Test (postgres) (pull_request) Successful in 1m2s
PR / Build (pull_request) Successful in 1m11s
PR / Trivy (image) (pull_request) Successful in 1m15s
PR / Static analysis (Semgrep) (pull_request) Successful in 48s
PR / Test (sqlite) (pull_request) Successful in 1m1s
aed40f69bb
New `.forgejo/workflows/secrets.yml` runs a gitleaks job on every PR
(scoped to BASE..HEAD) and on every push to `main` (full history). A
dedicated workflow file isolates the two-trigger concern from pr.yml,
which stays PR-only.

`scripts/ci/security-summary.mjs` learns gitleaks SARIF: a new
`parseGitleaks` + `renderGitleaks` path produces the same step-summary
table style as the existing scanners, with rule/file/line/commit
columns instead of severity buckets. The gate is binary — every leak
qualifies.

`.gitleaks.toml` at repo root is the shared config for both the
local pre-commit hook (PR #61) and the CI job; it inherits the
upstream default rule pack and ships with no live allowlist entries
(gitleaks rejects an empty `[allowlist]` block, so the template stays
commented until a real false positive shows up).

Policy in docs/ci.md ("Secret scanning"); rationale in ADR-0010.
james merged commit b8b7bed38d into main 2026-06-15 13:23:02 +00:00
Sign in to join this conversation.
No description provided.