Security scan (CVE) #211
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: Security scan (CVE) | |
| on: | |
| # Re-scan on every push so a fresh CVE in the dep graph surfaces on | |
| # the next merge, not at the next release. | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| # Re-scan daily — the CVE database advances even when the image | |
| # doesn't, so a bug disclosed today can affect a tag we shipped weeks | |
| # ago. Re-running picks that up. | |
| schedule: | |
| - cron: '17 6 * * *' | |
| # Manual trigger for ad-hoc audits. | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| security-events: write | |
| env: | |
| IMAGE_REF: ghcr.io/cairn-geocoder/cairn:latest | |
| jobs: | |
| trivy-image: | |
| name: Trivy — container image | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Build image (local tag for scan) | |
| run: | | |
| docker build -t cairn:scan . | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@v0.36.0 | |
| with: | |
| image-ref: cairn:scan | |
| format: sarif | |
| output: trivy-image.sarif | |
| severity: CRITICAL,HIGH,MEDIUM | |
| ignore-unfixed: false | |
| exit-code: 0 | |
| - name: Upload SARIF to GitHub Security | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() | |
| with: | |
| sarif_file: trivy-image.sarif | |
| category: trivy-image | |
| - name: Generate human-readable summary | |
| if: always() | |
| uses: aquasecurity/trivy-action@v0.36.0 | |
| with: | |
| image-ref: cairn:scan | |
| format: table | |
| output: trivy-summary.txt | |
| severity: CRITICAL,HIGH,MEDIUM | |
| - name: Generate compact JSON for the homepage | |
| if: always() | |
| uses: aquasecurity/trivy-action@v0.36.0 | |
| with: | |
| image-ref: cairn:scan | |
| format: json | |
| output: trivy-image.json | |
| severity: CRITICAL,HIGH,MEDIUM | |
| - name: Slim Trivy JSON into security/cves.json | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| mkdir -p security | |
| # Reduce Trivy's verbose output to a compact, page-friendly shape. | |
| # Keep id / severity / package / installed / fixed_in / title. | |
| jq -n \ | |
| --arg image "${IMAGE_REF}" \ | |
| --arg scanned "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ | |
| --slurpfile raw trivy-image.json \ | |
| '{ | |
| scanned_at: $scanned, | |
| image: $image, | |
| summary: ([ | |
| $raw[0].Results[]?.Vulnerabilities[]? | |
| ] | reduce .[] as $v ( | |
| {critical:0, high:0, medium:0, low:0, unknown:0}; | |
| if $v.Severity == "CRITICAL" then .critical += 1 | |
| elif $v.Severity == "HIGH" then .high += 1 | |
| elif $v.Severity == "MEDIUM" then .medium += 1 | |
| elif $v.Severity == "LOW" then .low += 1 | |
| else .unknown += 1 end | |
| )), | |
| vulnerabilities: [ | |
| $raw[0].Results[]?.Vulnerabilities[]? | | |
| { | |
| id: .VulnerabilityID, | |
| severity: .Severity, | |
| package: .PkgName, | |
| installed_version: .InstalledVersion, | |
| fixed_version: (.FixedVersion // ""), | |
| title: (.Title // (.Description // "" | .[0:120])), | |
| url: (.PrimaryURL // "") | |
| } | |
| ] | unique_by([.id, .package]) | |
| | sort_by( | |
| if .severity == "CRITICAL" then 0 | |
| elif .severity == "HIGH" then 1 | |
| elif .severity == "MEDIUM" then 2 | |
| elif .severity == "LOW" then 3 | |
| else 4 end | |
| ) | |
| }' > security/cves.json | |
| echo "security/cves.json size:" | |
| wc -c security/cves.json | |
| - name: Commit security/cves.json if changed | |
| if: always() && github.event_name != 'pull_request' | |
| run: | | |
| set -euo pipefail | |
| if git diff --quiet security/cves.json 2>/dev/null; then | |
| echo "no change" | |
| exit 0 | |
| fi | |
| git config user.name "cairn-security-bot" | |
| git config user.email "cairn-security-bot@users.noreply.github.com" | |
| # Race-safe push. The scan output lives in a tmp file so we | |
| # can rebase on top of fresh main without a merge conflict | |
| # against a concurrent run that touched the same file. | |
| # Each iteration re-applies our scan on top of whatever | |
| # main is right now. | |
| NEW=$(mktemp) | |
| cp security/cves.json "$NEW" | |
| for try in 1 2 3 4 5; do | |
| git fetch --quiet origin main | |
| git reset --hard origin/main | |
| cp "$NEW" security/cves.json | |
| if git diff --quiet security/cves.json; then | |
| echo "main already has identical content" | |
| exit 0 | |
| fi | |
| git add security/cves.json | |
| git commit -m "chore(security): refresh cves.json [skip ci]" | |
| if git push origin HEAD:main; then | |
| echo "pushed on attempt $try" | |
| exit 0 | |
| fi | |
| echo "push rejected on attempt $try; retrying" | |
| sleep 2 | |
| done | |
| echo "could not push after 5 attempts" >&2 | |
| exit 1 | |
| - name: Attach summary to job log | |
| if: always() | |
| run: | | |
| { | |
| echo '### Trivy image scan (`'"${IMAGE_REF}"'`)' | |
| echo | |
| echo '```' | |
| cat trivy-summary.txt | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| trivy-fs: | |
| name: Trivy — workspace filesystem | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run Trivy on the source tree (Cargo.lock + Dockerfile) | |
| uses: aquasecurity/trivy-action@v0.36.0 | |
| with: | |
| scan-type: fs | |
| format: sarif | |
| output: trivy-fs.sarif | |
| severity: CRITICAL,HIGH,MEDIUM | |
| ignore-unfixed: false | |
| exit-code: 0 | |
| - name: Upload SARIF to GitHub Security | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() | |
| with: | |
| sarif_file: trivy-fs.sarif | |
| category: trivy-fs |