From 70bdedf547358da7558a0a1216e56ab696ccdaa7 Mon Sep 17 00:00:00 2001 From: rohan-naik Date: Thu, 13 Aug 2026 17:00:54 +0530 Subject: [PATCH] feat: add trufflehog secret scanning Add trufflehog-based secret scanning at three points, mirroring contentstack/rte-micro-frontend#66: - husky pre-commit and pre-push hooks for local feedback - a Secret Scan GitHub Actions workflow on PRs and pushes to master All three run the same scripts/trufflehog-scan.sh so CI and developer machines cannot drift apart. The script scans the current filesystem (tracked + untracked, non-gitignored files mirrored to a temp tree) rather than git history or the PR diff. Repo-specific deviations from the source PR: - prepare was already "npm run build" for publishing, so husky is chained as "npm run build && (husky || true)" to keep publish and consumer installs working where no git dir exists - lib/ added to the exclude list, as this repo's build output Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/secret-scan.yml | 31 +++++++++++ .husky/pre-commit | 1 + .husky/pre-push | 1 + package-lock.json | 17 ++++++ package.json | 3 +- scripts/trufflehog-exclude.txt | 8 +++ scripts/trufflehog-scan.sh | 90 +++++++++++++++++++++++++++++++ 7 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/secret-scan.yml create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100644 scripts/trufflehog-exclude.txt create mode 100755 scripts/trufflehog-scan.sh diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 0000000..9975ebc --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,31 @@ +name: Secret Scan +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: [master] +jobs: + secret-scan: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + # Shallow, single-branch checkout: only the tip commit of the branch + # under test, no commit history. The scan is a filesystem scan of the + # working tree, so history would be fetched bandwidth for nothing. + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Install trufflehog + run: | + curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh \ + | sh -s -- -b /usr/local/bin + trufflehog --version + + # Runs the same script as the local husky hooks, so CI and the developer + # machine cannot drift apart. Scans the whole checked-out filesystem + # rather than the PR diff: a secret already present on the branch is a + # leak whether or not this PR is the commit that introduced it. + - name: Scan filesystem for secrets + run: sh scripts/trufflehog-scan.sh ci diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..d550123 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +sh scripts/trufflehog-scan.sh pre-commit diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..9c84a4a --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +sh scripts/trufflehog-scan.sh pre-push diff --git a/package-lock.json b/package-lock.json index 56bd380..cc0515e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@types/lodash.kebabcase": "^4.1.9", "@types/omit-deep-lodash": "^1.1.1", "esbuild": "^0.25.10", + "husky": "^9.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-html-reporter": "^3.10.2", @@ -3006,6 +3007,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", diff --git a/package.json b/package.json index 3793976..d8e04f1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "types": "lib/index.d.ts", "scripts": { "test": "jest", - "prepare": "npm run build", + "prepare": "npm run build && (husky || true)", "build:cjs": "esbuild src/index.tsx --bundle --outdir=lib --platform=node --minify", "build:esm": "esbuild src/index.tsx --bundle --outdir=lib --format=esm --out-extension:.js=.mjs --minify", "build": "npm run build:cjs && npm run build:esm && tsc --emitDeclarationOnly --outDir lib" @@ -49,6 +49,7 @@ "@types/lodash.kebabcase": "^4.1.9", "@types/omit-deep-lodash": "^1.1.1", "esbuild": "^0.25.10", + "husky": "^9.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-html-reporter": "^3.10.2", diff --git a/scripts/trufflehog-exclude.txt b/scripts/trufflehog-exclude.txt new file mode 100644 index 0000000..4668116 --- /dev/null +++ b/scripts/trufflehog-exclude.txt @@ -0,0 +1,8 @@ +(^|/)node_modules/ +(^|/)coverage/ +(^|/)\.git/ +(^|/)dist/ +(^|/)lib/ +(^|/)build/ +(^|/)package-lock\.json$ +(^|/)\.npmrc$ diff --git a/scripts/trufflehog-scan.sh b/scripts/trufflehog-scan.sh new file mode 100755 index 0000000..4080833 --- /dev/null +++ b/scripts/trufflehog-scan.sh @@ -0,0 +1,90 @@ +#!/bin/sh +# Secret scan with trufflehog, used by the husky pre-commit and pre-push hooks. +# +# Usage: scripts/trufflehog-scan.sh [hook-name] +# +# Scans the CURRENT FILESYSTEM only — never git history. The scanned set is +# tracked + untracked files that are not gitignored, mirrored into a temp tree +# so gitignored local files (e.g. an .npmrc auth token) cannot fail the hook. +# Paths in scripts/trufflehog-exclude.txt are skipped. +set -e + +HOOK="${1:-trufflehog}" +PAD=$(printf '%*s' ${#HOOK} '') + +# GUI git clients (VSCode Source Control, GitHub Desktop, Tower, Fork...) often +# launch git from a process tree rooted at Finder, which never sourced the +# user's login shell profile. Homebrew's bin dir is therefore missing from PATH +# and trufflehog looks uninstalled even when it is not. Add the standard +# Homebrew locations for Apple Silicon and Intel before looking it up. +PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" +export PATH + +if ! command -v trufflehog >/dev/null 2>&1; then + echo "$HOOK: trufflehog not found in PATH — cannot verify there are no secrets." + echo "$PAD install it with: brew install trufflehog" + echo "$PAD aborting: an unverifiable tree is treated as a failure, not a pass." + exit 1 +fi + +REPO_ROOT=$(git rev-parse --show-toplevel) + +TMPDIR_SCAN=$(mktemp -d) +trap 'rm -rf "$TMPDIR_SCAN"' EXIT + +# Build the file list first so a git failure aborts instead of silently +# producing an empty (and therefore trivially "clean") scan set. +FILE_LIST="$TMPDIR_SCAN.files" +if ! git -C "$REPO_ROOT" ls-files --cached --others --exclude-standard -z > "$FILE_LIST"; then + echo "$HOOK: failed to list repository files. Aborting." + rm -f "$FILE_LIST" + exit 1 +fi + +# rsync copies the NUL-delimited list in one pass and creates parent dirs for +# us. Deliberately not a `read -d ''` loop: that is a bashism, and this script +# runs under /bin/sh, which is dash on CI runners. +if ! command -v rsync >/dev/null 2>&1; then + echo "$HOOK: rsync not found in PATH — cannot assemble the scan set. Aborting." + rm -f "$FILE_LIST" + exit 1 +fi + +if ! rsync -a --files-from="$FILE_LIST" --from0 "$REPO_ROOT/" "$TMPDIR_SCAN/"; then + echo "$HOOK: failed to copy files for scanning. Aborting." + rm -f "$FILE_LIST" + exit 1 +fi +rm -f "$FILE_LIST" + +# trufflehog exits 0 on a path it cannot read, so an empty scan set would look +# clean. Refuse to pass in that case. +if [ -z "$(find "$TMPDIR_SCAN" -type f -print -quit)" ]; then + echo "$HOOK: no files were collected to scan. Aborting rather than passing." + exit 1 +fi + +echo "$HOOK: scanning current filesystem for secrets..." +set +e +trufflehog filesystem "$TMPDIR_SCAN" \ + --exclude-paths "$REPO_ROOT/scripts/trufflehog-exclude.txt" \ + --results=verified,unknown,unverified \ + --no-update --fail +SCAN_STATUS=$? +set -e + +# 183 is trufflehog's --fail code for "secrets found"; anything else non-zero +# means the scan itself broke. Both block: never pass on an unverified tree. +if [ "$SCAN_STATUS" = "183" ]; then + echo "" + echo "$HOOK: potential secrets found. Aborted." + echo "$PAD remove them, or re-run with --no-verify if this is a false positive." + exit 1 +elif [ "$SCAN_STATUS" != "0" ]; then + echo "" + echo "$HOOK: trufflehog exited with status $SCAN_STATUS (scan failed). Aborted." + echo "$PAD the tree could not be verified, so this is treated as a failure." + exit 1 +fi + +echo "$HOOK: no secrets found."