fix: ensure results.json is always included in trufflehog artifacts #8
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: Reusable TruffleHog Secret Scan | ||
|
Check failure on line 1 in .github/workflows/reusable-trufflehog.yml
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| fail-on-verified: | ||
| description: "Fail workflow on verified secrets" | ||
| required: false | ||
| default: "true" | ||
| type: string | ||
| fail-on-unverified: | ||
| description: "Fail workflow on unverified secrets" | ||
| required: false | ||
| default: "false" | ||
| type: string | ||
| runs-on: | ||
| description: "The runner to use for the job" | ||
| required: false | ||
| default: "ubuntu-latest" | ||
| type: string | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| env: | ||
| # renovate: datasource=github-releases depName=trufflesecurity/trufflehog | ||
| TRUFFLEHOG_VERSION: 3.89.2 | ||
| jobs: | ||
| trufflehog-scan: | ||
| runs-on: ${{ inputs.runs-on }} | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 | ||
| with: | ||
| fetch-depth: 100 | ||
| persist-credentials: false | ||
| - name: Install TruffleHog | ||
| run: | | ||
| # Download binary directly from GitHub releases for supply chain security | ||
| VERSION="v${{ env.TRUFFLEHOG_VERSION }}" | ||
| # Auto-detect architecture for cross-platform support | ||
| if [[ "$(uname -m)" == "aarch64" ]]; then | ||
| ARCH="linux_arm64" | ||
| else | ||
| ARCH="linux_amd64" | ||
| fi | ||
| BINARY_URL="https://github.com/trufflesecurity/trufflehog/releases/download/${VERSION}/trufflehog_${VERSION#v}_${ARCH}.tar.gz" | ||
| curl -sSfL "${BINARY_URL}" | tar -xz -C /tmp | ||
| sudo mv /tmp/trufflehog /usr/local/bin/trufflehog | ||
| sudo chmod +x /usr/local/bin/trufflehog | ||
| trufflehog --version | ||
| - name: Scan for secrets | ||
| id: scan | ||
| run: | | ||
| set +e | ||
| echo "[]" > results.json | ||
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | ||
| # PR: Scan only changed files | ||
| echo "Scanning changed files in PR..." | ||
| git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.sha }} > changed-files.txt | ||
| if [[ -s changed-files.txt ]]; then | ||
| while IFS= read -r file; do | ||
| if [[ -f "${file}" ]]; then | ||
| echo "Scanning: ${file}" | ||
| trufflehog filesystem "${file}" --json --no-update --results=verified,unverified >> results.ndjson || true | ||
| fi | ||
| done < changed-files.txt | ||
| else | ||
| echo "No files changed" | ||
| fi | ||
| else | ||
| # Push to main: Scan current filesystem | ||
| echo "Scanning current filesystem..." | ||
| trufflehog filesystem . --json --no-update --results=verified,unverified > results.ndjson || true | ||
| fi | ||
| # Process results | ||
| if [[ -s results.ndjson ]]; then | ||
| grep -v '^$' results.ndjson | jq -s '.' > results.json 2>/dev/null || echo "[]" > results.json | ||
| else | ||
| # Ensure results.json exists even if no results | ||
| echo "[]" > results.json | ||
| fi | ||
| # Count secrets | ||
| if jq empty results.json 2>/dev/null; then | ||
| VERIFIED=$(jq '[.[] | select(.Verified==true)] | length' results.json 2>/dev/null || echo "0") | ||
| UNVERIFIED=$(jq '[.[] | select(.Verified==false)] | length' results.json 2>/dev/null || echo "0") | ||
| else | ||
| VERIFIED=0 | ||
| UNVERIFIED=0 | ||
| echo "[]" > results.json | ||
| fi | ||
| TOTAL=$((VERIFIED+UNVERIFIED)) | ||
| echo "Found ${TOTAL} potential secrets (${VERIFIED} verified, ${UNVERIFIED} unverified)" | ||
| echo "verified=${VERIFIED}" >> "${GITHUB_OUTPUT}" | ||
| echo "unverified=${UNVERIFIED}" >> "${GITHUB_OUTPUT}" | ||
| echo "total=${TOTAL}" >> "${GITHUB_OUTPUT}" | ||
| - name: Generate PR comment | ||
| if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} | ||
| id: comment-body | ||
| run: | | ||
| VERIFIED=${{ steps.scan.outputs.verified }} | ||
| UNVERIFIED=${{ steps.scan.outputs.unverified }} | ||
| TOTAL=$((VERIFIED+UNVERIFIED)) | ||
| if [[ $TOTAL -eq 0 ]]; then | ||
| { | ||
| echo 'body<<EOF' | ||
| echo '**TruffleHog Scan Results**' | ||
| echo '' | ||
| echo 'No secrets detected in this PR.' | ||
| echo 'EOF' | ||
| } >> "$GITHUB_OUTPUT" | ||
| else | ||
| # Generate findings list | ||
| FINDINGS=$(jq -r '.[] | | ||
| "- " + | ||
| (if .Verified then "**VERIFIED SECRET**" else "**Possible secret**" end) + | ||
| " (" + .DetectorName + ") at `" + | ||
| ((.SourceMetadata?.Data?.Filesystem?.file // .SourceMetadata?.Data?.Git?.file) // "unknown") + | ||
| ":" + | ||
| ((.SourceMetadata?.Data?.Filesystem?.line // .SourceMetadata?.Data?.Git?.line) | tostring) + | ||
| "` → `" + | ||
| (if (.Raw | length) > 8 then (.Raw[:4] + "***" + .Raw[-4:]) else "***" end) + | ||
| "`"' results.json 2>/dev/null || echo "- Error processing results") | ||
| ACTION_TEXT="" | ||
| if [[ $VERIFIED -gt 0 ]]; then | ||
| ACTION_TEXT="**ACTION REQUIRED:** Rotate verified credentials immediately." | ||
| else | ||
| ACTION_TEXT="**Review:** Check if unverified secrets are false positives." | ||
| fi | ||
| { | ||
| echo 'body<<EOF' | ||
| echo "**TruffleHog Scan Results**" | ||
| echo '' | ||
| echo "**Summary:** Found ${TOTAL} potential secrets (${VERIFIED} verified, ${UNVERIFIED} unverified)" | ||
| echo '' | ||
| echo "${FINDINGS}" | ||
| echo '' | ||
| echo "${ACTION_TEXT}" | ||
| echo 'EOF' | ||
| } >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Post PR comment | ||
| if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} | ||
| uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 | ||
| with: | ||
| # We retain the final trufflehog-secret-scan-comment marker in case we want to chang the action to another one. | ||
| message: | | ||
| ${{ steps.comment-body.outputs.body }} | ||
| <!-- trufflehog-secret-scan-comment --> | ||
| message-id: trufflehog-secret-scan-comment | ||
| - name: Create scan report | ||
| env: | ||
| GITHUB_REF_NAME: ${{ github.ref_name }} | ||
| run: | | ||
| { | ||
| echo "TruffleHog Scan Report" | ||
| echo "=====================" | ||
| echo "Date: $(date)" | ||
| echo "Repository: ${{ github.repository }}" | ||
| echo "Branch: ${GITHUB_REF_NAME}" | ||
| echo "Commit: ${{ github.sha }}" | ||
| echo "" | ||
| echo "Summary:" | ||
| echo "- Total secrets: ${{ steps.scan.outputs.total }}" | ||
| echo "- Verified: ${{ steps.scan.outputs.verified }}" | ||
| echo "- Unverified: ${{ steps.scan.outputs.unverified }}" | ||
| echo "" | ||
| echo "Detailed Results:" | ||
| if [[ -f "results.json" && -s "results.json" ]] && jq empty results.json 2>/dev/null; then | ||
| jq -r '.[] | "- " + (if .Verified then "VERIFIED" else "Unverified" end) + " " + .DetectorName + " at " + ((.SourceMetadata?.Data?.Filesystem?.file // .SourceMetadata?.Data?.Git?.file) // "unknown") + ":" + ((.SourceMetadata?.Data?.Filesystem?.line // .SourceMetadata?.Data?.Git?.line) | tostring) + " → " + (if (.Raw | length) > 8 then (.Raw[:4] + "***" + .Raw[-4:]) else "***" end)' results.json 2>/dev/null || echo "Error processing results" | ||
| else | ||
| echo "No secrets detected" | ||
| fi | ||
| } > trufflehog_scan.txt | ||
| - name: Verify JSON file exists | ||
| if: always() | ||
| run: | | ||
| if [[ -f "results.json" ]]; then | ||
| echo "✅ results.json exists" | ||
| echo "File size: $(wc -c < results.json) bytes" | ||
| echo "JSON validity check:" | ||
| if jq empty results.json 2>/dev/null; then | ||
| echo "✅ Valid JSON" | ||
| echo "Number of findings: $(jq '. | length' results.json)" | ||
| else | ||
| echo "❌ Invalid JSON" | ||
| fi | ||
| else | ||
| echo "❌ results.json does not exist" | ||
| fi | ||
| - name: Prepare artifacts for upload | ||
| if: always() | ||
| run: | | ||
| # Ensure results.json exists | ||
| if [[ ! -f "results.json" ]]; then | ||
| echo "[]" > results.json | ||
| echo "Created empty results.json" | ||
| fi | ||
| # Create artifacts directory and copy files | ||
| mkdir -p trufflehog-artifacts | ||
| # Copy text report | ||
| if [[ -f "trufflehog_scan.txt" ]]; then | ||
| cp trufflehog_scan.txt trufflehog-artifacts/ | ||
| echo "✓ Copied trufflehog_scan.txt" | ||
| else | ||
| echo "⚠ trufflehog_scan.txt not found" | ||
| fi | ||
| # Copy JSON results | ||
| if [[ -f "results.json" ]]; then | ||
| cp results.json trufflehog-artifacts/ | ||
| echo "✓ Copied results.json" | ||
| else | ||
| echo "⚠ results.json not found, creating empty one" | ||
| echo "[]" > trufflehog-artifacts/results.json | ||
| fi | ||
| echo "" | ||
| echo "Files in artifacts directory:" | ||
| ls -lah trufflehog-artifacts/ | ||
| - name: Upload scan results | ||
| uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 | ||
| if: always() | ||
| with: | ||
| name: trufflehog_scan | ||
| path: trufflehog-artifacts/ | ||
| retention-days: 30 | ||
| - name: Setup Python for Grafana integration | ||
| if: ${{ !cancelled() && (secrets.LOKI_URL != '' || secrets.PROMETHEUS_PUSHGATEWAY_URL != '') }} | ||
| uses: actions/setup-python@0b6a380b5a7827e48e69b2e0e596c5c8c2b0e0b0 # v5.1.0 | ||
| with: | ||
| python-version: '3.x' | ||
| - name: Install Python dependencies | ||
| if: ${{ !cancelled() && (secrets.LOKI_URL != '' || secrets.PROMETHEUS_PUSHGATEWAY_URL != '') }} | ||
| run: | | ||
| pip install requests | ||
| - name: Send findings to Loki | ||
| if: ${{ !cancelled() && secrets.LOKI_URL != '' }} | ||
| continue-on-error: true | ||
| env: | ||
| LOKI_URL: ${{ secrets.LOKI_URL }} | ||
| LOKI_USERNAME: ${{ secrets.LOKI_USERNAME }} | ||
| LOKI_PASSWORD: ${{ secrets.LOKI_PASSWORD }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| COMMIT_SHA: ${{ github.sha }} | ||
| BRANCH: ${{ github.ref_name }} | ||
| TRUFFLEHOG_RESULTS_FILE: results.json | ||
| run: | | ||
| python trufflehog/send-to-loki.py | ||
| - name: Send metrics to Prometheus | ||
| if: ${{ !cancelled() && secrets.PROMETHEUS_PUSHGATEWAY_URL != '' }} | ||
| continue-on-error: true | ||
| env: | ||
| PROMETHEUS_PUSHGATEWAY_URL: ${{ secrets.PROMETHEUS_PUSHGATEWAY_URL }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| COMMIT_SHA: ${{ github.sha }} | ||
| BRANCH: ${{ github.ref_name }} | ||
| TRUFFLEHOG_RESULTS_FILE: results.json | ||
| run: | | ||
| python trufflehog/send-to-prometheus.py | ||
| - name: Check failure policy | ||
| env: | ||
| FAIL_ON_VERIFIED: ${{ inputs.fail-on-verified }} | ||
| FAIL_ON_UNVERIFIED: ${{ inputs.fail-on-unverified }} | ||
| VERIFIED_COUNT: ${{ steps.scan.outputs.verified }} | ||
| UNVERIFIED_COUNT: ${{ steps.scan.outputs.unverified }} | ||
| run: | | ||
| SHOULD_FAIL=false | ||
| if [[ "${FAIL_ON_VERIFIED}" == "true" && "${VERIFIED_COUNT}" != "0" ]]; then | ||
| SHOULD_FAIL=true | ||
| fi | ||
| if [[ "${FAIL_ON_UNVERIFIED}" == "true" && "${UNVERIFIED_COUNT}" != "0" ]]; then | ||
| SHOULD_FAIL=true | ||
| fi | ||
| if [[ "${SHOULD_FAIL}" == "true" ]]; then | ||
| echo "Workflow failed due to secrets found (verified: ${VERIFIED_COUNT}, unverified: ${UNVERIFIED_COUNT})" | ||
| exit 1 | ||
| else | ||
| echo "No action needed - secrets within configured thresholds" | ||
| fi | ||