name: 'Container Security Scan with Trivy' description: 'Scans container images for vulnerabilities using Trivy and reports results' author: 'Prowler' inputs: image-name: description: 'Container image name to scan' required: true image-tag: description: 'Container image tag to scan' required: true default: ${{ github.sha }} severity: description: 'Severities to scan for (comma-separated)' required: false default: 'CRITICAL,HIGH,MEDIUM,LOW' fail-on-severity: description: 'Fail the build on findings at this severity or above: critical, high, or none' required: false default: 'high' upload-sarif: description: 'Upload results to GitHub Security tab' required: false default: 'true' create-pr-comment: description: 'Create a comment on the PR with scan results' required: false default: 'true' artifact-retention-days: description: 'Days to retain the Trivy report artifact' required: false default: '2' outputs: critical-count: description: 'Number of critical vulnerabilities found' value: ${{ steps.security-check.outputs.critical }} high-count: description: 'Number of high vulnerabilities found' value: ${{ steps.security-check.outputs.high }} total-count: description: 'Total number of vulnerabilities found' value: ${{ steps.security-check.outputs.total }} runs: using: 'composite' steps: - name: Cache Trivy vulnerability database uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.cache/trivy key: trivy-db-${{ runner.os }}-${{ github.run_id }} restore-keys: | trivy-db-${{ runner.os }}- - name: Run Trivy vulnerability scan (JSON) uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ inputs.image-name }}:${{ inputs.image-tag }} format: 'json' output: 'trivy-report.json' severity: ${{ inputs.severity }} exit-code: '0' scanners: 'vuln' ignore-unfixed: 'true' # A finding with no available fix is not actionable, so it must not gate timeout: '5m' version: 'v0.74.0' # Not trivyignores: that input drops the .yaml extension Trivy parses by. env: TRIVY_IGNOREFILE: '.trivyignore.yaml' - name: Run Trivy vulnerability scan (SARIF) if: inputs.upload-sarif == 'true' && github.event_name == 'push' uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ inputs.image-name }}:${{ inputs.image-tag }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' exit-code: '0' scanners: 'vuln' ignore-unfixed: 'true' # A finding with no available fix is not actionable, so it must not gate timeout: '5m' version: 'v0.74.0' # Not trivyignores: that input drops the .yaml extension Trivy parses by. env: TRIVY_IGNOREFILE: '.trivyignore.yaml' - name: Upload Trivy results to GitHub Security tab if: inputs.upload-sarif == 'true' && github.event_name == 'push' uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 with: sarif_file: 'trivy-results.sarif' category: 'trivy-container' - name: Upload Trivy report artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: trivy-scan-report-${{ inputs.image-name }}-${{ inputs.image-tag }} path: trivy-report.json retention-days: ${{ inputs.artifact-retention-days }} - name: Generate security summary id: security-check shell: bash run: | CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy-report.json) HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' trivy-report.json) TOTAL=$(jq '[.Results[]?.Vulnerabilities[]?] | length' trivy-report.json) echo "critical=$CRITICAL" >> $GITHUB_OUTPUT echo "high=$HIGH" >> $GITHUB_OUTPUT echo "total=$TOTAL" >> $GITHUB_OUTPUT echo "### 🔒 Container Security Scan" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Image:** \`${INPUTS_IMAGE_NAME}:${INPUTS_IMAGE_TAG}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- 🔴 Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY echo "- 🟠 High: $HIGH" >> $GITHUB_STEP_SUMMARY echo "- **Total**: $TOTAL" >> $GITHUB_STEP_SUMMARY env: INPUTS_IMAGE_NAME: ${{ inputs.image-name }} INPUTS_IMAGE_TAG: ${{ inputs.image-tag }} - name: Comment scan results on PR if: >- inputs.create-pr-comment == 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: IMAGE_NAME: ${{ inputs.image-name }} GITHUB_SHA: ${{ inputs.image-tag }} SEVERITY: ${{ inputs.severity }} with: script: | const comment = require('./.github/scripts/trivy-pr-comment.js'); // Unique identifier to find our comment const marker = ``; const body = marker + '\n' + comment; // Find existing comment const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const existingComment = comments.find(c => c.body?.includes(marker)); if (existingComment) { // Update existing comment await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existingComment.id, body: body }); console.log('✅ Updated existing Trivy scan comment'); } else { // Create new comment await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: body }); console.log('✅ Created new Trivy scan comment'); } - name: Check for blocking vulnerabilities if: inputs.fail-on-severity != 'none' shell: bash run: | if [ "$CUTOFF" = "critical" ]; then BLOCKING=$CRITICAL SEVERITIES='["CRITICAL"]' else BLOCKING=$((CRITICAL + HIGH)) SEVERITIES='["CRITICAL","HIGH"]' fi if [ "$BLOCKING" -gt 0 ]; then echo "::error::Found $BLOCKING vulnerabilities at severity ${CUTOFF} or above ($CRITICAL critical, $HIGH high)" echo "::warning::Update the package, or add it to .trivyignore.yaml with a reason if nothing can be done" jq -r --argjson severities "$SEVERITIES" \ '.Results[]?.Vulnerabilities[]? | select(.Severity | IN($severities[])) | " \(.Severity)\t\(.VulnerabilityID)\t\(.PkgName) \(.InstalledVersion)"' \ trivy-report.json | sort -u exit 1 fi env: CUTOFF: ${{ inputs.fail-on-severity }} CRITICAL: ${{ steps.security-check.outputs.critical }} HIGH: ${{ steps.security-check.outputs.high }}