From c8ddf457f7944b9634ce8247da57b40fb612284e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 8 Jul 2026 16:34:22 +0200 Subject: [PATCH 1/4] ci: add Trivy vulnerability scanning workflow Blocking PR/main gate on fixable CRITICAL/HIGH Go dependency vulns, plus a weekly full scan that reports to Slack on findings. Both upload SARIF to code scanning (skipped on fork PRs). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/trivy.yml | 114 ++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 .github/workflows/trivy.yml diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 00000000..4467848a --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,114 @@ +name: Trivy Security Scan + +on: + pull_request: + push: + branches: + - main + schedule: + # Weekly full scan, Mondays 06:00 UTC. + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +# Cancel superseded PR runs; non-PR runs get a unique group so they are never +# interrupted (mirrors ci.yml). +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + # Blocking gate on PRs and pushes to main: fail only on fixable CRITICAL/HIGH + # vulnerabilities in Go dependencies, so a red check always has an actionable + # fix (bump the dependency). Vulns with no upstream fix don't wedge every PR. + scan-pr: + name: Dependency Scan + if: github.event_name == 'pull_request' || github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + security-events: write # upload SARIF to code scanning + steps: + - name: Checkout code + uses: actions/checkout@v7 + + # Writes the SARIF report before honoring exit-code, so a red check (on + # findings) and the always-run upload below are not mutually exclusive. + - name: Run Trivy (fixable CRITICAL/HIGH) + uses: aquasecurity/trivy-action@0.36.0 + with: + scan-type: fs + scanners: vuln + severity: CRITICAL,HIGH + ignore-unfixed: true + exit-code: "1" + format: sarif + output: trivy-results.sarif + + # Fork-PR tokens have no security-events:write, so skip the upload there + # (the scan + gate still run). + - name: Upload SARIF to code scanning + if: always() && github.event.pull_request.head.repo.fork != true + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-results.sarif + + # Weekly full scan: every severity, including vulns with no fix yet. New CVEs + # land against unchanged code, so this catches what the PR gate never sees. + # Reports to Slack only when something is found (no all-clear noise). + scan-weekly: + name: Full Scan + Slack Report + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Run Trivy (all severities) + id: trivy + uses: aquasecurity/trivy-action@0.36.0 + # Don't abort the job on findings — the Slack + fail steps run after. + continue-on-error: true + with: + scan-type: fs + scanners: vuln + exit-code: "1" + format: json + output: trivy.json + + - name: Build Slack summary + id: summary + if: steps.trivy.outcome == 'failure' + run: | + TOTAL=$(jq '[.Results[]?.Vulnerabilities[]?] | length' trivy.json) + BREAKDOWN=$(jq -r '[.Results[]?.Vulnerabilities[]?.Severity] + | group_by(.) | map("\(.[0]): \(length)") | join(" ")' trivy.json) + LIST=$(jq -r '[.Results[]?.Vulnerabilities[]?] + | unique_by(.VulnerabilityID + .PkgName) | .[:15] | .[] + | "• `\(.PkgName)` \(.InstalledVersion) — \(.VulnerabilityID) (\(.Severity))"' trivy.json) + NOUN="vulnerabilities" + [ "$TOTAL" -eq 1 ] && NOUN="vulnerability" + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + MSG=":rotating_light: *lstk weekly Trivy scan* found ${TOTAL} ${NOUN} + *By severity:* ${BREAKDOWN} + + ${LIST} + + <${RUN_URL}|View full log>" + jq -n --arg t "$MSG" '{text: $t}' > slack-payload.json + + - name: Notify Slack + if: steps.trivy.outcome == 'failure' + uses: slackapi/slack-github-action@v3.0.3 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload-file-path: slack-payload.json + + - name: Mark run failed + if: steps.trivy.outcome == 'failure' + run: exit 1 From 2d77398c8f59c5e8877b77bb806f5451c4dbd8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 8 Jul 2026 16:36:07 +0200 Subject: [PATCH 2/4] ci: fix trivy-action tag reference (v-prefixed) Co-Authored-By: Claude Opus 4.8 --- .github/workflows/trivy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 4467848a..580e989d 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -38,7 +38,7 @@ jobs: # Writes the SARIF report before honoring exit-code, so a red check (on # findings) and the always-run upload below are not mutually exclusive. - name: Run Trivy (fixable CRITICAL/HIGH) - uses: aquasecurity/trivy-action@0.36.0 + uses: aquasecurity/trivy-action@v0.36.0 with: scan-type: fs scanners: vuln @@ -70,7 +70,7 @@ jobs: - name: Run Trivy (all severities) id: trivy - uses: aquasecurity/trivy-action@0.36.0 + uses: aquasecurity/trivy-action@v0.36.0 # Don't abort the job on findings — the Slack + fail steps run after. continue-on-error: true with: From ed37f9173b5d6aa01a6b05fde10d466348150d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 8 Jul 2026 16:38:30 +0200 Subject: [PATCH 3/4] ci: split Trivy SARIF report from the blocking gate trivy-action ignores severity/ignore-unfixed when format is sarif and reports all severities, so a single sarif step failed the gate on a low-severity test-only CVE. Use a dedicated table-format gate step for CRITICAL/HIGH. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/trivy.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 580e989d..80a6491c 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -35,16 +35,14 @@ jobs: - name: Checkout code uses: actions/checkout@v7 - # Writes the SARIF report before honoring exit-code, so a red check (on - # findings) and the always-run upload below are not mutually exclusive. - - name: Run Trivy (fixable CRITICAL/HIGH) + # SARIF report for the Security tab covers all severities and never fails + # the job (the gate below owns pass/fail). trivy-action ignores severity + # filters when format is sarif, so this cannot double as the gate. + - name: Run Trivy (SARIF report) uses: aquasecurity/trivy-action@v0.36.0 with: scan-type: fs scanners: vuln - severity: CRITICAL,HIGH - ignore-unfixed: true - exit-code: "1" format: sarif output: trivy-results.sarif @@ -56,6 +54,17 @@ jobs: with: sarif_file: trivy-results.sarif + # The blocking gate: fail only on fixable CRITICAL/HIGH vulnerabilities. + - name: Run Trivy (fixable CRITICAL/HIGH gate) + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: fs + scanners: vuln + severity: CRITICAL,HIGH + ignore-unfixed: true + exit-code: "1" + format: table + # Weekly full scan: every severity, including vulns with no fix yet. New CVEs # land against unchanged code, so this catches what the PR gate never sees. # Reports to Slack only when something is found (no all-clear noise). From 1ec546e03143f477e437b7d3072d5f1774019a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 8 Jul 2026 16:53:30 +0200 Subject: [PATCH 4/4] ci: report weekly Trivy scan via code scanning instead of Slack Upload SARIF to the Security tab so alerts flow through GitHub-native notifications (watchers subscribed to security alerts), dropping the Slack webhook, jq summary, and SLACK_WEBHOOK_URL secret. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/trivy.yml | 50 ++++++++----------------------------- 1 file changed, 11 insertions(+), 39 deletions(-) diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 80a6491c..06e58458 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -67,57 +67,29 @@ jobs: # Weekly full scan: every severity, including vulns with no fix yet. New CVEs # land against unchanged code, so this catches what the PR gate never sees. - # Reports to Slack only when something is found (no all-clear noise). + # Results go to the Security tab (code scanning) — watchers subscribed to + # security alerts get notified there, no Slack/webhook plumbing. scan-weekly: - name: Full Scan + Slack Report + name: Full Scan if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read + security-events: write # upload SARIF to code scanning steps: - name: Checkout code uses: actions/checkout@v7 - name: Run Trivy (all severities) - id: trivy uses: aquasecurity/trivy-action@v0.36.0 - # Don't abort the job on findings — the Slack + fail steps run after. - continue-on-error: true with: scan-type: fs scanners: vuln - exit-code: "1" - format: json - output: trivy.json - - - name: Build Slack summary - id: summary - if: steps.trivy.outcome == 'failure' - run: | - TOTAL=$(jq '[.Results[]?.Vulnerabilities[]?] | length' trivy.json) - BREAKDOWN=$(jq -r '[.Results[]?.Vulnerabilities[]?.Severity] - | group_by(.) | map("\(.[0]): \(length)") | join(" ")' trivy.json) - LIST=$(jq -r '[.Results[]?.Vulnerabilities[]?] - | unique_by(.VulnerabilityID + .PkgName) | .[:15] | .[] - | "• `\(.PkgName)` \(.InstalledVersion) — \(.VulnerabilityID) (\(.Severity))"' trivy.json) - NOUN="vulnerabilities" - [ "$TOTAL" -eq 1 ] && NOUN="vulnerability" - RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - MSG=":rotating_light: *lstk weekly Trivy scan* found ${TOTAL} ${NOUN} - *By severity:* ${BREAKDOWN} - - ${LIST} - - <${RUN_URL}|View full log>" - jq -n --arg t "$MSG" '{text: $t}' > slack-payload.json + format: sarif + output: trivy-results.sarif - - name: Notify Slack - if: steps.trivy.outcome == 'failure' - uses: slackapi/slack-github-action@v3.0.3 + - name: Upload SARIF to code scanning + uses: github/codeql-action/upload-sarif@v3 with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload-file-path: slack-payload.json - - - name: Mark run failed - if: steps.trivy.outcome == 'failure' - run: exit 1 + sarif_file: trivy-results.sarif