Skip to content

chore(security): add gitleaks pre-commit + CI secret scan (LAC-2555) - #34

Merged
lacymorrow merged 3 commits into
mainfrom
lac-2555-secret-scanner
Sep 16, 2026
Merged

lacymorrow merged 3 commits into
mainfrom
lac-2555-secret-scanner

Conversation

@lacymorrow

Copy link
Copy Markdown
Contributor

Summary

Adds a two-layer secret scanner per LAC-2555 (Paperclip): CI-side gate + local pre-commit hook. Follows LAC-2548, where a plaintext OpenAI key made it into the repo.

  • CI gate: .github/workflows/gitleaks.yml runs gitleaks-action on every PR and every push to main. Fails the check if a plaintext credential appears in the diff.
  • Local hook: .githooks/pre-commit runs gitleaks protect --staged when the binary is installed. Graceful skip + warning if not — CI still catches it either way.
  • Auto-wiring: postinstall sets git config core.hooksPath .githooks so the hook activates on the next install without adding a new devDependency.
  • Config: .gitleaks.toml extends the default rule set and allowlists placeholder-credential files (.env.example*, docs/, LLM crawl fixtures) so we don't false-positive.

Scope kept tight per Rule 28 — no refactors, no other changes.

Test plan

  • CI job "gitleaks" appears on this PR and passes (nothing sensitive in the diff).
  • After merge, git config core.hooksPath reads .githooks after pnpm install / bun install.
  • Manual leak test: echo "AWS_SECRET_ACCESS_KEY=AKIA...redacted" > /tmp/leak.txt && git add /tmp/leak.txt && git commit -m test should fail locally (if gitleaks installed) or on CI.

Refs: LAC-2555, LAC-2548, LAC-2554 (credential rotation, tracked separately).

Defense-in-depth to prevent plaintext credentials from reaching the repo.

- `.github/workflows/gitleaks.yml` — Gitleaks Action on every PR/main push.
- `.gitleaks.toml` — extends default rules; allowlists example env files,
  docs, and generated LLM crawl fixtures.
- `.githooks/pre-commit` — local scan of staged changes when gitleaks is
  installed; graceful skip + warning otherwise (CI still runs).
- `package.json` postinstall wires `core.hooksPath` to `.githooks/` on
  every install so the hook auto-activates without a new devDependency.

If a real secret is caught: rotate first, then remove from the diff
(knowledge/agent-common.md → Secret handling).
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
bones Ready Ready Preview Aug 25, 2026 6:20pm

Request Review

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Failed to generate code suggestions for PR

gitleaks-action@v2 requires a paid license on GitHub Organizations
(shipkit-io hit "missing gitleaks license" and failed every run).
Install the CLI directly and scan the PR/push diff — same coverage,
no license needed, matches the pattern merged in lacymorrow/shipkit#277.
@lacymorrow

Copy link
Copy Markdown
Contributor Author

Pushed 8c7a078 — replaced gitleaks/gitleaks-action@v2 with the raw gitleaks CLI (matches the pattern in the merged lacymorrow/shipkit#277).

Root cause of the 6-week check failure: gitleaks-action@v2 requires a paid GitHub Organization license. Every run since 2026-07-06 died with 🛑 missing gitleaks license before scanning a single line. Personal-repo forks (lash-www, shipkit-sink) never hit this because they're under a user account, which is why only bones was stuck.

Verified: new gitleaks run on this PR passes in 7s (https://github.com/shipkit-io/bones/actions/runs/32883093758).

Remaining non-blocking noise on this PR (unrelated to the secret-scanner add):

  • smoke-test fail from 2026-07-06 — Resource not accessible by integration (403) when the workflow tried to POST a commit status. Token-permission issue in the Deployment Check workflow, pre-dates this PR.
  • audit (Lighthouse loop) — Vercel preview URL churn, always flaky on stale PRs.

The PR content itself (gitleaks CI + config + hook + unrelated repo tidy) is unchanged from the 2026-07-06 commit; only the workflow install method changed. Ready for merge review — flagged on LAC-3287.

Resolves the package.json scripts conflict. Both entries survive:
main's "verify" gate and this branch's "postinstall" hooksPath wiring.
@lacymorrow
lacymorrow merged commit 89c9742 into main Sep 16, 2026
2 checks passed
@lacymorrow
lacymorrow deleted the lac-2555-secret-scanner branch September 16, 2026 18:21
@lacymorrow

Copy link
Copy Markdown
Contributor Author

Merged. Resolved the package.json conflict by keeping both scripts (verify from main, postinstall from this branch) and merged main in rather than rebasing, so no force-push. The gitleaks check passed on the updated head.

Three things found during triage that this PR does not cover, worth separate tickets:

1. One real credential in git history, not caught by this config. The CI job scans the PR/push diff only, so it never looks at history. A full-history scan run locally with this exact .gitleaks.toml reports 1 finding:

  • rule generic-api-key, file next.config.ts line 148, commit 0055fb9 ("Initial commit", 2025-02-16)
  • a Logflare apiKey committed in plaintext inside a commented-out config block, with a // Move to env note next to it
  • it is no longer present on main, but it is still reachable in history on a public repo

The value needs rotating. Removing it from HEAD did not un-expose it. This is the same class of problem as LAC-2554.

2. GitHub-native secret scanning is off. shipkit-io/bones is public, and secret_scanning, secret_scanning_push_protection, secret_scanning_validity_checks and dependabot_security_updates are all disabled. Push protection is free on public repos and blocks at push time rather than at PR time, which is strictly earlier than this workflow. Turning it on complements gitleaks instead of duplicating it.

3. [skip ci] bypasses this gate. CLAUDE.md line 164 tells contributors to use [skip ci] after running scripts/verify.sh, and notes that this skips gitleaks. verify.sh does not run gitleaks, so the documented local workflow has no secret scan in it at all. Either add a gitleaks step to verify.sh or drop the [skip ci] guidance for this check.

Config notes for the record, verified by running the scanner against fixtures:

  • a committed .env holding a live key is caught (fired on stripe-access-token)
  • .env.test, .env.example*, docs/**, README.md, CHANGELOG.md and public/llms*.txt are allowlisted, so a real key in any of those is silently missed. docs/** is the broad one
  • the allowlist is path-scoped and does not disable any rule class, which is the right shape
  • the job uses --exit-code 1, so it fails the build rather than warning
  • the pre-commit hook is genuinely wired, via postinstall setting core.hooksPath to .githooks. bones has no husky or lefthook, so nothing is clobbered. It activates on the next pnpm install / bun install, and no-ops with a warning if the gitleaks binary is absent

Cross-repo: .gitleaks.toml and .githooks/pre-commit are byte-identical to lacymorrow/shipkit-www#57 and lacymorrow/lash-www#3. Only the workflow differs. This repo carries the LAC-3287 fix that swaps gitleaks-action@v2 for the CLI, because the action requires a paid license on GitHub Organizations. The other two still use the action, which is fine under a personal account but leaves them on the older shape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant