feat(security): secret-scan в CI + pre-push хуки (#142) #5

Merged
eSlider merged 1 commits from feat/secret-scan#142 into main 2026-08-23 22:46:45 +01:00
5 changed files with 199 additions and 0 deletions
+32
View File
@@ -10,6 +10,38 @@ permissions:
contents: read
jobs:
secret-scan:
name: Secret scan (gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Compute scan range (diff of new commits only)
id: range
run: |
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
RANGE="${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}"
else
BEFORE="${{ github.event.before }}"
if [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
RANGE="$(git rev-list --max-parents=0 HEAD | tail -1)..$GITHUB_SHA"
else
RANGE="$BEFORE..$GITHUB_SHA"
fi
fi
echo "RANGE=$RANGE" >> "$GITHUB_ENV"
echo "Scanning range: $RANGE"
- name: Gitleaks (diff-only, fail on leak)
uses: docker://zricethezav/gitleaks:latest
env:
GITLEAKS_RANGE: ${{ env.RANGE }}
with:
args: detect --source "${{ github.workspace }}" --log-opts="$GITLEAKS_RANGE" --redact --verbose
test:
name: Test (Go ${{ matrix.go }})
runs-on: ubuntu-latest
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
#
# install.sh — symlinks the shared githooks (pre-push, pre-commit) into .git/hooks
# for this repository. Safe to run repeatedly.
#
# Usage:
# ./scripts/githooks/install.sh
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
SRC="$ROOT/scripts/githooks"
HOOKS="$ROOT/.git/hooks"
mkdir -p "$HOOKS"
chmod +x "$SRC"/secret-scan.sh "$SRC"/pre-push "$SRC"/pre-commit
for h in pre-push pre-commit; do
if [[ -e "$HOOKS/$h" ]] && [[ ! -L "$HOOKS/$h" ]]; then
echo "error: $HOOKS/$h already exists and is not a symlink; remove it first" >&2
exit 1
fi
ln -sfn "$SRC/$h" "$HOOKS/$h"
echo "installed $h -> $SRC/$h"
done
echo "githooks installed for $(basename "$ROOT")"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
#
# pre-commit git hook — blocks a commit if staged changes contain a secret.
# Scans only the staged (index) diff with gitleaks (via secret-scan.sh).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
ROOT="$(git rev-parse --show-toplevel)"
if [[ "$SCRIPT_DIR" == "$ROOT/scripts/githooks" ]]; then
SCAN="$SCRIPT_DIR/secret-scan.sh"
else
SCAN="$ROOT/scripts/githooks/secret-scan.sh"
fi
if ! "$SCAN" --staged; then
echo "pre-commit: LEAK FOUND in staged changes; commit BLOCKED. Remove the secret first." >&2
exit 1
fi
exit 0
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
#
# pre-push git hook — blocks a push if any NEW commit leaks a secret.
#
# Reads the refs being pushed from stdin (format:
# <local ref> <local sha> <remote ref> <remote sha>
# per ref). Scans only the new commits with gitleaks (via secret-scan.sh) and
# aborts (exit 1) if anything is found.
#
# Install: ln -s ../../scripts/githooks/pre-push .git/hooks/pre-push
# (or run scripts/githooks/install.sh)
set -euo pipefail
# Resolve symlinks so the hook works whether copied into .git/hooks or
# symlinked from scripts/githooks (and in worktrees sharing the main repo hooks).
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
ROOT="$(git rev-parse --show-toplevel)"
if [[ "$SCRIPT_DIR" == "$ROOT/scripts/githooks" ]]; then
SCAN="$SCRIPT_DIR/secret-scan.sh"
else
SCAN="$ROOT/scripts/githooks/secret-scan.sh"
fi
zero=0000000000000000000000000000000000000000
failed=0
while read -r local_ref local_sha remote_ref remote_sha; do
[[ -n "$local_sha" ]] || continue
# Deletion push — nothing to scan.
if [[ "$local_sha" == "$zero" ]]; then
continue
fi
# New branch (no remote ref yet): scan only the commits this branch ADDS over
# its merge-base with the integration branch (PR target), NOT all history.
# This keeps pre-existing historical leaks (see #140) from blocking new work.
if [[ "$remote_sha" == "$zero" ]]; then
base=""
for base_ref in origin/release/v1 origin/release/v2 origin/master origin/main; do
if git rev-parse --verify "$base_ref" >/dev/null 2>&1; then
base="$(git merge-base "$base_ref" "$local_sha" 2>/dev/null)"
break
fi
done
if [[ -z "$base" ]] && git rev-parse --verify origin/HEAD >/dev/null 2>&1; then
base="$(git merge-base origin/HEAD "$local_sha" 2>/dev/null)"
fi
[[ -z "$base" ]] && base="$(git rev-list --max-parents=0 "$local_sha" 2>/dev/null | tail -1)"
range="${base}..${local_sha}"
else
range="${remote_sha}..${local_sha}"
fi
echo "secret-scan: scanning new commits ${range} (ref ${local_ref})"
if ! "$SCAN" --range "$range"; then
echo "secret-scan: LEAK FOUND in ${local_ref}; push BLOCKED. Remove the secret before pushing." >&2
failed=1
fi
done
if [[ "$failed" -ne 0 ]]; then
exit 1
fi
exit 0
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
#
# secret-scan.sh — shared secret scanner used by git hooks (pre-push, pre-commit)
# and by SE agents before any push.
#
# Scans ONLY the diff of new commits (or staged changes) with gitleaks, never the
# full history. Fails (exit non-zero) on any finding, so a leaking push is blocked.
#
# Uses the gitleaks Docker image (gitleaks/gitleaks) if docker is available,
# otherwise a locally installed `gitleaks` binary. No secret VALUES are ever
# printed: findings are emitted redacted.
#
# Usage:
# secret-scan.sh <range> scan a git log range, e.g. origin/release/v1..HEAD
# secret-scan.sh --staged scan staged (index) changes
# secret-scan.sh --all scan full history (warning: not for normal use)
#
# Exit codes: 0 = clean, 1 = leaks found (caller should abort), 2 = scan failed.
set -euo pipefail
GITLEAKS_IMAGE="zricethezav/gitleaks:latest"
run_gitleaks() {
# $@ = gitleaks args; runs in current dir (a git repo).
if command -v gitleaks >/dev/null 2>&1; then
gitleaks "$@"
elif command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
docker run --rm -v "$PWD:/repo" -w /repo "$GITLEAKS_IMAGE" "$@"
else
echo "error: secret-scan: neither 'gitleaks' binary nor docker image available" >&2
exit 2
fi
}
mode="${1:---range}"
shift || true
case "$mode" in
--range)
range="${1:?usage: secret-scan.sh <range>}"
run_gitleaks detect --source "$PWD" --no-banner --redact --log-opts="$range" >&2
;;
--staged)
# Scan only staged (index) content: pipe `git diff --cached` through gitleaks --pipe.
git diff --cached --binary | run_gitleaks detect --pipe --no-banner --redact >&2
;;
--all)
run_gitleaks detect --source "$PWD" --no-banner --redact >&2
;;
*)
echo "error: secret-scan: unknown mode '$mode'" >&2
exit 2
;;
esac