Repository Security Scan #35
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: Repository Security Scan | |
| on: | |
| push: | |
| branches: [main, develop] | |
| paths-ignore: ['**.md', 'docs/**', '.gitignore'] | |
| pull_request: | |
| branches: [main, develop] | |
| paths-ignore: ['**.md', 'docs/**', '.gitignore'] | |
| schedule: | |
| - cron: '0 2 * * 1' # Weekly security scan | |
| workflow_dispatch: | |
| inputs: | |
| fail_on_critical: | |
| description: 'Fail on critical findings' | |
| required: false | |
| default: true | |
| type: boolean | |
| permissions: | |
| contents: read | |
| security-events: write | |
| pull-requests: write | |
| packages: read | |
| actions: read | |
| jobs: | |
| security-scan: | |
| name: Security Scan | |
| runs-on: ubuntu-latest | |
| outputs: | |
| scan-status: ${{ steps.scan.outputs.status }} | |
| risk-level: ${{ steps.scan.outputs.risk-level }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ github.token }} | |
| - name: Determine scanner image to use | |
| id: scanner-image | |
| run: | | |
| # Determine the appropriate Docker tag based on the event and branch | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| TAG="pr-${{ github.event.number }}" | |
| elif [[ "${{ github.ref_name }}" == "main" ]]; then | |
| TAG="latest" | |
| else | |
| # Sanitize branch name for Docker tag (replace / with -) | |
| TAG=$(echo "${{ github.ref_name }}" | sed 's/\//-/g') | |
| fi | |
| PRIMARY_IMAGE="ghcr.io/${{ github.repository_owner }}/git-security-scanner:${TAG}" | |
| FALLBACK_IMAGE="ghcr.io/${{ github.repository_owner }}/git-security-scanner:latest" | |
| echo "Checking if primary scanner image is available: ${PRIMARY_IMAGE}" | |
| if docker manifest inspect "${PRIMARY_IMAGE}" > /dev/null 2>&1; then | |
| echo "✅ Primary scanner image is available!" | |
| echo "image=${PRIMARY_IMAGE}" >> $GITHUB_OUTPUT | |
| else | |
| echo "⚠️ Primary image not found, checking fallback: ${FALLBACK_IMAGE}" | |
| if docker manifest inspect "${FALLBACK_IMAGE}" > /dev/null 2>&1; then | |
| echo "✅ Fallback scanner image is available!" | |
| echo "image=${FALLBACK_IMAGE}" >> $GITHUB_OUTPUT | |
| else | |
| echo "❌ Neither primary nor fallback image available" | |
| exit 1 | |
| fi | |
| fi | |
| - name: Prepare scan results directory | |
| run: | | |
| # Create directory with proper permissions for container user | |
| mkdir -p /tmp/scan-results | |
| chmod 777 /tmp/scan-results | |
| - name: Run security scan | |
| id: scan | |
| run: | | |
| # Scanner handles all configuration internally | |
| docker run --rm \ | |
| -v ${{ github.workspace }}:/scan:ro \ | |
| -v /tmp/scan-results:/reports \ | |
| -e GITHUB_ACTIONS=true \ | |
| -e FAIL_ON_CRITICAL=${{ github.event.inputs.fail_on_critical || 'true' }} \ | |
| -e GITHUB_REPOSITORY="${{ github.repository }}" \ | |
| -e GITHUB_REF="${{ github.ref }}" \ | |
| -e GITHUB_SHA="${{ github.sha }}" \ | |
| ${{ steps.scanner-image.outputs.image }} | |
| # Extract results for GitHub Actions | |
| if [[ -f "/tmp/scan-results/json/final-security-report.json" ]]; then | |
| RISK_LEVEL=$(jq -r '.metadata.risk_level // "UNKNOWN"' /tmp/scan-results/json/final-security-report.json 2>/dev/null || echo "UNKNOWN") | |
| echo "status=completed" >> $GITHUB_OUTPUT | |
| echo "risk-level=${RISK_LEVEL}" >> $GITHUB_OUTPUT | |
| cp /tmp/scan-results/json/final-security-report.json /tmp/scan-results/summary.json | |
| else | |
| echo "status=failed" >> $GITHUB_OUTPUT | |
| echo "risk-level=UNKNOWN" >> $GITHUB_OUTPUT | |
| fi | |
| # Stricter check for main branch | |
| if [[ "${{ github.ref }}" == "refs/heads/main" && "$HIGH_COUNT" -gt "5" ]]; then | |
| echo "❌ Quality gate failed: Too many high-severity issues (${HIGH_COUNT}) for main branch" | |
| exit 1 | |
| fi | |
| echo "✅ Quality gates passed" | |
| - name: Upload SARIF results to GitHub (optional) | |
| if: always() && vars.ENABLE_CODEQL_UPLOAD == 'true' | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: /tmp/scan-results/*.sarif | |
| category: 'git-security-scanner' | |
| continue-on-error: true | |
| - name: Upload scan artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: security-scan-results | |
| path: /tmp/scan-results/ | |
| retention-days: 30 | |
| - name: Comment on PR | |
| if: github.event_name == 'pull_request' && always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let summary = {}; | |
| try { | |
| if (fs.existsSync('/tmp/scan-results/summary.json')) { | |
| summary = JSON.parse(fs.readFileSync('/tmp/scan-results/summary.json', 'utf8')); | |
| } | |
| } catch (e) { | |
| console.log('Could not read summary:', e); | |
| } | |
| const riskLevel = summary.metadata?.risk_level || 'UNKNOWN'; | |
| const critical = summary.metadata?.critical_issues || 0; | |
| const high = summary.metadata?.high_issues || 0; | |
| const medium = summary.metadata?.medium_issues || 0; | |
| const low = summary.metadata?.low_issues || 0; | |
| const statusEmoji = {'CRITICAL': '🚨', 'HIGH': '⚠️', 'MEDIUM': '🟡', 'LOW': '🟢', 'UNKNOWN': '❓'}[riskLevel] || '❓'; | |
| const comment = `## ${statusEmoji} Security Scan Results | |
| **Risk Level**: ${riskLevel} | |
| | Severity | Count | Status | | |
| |----------|-------|--------| | |
| | 🚨 Critical | ${critical} | ${critical === 0 ? '✅' : '❌'} | | |
| | ⚠️ High | ${high} | ${high === 0 ? '✅' : '⚠️'} | | |
| | 🟡 Medium | ${medium} | ${medium <= 5 ? '✅' : 'ℹ️'} | | |
| | 🔵 Low | ${low} | ℹ️ | | |
| ${critical > 0 ? '### ⚠️ Action Required\nCritical vulnerabilities must be fixed before merge.\n' : ''} | |
| [View Full Report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | |
| --- | |
| *Powered by Security Scanner | Run #${{ github.run_number }}*`; | |
| // Update or create comment | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const botComment = comments.find(c => c.body.includes('Security Scan Results')); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: comment | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: comment | |
| }); | |
| } |