diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..ce54c8b --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +repo_root=$(git rev-parse --show-toplevel) +exec "$repo_root/scripts/secret-scan.sh" staged diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 0000000..6174b1b --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,41 @@ +name: Secret scan + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + gitleaks: + name: Gitleaks working tree and history + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out complete history + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + fetch-depth: 0 + + - name: Install verified Gitleaks binary + env: + GITLEAKS_VERSION: 8.30.1 + run: | + set -euo pipefail + archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + base="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}" + curl --fail --silent --show-error --location --output "$RUNNER_TEMP/$archive" "$base/$archive" + curl --fail --silent --show-error --location --output "$RUNNER_TEMP/checksums.txt" "$base/gitleaks_${GITLEAKS_VERSION}_checksums.txt" + cd "$RUNNER_TEMP" + grep " $archive$" checksums.txt | sha256sum --check --strict + tar --extract --gzip --file "$archive" gitleaks + chmod 700 gitleaks + + - name: Scan working tree and full Git history + env: + GITLEAKS_BIN: ${{ runner.temp }}/gitleaks + run: ./scripts/secret-scan.sh all diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..bf5dc3b --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,4 @@ +title = "Repository secret scanning" + +[extend] +useDefault = true diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..20c4af7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ +# Security and secret handling + +Live credentials, private keys, bearer values, recovery material, and +secret-bearing production exports do not belong in this repository, whether it +is private or public. + +## Secret scanning + +Install Gitleaks and run: + +```bash +./scripts/secret-scan.sh staged +./scripts/secret-scan.sh all +``` + +Enable the versioned local pre-commit hook with: + +```bash +git config core.hooksPath .githooks +``` + +The hook fails closed when Gitleaks is unavailable. CI downloads the pinned +Gitleaks release, verifies its upstream checksum, and scans both the working +tree and complete fetched history with read-only repository permissions. + +Do not bypass a failed scan. If a finding is real, revoke or rotate it first, +then remove the material and rescan. Do not copy a credential into an issue, +pull request, log, or remediation note. Any allowlist exception must be narrow +to the exact rule, path, and proven synthetic or false-positive value. + +Before making a private repository public, also review every branch and tag, +Git LFS and submodule content, release and Actions artifacts, generated +archives, source maps, notebooks, fixtures, logs, screenshots, workflows, and +private topology. Repeat the scan from a fresh clone and obtain repository-owner +approval before changing visibility. diff --git a/scripts/secret-scan.sh b/scripts/secret-scan.sh new file mode 100755 index 0000000..105ba90 --- /dev/null +++ b/scripts/secret-scan.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +mode="${1:-all}" +repo_root=$(git rev-parse --show-toplevel) +gitleaks_bin="${GITLEAKS_BIN:-gitleaks}" + +if [[ "$gitleaks_bin" == */* ]]; then + if [[ ! -x "$gitleaks_bin" ]]; then + printf 'error: Gitleaks executable not found at %s\n' "$gitleaks_bin" >&2 + exit 2 + fi +elif ! command -v "$gitleaks_bin" >/dev/null 2>&1; then + printf 'error: gitleaks is required; install it or set GITLEAKS_BIN\n' >&2 + exit 2 +fi + +common=( + --no-banner + --no-color + --redact + --config "$repo_root/.gitleaks.toml" +) + +scan_staged() { + ( + cd "$repo_root" + "$gitleaks_bin" git "${common[@]}" --pre-commit --staged . + ) +} + +scan_tree() { + ( + cd "$repo_root" + "$gitleaks_bin" dir "${common[@]}" . + ) +} + +scan_history() { + ( + cd "$repo_root" + "$gitleaks_bin" git "${common[@]}" --log-opts="--all" . + ) +} + +case "$mode" in + staged) + scan_staged + ;; + tree) + scan_tree + ;; + history) + scan_history + ;; + all) + scan_tree + scan_history + ;; + *) + printf 'usage: %s [staged|tree|history|all]\n' "$0" >&2 + exit 2 + ;; +esac