Skip to content

PDP-684 Add TruffleHog secret scanning workflow for PR validation #4

PDP-684 Add TruffleHog secret scanning workflow for PR validation

PDP-684 Add TruffleHog secret scanning workflow for PR validation #4

Workflow file for this run

name: TruffleHog Secret Scan
on:
pull_request:
types: [opened, synchronize, reopened]
pull_request_target:
types: [opened, synchronize, reopened]
workflow_dispatch:
permissions:
contents: read
pull-requests: write
statuses: write
# Default exclusion patterns (regex format)
# Supports: exact filenames, wildcards, regex patterns
# Examples:
# Exact file: ^config/settings\.json$
# Directory: ^node_modules/
# Extension: \.lock$
# Wildcard: .*\.min\.js$
# Regex: ^src/test/.*_test\.py$
env:
DEFAULT_EXCLUDES: |
^node_modules/
^vendor/
^\.git/
\.lock$
^package-lock\.json$
^yarn\.lock$
^pnpm-lock\.yaml$
\.min\.js$
\.min\.css$
jobs:
trufflehog-scan:
name: Scan PR for Secrets
runs-on: ubuntu-latest
# Run pull_request_target only for fork PRs, pull_request only for same-repo PRs
# This prevents duplicate runs
if: |
(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository) ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch PR head commits
if: github.event_name != 'workflow_dispatch'
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
echo "Fetched PR #${{ github.event.pull_request.number }} head commit: ${{ github.event.pull_request.head.sha }}"
- name: Setup exclude config
id: config
run: |
if [ -n "${{ vars.TRUFFLEHOG_EXCLUDES }}" ]; then
echo "Using repo/org-level TRUFFLEHOG_EXCLUDES variable"
# Support both comma-separated and newline-separated patterns
echo "${{ vars.TRUFFLEHOG_EXCLUDES }}" | tr ',' '\n' | sed '/^$/d' > .trufflehog-ignore
else
echo "Using default exclusions from central workflow"
cat << 'EOF' > .trufflehog-ignore
${{ env.DEFAULT_EXCLUDES }}
EOF
fi
echo "Exclusion patterns:"
cat .trufflehog-ignore
echo "exclude_args=--exclude-paths=.trufflehog-ignore" >> $GITHUB_OUTPUT
- name: TruffleHog Scan
id: trufflehog
uses: trufflesecurity/trufflehog@main
continue-on-error: true
with:
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
extra_args: --json --fail ${{ steps.config.outputs.exclude_args }}
- name: Capture scan output
id: capture
if: github.event_name != 'workflow_dispatch'
run: |
# Capture TruffleHog output from the action's output file if available
# The trufflehog action writes JSON to stdout, we need to parse workflow logs
# For now, we'll run trufflehog again to capture output to a file
echo "Running TruffleHog to capture detailed output..."
# Install trufflehog CLI
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin
# Run scan and capture JSON output
trufflehog git file://. --since-commit=${{ github.event.pull_request.base.sha }} \
--branch=origin/pr-head --json --fail \
${{ steps.config.outputs.exclude_args }} 2>/dev/null > trufflehog_results.json || true
# Check if we have results
if [ -s trufflehog_results.json ]; then
echo "has_results=true" >> $GITHUB_OUTPUT
# Parse JSON and create markdown table
# Each line is a separate JSON object
echo "| File | Line | Secret Type | Verified |" > findings_table.md
echo "|------|------|-------------|----------|" >> findings_table.md
while IFS= read -r line; do
if [ -n "$line" ]; then
file=$(echo "$line" | jq -r '.SourceMetadata.Data.Git.file // "N/A"')
line_num=$(echo "$line" | jq -r '.SourceMetadata.Data.Git.line // "N/A"')
detector=$(echo "$line" | jq -r '.DetectorName // "Unknown"')
verified=$(echo "$line" | jq -r 'if .Verified then "Yes" else "No" end')
echo "| \`$file\` | $line_num | $detector | $verified |" >> findings_table.md
fi
done < trufflehog_results.json
# Store table content for PR comment
findings_table=$(cat findings_table.md)
# Escape for GitHub Actions output
findings_table="${findings_table//'%'/'%25'}"
findings_table="${findings_table//$'\n'/'%0A'}"
findings_table="${findings_table//$'\r'/'%0D'}"
echo "findings_table<<EOF" >> $GITHUB_OUTPUT
cat findings_table.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "has_results=false" >> $GITHUB_OUTPUT
echo "findings_table=" >> $GITHUB_OUTPUT
fi
- name: Process scan results
id: process
if: github.event_name != 'workflow_dispatch'
run: |
# Check if TruffleHog found any secrets
if [ "${{ steps.trufflehog.outcome }}" == "failure" ]; then
echo "has_secrets=true" >> $GITHUB_OUTPUT
echo "status=failure" >> $GITHUB_OUTPUT
echo "description=Secret scanning found exposed credentials" >> $GITHUB_OUTPUT
else
echo "has_secrets=false" >> $GITHUB_OUTPUT
echo "status=success" >> $GITHUB_OUTPUT
echo "description=No secrets detected in PR changes" >> $GITHUB_OUTPUT
fi
- name: Post PR comment on findings
if: steps.process.outputs.has_secrets == 'true' && github.event_name != 'workflow_dispatch'
uses: actions/github-script@v7
env:
FINDINGS_TABLE: ${{ steps.capture.outputs.findings_table }}
with:
script: |
const commentMarker = '<!-- TRUFFLEHOG-SCAN-COMMENT -->';
const findingsTable = process.env.FINDINGS_TABLE || '';
let findingsSection = '';
if (findingsTable && findingsTable.trim()) {
findingsSection = `
### Detected Secrets
${findingsTable}
> **Note:** "Verified = Yes" means the credential was confirmed to be active/valid.
`;
} else {
findingsSection = `
### Finding Details
Check the [workflow run logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for:
- File paths containing secrets
- Line numbers
- Secret types (API key, password, token, etc.)
- Verification status (verified = confirmed active)
`;
}
const body = `${commentMarker}
## :rotating_light: Secret Scanning Alert
**TruffleHog detected potential secrets in this pull request.**
### What to do:
1. **Remove the exposed secret** from your code
2. **Rotate the credential immediately** - assume it's compromised
3. **Push the fix** to this branch
4. The scan will re-run automatically
${findingsSection}
### Workflow Logs
[View detailed scan logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
---
*This scan only checks files modified in this PR. Secrets are classified as **verified** (confirmed active) or **unverified** (potential match).*
`;
// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
per_page: 100
});
const existing = comments.find(c => c.body && c.body.includes(commentMarker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: body
});
}
- name: Set commit status
if: github.event_name != 'workflow_dispatch'
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: '${{ github.event.pull_request.head.sha }}',
state: '${{ steps.process.outputs.status }}',
target_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}',
description: '${{ steps.process.outputs.description }}',
context: 'TruffleHog Secret Scan'
});
- name: Fail workflow if secrets found
if: steps.process.outputs.has_secrets == 'true'
run: |
echo "::error::Secrets detected in PR. Review the logs and PR comment for details."
exit 1