Periodic Container Security Scan #33
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: Periodic Container Security Scan | |
| on: | |
| schedule: | |
| - cron: '0 2 * * 1' # Weekly on Monday at 2am UTC | |
| workflow_dispatch: # Allow manual trigger | |
| permissions: {} | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| discover-published-images: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| configs: ${{ steps.find-configs.outputs.configs }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| - name: Find all configuration files | |
| id: find-configs | |
| run: | | |
| # Find all spec.yaml files - scan all published images | |
| all_configs=$(find npx uvx go -name "spec.yaml" -type f 2>/dev/null | sort) | |
| configs_json=$(echo "$all_configs" | jq -R -s -c 'split("\n")[:-1]') | |
| echo "configs=$configs_json" >> $GITHUB_OUTPUT | |
| echo "Found $(echo "$all_configs" | wc -l) configurations to scan" | |
| scan-images: | |
| needs: discover-published-images | |
| runs-on: ubuntu-latest | |
| if: ${{ needs.discover-published-images.outputs.configs != '[]' }} | |
| strategy: | |
| matrix: | |
| config: ${{ fromJson(needs.discover-published-images.outputs.configs) }} | |
| fail-fast: false | |
| permissions: | |
| contents: read | |
| packages: read | |
| security-events: write | |
| issues: write # To create issues for critical findings | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| - name: Install yq | |
| uses: mikefarah/yq@1b9b4ac5187171d2e5e3129be0cfa827c7f9d53d # v4.53.3 | |
| - name: Extract metadata from config | |
| id: meta | |
| env: | |
| CONFIG_FILE: ${{ matrix.config }} | |
| run: | | |
| config_file="$CONFIG_FILE" | |
| protocol=$(echo "$config_file" | cut -d'/' -f1) | |
| server_name=$(echo "$config_file" | cut -d'/' -f2) | |
| echo "protocol=$protocol" >> $GITHUB_OUTPUT | |
| echo "server_name=$server_name" >> $GITHUB_OUTPUT | |
| # Extract version | |
| spec_version=$(yq '.spec.version' "$config_file" 2>/dev/null || echo "") | |
| if [ -n "$spec_version" ]; then | |
| version="$spec_version" | |
| else | |
| version="latest" | |
| fi | |
| echo "version=$version" >> $GITHUB_OUTPUT | |
| # Generate image name | |
| image_name="${REGISTRY}/${IMAGE_NAME}/${protocol}/${server_name}" | |
| echo "image_name=$image_name" >> $GITHUB_OUTPUT | |
| echo "image_ref=${image_name}:${version}" >> $GITHUB_OUTPUT | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Run Grype vulnerability scan (SARIF) | |
| id: grype-scan | |
| uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 | |
| with: | |
| image: "${{ steps.meta.outputs.image_ref }}" | |
| severity-cutoff: "low" | |
| output-format: "sarif" | |
| - name: Upload SARIF to GitHub Security | |
| uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 | |
| if: always() | |
| with: | |
| sarif_file: ${{ steps.grype-scan.outputs.sarif }} | |
| category: 'periodic-grype-${{ steps.meta.outputs.server_name }}' | |
| - name: Run Grype vulnerability scan (JSON) | |
| id: grype-scan-json | |
| uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 | |
| with: | |
| image: "${{ steps.meta.outputs.image_ref }}" | |
| severity-cutoff: "low" | |
| output-format: "json" | |
| - name: Check for critical issues | |
| id: check-critical | |
| run: | | |
| critical=$(jq '[.matches[]? | select(.vulnerability.severity == "Critical")] | length' ${{ steps.grype-scan-json.outputs.json }}) | |
| high=$(jq '[.matches[]? | select(.vulnerability.severity == "High")] | length' ${{ steps.grype-scan-json.outputs.json }}) | |
| echo "critical=$critical" >> $GITHUB_OUTPUT | |
| echo "high=$high" >> $GITHUB_OUTPUT | |
| if [ "$critical" -gt 0 ]; then | |
| echo "should_create_issue=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "should_create_issue=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create issue for critical findings | |
| if: steps.check-critical.outputs.should_create_issue == 'true' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| env: | |
| GRYPE_JSON_PATH: ${{ steps.grype-scan-json.outputs.json }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const results = JSON.parse(fs.readFileSync(process.env.GRYPE_JSON_PATH, 'utf8')); | |
| const critical = ${{ steps.check-critical.outputs.critical }}; | |
| const high = ${{ steps.check-critical.outputs.high }}; | |
| let body = `## 🚨 Security Scan Alert\n\n`; | |
| body += `A periodic security scan found critical issues in the container image:\n\n`; | |
| body += `- **Image**: \`${{ steps.meta.outputs.image_ref }}\`\n`; | |
| body += `- **Critical vulnerabilities**: ${critical}\n`; | |
| body += `- **High vulnerabilities**: ${high}\n\n`; | |
| body += `### Details\n\n`; | |
| body += `See the [Security tab](../../security/code-scanning) for full details.\n\n`; | |
| if (critical > 0) { | |
| body += `#### Critical Vulnerabilities\n\n`; | |
| const criticalVulns = (results.matches || []) | |
| .filter(m => m.vulnerability.severity === 'Critical') | |
| .slice(0, 5); | |
| for (const match of criticalVulns) { | |
| body += `- **${match.vulnerability.id}** in \`${match.artifact.name}@${match.artifact.version}\`: ${match.vulnerability.description || 'No description'}\n`; | |
| } | |
| if (critical > 5) { | |
| body += `\n_... and ${critical - 5} more. See Security tab for complete list._\n`; | |
| } | |
| } | |
| body += `\n---\n`; | |
| body += `_Automated security scan from [periodic-security-scan workflow](../actions/workflows/periodic-security-scan.yml)_`; | |
| // Check if an issue already exists for this image | |
| const { data: issues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'security,grype', | |
| }); | |
| const existingIssue = issues.find(issue => | |
| issue.title.includes('${{ steps.meta.outputs.server_name }}') | |
| ); | |
| if (existingIssue) { | |
| // Update existing issue | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: existingIssue.number, | |
| body: `## Updated Scan Results\n\n${body}` | |
| }); | |
| console.log(`Updated existing issue #${existingIssue.number}`); | |
| } else { | |
| // Create new issue | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `🚨 Security: Critical issues in ${{ steps.meta.outputs.server_name }} container`, | |
| body: body, | |
| labels: ['security', 'grype', 'critical'] | |
| }); | |
| console.log('Created new security issue'); | |
| } | |
| - name: Upload scan results | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: periodic-scan-${{ steps.meta.outputs.server_name }} | |
| path: | | |
| ${{ steps.grype-scan-json.outputs.json }} | |
| ${{ steps.grype-scan.outputs.sarif }} | |
| retention-days: 90 | |
| summary: | |
| needs: scan-images | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| if: always() | |
| steps: | |
| - name: Generate summary | |
| run: | | |
| echo "## Periodic Security Scan Complete" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Scan Type**: Vulnerability scan (Grype)" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Severity Levels**: CRITICAL, HIGH, MEDIUM, LOW" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Status**: ${{ needs.scan-images.result }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "View detailed results in the [Security tab](../../security/code-scanning)." >> $GITHUB_STEP_SUMMARY |