From 1322d26fac427a512b155f551469cdeb6057419e Mon Sep 17 00:00:00 2001 From: Simon Axelsson Date: Tue, 8 Sep 2026 01:16:08 +0200 Subject: [PATCH] fix(ci): make the required security gate capable of failing Branch protection on `main` requires exactly two status contexts: CI (SIAX Cloud) / security (push) CI (SIAX Cloud) / quality (push) The `security` half could not fail. All three Trivy invocations in this repo ran with `--exit-code 0`, so the scanner reported findings to stdout and then exited 0 regardless. Measured on main @4514d97, 2026-09-08, same scanner (Trivy 0.73.0), same target, only the flag differing: A --exit-code 0, clean tree -> exit 0 B --exit-code 0, planted AWS secret access key -> exit 0 (DETECTED as CRITICAL in the report, gate still green) C --exit-code 1, same planted secret -> exit 1 D --exit-code 1, probe removed -> exit 0 B is the defect: the required context reported success while the scanner had a CRITICAL secret in hand. D is why this is safe to land now -- the tree is clean today, so flipping the flag does not open the gate red. (PR #19 measured 19 HIGH findings on 2026-08-11; those dependencies have since been updated on main, 142 commits later.) The other required context, `quality`, was verified real and left alone: a planted type error fails `pnpm run typecheck` (exit 2) and a planted failing assertion fails `pnpm run test` (exit 1, 945 passed baseline). nightly.yml carried the same flag. A nightly scan exists to catch CVEs published against code that did not change; with `--exit-code 0` it could never raise that alarm. Not fixed here, needs an owner decision: services/scrapers/requirements.txt uses `>=` ranges only. Trivy's pip scanner cannot resolve unpinned specifiers, so it omits the file from the scan entirely -- it never appears as a target. Pinning those same floors with `==` surfaces 3 HIGH findings (aiohttp CVE-2025-69223, aiohttp CVE-2026-69244, lxml CVE-2026-41066). Deliberately NOT suppressed with `--skip-dirs`, which would turn a measurable blind spot into declared policy. Job names are unchanged, so both required context strings are preserved. Co-Authored-By: Claude Opus 5 --- .gitea/workflows/ci.yml | 37 +++++++++++++++++++++++++++-------- .gitea/workflows/nightly.yml | 8 ++++++-- .gitea/workflows/security.yml | 18 ++++++++++++----- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 86fd116..f659e24 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -9,8 +9,12 @@ name: CI (SIAX Cloud) # is inherited debt; continue-on-error so it never blocks). # * build (BLOCKING) -> `next build`. # -# Nightly re-runs + Trivy security scanning live in nightly.yml and -# security.yml (this file is push/PR-only). +# * security (BLOCKING) -> `trivy fs` over the repo; fails the build on +# HIGH/CRITICAL vulns or detected secrets. This is a REQUIRED +# protected-branch context -- see the job's own comment. +# +# Nightly re-runs live in nightly.yml; security.yml is the dedicated +# push/PR/weekly scanner (this file is push/PR-only). on: push: branches: ["**"] @@ -149,10 +153,27 @@ jobs: echo "FATAL: integration suites skipped instead of running"; exit 1 fi - # Kept in ci.yml (advisory) so the protected-branch status check contexts - # "CI (SIAX Cloud) / quality (push)" + "CI (SIAX Cloud) / security (push)" - # are always reported on push. security.yml is the dedicated scanner on - # push/PR/weekly; this job guarantees the required check exists. + # BLOCKING. This job publishes the protected-branch status check context + # "CI (SIAX Cloud) / security (push)", which branch protection on `main` + # lists as required alongside "CI (SIAX Cloud) / quality (push)". + # + # It previously ran Trivy with `--exit-code 0`, which made the REQUIRED + # context incapable of ever reporting failure. Measured 2026-09-08 on + # main @4514d97: a planted CRITICAL secret (AWS secret access key) was + # DETECTED by the scanner and the job still exited 0 -- i.e. the gate + # reported success while holding a critical finding in hand. The same + # command with `--exit-code 1` exits 1 on that planted secret and exits 0 + # on the clean tree, so this flag is the whole difference between a real + # gate and a decorative one. + # + # KNOWN COVERAGE GAP (not fixed here, needs an owner decision): + # services/scrapers/requirements.txt pins nothing (`>=` ranges only), so + # Trivy's pip scanner cannot resolve versions and silently omits the file + # from the scan entirely -- it is not a target in the report at all. + # Pinning those same floors with `==` surfaces 3 HIGH findings today + # (aiohttp CVE-2025-69223, aiohttp CVE-2026-69244, lxml CVE-2026-41066). + # Do NOT paper over this with `--skip-dirs`; that would convert a + # measurable blind spot into declared policy. security: runs-on: ubuntu-latest steps: @@ -160,6 +181,6 @@ jobs: - name: Install Trivy run: | curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin - - name: Trivy scan (vuln + secrets, advisory) + - name: Trivy scan (vuln + secrets -- BLOCKING, fails on HIGH/CRITICAL) run: | - trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 0 . + trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 . diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml index ab56288..e18a8c8 100644 --- a/.gitea/workflows/nightly.yml +++ b/.gitea/workflows/nightly.yml @@ -39,6 +39,10 @@ jobs: - name: Install Trivy run: | curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin - - name: Trivy fs scan (vuln + secrets, advisory) + # A nightly scan exists to catch CVEs published against code that did + # not change. With `--exit-code 0` it could never report failure, so it + # could never raise that alarm -- the one job whose entire purpose is + # detection was the one guaranteed to stay green. Fixed 2026-09-08. + - name: Trivy fs scan (vuln + secrets -- fails on HIGH/CRITICAL) run: | - trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 0 . + trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 . diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml index 5717347..bcd987c 100644 --- a/.gitea/workflows/security.yml +++ b/.gitea/workflows/security.yml @@ -1,8 +1,16 @@ name: Security Scan (SIAX Cloud) -# Advisory security scanning on every push/PR + weekly re-run, matching the -# fleet convention (--exit-code 0 so findings never block merges; they are -# triaged via the repo's security channel and BACKLOG). +# Security scanning on every push/PR + weekly re-run. +# +# This job runs the same Trivy invocation as ci.yml's `security` job and +# previously carried the same defect: `--exit-code 0` meant it could report +# success while holding a CRITICAL finding (measured 2026-09-08 with a +# planted AWS secret key -- detected, still exit 0). Its context +# "Security Scan (SIAX Cloud) / security" is NOT in branch protection's +# required list, so this one never gated a merge either way; it is fixed +# here so a green tick on this workflow means what a reader assumes it +# means. See ci.yml's `security` job for the measurement and for the +# services/scrapers unpinned-requirements coverage gap. # # NOTE: findings on services/scrapers/* (FlareSolverr/Tor/curl_cffi) are # expected and out of scope for CI gating -- kept per the account owner's @@ -25,6 +33,6 @@ jobs: - name: Install Trivy run: | curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin - - name: Trivy fs scan (vuln + secrets, advisory) + - name: Trivy fs scan (vuln + secrets -- fails on HIGH/CRITICAL) run: | - trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 0 . + trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 .