From 1de97d1e7264cbe1c1846f73c3aa27a093776bb2 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 14:19:41 -0700 Subject: [PATCH 01/10] PDP-1182: Add org ruleset support to trufflehog-scan.yml Same pattern as copyright-check.yml: - Add workflow_call trigger for backward compat with per-repo callers - Fix Fetch PR head commits: scope git -c extraheader to this repo only (https://github.com/OWNER/REPO/) instead of all of github.com. Credentials are passed in-memory via git -c and never written to .git/config. - Add comment explaining continue-on-error: true is intentional (without it, Parse step / annotations never run when action finds secrets in git history) - Fix grep -qx -> grep -qxF in Parse step (filenames are literals not regex) - Skip Post PR comment for pull_request_target: org required workflow token is read-only for the triggering repo, createComment/updateComment always 403. Annotations (::error, ::warning) from Parse step are visible in the workflow run and are sufficient for developers to find exposed secrets. - Add try/catch to Post PR comment for workflow_call path - Add permission comments explaining why pull-requests: write is retained Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/trufflehog-scan.yml | 35 +++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index 8999672..f2dd46d 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -1,12 +1,22 @@ name: TruffleHog Secret Scan on: + # Direct trigger for org-level repository rulesets — works for PRs from forks + # since pull_request_target runs with base repo context. The GITHUB_TOKEN is + # explicitly scoped to least privilege in the job permissions block below. pull_request_target: types: [opened, synchronize, reopened] + + # Also support being called as a reusable workflow from individual repos + workflow_call: + workflow_dispatch: permissions: contents: read + # pull-requests: write is needed for the PR comment step (workflow_call path only). + # pull_request_target (org ruleset) skips that step, but GitHub Actions has no + # per-event conditional permissions within a single job. pull-requests: write # Default exclusion patterns (regex format) @@ -44,9 +54,14 @@ jobs: - name: Fetch PR head commits if: github.event_name != 'workflow_dispatch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Fetch PR commits using GitHub's merge ref (works for all PRs including forks) - git fetch origin +refs/pull/${{ github.event.pull_request.number }}/head:refs/remotes/origin/pr-head + # Scope credentials to this repo only — git -c passes the header in-memory + # and is never written to .git/config, so persist-credentials: false is preserved. + AUTH_HEADER="Authorization: basic $(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 -w0)" + git -c "http.${{ github.server_url }}/${{ github.repository }}/.extraheader=${AUTH_HEADER}" \ + fetch origin +refs/pull/${{ github.event.pull_request.number }}/head:refs/remotes/origin/pr-head echo "Fetched PR #${{ github.event.pull_request.number }} head commit: ${{ github.event.pull_request.head.sha }}" - name: Setup exclude config @@ -75,6 +90,9 @@ jobs: id: trufflehog # Pinned to v3.94.2 commit SHA to prevent supply chain attacks via mutable tag uses: trufflesecurity/trufflehog@6bd2d14f7a4bc1e569fa3550efa7ec632a4fa67b # v3.94.2 + # continue-on-error: true is intentional — without it, the job stops here if the + # action finds secrets in git history, and the Parse step (which creates the + # per-file annotations developers see) never runs. continue-on-error: true with: base: ${{ github.event.pull_request.base.sha }} @@ -132,7 +150,7 @@ jobs: FILE="${FILE#/tmp/}" # Only count secrets in files that are part of this PR - if ! echo "$CHANGED_FILES" | grep -qx "$FILE"; then + if ! echo "$CHANGED_FILES" | grep -qxF "$FILE"; then continue fi @@ -184,7 +202,10 @@ jobs: fi - name: Post PR comment on findings - if: github.event_name != 'workflow_dispatch' + # workflow_call: token is scoped to the calling repo — write access works. + # pull_request_target (org ruleset): token is read-only for the triggering repo — + # createComment/updateComment will 403. Skip and rely on job annotations instead. + if: github.event_name == 'workflow_call' uses: actions/github-script@v7 with: script: | @@ -195,7 +216,7 @@ jobs: const hasVerified = '${{ steps.process.outputs.has_verified }}' === 'true'; const verifiedCount = '${{ steps.parse.outputs.verified_count }}' || '0'; const unverifiedCount = '${{ steps.parse.outputs.unverified_count }}' || '0'; - + try { // Find existing comment const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, @@ -299,6 +320,10 @@ jobs: body: body }); } + } catch(e) { + core.error(`Failed to post PR comment: ${e.status || ''} ${e.message}`); + if (e.response) core.error(`Response: ${JSON.stringify(e.response.data).slice(0,400)}`); + } - name: Fail workflow if verified secrets found if: steps.process.outputs.has_verified == 'true' From 21e1c80e120a3d7f44c63051d4ddda579c64a217 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 15:20:48 -0700 Subject: [PATCH 02/10] simplify: replace dual-scanner with Docker-only filesystem scan Remove the TruffleHog GitHub Action step (git history scanner) which ran with continue-on-error: true and never blocked PRs or produced annotations. The Docker filesystem scanner already handles all real work: - Scans current state of PR-changed files - Generates file:line annotations (::warning / ::error) - Drives verified/unverified pass-fail logic The Action step added complexity with no user-visible benefit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/trufflehog-scan.yml | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index f2dd46d..f03c860 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -86,26 +86,11 @@ jobs: cat .trufflehog-ignore echo "exclude_args=--exclude-paths=.trufflehog-ignore" >> $GITHUB_OUTPUT - - name: TruffleHog Scan - id: trufflehog - # Pinned to v3.94.2 commit SHA to prevent supply chain attacks via mutable tag - uses: trufflesecurity/trufflehog@6bd2d14f7a4bc1e569fa3550efa7ec632a4fa67b # v3.94.2 - # continue-on-error: true is intentional — without it, the job stops here if the - # action finds secrets in git history, and the Parse step (which creates the - # per-file annotations developers see) never runs. - continue-on-error: true - with: - base: ${{ github.event.pull_request.base.sha }} - head: ${{ github.event.pull_request.head.sha }} - extra_args: --json ${{ steps.config.outputs.exclude_args }} - - - name: Parse scan results + - name: Scan changed files for secrets id: parse if: github.event_name != 'workflow_dispatch' run: | - # Scan the current state of PR files (not git history) - # This ensures renamed files and removed secrets are handled correctly - echo "Parsing TruffleHog results..." + echo "Scanning PR changed files for secrets..." VERIFIED_COUNT=0 UNVERIFIED_COUNT=0 @@ -128,8 +113,7 @@ jobs: echo "Scanning changed files:" echo "$CHANGED_FILES" - # Scan only the changed files in their current state using filesystem scanner - # Pinned to v3.94.2 to match the action version above + # Scan changed files using TruffleHog filesystem mode — pinned tag for reproducibility SCAN_OUTPUT=$(docker run --rm -v "$(pwd)":/tmp -w /tmp \ ghcr.io/trufflesecurity/trufflehog:3.94.2 \ filesystem /tmp/ \ From 8601f46198bffdd8164ced60a0da67d75041d473 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 15:34:55 -0700 Subject: [PATCH 03/10] fix: honor exclusion patterns in Docker filesystem scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docker mounts the repo at /tmp/, so TruffleHog sees paths as /tmp/vendor/config.js. Patterns like ^vendor/ in .trufflehog-ignore are anchored to the start and don't match /tmp/vendor/... — causing --exclude-paths to silently skip all exclusions in filesystem mode. Fix: after stripping the /tmp/ prefix from each detected file path, re-check it against .trufflehog-ignore patterns in the bash parse loop. This ensures both default excludes and TRUFFLEHOG_EXCLUDES repo/org variables are correctly enforced. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/trufflehog-scan.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index f03c860..5db4cd7 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -130,10 +130,22 @@ jobs: fi FILE=$(echo "$line" | jq -r '.SourceMetadata.Data.Filesystem.file // "unknown"') - # Remove /tmp/ prefix if present + # Remove /tmp/ prefix (Docker mounts the repo at /tmp/) FILE="${FILE#/tmp/}" - # Only count secrets in files that are part of this PR + # Re-apply exclusion patterns against the relative path. + # Docker sees absolute paths (/tmp/vendor/config.js), so TruffleHog's + # --exclude-paths misses patterns anchored with ^ (e.g. ^vendor/). + # We check here after stripping the /tmp/ prefix to enforce them correctly. + EXCLUDED=false + while IFS= read -r excl_pattern; do + excl_pattern="${excl_pattern#"${excl_pattern%%[! ]*}"}" # ltrim whitespace + [ -z "$excl_pattern" ] && continue + echo "$FILE" | grep -qE "$excl_pattern" && EXCLUDED=true && break + done < .trufflehog-ignore + [ "$EXCLUDED" = "true" ] && continue + + # Only process files changed in this PR if ! echo "$CHANGED_FILES" | grep -qxF "$FILE"; then continue fi From 516c278efb7b7b6272e11768d1bcc8075cd107ae Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 15:49:02 -0700 Subject: [PATCH 04/10] security: pin actions to commit SHAs (SECCMP-1797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin all mutable action version tags to immutable commit SHAs to prevent supply chain attacks where a compromised upstream tag could run malicious code with elevated pull_request_target permissions. actions/checkout@v4 → @34e114876b (v4) actions/github-script@v7 → @f28e40c7f3 (v7) actions/setup-python@v4 → @7f4fc3e22c (v4) This was one of the findings from SECCMP-1797 / PDP-1182 security review (Aditya's PR #42 — trufflehog-scan.yml pinning). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 6 +++--- .github/workflows/trufflehog-scan.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index e290f8e..111cdc0 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -14,14 +14,14 @@ jobs: steps: - name: Checkout PR head - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: ${{ github.event.pull_request.head.sha }} path: target-repo persist-credentials: false - name: Checkout pr-workflows repo - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: repository: ${{ github.repository_owner }}/pr-workflows ref: main @@ -47,7 +47,7 @@ jobs: echo "config-file=$cfg" >> $GITHUB_OUTPUT - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4 with: python-version: '3.11' diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index 5db4cd7..31c63eb 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -47,7 +47,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 persist-credentials: false @@ -202,7 +202,7 @@ jobs: # pull_request_target (org ruleset): token is read-only for the triggering repo — # createComment/updateComment will 403. Skip and rely on job annotations instead. if: github.event_name == 'workflow_call' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const commentMarker = ''; From 7bda72c79c6bcce24b394b26d11b4e3513543b86 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 15:52:51 -0700 Subject: [PATCH 05/10] revert: remove copyright-check.yml changes from trufflehog PR copyright-check.yml action pinning belongs in PR #43 only. Keep each PR to a single workflow file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/copyright-check.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index 111cdc0..e290f8e 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -14,14 +14,14 @@ jobs: steps: - name: Checkout PR head - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} path: target-repo persist-credentials: false - name: Checkout pr-workflows repo - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + uses: actions/checkout@v4 with: repository: ${{ github.repository_owner }}/pr-workflows ref: main @@ -47,7 +47,7 @@ jobs: echo "config-file=$cfg" >> $GITHUB_OUTPUT - name: Set up Python - uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4 + uses: actions/setup-python@v4 with: python-version: '3.11' From bcc354da109ebce3a7c96f7b33065fe694e0c5ba Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 16:08:14 -0700 Subject: [PATCH 06/10] fix: address valid PR review comments (comments 5, 6, 7) - Pin Docker image by digest to prevent supply chain tag mutation (ghcr.io/trufflesecurity/trufflehog:3.94.2@sha256:dabd9a5f...) - Add -- to grep -qE and grep -qxF to prevent filenames/patterns starting with '-' being misinterpreted as grep options - Skip comment lines (#) in exclusion pattern loop Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/trufflehog-scan.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index 31c63eb..d1da35f 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -113,9 +113,9 @@ jobs: echo "Scanning changed files:" echo "$CHANGED_FILES" - # Scan changed files using TruffleHog filesystem mode — pinned tag for reproducibility + # Scan changed files using TruffleHog filesystem mode — pinned by digest for reproducibility SCAN_OUTPUT=$(docker run --rm -v "$(pwd)":/tmp -w /tmp \ - ghcr.io/trufflesecurity/trufflehog:3.94.2 \ + ghcr.io/trufflesecurity/trufflehog:3.94.2@sha256:dabd9a5f78ed81211792afc39a35100e2b947baa9f43f1e73a97759d7d15ab86 \ filesystem /tmp/ \ --json \ ${{ steps.config.outputs.exclude_args }} \ @@ -141,12 +141,13 @@ jobs: while IFS= read -r excl_pattern; do excl_pattern="${excl_pattern#"${excl_pattern%%[! ]*}"}" # ltrim whitespace [ -z "$excl_pattern" ] && continue - echo "$FILE" | grep -qE "$excl_pattern" && EXCLUDED=true && break + [ "${excl_pattern#\#}" != "$excl_pattern" ] && continue # skip comment lines + echo "$FILE" | grep -qE -- "$excl_pattern" && EXCLUDED=true && break done < .trufflehog-ignore [ "$EXCLUDED" = "true" ] && continue # Only process files changed in this PR - if ! echo "$CHANGED_FILES" | grep -qxF "$FILE"; then + if ! echo "$CHANGED_FILES" | grep -qxF -- "$FILE"; then continue fi From b866cc0c95b087c380798ebcb7b9b1c60e728e1c Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 16:24:09 -0700 Subject: [PATCH 07/10] fix: add issues: write permission for PR comment step (github.rest.issues.*) The PR comment step uses github.rest.issues.listComments/createComment/ updateComment APIs, which require issues: write. Without it, the comment step 403s silently. copyright-check.yml already has this permission for the same reason. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/trufflehog-scan.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index d1da35f..07f5b0e 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -14,10 +14,14 @@ on: permissions: contents: read - # pull-requests: write is needed for the PR comment step (workflow_call path only). - # pull_request_target (org ruleset) skips that step, but GitHub Actions has no - # per-event conditional permissions within a single job. + # pull-requests: write and issues: write are needed for the PR comment step + # (workflow_call path only). The PR comment step uses github.rest.issues.* + # APIs which require issues: write; pull-requests: write is also kept for + # potential future comment resolution. pull_request_target skips the comment + # step, but GitHub Actions has no per-event conditional permissions within a + # single job — splitting into two jobs would add significant complexity. pull-requests: write + issues: write # Default exclusion patterns (regex format) # Supports: exact filenames, wildcards, regex patterns From 4814b109cab3a03c7c2b21b652edd3360e508588 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 16:31:13 -0700 Subject: [PATCH 08/10] security: move all event payload expressions to env: vars (SECCMP-1797) For consistency with copyright-check.yml, move all ${{ }} expressions out of run: shell commands and github-script: blocks into env: vars, accessed via shell $VAR and process.env.VAR respectively. Changes: - Fetch PR head commits: SERVER_URL, REPO, PR_NUMBER, PR_HEAD_SHA -> env: - Setup exclude config: replace quoted heredoc with printf '\''%s\n'\'' "$DEFAULT_EXCLUDES" (DEFAULT_EXCLUDES is a workflow-level env var, not attacker-controlled) - Scan changed files: PR_HEAD_SHA, PR_BASE_SHA, EXCLUDE_ARGS -> env: - Post PR comment (github-script): PR_HEAD_SHA, HAS_SECRETS, HAS_VERIFIED, VERIFIED_COUNT, UNVERIFIED_COUNT, SERVER_URL, REPO, RUN_ID -> env:; all references updated to use process.env.* None of these values (SHAs, integers, fixed URLs) were exploitable, but the env: pattern is the documented SECCMP-1797 best practice and matches the approach used in copyright-check.yml. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/trufflehog-scan.yml | 52 ++++++++++++++++++--------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index 07f5b0e..8adca67 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -60,13 +60,17 @@ jobs: if: github.event_name != 'workflow_dispatch' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SERVER_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | # Scope credentials to this repo only — git -c passes the header in-memory # and is never written to .git/config, so persist-credentials: false is preserved. AUTH_HEADER="Authorization: basic $(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 -w0)" - git -c "http.${{ github.server_url }}/${{ github.repository }}/.extraheader=${AUTH_HEADER}" \ - fetch origin +refs/pull/${{ github.event.pull_request.number }}/head:refs/remotes/origin/pr-head - echo "Fetched PR #${{ github.event.pull_request.number }} head commit: ${{ github.event.pull_request.head.sha }}" + git -c "http.${SERVER_URL}/${REPO}/.extraheader=${AUTH_HEADER}" \ + fetch origin +refs/pull/${PR_NUMBER}/head:refs/remotes/origin/pr-head + echo "Fetched PR #${PR_NUMBER} head commit: ${PR_HEAD_SHA}" - name: Setup exclude config id: config @@ -75,9 +79,7 @@ jobs: run: | # Always include default exclusions echo "Adding default exclusions" - cat << 'EOF' > .trufflehog-ignore - ${{ env.DEFAULT_EXCLUDES }} - EOF + printf '%s\n' "$DEFAULT_EXCLUDES" > .trufflehog-ignore # Append repo/org-level custom exclusions if defined if [ -n "$TRUFFLEHOG_EXCLUDES" ]; then @@ -93,6 +95,10 @@ jobs: - name: Scan changed files for secrets id: parse if: github.event_name != 'workflow_dispatch' + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + EXCLUDE_ARGS: ${{ steps.config.outputs.exclude_args }} run: | echo "Scanning PR changed files for secrets..." @@ -100,12 +106,12 @@ jobs: UNVERIFIED_COUNT=0 # Checkout PR head to scan current file state - git checkout ${{ github.event.pull_request.head.sha }} --quiet + git checkout "${PR_HEAD_SHA}" --quiet # Get list of files changed in this PR (with rename detection) # -M enables rename detection, showing only the new filename for renamed files # --diff-filter=d excludes deleted files (we only want files that exist in the PR head) - CHANGED_FILES=$(git diff --name-only -M --diff-filter=d ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} | grep -v '^$' || true) + CHANGED_FILES=$(git diff --name-only -M --diff-filter=d "${PR_BASE_SHA}...${PR_HEAD_SHA}" | grep -v '^$' || true) if [ -z "$CHANGED_FILES" ]; then echo "No files changed in PR" @@ -122,7 +128,7 @@ jobs: ghcr.io/trufflesecurity/trufflehog:3.94.2@sha256:dabd9a5f78ed81211792afc39a35100e2b947baa9f43f1e73a97759d7d15ab86 \ filesystem /tmp/ \ --json \ - ${{ steps.config.outputs.exclude_args }} \ + $EXCLUDE_ARGS \ --no-update 2>/dev/null || true) # Parse JSON lines and filter to only changed files @@ -207,16 +213,28 @@ jobs: # pull_request_target (org ruleset): token is read-only for the triggering repo — # createComment/updateComment will 403. Skip and rely on job annotations instead. if: github.event_name == 'workflow_call' + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + HAS_SECRETS: ${{ steps.process.outputs.has_secrets }} + HAS_VERIFIED: ${{ steps.process.outputs.has_verified }} + VERIFIED_COUNT: ${{ steps.parse.outputs.verified_count }} + UNVERIFIED_COUNT: ${{ steps.parse.outputs.unverified_count }} + SERVER_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const commentMarker = ''; - const commitSha = '${{ github.event.pull_request.head.sha }}'; + const commitSha = process.env.PR_HEAD_SHA; const shortSha = commitSha.substring(0, 7); - const hasSecrets = '${{ steps.process.outputs.has_secrets }}' === 'true'; - const hasVerified = '${{ steps.process.outputs.has_verified }}' === 'true'; - const verifiedCount = '${{ steps.parse.outputs.verified_count }}' || '0'; - const unverifiedCount = '${{ steps.parse.outputs.unverified_count }}' || '0'; + const hasSecrets = process.env.HAS_SECRETS === 'true'; + const hasVerified = process.env.HAS_VERIFIED === 'true'; + const verifiedCount = process.env.VERIFIED_COUNT || '0'; + const unverifiedCount = process.env.UNVERIFIED_COUNT || '0'; + const serverUrl = process.env.SERVER_URL; + const repo = process.env.REPO; + const runId = process.env.RUN_ID; try { // Find existing comment const { data: comments } = await github.rest.issues.listComments({ @@ -246,7 +264,7 @@ jobs: **No secrets detected in this pull request.** - **Scanned commit:** \`${shortSha}\` ([${commitSha}](${{ github.server_url }}/${{ github.repository }}/commit/${commitSha})) + **Scanned commit:** \`${shortSha}\` ([${commitSha}](${serverUrl}/${repo}/commit/${commitSha})) Previous ${previousType} have been resolved. Thank you for addressing the security concerns! @@ -284,7 +302,7 @@ jobs: - **Verified (active) secrets:** ${verifiedCount} ${verifiedCount > 0 ? ':x:' : ':white_check_mark:'} - **Unverified (potential) secrets:** ${unverifiedCount} ${unverifiedCount > 0 ? ':warning:' : ':white_check_mark:'} - **Scanned commit:** \`${shortSha}\` ([${commitSha}](${{ github.server_url }}/${{ github.repository }}/commit/${commitSha})) + **Scanned commit:** \`${shortSha}\` ([${commitSha}](${serverUrl}/${repo}/commit/${commitSha})) ${action} @@ -300,7 +318,7 @@ jobs: | **Verified** | Confirmed active credential | **Must remove & rotate** - PR blocked | | **Unverified** | Potential secret pattern | Review recommended - PR can proceed | - Check the [workflow run logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. + Check the [workflow run logs](${serverUrl}/${repo}/actions/runs/${runId}) for details. --- *Verified secrets are confirmed active by TruffleHog. Unverified secrets match known patterns but couldn't be validated.* From 107b04ccfddf7190d3f700a25460277acec479df Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 16:42:05 -0700 Subject: [PATCH 09/10] fix: use herestring instead of echo pipe for grep (prevents -n/-e filename edge case) echo "$VAR" | grep can mishandle filenames literally named '-n' or '-e' because bash's built-in echo treats them as options, suppressing output and causing grep to match against nothing (false negative for secret scan). Replace with grep ... <<<"$VAR" (herestring) which is not subject to echo option interpretation. Fixes two instances: - Exclusion pattern check: grep -qE -- "$excl_pattern" <<<"$FILE" - Changed files membership: grep -qxF -- "$FILE" <<<"$CHANGED_FILES" Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/trufflehog-scan.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index 8adca67..b52256c 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -152,12 +152,12 @@ jobs: excl_pattern="${excl_pattern#"${excl_pattern%%[! ]*}"}" # ltrim whitespace [ -z "$excl_pattern" ] && continue [ "${excl_pattern#\#}" != "$excl_pattern" ] && continue # skip comment lines - echo "$FILE" | grep -qE -- "$excl_pattern" && EXCLUDED=true && break + grep -qE -- "$excl_pattern" <<< "$FILE" && EXCLUDED=true && break done < .trufflehog-ignore [ "$EXCLUDED" = "true" ] && continue # Only process files changed in this PR - if ! echo "$CHANGED_FILES" | grep -qxF -- "$FILE"; then + if ! grep -qxF -- "$FILE" <<< "$CHANGED_FILES"; then continue fi From b2f0af26977223b61d9e5e230fd18f4df7a2bdc9 Mon Sep 17 00:00:00 2001 From: Sameera Priyatham Tadikonda Date: Wed, 8 Apr 2026 16:52:16 -0700 Subject: [PATCH 10/10] fix: fail explicitly when Docker scan fails to prevent fail-open bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker run ... 2>/dev/null || true suppresses all stderr and forces exit 0 in every case. If TruffleHog or Docker fails at runtime (image pull error, OOM, container crash), SCAN_OUTPUT is empty → VERIFIED_COUNT=0 → job passes silently — a fail-open that bypasses the required ruleset check. Fix: capture the exit code without || true. TruffleHog exits 0 (no secrets) or 183 (secrets found) on a successful run. Any other code means the scanner itself failed — fail the job explicitly with ::error rather than silently passing with zero findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/trufflehog-scan.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index b52256c..337d2be 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -124,12 +124,22 @@ jobs: echo "$CHANGED_FILES" # Scan changed files using TruffleHog filesystem mode — pinned by digest for reproducibility + SCAN_EXIT=0 SCAN_OUTPUT=$(docker run --rm -v "$(pwd)":/tmp -w /tmp \ ghcr.io/trufflesecurity/trufflehog:3.94.2@sha256:dabd9a5f78ed81211792afc39a35100e2b947baa9f43f1e73a97759d7d15ab86 \ filesystem /tmp/ \ --json \ $EXCLUDE_ARGS \ - --no-update 2>/dev/null || true) + --no-update 2>/dev/null) || SCAN_EXIT=$? + + # TruffleHog exits 0 (no secrets found) or 183 (secrets found) on a successful run. + # Any other exit code means Docker or TruffleHog itself failed to execute. + # We must not silently pass in that case — an empty SCAN_OUTPUT would report 0 findings + # and let the job succeed, bypassing the required ruleset check (fail-open). + if [ "$SCAN_EXIT" -ne 0 ] && [ "$SCAN_EXIT" -ne 183 ]; then + echo "::error::TruffleHog Docker scan failed (exit ${SCAN_EXIT}). Blocking to prevent fail-open bypass of secret scanning." + exit 1 + fi # Parse JSON lines and filter to only changed files if [ -n "$SCAN_OUTPUT" ]; then