feat: add gitleaks secret scanning setup and skill

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 11:29:10 +00:00
parent 76cdbdff7a
commit 25a6a454b9
9 changed files with 624 additions and 2 deletions

View File

@@ -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)"

View File

@@ -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
```

View File

@@ -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
---
<requirements>
## 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
</requirements>
<steps>
## Process
### Install and configure
1. **Confirm target.** State: "I will run `scripts/setup-gitleaks.sh <path>` 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 <TARGET_REPO>
```
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 <TARGET_REPO>` 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 <path>` 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 <path>` 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 <path>` | Scan uncommitted files |
| Debug allowlists | `gitleaks dir --log-level debug <path>` | 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
</steps>
<checks>
## 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`
</checks>

View File

@@ -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: <fingerprint>:<line-number>
# 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
```

27
.gitleaks.toml Normal file
View File

@@ -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/.*''']

View File

@@ -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.

24
scripts/gitleaks.toml Normal file
View File

@@ -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"]

124
scripts/setup-gitleaks.sh Executable file
View File

@@ -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 <<BLOCK
$MARKER
if command -v gitleaks &>/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 ..."

151
tests/test-setup-gitleaks.sh Executable file
View File

@@ -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 ]]