From 7370652c4dc55c50f77d8a76227385c925b52fce Mon Sep 17 00:00:00 2001 From: Halemo GPA <73307941+HalemoGPA@users.noreply.github.com> Date: Wed, 8 Jul 2026 01:21:08 +0300 Subject: [PATCH 1/8] Add gitleaks secret-scan PR gate (GENG-112) --- .github/workflows/secret-scan.yml | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/secret-scan.yml diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 0000000..7cd495c --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,68 @@ +name: secret-scan + +# Reusable secret-scanning gate. Two ways to use it: +# 1. Drop this file into a repo's .github/workflows/ directly, OR +# 2. Host it once in tjmlabs/workflows and call it with a thin caller +# (uses: tjmlabs/workflows/.github/workflows/secret-scan.yml@v1) +# +# Uses the MIT-licensed gitleaks CLI directly (NOT the gitleaks-action, +# which requires a paid license for org-owned repos beyond the first). + +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: + gitleaks: + runs-on: ubuntu-latest + steps: + - name: Checkout (full history on PRs so we scan the whole diff range) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install gitleaks + run: | + VERSION=8.28.0 + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + gitleaks version + + - name: Scan pushed/PR commits + run: | + # On a PR, scan only the commits the PR introduces (fast, no legacy noise). + # On push to main/master, scan the same way against the previous head. + if [ "${{ github.event_name }}" = "pull_request" ]; then + RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + else + RANGE="${{ github.event.before }}..${{ github.sha }}" + fi + 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."; + exit 1; + } + + - name: Upload findings (kept even when the job fails) + if: always() + uses: actions/upload-artifact@v4 + with: + name: gitleaks-sarif + path: gitleaks.sarif + if-no-files-found: ignore From af76950a7d9740994ee9a07654c494672b2a91bf Mon Sep 17 00:00:00 2001 From: Halemo GPA <73307941+HalemoGPA@users.noreply.github.com> Date: Wed, 8 Jul 2026 01:21:09 +0300 Subject: [PATCH 2/8] Add gitleaks config (GENG-112) --- .gitleaks.toml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .gitleaks.toml diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..5b798c6 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,24 @@ +# 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" +paths = [ + '''(^|/)\.env\.example$''', + '''(^|/)\.env\.test$''', + '''(^|/)\.env\.sample$''', + '''(^|/)tests?/test\.env$''', + '''(^|/)tests?/fixtures/''', + '''(^|/)README(\.md)?$''', + '''(^|/)docs/''', +] + +# Ignore obvious placeholder values so example files with dummy keys stay quiet. +regexTarget = "match" +regexes = [ + '''(?i)(your[-_]?api[-_]?key|changeme|placeholder|xxxx+|example|dummy|<[^>]+>)''', +] From 73cb3f2ff78367a9723a742452a6c71d58a2eb4b Mon Sep 17 00:00:00 2001 From: Halemo GPA <73307941+HalemoGPA@users.noreply.github.com> Date: Wed, 8 Jul 2026 01:37:09 +0300 Subject: [PATCH 3/8] Add pre-commit config (local secret scanning) (GENG-112) --- .pre-commit-config.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7c4644c --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 From 8195ace7852af55eec7b458db4943f704de5a333 Mon Sep 17 00:00:00 2001 From: Halemo GPA <73307941+HalemoGPA@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:43:28 +0300 Subject: [PATCH 4/8] Refresh gitleaks gate per review (GENG-112) --- .github/workflows/secret-scan.yml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 7cd495c..65c9729 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -1,12 +1,11 @@ name: secret-scan -# Reusable secret-scanning gate. Two ways to use it: -# 1. Drop this file into a repo's .github/workflows/ directly, OR -# 2. Host it once in tjmlabs/workflows and call it with a thin caller -# (uses: tjmlabs/workflows/.github/workflows/secret-scan.yml@v1) +# 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). # -# 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: @@ -23,19 +22,22 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }} jobs: - gitleaks: + secret-scan: runs-on: ubuntu-latest steps: - name: Checkout (full history on PRs so we scan the whole diff range) - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - - name: Install gitleaks + - name: Install gitleaks (pinned + checksum-verified) run: | VERSION=8.28.0 - curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_x64.tar.gz" \ - | tar -xz -C /usr/local/bin gitleaks + 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 @@ -55,13 +57,13 @@ jobs: --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."; + 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@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: gitleaks-sarif path: gitleaks.sarif From 6ec79a78d9cf726c9a1df8d41802ef1ec6b9dfc7 Mon Sep 17 00:00:00 2001 From: Halemo GPA <73307941+HalemoGPA@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:43:29 +0300 Subject: [PATCH 5/8] Refresh gitleaks config per review (GENG-112) --- .gitleaks.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitleaks.toml b/.gitleaks.toml index 5b798c6..4932e54 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -7,14 +7,16 @@ 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/''', - '''(^|/)README(\.md)?$''', - '''(^|/)docs/''', ] # Ignore obvious placeholder values so example files with dummy keys stay quiet. From 6d5be5822352d9b1079064a104ca50cd3967bb9c Mon Sep 17 00:00:00 2001 From: Halemo GPA <73307941+HalemoGPA@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:31:32 +0300 Subject: [PATCH 6/8] Fix secret-scan gate: handle new-branch/delete push ranges (GENG-112) --- .github/workflows/secret-scan.yml | 43 +++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 65c9729..d6b40ee 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -41,14 +41,47 @@ jobs: 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: | - # On a PR, scan only the commits the PR introduces (fast, no legacy noise). - # On push to main/master, scan the same way against the previous head. - if [ "${{ github.event_name }}" = "pull_request" ]; then - RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + # Compute the git range to scan. Passed via env (not inline ${{ }}) so a + # branch or SHA can never break out of the shell. See the range table below. + 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 - RANGE="${{ github.event.before }}..${{ github.sha }}" + # 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" \ From 0fecb421bddda0c56f8175eaa9c04353ce4919dc Mon Sep 17 00:00:00 2001 From: Halemo GPA <73307941+HalemoGPA@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:31:34 +0300 Subject: [PATCH 7/8] Refresh gitleaks config (GENG-112) From 4dad585d7402ee6b3da6568cd0fdd13e5f71a3f1 Mon Sep 17 00:00:00 2001 From: Halemo GPA <73307941+HalemoGPA@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:55:00 +0300 Subject: [PATCH 8/8] Fix workflow parse error in secret-scan gate (GENG-112) --- .github/workflows/secret-scan.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index d6b40ee..397cace 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -48,8 +48,8 @@ jobs: PUSH_BEFORE: ${{ github.event.before }} PUSH_AFTER: ${{ github.sha }} run: | - # Compute the git range to scan. Passed via env (not inline ${{ }}) so a - # branch or SHA can never break out of the shell. See the range table below. + # 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.