PDP-684 Add TruffleHog secret scanning workflow for PR validation #4
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |