diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..27db6b0 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,25 @@ +#!/usr/bin/env sh +# Secret scanner — LAC-2555 +# +# Blocks commits that introduce plaintext credentials. +# Requires `gitleaks` locally; if not installed we print a warning and +# fall through — CI still runs the same scan on the PR, so this is +# defense-in-depth, not the only gate. +# +# Install: +# macOS → brew install gitleaks +# linux → https://github.com/gitleaks/gitleaks/releases + +if command -v gitleaks >/dev/null 2>&1; then + if ! gitleaks protect --staged --redact --no-banner; then + echo "" + echo "gitleaks found a potential secret in staged changes." + echo "If real: rotate the credential first, then remove it from the diff." + echo "If a false positive: extend .gitleaks.toml allowlist or add an inline" + echo "'gitleaks:allow' comment. Do not disable the hook." + exit 1 + fi +else + echo "gitleaks not installed — skipping local secret scan (CI still runs)." + echo " install: brew install gitleaks" +fi diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml new file mode 100644 index 0000000..235c0ec --- /dev/null +++ b/.github/workflows/gitleaks.yml @@ -0,0 +1,30 @@ +# Secret scan — LAC-2555 +# +# Fails the PR/push if plaintext credentials appear in the diff. +# Config lives in .gitleaks.toml at the repo root. +# +# Local defense-in-depth: .husky/pre-commit runs the same tool on staged +# changes when the gitleaks binary is installed. + +name: gitleaks + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + gitleaks: + name: gitleaks + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..ca606d3 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,23 @@ +# gitleaks config — LAC-2555 +# +# Extends the built-in rule set. Allowlist covers files that legitimately +# contain placeholder credentials (example env files, docs, changelogs). +# When you add a new placeholder file that trips gitleaks, extend the +# `paths` list here rather than removing the scan. + +[extend] +useDefault = true + +[allowlist] +description = "Placeholder-credential files and generated docs" +paths = [ + '''(^|/)\.env\.example(\..+)?$''', + '''(^|/)\.env\.example\.local$''', + '''(^|/)\.env\.test$''', + '''(^|/)\.env\.example\.production$''', + '''(^|/)README\.md$''', + '''(^|/)CHANGELOG\.md$''', + '''(^|/)LICENSE$''', + '''(^|/)docs/''', + '''(^|/)public/llms(-full)?\.txt$''', +] diff --git a/package.json b/package.json index d554b3d..bbfd941 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,8 @@ "test:coverage": "vitest run --coverage", "test:browser": "vitest --config=vitest.config.browser.ts", "test:node": "vitest --config=vitest.config.node.ts", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "postinstall": "git config core.hooksPath .githooks 2>/dev/null || true" }, "dependencies": { "@auth/drizzle-adapter": "1.11.1",