Skip to content
Merged
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
90 changes: 59 additions & 31 deletions .github/workflows/trufflehog-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ permissions:
contents: read
pull-requests: 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/
Expand All @@ -30,19 +39,28 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
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: |
# Always include default exclusions
echo "Adding default exclusions"
cat << 'EOF' > .trufflehog-ignore
${{ env.DEFAULT_EXCLUDES }}
EOF

# Append repo/org-level custom exclusions if defined
if [ -n "${{ vars.TRUFFLEHOG_EXCLUDES }}" ]; then
echo "Adding repo/org-level TRUFFLEHOG_EXCLUDES patterns"
# Support both comma-separated and newline-separated patterns
echo "${{ vars.TRUFFLEHOG_EXCLUDES }}" | tr ',' '\n' | sed '/^$/d' >> .trufflehog-ignore
fi

Expand All @@ -55,62 +73,49 @@ jobs:
uses: trufflesecurity/trufflehog@main
continue-on-error: true
with:
base: ${{ github.event.pull_request.head.sha }}~1
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
id: parse
if: github.event_name != 'workflow_dispatch'
run: |
# Capture TruffleHog JSON output by re-running with same args
echo "Parsing TruffleHog results..."

VERIFIED_COUNT=0
UNVERIFIED_COUNT=0

# Get changed files list
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
echo "Changed files:"
echo "$CHANGED_FILES"

# Scan only HEAD commit (current state), not history
SCAN_OUTPUT=$(docker run --rm -v "$(pwd)":/tmp -w /tmp \
-e GIT_CONFIG_COUNT=2 \
-e GIT_CONFIG_KEY_0=diff.renames \
-e GIT_CONFIG_VALUE_0=false \
-e GIT_CONFIG_KEY_1=diff.renameLimit \
-e GIT_CONFIG_VALUE_1=0 \
ghcr.io/trufflesecurity/trufflehog:latest \
git file:///tmp/ \
--since-commit ${{ github.event.pull_request.head.sha }}~1 \
--since-commit ${{ github.event.pull_request.base.sha }} \
--branch ${{ github.event.pull_request.head.sha }} \
--max-depth=1 \
--json \
${{ steps.config.outputs.exclude_args }} \
--no-update 2>/dev/null || true)

# Parse JSON lines and create GitHub annotations
if [ -n "$SCAN_OUTPUT" ]; then
while IFS= read -r line; do
# Skip non-JSON lines (info logs)
if ! echo "$line" | jq -e '.DetectorName' > /dev/null 2>&1; then
continue
fi

FILE=$(echo "$line" | jq -r '.SourceMetadata.Data.Git.file // "unknown"')

# Only report if file is in the changed files list
if ! echo "$CHANGED_FILES" | grep -qxF "$FILE"; then
continue
fi

LINE_NUM=$(echo "$line" | jq -r '.SourceMetadata.Data.Git.line // 1')
DETECTOR=$(echo "$line" | jq -r '.DetectorName // "Secret"')
VERIFIED=$(echo "$line" | jq -r '.Verified // false')

if [ "$VERIFIED" == "true" ]; then
VERIFIED_COUNT=$((VERIFIED_COUNT + 1))
echo "::error file=${FILE},line=${LINE_NUM},title=${DETECTOR} [VERIFIED]::VERIFIED ACTIVE CREDENTIAL: ${DETECTOR} found in ${FILE} at line ${LINE_NUM}. Remove and rotate immediately!"
# Error annotation for verified secrets
echo "::error file=${FILE},line=${LINE_NUM},title=${DETECTOR} [VERIFIED]::VERIFIED ACTIVE CREDENTIAL: ${DETECTOR} found in ${FILE} at line ${LINE_NUM}. This secret is confirmed active. Remove and rotate immediately!"
else
UNVERIFIED_COUNT=$((UNVERIFIED_COUNT + 1))
# Warning annotation for unverified secrets
echo "::warning file=${FILE},line=${LINE_NUM},title=${DETECTOR} [Unverified]::Potential secret: ${DETECTOR} found in ${FILE} at line ${LINE_NUM}. Review and remove if this is a real credential."
fi
done <<< "$SCAN_OUTPUT"
Expand All @@ -128,14 +133,17 @@ jobs:
UNVERIFIED=${{ steps.parse.outputs.unverified_count || 0 }}

if [ "$VERIFIED" -gt 0 ]; then
# Verified secrets found - must fail
echo "has_verified=true" >> $GITHUB_OUTPUT
echo "has_secrets=true" >> $GITHUB_OUTPUT
echo "description=Found ${VERIFIED} verified (active) secrets - action required" >> $GITHUB_OUTPUT
elif [ "$UNVERIFIED" -gt 0 ]; then
# Only unverified secrets - warn but pass
echo "has_verified=false" >> $GITHUB_OUTPUT
echo "has_secrets=true" >> $GITHUB_OUTPUT
echo "description=Found ${UNVERIFIED} unverified potential secrets - review recommended" >> $GITHUB_OUTPUT
else
# No secrets
echo "has_verified=false" >> $GITHUB_OUTPUT
echo "has_secrets=false" >> $GITHUB_OUTPUT
echo "description=No secrets detected in PR changes" >> $GITHUB_OUTPUT
Expand All @@ -154,6 +162,7 @@ jobs:
const verifiedCount = '${{ steps.parse.outputs.verified_count }}' || '0';
const unverifiedCount = '${{ steps.parse.outputs.unverified_count }}' || '0';

// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
Expand All @@ -165,26 +174,38 @@ jobs:

let body;
if (!hasSecrets) {
// No secrets found
if (existing) {
// Update to show secrets are now resolved (whether verified or unverified)
body = `${commentMarker}
// Check if existing comment was a critical/blocking one (had verified secrets)
const wasBlocking = existing.body.includes('CRITICAL') || existing.body.includes(':rotating_light:');
if (wasBlocking) {
// Update to show verified secrets are now resolved
body = `${commentMarker}
## :white_check_mark: Secret Scanning Passed

**No secrets detected in this pull request.**

**Scanned commit:** \`${shortSha}\` ([${commitSha}](${{ github.server_url }}/${{ github.repository }}/commit/${commitSha}))

Previous issues have been resolved. Thank you for addressing the security concerns!

---
*This comment will be updated if new secrets are detected in future commits.*
`;
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body
});
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body
});
}
// If it was just a warning (unverified only), leave it as-is
}
// If no existing comment and no secrets, don't post anything
return;
}

// Secrets found - create or update warning comment
let severity, icon, action;
if (hasVerified) {
severity = 'CRITICAL';
Expand All @@ -198,22 +219,29 @@ jobs:

body = `${commentMarker}
## ${icon} Secret Scanning ${severity}

**TruffleHog scan results:**
- **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}))

${action}

### What to do:
1. **Review the workflow annotations** - they point to exact file and line locations
2. **Remove any exposed secrets** from your code
3. **Rotate compromised credentials** - especially verified ones
4. **Push the fix** to this branch

### Understanding Results
| Type | Meaning | Action Required |
|------|---------|-----------------|
| **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.

---
*Verified secrets are confirmed active by TruffleHog. Unverified secrets match known patterns but couldn't be validated.*
`;
Expand Down