Implement Basic Trivy Scanning Workflow #13
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: Docker Security Scan | |
| on: | |
| push: | |
| branches: [main, develop] | |
| paths: | |
| - "docker/**" | |
| - "templates/docker-compose/**" | |
| - ".github/workflows/docker-security-scan.yml" | |
| pull_request: | |
| paths: | |
| - "docker/**" | |
| - "templates/docker-compose/**" | |
| - ".github/workflows/docker-security-scan.yml" | |
| # Scheduled scans are important because new CVEs appear | |
| # even if the code or images didn’t change | |
| schedule: | |
| - cron: "0 6 * * *" # Daily at 6 AM UTC | |
| workflow_dispatch: | |
| jobs: | |
| scan-project-images: | |
| name: Scan Project-Built Docker Images | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| image: | |
| - dockerfile: docker/provisioned-instance/Dockerfile | |
| context: docker/provisioned-instance | |
| name: provisioned-instance | |
| - dockerfile: docker/ssh-server/Dockerfile | |
| context: docker/ssh-server | |
| name: ssh-server | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| # Build images locally so Trivy scans exactly | |
| # what this repository produces | |
| - name: Build Docker image | |
| run: | | |
| docker build \ | |
| -t torrust-tracker-deployer/${{ matrix.image.name }}:latest \ | |
| -f ${{ matrix.image.dockerfile }} \ | |
| . | |
| # Human-readable output in logs | |
| # This NEVER fails the job; it’s only for visibility | |
| - name: Display vulnerabilities (table format) | |
| uses: aquasecurity/[email protected] | |
| with: | |
| image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest | |
| format: "table" | |
| severity: "HIGH,CRITICAL" | |
| exit-code: "0" | |
| # SARIF generation for GitHub Code Scanning | |
| # | |
| # IMPORTANT: | |
| # - exit-code MUST be 0 | |
| # - Trivy sometimes exits with 1 even when no vulns exist | |
| # - GitHub Security UI is responsible for enforcement | |
| - name: Generate SARIF (Code Scanning) | |
| uses: aquasecurity/[email protected] | |
| with: | |
| image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest | |
| format: "sarif" | |
| output: "trivy-${{ matrix.image.name }}.sarif" | |
| severity: "HIGH,CRITICAL" | |
| exit-code: "0" | |
| scanners: "vuln" | |
| - name: Upload SARIF artifact | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: sarif-project-${{ matrix.image.name }}-${{ github.run_id }} | |
| path: trivy-${{ matrix.image.name }}.sarif | |
| retention-days: 30 | |
| scan-third-party-images: | |
| name: Scan Third-Party Docker Images | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # These must match docker-compose templates | |
| # in templates/docker-compose/docker-compose.yml.tera | |
| image: | |
| - torrust/tracker:develop | |
| - mysql:8.0 | |
| - grafana/grafana:11.4.0 | |
| - prom/prometheus:v3.0.1 | |
| steps: | |
| - name: Display vulnerabilities (table format) | |
| uses: aquasecurity/[email protected] | |
| with: | |
| image-ref: ${{ matrix.image }} | |
| format: "table" | |
| severity: "HIGH,CRITICAL" | |
| exit-code: "0" | |
| # Third-party images should NEVER block CI. | |
| # We only report findings to GitHub Security. | |
| - name: Generate SARIF (Code Scanning) | |
| uses: aquasecurity/[email protected] | |
| with: | |
| image-ref: ${{ matrix.image }} | |
| format: "sarif" | |
| output: "trivy.sarif" | |
| severity: "HIGH,CRITICAL" | |
| exit-code: "0" | |
| scanners: "vuln" | |
| # Needed to produce stable artifact names | |
| - name: Sanitize image name | |
| id: sanitize | |
| run: | | |
| echo "name=$(echo '${{ matrix.image }}' | tr '/:' '-')" >> "$GITHUB_OUTPUT" | |
| - name: Upload SARIF artifact | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: sarif-third-party-${{ steps.sanitize.outputs.name }}-${{ github.run_id }} | |
| path: trivy.sarif | |
| retention-days: 30 | |
| upload-sarif-results: | |
| name: Upload SARIF Results to GitHub Security | |
| runs-on: ubuntu-latest | |
| needs: | |
| - scan-project-images | |
| - scan-third-party-images | |
| # Always run so we don’t lose security visibility | |
| if: always() | |
| permissions: | |
| security-events: write | |
| steps: | |
| - name: Download all SARIF artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: sarif-*-${{ github.run_id }} | |
| # We use gh CLI because it’s easier to loop | |
| # and assign stable categories per image | |
| - name: Upload SARIF files | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| find . -name "*.sarif" -type f | while read -r sarif; do | |
| category=$(basename "$(dirname "$sarif")" | sed 's/^sarif-//' | sed 's/-[0-9]*$//') | |
| echo "Uploading $sarif as docker-$category" | |
| gh api \ | |
| --method POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| /repos/${{ github.repository }}/code-scanning/sarifs \ | |
| -f sarif=@-"$sarif" \ | |
| -f ref="${{ github.ref }}" \ | |
| -f commit_sha="${{ github.sha }}" \ | |
| -f category="docker-$category" || echo "Upload failed for $sarif" | |
| done |