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

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