|
| 1 | +name: Gitleaks Scan |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + pull_request: |
| 6 | + workflow_dispatch: |
| 7 | + |
| 8 | +permissions: |
| 9 | + actions: write |
| 10 | + contents: read |
| 11 | + pull-requests: write |
| 12 | + |
| 13 | +jobs: |
| 14 | + scan: |
| 15 | + name: Scan for Leaked Secrets |
| 16 | + runs-on: ubuntu-latest |
| 17 | + env: |
| 18 | + RESPONSE_GUIDE_URL: https://github.com/CivicTechWR/.github/blob/main/docs/gitleaks-response.md |
| 19 | + steps: |
| 20 | + - name: Check out code |
| 21 | + uses: actions/checkout@v4 |
| 22 | + with: |
| 23 | + fetch-depth: 0 |
| 24 | + |
| 25 | + - name: Run Gitleaks (redacted) |
| 26 | + id: gitleaks |
| 27 | + continue-on-error: true |
| 28 | + uses: gitleaks/gitleaks-action@v2 |
| 29 | + with: |
| 30 | + args: >- |
| 31 | + detect --source . --no-banner --redact --report-format json --report-path gitleaks-report.json |
| 32 | + env: |
| 33 | + GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} |
| 34 | + |
| 35 | + - name: Summarize findings |
| 36 | + id: summarize |
| 37 | + run: | |
| 38 | + if [ ! -f gitleaks-report.json ]; then |
| 39 | + echo "[]" > gitleaks-report.json |
| 40 | + fi |
| 41 | +
|
| 42 | + count=$(jq 'length' gitleaks-report.json 2>/dev/null || echo 0) |
| 43 | +
|
| 44 | + if [ "$count" -gt 0 ]; then |
| 45 | + echo "found=true" >> "$GITHUB_OUTPUT" |
| 46 | + echo "count=$count" >> "$GITHUB_OUTPUT" |
| 47 | + echo "::warning::Gitleaks detected ${count} potential secret(s)." |
| 48 | + else |
| 49 | + echo "found=false" >> "$GITHUB_OUTPUT" |
| 50 | + echo "count=0" >> "$GITHUB_OUTPUT" |
| 51 | + echo "::notice::Gitleaks found no potential secrets." |
| 52 | + fi |
| 53 | +
|
| 54 | + - name: Note scan issues |
| 55 | + if: ${{ steps.gitleaks.outcome == 'failure' && steps.summarize.outputs.found == 'false' }} |
| 56 | + run: | |
| 57 | + echo "::warning::Gitleaks scan ended with an error before producing findings. Review the action logs." |
| 58 | +
|
| 59 | + - name: Add pull request comment |
| 60 | + if: ${{ steps.summarize.outputs.found == 'true' && github.event_name == 'pull_request' }} |
| 61 | + uses: actions/github-script@v7 |
| 62 | + env: |
| 63 | + LEAK_COUNT: ${{ steps.summarize.outputs.count }} |
| 64 | + with: |
| 65 | + script: | |
| 66 | + const fs = require('fs'); |
| 67 | + const path = 'gitleaks-report.json'; |
| 68 | + const responseGuide = process.env.RESPONSE_GUIDE_URL; |
| 69 | + const findings = JSON.parse(fs.readFileSync(path, 'utf8')); |
| 70 | +
|
| 71 | + if (!Array.isArray(findings) || findings.length === 0) { |
| 72 | + return; |
| 73 | + } |
| 74 | +
|
| 75 | + const lines = findings.slice(0, 10).map((finding) => { |
| 76 | + const file = finding.file ? `\`${finding.file}\`` : 'unknown file'; |
| 77 | + const line = finding.startLine ? ` (line ${finding.startLine})` : ''; |
| 78 | + const rule = finding.ruleId || finding.rule || 'potential secret'; |
| 79 | + return `- ${file}${line} — ${rule}`; |
| 80 | + }).join('\n'); |
| 81 | +
|
| 82 | + const remainder = findings.length > 10 |
| 83 | + ? `\n\n…and ${findings.length - 10} more finding${findings.length - 10 === 1 ? '' : 's'} in the attached report.` |
| 84 | + : ''; |
| 85 | +
|
| 86 | + const body = [ |
| 87 | + `@CivicTechWR/organizers Gitleaks flagged **${process.env.LEAK_COUNT}** potential secret${findings.length === 1 ? '' : 's'} in this pull request.`, |
| 88 | + '', |
| 89 | + `Please review the [Gitleaks response guide](${responseGuide}) for next steps.`, |
| 90 | + '', |
| 91 | + lines, |
| 92 | + remainder, |
| 93 | + '', |
| 94 | + '_Secret values are redacted by the scanner. Follow the guide before merging._' |
| 95 | + ].filter(Boolean).join('\n'); |
| 96 | +
|
| 97 | + await github.rest.issues.createComment({ |
| 98 | + owner: context.repo.owner, |
| 99 | + repo: context.repo.repo, |
| 100 | + issue_number: context.issue.number, |
| 101 | + body, |
| 102 | + }); |
| 103 | +
|
| 104 | + - name: Upload redacted report |
| 105 | + if: ${{ steps.summarize.outputs.found == 'true' }} |
| 106 | + uses: actions/upload-artifact@v4 |
| 107 | + with: |
| 108 | + name: gitleaks-report |
| 109 | + path: gitleaks-report.json |
| 110 | + retention-days: 7 |
| 111 | + |
| 112 | + - name: Record scan summary |
| 113 | + if: ${{ always() }} |
| 114 | + env: |
| 115 | + LEAK_COUNT: ${{ steps.summarize.outputs.count }} |
| 116 | + HAS_FINDINGS: ${{ steps.summarize.outputs.found }} |
| 117 | + run: | |
| 118 | + if [ "$HAS_FINDINGS" = "true" ]; then |
| 119 | + { |
| 120 | + echo "## Gitleaks Findings" |
| 121 | + echo "" |
| 122 | + echo "Potential secrets detected: $LEAK_COUNT" |
| 123 | + echo "Report: gitleaks-report.json (uploaded as artifact when available)" |
| 124 | + echo "Guide: $RESPONSE_GUIDE_URL" |
| 125 | + } >> "$GITHUB_STEP_SUMMARY" |
| 126 | + else |
| 127 | + { |
| 128 | + echo "## Gitleaks Findings" |
| 129 | + echo "" |
| 130 | + echo "No potential secrets detected." |
| 131 | + } >> "$GITHUB_STEP_SUMMARY" |
| 132 | + fi |
0 commit comments