Skip to content
Merged
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
94 changes: 94 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# Two-tier secret-leak guard, run on every local commit.
# Tier 1: auto-redact gitleaks findings inside src/_generated_ (driven live
# by Rule/gitleaks.toml via scripts/patch-generated-secrets.js, not
# a hand-maintained list) and re-stage the files it touched. Never
# blocks the commit by itself - it either fixes generated code or
# leaves it untouched for tier 2 to catch.
# Tier 2: run the real gitleaks scan against the staged diff and block on
# any finding tier 1 didn't (or couldn't) resolve.
# Installed via the repo's own "prepare" npm script - see package.json.
set -uo pipefail

repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"

generated_dir="src/ _generated_"

# Snapshot what's already staged in the generated dir before tier 1 runs, so
# an intentionally-unstaged, in-progress change there (e.g. mid-way through
# testing a Fern regen) isn't unconditionally swept into this commit. Tier 1
# only edits working-tree files, never the index, so this snapshot stays
# accurate regardless of what it does next.
already_staged_in_generated="$(git diff --cached --name-only -- "$generated_dir" || true)"

git_dir="$(git rev-parse --git-dir)"
touched_file_list="$git_dir/leak-guard-touched-files.txt"
# A list from a previous commit's tier 1 run must never be reused here - if
# tier 1 doesn't run this time (node missing, below), stale entries from
# that earlier run would otherwise get staged again.
rm -f "$touched_file_list"

# git invokes hooks with a leaner PATH than your interactive shell, so a
# Node install managed by nvm/volta/fnm or bundled with an IDE (rather than
# a system package) is often invisible here even though `node` works fine
# in your terminal. If that's you, run this once and commit again:
# git config leakguard.nodepath "$(dirname "$(command -v node)")"
# It's a local git config value (not committed), so it won't affect anyone
# else's machine.
if ! command -v node >/dev/null 2>&1; then
custom_node_dir="$(git config --get leakguard.nodepath || true)"
[ -n "$custom_node_dir" ] && PATH="$custom_node_dir:$PATH" && export PATH
fi

if ! command -v node >/dev/null 2>&1; then
echo "[leak-guard] tier 1 skipped: 'node' not found on PATH inside the git hook environment."
echo "[leak-guard] If 'node' works in your terminal, git hooks are likely just seeing a different PATH."
echo "[leak-guard] Fix: git config leakguard.nodepath \"\$(dirname \"\$(command -v node)\")\", then commit again."
echo "[leak-guard] continuing to tier 2; this alone will not block the commit."
else
echo "[leak-guard] tier 1: auto-redacting gitleaks findings in generated code..."
if ! node scripts/patch-generated-secrets.js; then
echo "[leak-guard] tier 1 couldn't fully resolve generated code - see the message above."
echo "[leak-guard] continuing to tier 2; this alone will not block the commit."
fi
fi

# Stages only what tier 1 actually touched this run (recorded to
# $touched_file_list - see recordTouchedFiles in the script) plus whatever
# was already staged above. A file sitting in src/_generated_ that tier 1
# didn't touch and the caller hadn't staged is left alone rather than
# force-added - see the med-severity review finding on the old
# unconditional `git add` of the whole directory.
to_stage=()
if [ -f "$touched_file_list" ]; then
while IFS= read -r f; do
[ -n "$f" ] && to_stage+=("$f")
done < "$touched_file_list"
fi
while IFS= read -r f; do
[ -n "$f" ] && to_stage+=("$f")
done <<< "$already_staged_in_generated"

if [ "${#to_stage[@]}" -gt 0 ]; then
git add -- "${to_stage[@]}"
fi

if ! command -v gitleaks >/dev/null 2>&1; then
echo "[leak-guard] tier 2 skipped: 'gitleaks' binary not found locally."
echo "[leak-guard] CI will still block this PR on any finding (see Gitleaks.yml) - install gitleaks locally to catch issues before pushing instead of waiting on CI."
exit 0
Comment thread
skyflow-himanshupal marked this conversation as resolved.
fi

echo "[leak-guard] tier 2: scanning staged changes with gitleaks..."
if ! gitleaks protect --staged --config="Rule/gitleaks.toml" --redact; then
echo ""
echo "[leak-guard] commit blocked - gitleaks found something in your staged changes."
echo "[leak-guard] known false positive in generated code -> re-run tier 1 (node scripts/patch-generated-secrets.js) and check its output."
echo "[leak-guard] false positive elsewhere -> add an allowlist entry to Rule/gitleaks.toml."
echo "[leak-guard] real secret -> remove it and rotate the credential before committing."
exit 1
fi

echo "[leak-guard] clean - proceeding with commit."
exit 0
23 changes: 23 additions & 0 deletions .github/workflows/Gitleaks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,26 @@ jobs:
-H "Accept: application/vnd.github.v3+json" \
-d "{\"body\":\"$COMMENT\"}" \
"https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments"

- name: Fail if secrets were found
# The scan step above runs with --exit-code=0 so it always reaches
# the comment step regardless of findings - reporting and
# enforcement are deliberately separate steps. This is the
# enforcement half: without it, this whole workflow only ever
# comments and never blocks a PR, even when it finds a real
# secret. gitleaks-auto-redact.yml pushes an auto-fix commit for
# fake secrets in generated code, which re-triggers this workflow
# and clears this failure on its own once that commit lands - a
# failure here that persists past that point means a human needs
# to look at it.
run: |
if [ ! -f gitleaks-report.json ]; then
echo "Report file not found!"
exit 1
fi
COUNT=$(jq 'length' gitleaks-report.json)
if [ "$COUNT" -gt 0 ]; then
echo "::error::Gitleaks found $COUNT secret(s) in this PR - see the PR comment above, or download the gitleaks-report artifact."
exit 1
fi
echo "No secrets detected - safe to proceed."
68 changes: 68 additions & 0 deletions .github/workflows/gitleaks-auto-redact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Gitleaks Auto-Redact

# Runs scripts/patch-generated-secrets.js on every push to a PR and, if it
# finds anything to redact in the Fern-generated code, commits and pushes
# the fix straight to the PR branch - the same "run in CI, commit the
# result back" pattern common-release.yml uses for its version bump. This
# closes the gap where a contributor without gitleaks or node installed
# locally never gets tier 1's local auto-redaction (see
# .githooks/pre-commit): CI now does it for them instead of only warning.
#
# This is additive to Gitleaks.yml, not a replacement for it - it doesn't
# change that workflow's scan or its exit behavior.

on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- main

permissions:
contents: write

jobs:
auto-redact:
runs-on: ubuntu-latest
# A fork PR's checkout token can't push back to someone else's fork -
# GitHub blocks that regardless of what token this job holds - so skip
# cleanly rather than fail noisily. Gitleaks.yml still scans fork PRs
# as usual; this job only ever adds a redaction commit for same-repo
# branches.
if: github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.PAT_ACTIONS }}
ref: ${{ github.head_ref }}

- uses: actions/setup-node@v3
with:
node-version: '20.x'

- name: Install packages
run: npm install --ignore-scripts

- name: Install gitleaks
run: |
GITLEAKS_VERSION="$(curl -fsSL https://api.github.com/repos/gitleaks/gitleaks/releases/latest | grep -m1 '"tag_name"' | cut -d '"' -f4 | sed 's/^v//')"
curl -fsSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o gitleaks.tar.gz
tar -xzf gitleaks.tar.gz gitleaks
sudo install -m 0755 gitleaks /usr/local/bin/gitleaks
rm -f gitleaks.tar.gz gitleaks
gitleaks version

- name: Auto-redact generated code
run: node scripts/patch-generated-secrets.js

- name: Commit and push redaction, if any
run: |
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git checkout "${{ github.head_ref }}"
if git diff --quiet -- "src/ _generated_"; then
echo "No fake secrets found to redact."
else
git add -- "src/ _generated_"
git commit -m "[AUTOMATED] redact fake secrets in generated code"
git push origin "HEAD:${{ github.head_ref }}"
fi
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"lint": "npm run eslint",
"lint-fix": "prettier --write '**/*.{js,ts}' && eslint --fix '**/*.{js,ts}'",
"spellcheck": "cspell '**/*.{ts,js,md}'",
"docs-gen": "typedoc && node scripts/docs-script/markdown-gen.js && npx ts-node scripts/docs-script/processMarkdown.ts"
"docs-gen": "typedoc && node scripts/docs-script/markdown-gen.js && npx ts-node scripts/docs-script/processMarkdown.ts",
"patch-generated-secrets": "node scripts/patch-generated-secrets.js",
"prepare": "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git config core.hooksPath .githooks || true"
},
"repository": {
"type": "git",
Expand Down
Loading
Loading