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:
84
.agents/evals/cross-cutting/gitleaks/eval.yaml
Normal file
84
.agents/evals/cross-cutting/gitleaks/eval.yaml
Normal 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)"
|
||||
15
.agents/skills/gitleaks/META.md
Normal file
15
.agents/skills/gitleaks/META.md
Normal 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
|
||||
```
|
||||
111
.agents/skills/gitleaks/SKILL.md
Normal file
111
.agents/skills/gitleaks/SKILL.md
Normal 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>
|
||||
83
.agents/skills/gitleaks/references/allowlist-patterns.md
Normal file
83
.agents/skills/gitleaks/references/allowlist-patterns.md
Normal 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
27
.gitleaks.toml
Normal 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/.*''']
|
||||
@@ -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
24
scripts/gitleaks.toml
Normal 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
124
scripts/setup-gitleaks.sh
Executable 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
151
tests/test-setup-gitleaks.sh
Executable 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 ]]
|
||||
Reference in New Issue
Block a user