Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .github/workflows/secret-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: secret-scan

# Reusable secret-scanning gate. Drop this file into a repo's .github/workflows/.
# Uses the MIT-licensed gitleaks CLI directly (NOT the gitleaks-action, which
# requires a paid license for org-owned repos beyond the first).
#
# The check that appears on the PR is named "secret-scan" (the job id below).
# When requiring this as a status check in an org ruleset, require "secret-scan".

on:
pull_request:
push:
branches: [main, master]
workflow_call:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: secret-scan-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }}

jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout (full history on PRs so we scan the whole diff range)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0

- name: Install gitleaks (pinned + checksum-verified)
run: |
VERSION=8.28.0
SHA256=a65b5253807a68ac0cafa4414031fd740aeb55f54fb7e55f386acb52e6a840eb
TARBALL="gitleaks_${VERSION}_linux_x64.tar.gz"
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/${TARBALL}" -o "$TARBALL"
echo "${SHA256} ${TARBALL}" | sha256sum -c -
tar -xz -C /usr/local/bin -f "$TARBALL" gitleaks
gitleaks version

- name: Scan pushed/PR commits
env:
EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE: ${{ github.event.before }}
PUSH_AFTER: ${{ github.sha }}
run: |
# Compute the git range to scan. Passed via env (not inline GitHub Actions
# expressions) so a branch or SHA can never break out of the shell.
ZERO=0000000000000000000000000000000000000000
if [ "$EVENT_NAME" = "pull_request" ]; then
# PR: scan exactly the commits the PR introduces.
RANGE="${PR_BASE_SHA}..${PR_HEAD_SHA}"
elif [ "$PUSH_AFTER" = "$ZERO" ]; then
# Branch DELETE push: the after-SHA is all-zeros and there are no
# commits to scan (the ref is gone). Nothing to do.
echo "Branch deletion push (after-SHA is all-zeros); nothing to scan."
exit 0
elif [ "$PUSH_BEFORE" = "$ZERO" ] || [ -z "$PUSH_BEFORE" ]; then
# First push to a new branch (or a deleted-then-recreated ref): there is
# no "before" commit, so github.event.before is all-zeros. A
# ${ZERO}..HEAD range is invalid and gitleaks errors out. Fall back to
# scanning just the tip commit, which is all this push actually added
# that a PR gate wouldn't already have covered.
RANGE="${PUSH_AFTER}~1..${PUSH_AFTER}"
# If the tip is a root commit (no parent), ~1 doesn't resolve; scan the
# single commit by itself.
if ! git rev-parse --verify --quiet "${PUSH_AFTER}~1" >/dev/null; then
RANGE="$PUSH_AFTER"
fi
else
# Normal push to an existing branch: scan what this push added.
RANGE="${PUSH_BEFORE}..${PUSH_AFTER}"
fi
# Backstop: a usable range must never contain the null SHA and must be
# non-empty. This mirrors scripts/compute-scan-range.sh so the two stay
# in lockstep (asserted by the secret-scan-selftest sync check).
case "$RANGE" in
*"$ZERO"*) echo "::error::computed an invalid range containing the null SHA: $RANGE"; exit 1 ;;
"") echo "::error::computed an empty range"; exit 1 ;;
esac
echo "Scanning range: $RANGE"
gitleaks git \
--log-opts="$RANGE" \
--redact \
--config .gitleaks.toml \
--exit-code 1 \
--report-format sarif \
--report-path gitleaks.sarif || {
echo "::error::Potential secret detected. Review the log above. If this is a real credential, ROTATE IT immediately, then remove it from history. (If it is a genuine example/placeholder, add a '# gitleaks:allow' comment on the line.)";
exit 1;
}

- name: Upload findings (kept even when the job fails)
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: gitleaks-sarif
path: gitleaks.sarif
if-no-files-found: ignore
26 changes: 26 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# tjmlabs shared gitleaks config.
# Extends the built-in ruleset (100+ detectors) and adds an allowlist so the
# legitimate example/fixture env files in our templates don't trip the gate.

[extend]
useDefault = true

[allowlist]
description = "tjmlabs allowlisted example/fixture files and paths"
# NOTE: docs/ and README are deliberately NOT allowlisted. A real key pasted into
# a readme is a common leak path, so we keep scanning them and rely on the
# placeholder regex below to suppress obvious examples. If a legit example still
# trips the gate, add a '# gitleaks:allow' comment on that line.
paths = [
'''(^|/)\.env\.example$''',
'''(^|/)\.env\.test$''',
'''(^|/)\.env\.sample$''',
'''(^|/)tests?/test\.env$''',
'''(^|/)tests?/fixtures/''',
]

# Ignore obvious placeholder values so example files with dummy keys stay quiet.
regexTarget = "match"
regexes = [
'''(?i)(your[-_]?api[-_]?key|changeme|placeholder|xxxx+|example|dummy|<[^>]+>)''',
]
21 changes: 21 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# tjmlabs shared pre-commit config. Ship this into each bot repo (and the
# templates, so new bots inherit it). Catches secrets at the keyboard, before
# they ever reach GitHub. Local hooks are skippable with --no-verify, which is
# why CI (secret-scan.yml) re-runs the same gitleaks as a non-skippable gate.
#
# One-time per machine: uv tool install pre-commit (or: pipx install pre-commit)
# Then in the repo: pre-commit install
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.28.0
hooks:
- id: gitleaks
args: ["--config", ".gitleaks.toml"]

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key # SSH/PEM private keys
- id: check-added-large-files # blocks accidental big blobs
args: ["--maxkb=1024"]
- id: check-merge-conflict
Loading