From 25a6a454b9060cf47da45d142fc9c210b39ea06b Mon Sep 17 00:00:00 2001 From: Defame1297 Date: Sat, 20 Jun 2026 11:29:10 +0000 Subject: [PATCH] feat: add gitleaks secret scanning setup and skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/setup-gitleaks.sh — installs gitleaks v8.24.2, seeds .gitleaks.toml on first run, writes managed pre-commit hook block; re-run replaces block in place without disturbing other hook content - scripts/gitleaks.toml — base config template extending default ruleset - .gitleaks.toml — repo config with docs/research/ path allowlist (high-entropy terminal captures; v8.24.2 [allowlist] syntax) - tests/test-setup-gitleaks.sh — 6 behavior tests including stale-block replacement and idempotency - .agents/skills/gitleaks/ — cross-cutting skill covering install, update, allowlist tuning, scan modes, and real-finding remediation - .agents/evals/cross-cutting/gitleaks/eval.yaml — 7 trigger + 3 output tests including version-aware allowlist guidance case - docs/spec/overview.md — updated to reflect new tooling and skill Co-Authored-By: Claude Sonnet 4.6 --- .../evals/cross-cutting/gitleaks/eval.yaml | 84 ++++++++++ .agents/skills/gitleaks/META.md | 15 ++ .agents/skills/gitleaks/SKILL.md | 111 +++++++++++++ .../gitleaks/references/allowlist-patterns.md | 83 ++++++++++ .gitleaks.toml | 27 ++++ docs/spec/overview.md | 7 +- scripts/gitleaks.toml | 24 +++ scripts/setup-gitleaks.sh | 124 ++++++++++++++ tests/test-setup-gitleaks.sh | 151 ++++++++++++++++++ 9 files changed, 624 insertions(+), 2 deletions(-) create mode 100644 .agents/evals/cross-cutting/gitleaks/eval.yaml create mode 100644 .agents/skills/gitleaks/META.md create mode 100644 .agents/skills/gitleaks/SKILL.md create mode 100644 .agents/skills/gitleaks/references/allowlist-patterns.md create mode 100644 .gitleaks.toml create mode 100644 scripts/gitleaks.toml create mode 100755 scripts/setup-gitleaks.sh create mode 100755 tests/test-setup-gitleaks.sh diff --git a/.agents/evals/cross-cutting/gitleaks/eval.yaml b/.agents/evals/cross-cutting/gitleaks/eval.yaml new file mode 100644 index 0000000..7f7f05e --- /dev/null +++ b/.agents/evals/cross-cutting/gitleaks/eval.yaml @@ -0,0 +1,84 @@ +skill_name: gitleaks + +trigger_tests: + - id: explicit-trigger-install + name: Explicit trigger — install and configure + query: "set up gitleaks in this repo" + should_trigger: true + + - id: explicit-trigger-update-hook + name: Explicit trigger — update hook + query: "update the gitleaks pre-commit hook" + should_trigger: true + + - id: implicit-trigger-false-positive + name: Implicit trigger — suppress false positive in pre-commit hook + query: "my pre-commit hook keeps blocking commits because it thinks my test fixture has an API key, how do I suppress it?" + should_trigger: true + + - id: implicit-trigger-scan-history + name: Implicit trigger — audit repo history for secrets + query: "I want to scan my entire git history to make sure no credentials were ever committed" + should_trigger: true + + - id: negative-trigger-security-review + name: Negative trigger — general code security review + query: "do a security review of this pull request" + should_trigger: false + + - id: negative-trigger-ci-scan + name: Negative trigger — CI pipeline secret scanning + query: "add secret scanning to my GitHub Actions CI pipeline" + should_trigger: false + + - id: negative-trigger-other-tool + name: Negative trigger — different secret scanning tool + query: "trufflehog is flagging false positives in my repo, how do I configure it?" + should_trigger: false + +output_tests: + - id: output-states-intent-before-running + name: Deterministic — states intent and waits for confirmation before running setup script + type: deterministic + prompt: "set up gitleaks in /home/user/myproject" + expected_output: > + The skill states what it is about to do (run setup-gitleaks.sh, which will modify + .git/hooks/pre-commit and seed .gitleaks.toml) and asks for confirmation before + executing the script. The confirmation request appears before any Bash command + that runs the script. + assertions: + - "Output contains 'setup-gitleaks.sh' before any Bash tool invocation that runs it" + - "Output contains a confirmation prompt (e.g. 'Proceed?') before running the setup script" + - "Output does not run the setup script without an explicit confirmation step" + + - id: output-redacts-secret-values + name: Deterministic — redacts secret value and prescribes rotation before history rewrite + type: deterministic + prompt: "gitleaks found an AWS secret key on line 42 of config/settings.py — it says the value is AKIAIOSFODNN7EXAMPLE. What do I do?" + expected_output: > + The skill does not echo or repeat the secret value. It references the finding by + rule ID, file path, and line number only. It states that the secret must be rotated + immediately. It mentions git filter-repo (not git filter-branch) for history rewrite + and asks for user confirmation before running the rewrite. + assertions: + - "Output does not contain the literal string 'AKIAIOSFODNN7EXAMPLE'" + - "Output mentions rotating or revoking the credential as the first action" + - "Output references 'git filter-repo' for history rewrite, not 'git filter-branch'" + - "Output states that user confirmation is required before running the history rewrite" + + - id: output-quality-allowlist-guidance + name: LLM-rubric — allowlist guidance is correct, version-aware, and minimal + type: llm-rubric + prompt: "gitleaks keeps flagging my docs/research/ directory as containing secrets, how do I suppress it?" + expected_output: > + High-quality output checks the installed gitleaks version before prescribing any + TOML syntax, recommends a path-based allowlist entry in .gitleaks.toml (not a + .gitleaksignore fingerprint), uses the correct TOML syntax for the detected version, + adds only the minimal allowlist entry needed for the identified false positive, and + includes a verification step (re-run gitleaks dir -v or gitleaks dir --log-level debug) + after making the change. + assertions: + - "Output checks or asks about the gitleaks version before writing TOML syntax" + - "Output recommends a path-based allowlist entry in .gitleaks.toml rather than .gitleaksignore" + - "Output includes a command to verify the suppression works after the change" + - "Output explains why .gitleaksignore fingerprints are fragile (line numbers shift)" diff --git a/.agents/skills/gitleaks/META.md b/.agents/skills/gitleaks/META.md new file mode 100644 index 0000000..ee41cf2 --- /dev/null +++ b/.agents/skills/gitleaks/META.md @@ -0,0 +1,15 @@ +```yaml +version: "1.0" +updated: 2026-06-20 + +when: > + Invoked when the user wants to install gitleaks and wire it as a git pre-commit secret + scanner, update the hook in an existing repo, tune allowlist rules to suppress false + positives, debug a scan finding, or rotate a real secret that was found. Covers the full + lifecycle: install → configure → maintain → remediate. Not invoked for general code + security review (security-review skill) or CI pipeline secret scanning (write-ci-pipeline skill). + +references: + - https://github.com/gitleaks/gitleaks/releases/tag/v8.24.2 + - https://github.com/gitleaks/gitleaks/blob/main/README.md +``` diff --git a/.agents/skills/gitleaks/SKILL.md b/.agents/skills/gitleaks/SKILL.md new file mode 100644 index 0000000..4e5d5b9 --- /dev/null +++ b/.agents/skills/gitleaks/SKILL.md @@ -0,0 +1,111 @@ +--- +name: gitleaks +description: Use when the user wants to install gitleaks, wire it as a git pre-commit secret scanner, update the hook in an existing repo, tune allowlist rules, resolve false positives, or debug a gitleaks scan finding. Do NOT use when the user wants a general security review of code (use security-review), wants to add secret scanning to a CI pipeline (use write-ci-pipeline), or is asking about a different secret scanning tool such as trufflehog or git-secrets. +metadata: + category: cross-cutting +allowed-tools: + - Bash + - Read + - Edit +--- + + + +## Required inputs + +- **Target repo path** — absolute path to the git repository to configure; inferred from current working directory if not stated, ask if ambiguous +- **Task type** — install/configure, update hook, tune allowlist, debug finding; inferred from the user's request + +## Constraints + +- Always state what you are about to do before running `setup-gitleaks.sh` — the script modifies `.git/hooks/pre-commit` and seeds `.gitleaks.toml` +- Never modify `.gitleaks.toml` if the user has not asked for allowlist changes — it is project-owned once seeded; treat it as user-controlled config +- Never run `gitleaks git` or `gitleaks dir` across the full history without warning the user it may be slow on large repos +- Redact any secret values that appear in gitleaks output before showing them to the user — show the rule ID, file, and line number only +- When the installed gitleaks version is unknown, check it with `gitleaks version` before suggesting config syntax — v8.24.2 uses `[allowlist]`; v8.25.0+ uses `[[allowlists]]` +- False positive suppression: prefer path-based allowlists in `.gitleaks.toml` over fingerprint-based entries in `.gitleaksignore` — fingerprints are line-number-sensitive and break on file edits + + + + + +## Process + +### Install and configure + +1. **Confirm target.** State: "I will run `scripts/setup-gitleaks.sh ` which will install gitleaks (if absent), seed `.gitleaks.toml` (first run only), and write the pre-commit hook. Proceed?" Wait for confirmation — this modifies the repo's git hook. + +2. **Run setup script.** Execute from the ai-development repo root: + ``` + bash scripts/setup-gitleaks.sh + ``` + The script is idempotent — it replaces the gitleaks block in the hook on every run without disturbing other hook content. + +3. **Verify installation.** Run `gitleaks version` to confirm the binary is available. Run `gitleaks git --staged --redact -v` in the target repo to confirm the hook would work on a staged commit (add a dummy change if needed to test). + +4. **Commit `.gitleaks.toml`.** Remind the user that `.gitleaks.toml` belongs in version control so all contributors share the same allowlist rules. + +### Update hook + +Re-run `bash scripts/setup-gitleaks.sh ` from the ai-development repo root. The managed block (delimited by `# managed by setup-gitleaks.sh` / `# end gitleaks` markers) is always replaced with the current version. Non-gitleaks hook content is preserved. + +### Tune allowlist / resolve false positives + +1. **Identify the false positive.** Run `gitleaks dir --log-level debug ` to see which rule fired and which allowlist entries (if any) are already active. + +2. **Check the gitleaks version.** Run `gitleaks version`. Use `[allowlist]` syntax for v8.24.2; use `[[allowlists]]` syntax for v8.25.0+. Using the wrong syntax silently produces no errors but the allowlist does nothing — this is the most common configuration trap. + +3. **Choose suppression strategy.** Read `.gitleaks.toml` first. See `references/allowlist-patterns.md` for syntax examples and when to use each approach: + - Path regex in `[allowlist]` — for files that can never contain real secrets (research notes, terminal captures, test fixtures). Preferred. + - Stopwords in `[allowlist]` — for placeholder patterns like "example", "changeme". + - `disabledRules` in `[extend]` — to disable a noisy default rule entirely. Use only when the rule has no value for this repo. + - `.gitleaksignore` fingerprint — last resort; breaks when the file is edited because line numbers shift. + +4. **Edit `.gitleaks.toml`.** Add the minimal allowlist entry needed. Do not suppress more than the identified false positive. + +5. **Verify.** Re-run `gitleaks dir -v ` or `gitleaks git -v` to confirm the false positive is suppressed and no real findings are hidden. + +### Scan modes + +| Mode | Command | When to use | +|---|---|---| +| Staged changes (pre-commit) | `gitleaks git --staged --redact -v` | What the hook runs | +| Full commit history | `gitleaks git -v` | Audit existing repo history | +| Working directory files | `gitleaks dir -v ` | Scan uncommitted files | +| Debug allowlists | `gitleaks dir --log-level debug ` | See which files are skipped and which allowlists fire | + +### Resolve a real finding + +1. Do not redact or show the secret value. Reference the rule ID, file, and line number only. +2. The secret is compromised the moment it was committed — rotate it immediately, regardless of whether the commit is reachable from the public remote. +3. Remove the secret from history using `git filter-repo` (not `git filter-branch`). This is a history-rewrite — confirm with the user before running. Force-push to all remotes after rewriting. +4. Add the file path to the `.gitleaks.toml` allowlist only if the file is known to be a false-positive source going forward (e.g. a test fixture). Do not add an allowlist entry to suppress a real finding that has been removed. + +## Output format + +No structured output file. The skill produces: +- Modified `.git/hooks/pre-commit` in the target repo (via the setup script) +- Modified `.gitleaks.toml` in the target repo (allowlist changes only, when requested) +- Terminal confirmation of what was changed and what to do next + + + + + +## Failure handling + +- `setup-gitleaks.sh` not found — stop; instruct the user to run from the ai-development repo root at `/root/ai-development/` +- Target path is not a git repository — report the error from the script and ask the user to confirm the correct path +- `gitleaks` binary not installed and download fails — report the curl/network error; direct the user to manual install at `https://github.com/gitleaks/gitleaks/releases` +- Wrong TOML syntax for installed version — detect via `gitleaks version`, show the correct syntax for that version, do not guess + +## Self-check + +- [ ] Target repo confirmed before running the setup script +- [ ] `gitleaks version` checked before writing any `.gitleaks.toml` allowlist syntax +- [ ] Secret values in scan output redacted before displaying to the user +- [ ] `.gitleaks.toml` edits are minimal — only the identified false positive suppressed +- [ ] After any allowlist change: re-ran scan to verify suppression works and no real findings are hidden +- [ ] For real findings: rotation step stated before history rewrite, user confirmed history rewrite before running `git filter-repo` + + diff --git a/.agents/skills/gitleaks/references/allowlist-patterns.md b/.agents/skills/gitleaks/references/allowlist-patterns.md new file mode 100644 index 0000000..c5e17e6 --- /dev/null +++ b/.agents/skills/gitleaks/references/allowlist-patterns.md @@ -0,0 +1,83 @@ +# Gitleaks allowlist patterns + +## Version syntax + +| Version | Allowlist syntax | +|---|---| +| v8.24.2 and earlier | `[allowlist]` (singular table) | +| v8.25.0 and later | `[[allowlists]]` (array of tables) | + +**Critical**: using the wrong syntax produces no error but the allowlist silently does nothing. Always check `gitleaks version` first. + +## v8.24.2 syntax (this repo uses 8.24.2) + +### Suppress by path regex + +Use for files that can never contain real secrets (research notes, terminal captures, test fixtures, generated docs). + +```toml +[allowlist] +description = "research notes and terminal captures" +paths = [ + '''docs/research/.*''', + '''tests/fixtures/.*''', +] +``` + +### Suppress by stopword + +Use for placeholder values that match secret patterns but are clearly not real. + +```toml +[allowlist] +description = "placeholder values" +stopwords = ["example", "placeholder", "changeme", "your-api-key-here"] +``` + +### Disable a default rule entirely + +Use only when a rule has no value for this repo and produces pervasive false positives. + +```toml +[extend] +useDefault = true +disabledRules = ["generic-api-key"] +``` + +## v8.25.0+ syntax (for reference) + +```toml +[[allowlists]] +description = "research notes" +paths = ['''docs/research/.*'''] + +[[allowlists]] +description = "placeholder values" +stopwords = ["example", "placeholder"] +``` + +## .gitleaksignore (fingerprint-based — last resort) + +``` +# Format: : +# Generated by: gitleaks git -v --report-format json | jq -r '.[] | "\(.Fingerprint):\(.StartLine)"' +abc123def456:42 +``` + +Avoid this approach: fingerprints embed line numbers. Any edit to the file shifts line numbers and invalidates the entry, re-surfacing the false positive. + +## Verification after any change + +```bash +# Scan current files +gitleaks dir -v . + +# Scan with debug output to see which allowlists fired +gitleaks dir --log-level debug . + +# Scan commit history +gitleaks git -v + +# Scan only staged changes (what the pre-commit hook runs) +gitleaks git --staged --redact -v +``` diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..f740e1d --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,27 @@ +title = "gitleaks config" + +[extend] +# Extends the default ruleset built into gitleaks. +# Remove useDefault and define [[rules]] from scratch if you want full control. +useDefault = true + +# Rules to disable from the default set — uncomment and add IDs for known false positives. +# Run `gitleaks git -v` on your repo first to discover which rules fire. +# disabledRules = ["generic-api-key"] + +# Project-specific allowlists — entries here apply to all rules. +# Add fingerprints from .gitleaksignore, or path/regex patterns to suppress noise. +# +# Example: ignore test fixtures +# [[allowlists]] +# description = "test fixtures" +# paths = ['''tests/fixtures/.*'''] +# +# Example: ignore a known false-positive secret value +# [[allowlists]] +# description = "placeholder values used in docs" +# stopwords = ["example", "placeholder", "changeme"] + +[allowlist] +description = "research session notes — no secrets, high-entropy text from terminal captures" +paths = ['''docs/research/.*'''] diff --git a/docs/spec/overview.md b/docs/spec/overview.md index 8e7ce08..6e6a754 100644 --- a/docs/spec/overview.md +++ b/docs/spec/overview.md @@ -2,7 +2,7 @@ Current deployed state of this repo — what you get if you run `install.sh` today. Updated at the close of each chunk and in the same PR as any behavior change. -*Last updated: 2026-05-18 (issue 0018 phase 1 refactor)* +*Last updated: 2026-06-20 (gitleaks tooling + skill)* ## What is deployed @@ -14,7 +14,7 @@ Current deployed state of this repo — what you get if you run `install.sh` tod - `write-skill` — authors new SKILL.md files and converts placeholders to canonical format. Hand-written (bootstrap — cannot author itself before it exists). Eval at `.agents/evals/factory/write-skill/eval.yaml`. Invokes `write-eval` as part of its own process. - `write-docs` — produces technical documentation derived from code and spec; never invents behaviour. **First factory-authored skill** (SKILL.md produced via `write-skill`, eval via `write-eval`). Eval at `.agents/evals/implement/write-docs/eval.yaml`. Sources: anthropics/skills `doc-coauthoring`, mattpocock/skills `write-a-skill`, bmad-code-org/BMAD-METHOD `bmad-advanced-elicitation`. -Current skills: `caveman`, `diagnose`, `grill-me`, `grill-with-docs`, `improve-codebase-architecture`, `prototype`, `tdd`, `to-issues`, `to-prd`, `triage`, `write-docs`, `write-eval`, `write-skill`, `zoom-out`. +Current skills: `caveman`, `diagnose`, `gitleaks`, `grill-me`, `grill-with-docs`, `improve-codebase-architecture`, `prototype`, `tdd`, `to-issues`, `to-prd`, `triage`, `write-docs`, `write-eval`, `write-skill`, `zoom-out`. **Chunk 3 target:** 42 skills across 9 categories. PRD: `docs/prd/chunk-3-skills-library.md`. Canonical build reference: `docs/research/ai-coding-factory/ai-coding-factory-skills-index.md` (delete once all skills exist). Skills stored flat (`skill-name/SKILL.md`) per ADR-0009; category in `metadata.category` frontmatter. Categories: design, factory, implement, test, review, deploy, operate, cross-cutting, iac (2 skills only — docker-compose + iac-security-review). Role skills (6) deferred to Chunk 5. Gitea skills moved to `providers/gitea/` provider adapter. @@ -42,11 +42,14 @@ Current skills: `caveman`, `diagnose`, `grill-me`, `grill-with-docs`, `improve-c - `init-project.sh` — bootstraps a new project (Chunk 6) - Copilot provider adapter (Chunk 7) - Formal CI/pre-commit enforcement of governance rules (Chunk 6) +- `setup-gitleaks.sh` not yet wired into `init-project.sh` (Chunk 6) — run manually against new repos For chunk planning and open questions, see `docs/ROADMAP.md`. ## Recent changes +- 2026-06-20 — Gitleaks secret scanning added. `scripts/setup-gitleaks.sh` installs gitleaks v8.24.2, seeds `.gitleaks.toml` (first run only — project-owned after that), and writes a managed pre-commit hook block that is replaced on re-run. `scripts/gitleaks.toml` is the base config template extending gitleaks defaults. `tests/test-setup-gitleaks.sh` covers 6 behaviors (reject non-git dir, config deploy, hook create, append, stale-block replace, idempotency). `.gitleaks.toml` in repo root adds path allowlist for `docs/research/` (high-entropy terminal captures). `gitleaks` skill added (`cross-cutting`) covering full lifecycle: install, update, tune allowlist, scan modes, resolve real findings. Key lesson: v8.24.2 uses `[allowlist]` (singular); v8.25.0+ uses `[[allowlists]]` — wrong syntax silently does nothing. + - 2026-05-18 — Issue 0018 phase 1 refactor complete: `write-skill` redesigned from scratch. New files added to skill directory: `SKILL-TEMPLATE.md` (authoritative 6-section template with XML blocks, human-usable), `META-TEMPLATE.md` (provenance schema with inline-commented YAML), `CATEGORIES.md` (self-contained category table), `META.md` (write-skill's own provenance). SKILL.md rewritten: 6 sections replacing 8 (Role and When/When not dropped — not in agentskills.io spec); frontmatter reduced to 3 fields (`name`, `description`, `metadata.category`); provenance fields (`version`, `updated`, `when`, `source`, `references`) moved to META.md (progressive disclosure — not loaded at startup). `docs/notes/skill-implementation-workflow.md` updated to reference SKILL-TEMPLATE.md as the authoritative template. - 2026-05-17 — Issue 0018 phase 2 complete: `write-docs` skill written and deployed. First skill produced end-to-end by the factory (SKILL.md via `write-skill`, eval via `write-eval`). Category: implement. Key decisions: file-approval gate before reading (user names files or approves proposals); gap check before drafting (user fills what code doesn't explain); stage skipping allowed with logged reason; full revised section shown before confirmation gate; surgical edits only with per-round delta summary; Reader Testing via scoped sub-agent (doc + questions only, no source files); summary/overview sections written last. Sources: anthropics/skills doc-coauthoring (Reader Testing stage, surgical-edit constraint), mattpocock/skills write-a-skill (trigger pattern), bmad-code-org/BMAD-METHOD bmad-advanced-elicitation (confirmation gate). Open follow-up: documentation convention (file/folder/content structure, global vs repo-specific) — not yet defined. diff --git a/scripts/gitleaks.toml b/scripts/gitleaks.toml new file mode 100644 index 0000000..97b8fa8 --- /dev/null +++ b/scripts/gitleaks.toml @@ -0,0 +1,24 @@ +title = "gitleaks config" + +[extend] +# Extends the default ruleset built into gitleaks. +# Remove useDefault and define [[rules]] from scratch if you want full control. +useDefault = true + +# Rules to disable from the default set — uncomment and add IDs for known false positives. +# Run `gitleaks git -v` on your repo first to discover which rules fire. +# disabledRules = ["generic-api-key"] + +# Global allowlist — applies to all rules. +# Note: uses [allowlist] (v8 syntax). v8.25.0+ uses [[allowlists]] (array of tables). +# Add path regexes or stopwords to suppress known false positives. +# +# Example: ignore test fixtures +# [allowlist] +# description = "test fixtures" +# paths = ['''tests/fixtures/.*'''] +# +# Example: ignore a known false-positive secret value +# [allowlist] +# description = "placeholder values in docs" +# stopwords = ["example", "placeholder", "changeme"] diff --git a/scripts/setup-gitleaks.sh b/scripts/setup-gitleaks.sh new file mode 100755 index 0000000..ea322c5 --- /dev/null +++ b/scripts/setup-gitleaks.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Sets up gitleaks as a git pre-commit hook in a target repository. +# Usage: setup-gitleaks.sh [TARGET_REPO] +# TARGET_REPO — path to the git repo to configure (default: current directory) +# Idempotent: safe to re-run; always replaces the hook block with the current version. + +GITLEAKS_VERSION="8.24.2" +GITLEAKS_INSTALL_DIR="${GITLEAKS_INSTALL_DIR:-/usr/local/bin}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TARGET="${1:-$(pwd)}" +HOOK_FILE="$TARGET/.git/hooks/pre-commit" +CONFIG_SRC="$SCRIPT_DIR/gitleaks.toml" +CONFIG_DEST="$TARGET/.gitleaks.toml" +MARKER="# managed by setup-gitleaks.sh" +END_MARKER="# end gitleaks" + +# --- Install gitleaks if not present --- + +install_gitleaks() { + local os arch tarball url tmp_dir + + case "$(uname -s)" in + Linux) os="linux" ;; + Darwin) os="darwin" ;; + *) + echo "Error: unsupported OS '$(uname -s)' — install gitleaks manually from https://github.com/gitleaks/gitleaks/releases" >&2 + exit 1 + ;; + esac + + case "$(uname -m)" in + x86_64) arch="x64" ;; + aarch64 | arm64) arch="arm64" ;; + *) + echo "Error: unsupported architecture '$(uname -m)' — install gitleaks manually from https://github.com/gitleaks/gitleaks/releases" >&2 + exit 1 + ;; + esac + + tarball="gitleaks_${GITLEAKS_VERSION}_${os}_${arch}.tar.gz" + url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarball}" + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' RETURN + + echo "Installing gitleaks v${GITLEAKS_VERSION}..." + curl -fsSL "$url" -o "$tmp_dir/$tarball" + tar -xzf "$tmp_dir/$tarball" -C "$tmp_dir" gitleaks + install -m 755 "$tmp_dir/gitleaks" "$GITLEAKS_INSTALL_DIR/gitleaks" + echo "Installed: $GITLEAKS_INSTALL_DIR/gitleaks" +} + +if ! command -v gitleaks &>/dev/null; then + install_gitleaks +fi + +# --- Validate --- + +if [ ! -d "$TARGET/.git" ]; then + echo "Error: $TARGET is not a git repository" >&2 + exit 1 +fi + +if [ ! -f "$CONFIG_SRC" ]; then + echo "Error: config template not found at $CONFIG_SRC" >&2 + exit 1 +fi + +# --- Deploy config --- + +if [ -f "$CONFIG_DEST" ]; then + echo "Skipped: $CONFIG_DEST already exists — edit it directly to customise rules." +else + cp "$CONFIG_SRC" "$CONFIG_DEST" + echo "Wrote: $CONFIG_DEST" + echo " Commit this file — it belongs in version control." +fi + +# --- Deploy hook --- + +hook_block() { + cat </dev/null; then + gitleaks git --staged --redact -v +else + echo "Warning: gitleaks not installed — secret scan skipped (https://github.com/gitleaks/gitleaks/releases)" >&2 +fi +$END_MARKER +BLOCK +} + +write_hook() { + local hook_file="$1" + + if grep -qF "$MARKER" "$hook_file"; then + # Remove old block (start marker through end marker inclusive) then append current version + awk -v start="$MARKER" -v end="$END_MARKER" ' + $0 == start { skip=1; next } + skip && $0 == end { skip=0; next } + !skip { print } + ' "$hook_file" > "${hook_file}.tmp" && mv "${hook_file}.tmp" "$hook_file" + hook_block >> "$hook_file" + echo "Updated: $hook_file (gitleaks block replaced)" + else + hook_block >> "$hook_file" + echo "Updated: $hook_file (gitleaks block appended to existing hook)" + fi +} + +if [ -f "$HOOK_FILE" ]; then + write_hook "$HOOK_FILE" +else + { echo '#!/usr/bin/env bash'; echo 'set -euo pipefail'; hook_block; } > "$HOOK_FILE" + chmod +x "$HOOK_FILE" + echo "Created: $HOOK_FILE" +fi + +echo "" +echo "Done. Staged secrets will be scanned on every commit in $TARGET." +echo "To skip on a single commit: SKIP=gitleaks git commit ..." diff --git a/tests/test-setup-gitleaks.sh b/tests/test-setup-gitleaks.sh new file mode 100755 index 0000000..57c81b2 --- /dev/null +++ b/tests/test-setup-gitleaks.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPT="$REPO_ROOT/scripts/setup-gitleaks.sh" +PASS=0 +FAIL=0 + +pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } +fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } + +# Fake gitleaks binary — prevents the install step from running during tests +FAKE_BIN="$(mktemp -d)" +trap 'rm -rf "$FAKE_BIN"' EXIT +printf '#!/bin/sh\necho "gitleaks fake"\n' > "$FAKE_BIN/gitleaks" +chmod +x "$FAKE_BIN/gitleaks" +export PATH="$FAKE_BIN:$PATH" + +# Helper: create an isolated git repo in a temp dir +make_repo() { + local dir + dir="$(mktemp -d)" + git -C "$dir" init -q + echo "$dir" +} + +# Helper: run setup script against a target repo; capture output; always return exit code +run_setup() { + local target="$1" + bash "$SCRIPT" "$target" 2>&1 +} + +# --- 1. Rejects non-git directory --- +echo "" +echo "--- rejects non-git directory ---" +NON_GIT="$(mktemp -d)" +trap 'rm -rf "$NON_GIT"' EXIT +if bash "$SCRIPT" "$NON_GIT" >/dev/null 2>&1; then + fail "exited 0 for non-git directory — expected exit 1" +else + pass "exits non-zero for non-git directory" +fi + +# --- 2. Config deployed --- +echo "" +echo "--- config deployed to target repo ---" +REPO="$(make_repo)" +trap 'rm -rf "$REPO"' EXIT +run_setup "$REPO" > /dev/null +if [ -f "$REPO/.gitleaks.toml" ]; then + pass ".gitleaks.toml created in target repo" +else + fail ".gitleaks.toml missing from target repo" +fi +if diff -q "$REPO_ROOT/scripts/gitleaks.toml" "$REPO/.gitleaks.toml" > /dev/null 2>&1; then + pass ".gitleaks.toml matches the template" +else + fail ".gitleaks.toml content differs from template" +fi + +# --- 3. Hook created from scratch --- +echo "" +echo "--- hook created when none exists ---" +REPO2="$(make_repo)" +trap 'rm -rf "$REPO2"' EXIT +run_setup "$REPO2" > /dev/null +HOOK="$REPO2/.git/hooks/pre-commit" +if [ -f "$HOOK" ]; then + pass "pre-commit hook created" +else + fail "pre-commit hook not created" +fi +if [ -x "$HOOK" ]; then + pass "pre-commit hook is executable" +else + fail "pre-commit hook is not executable" +fi +if head -1 "$HOOK" | grep -q "^#!"; then + pass "pre-commit hook has a shebang" +else + fail "pre-commit hook missing shebang" +fi +if grep -q "gitleaks git --staged" "$HOOK"; then + pass "pre-commit hook contains gitleaks command" +else + fail "pre-commit hook missing gitleaks command" +fi +if grep -q "# managed by setup-gitleaks.sh" "$HOOK"; then + pass "pre-commit hook contains idempotency marker" +else + fail "pre-commit hook missing idempotency marker" +fi + +# --- 4. Appends to existing hook; existing content retained --- +echo "" +echo "--- appends to existing hook; prior content retained ---" +REPO3="$(make_repo)" +trap 'rm -rf "$REPO3"' EXIT +HOOK3="$REPO3/.git/hooks/pre-commit" +printf '#!/usr/bin/env bash\nnpm test\n' > "$HOOK3" +chmod +x "$HOOK3" +run_setup "$REPO3" > /dev/null +if grep -q "npm test" "$HOOK3"; then + pass "existing hook content retained after append" +else + fail "existing hook content lost after append" +fi +if grep -q "gitleaks git --staged" "$HOOK3"; then + pass "gitleaks block appended to existing hook" +else + fail "gitleaks block missing after append" +fi + +# --- 5. Second run replaces stale block; existing content still retained --- +echo "" +echo "--- second run replaces stale block; existing content still retained ---" +# Corrupt the gitleaks block to simulate stale content from an older version +sed -i 's/gitleaks git --staged/gitleaks protect --staged/' "$HOOK3" +run_setup "$REPO3" > /dev/null +if grep -q "npm test" "$HOOK3"; then + pass "existing content retained after block replacement" +else + fail "existing content lost after block replacement" +fi +marker_count="$(grep -c "# managed by setup-gitleaks.sh" "$HOOK3")" +if [ "$marker_count" -eq 1 ]; then + pass "gitleaks block appears exactly once after second run" +else + fail "gitleaks block duplicated — found $marker_count occurrences of marker" +fi +if grep -q "gitleaks git --staged" "$HOOK3"; then + pass "stale gitleaks command replaced with current command" +else + fail "stale gitleaks command not replaced — block was skipped, not updated" +fi + +# --- 6. Third run still idempotent --- +echo "" +echo "--- repeated runs stay idempotent ---" +run_setup "$REPO3" > /dev/null +run_setup "$REPO3" > /dev/null +marker_count="$(grep -c "# managed by setup-gitleaks.sh" "$HOOK3")" +if [ "$marker_count" -eq 1 ]; then + pass "gitleaks block still appears exactly once after four total runs" +else + fail "gitleaks block duplicated — found $marker_count occurrences after four runs" +fi + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[[ $FAIL -eq 0 ]]