diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 86fd116..aaf0e46 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: ["**"] @@ -137,22 +141,75 @@ jobs: [ -n "$FOUND" ] || { echo "FATAL: no ClickHouse behind the alias accepts this run's credentials"; exit 1; } echo "CLICKHOUSE_URL=http://$FOUND:8123" >> "$GITHUB_ENV" echo "CLICKHOUSE_AUTH=opt_ci:$PASS" >> "$GITHUB_ENV" + # The anti-skip guard here used to grep vitest's HUMAN output: + # + # grep -qE "skipped" && ! grep -qE "Tests [0-9]+ passed" + # + # vitest colours that summary, so the real line on this runner is + # ESC[2m Tests ESC[22m ESC[1mESC[32m97 passedESC[39m ... — ANSI escapes + # sit between "Tests " and the digits, so the two-spaces-then-digit + # pattern can never match. The first grep still matched "skipped", so + # the guard fired on ANY run containing a single skipped test while + # printing "integration suites skipped instead of running" — a false + # statement. Measured 2026-09-08, run 13911 job 31183: 13 files / 97 + # tests PASSED, 1 file / 2 tests skipped → FATAL anyway. + # + # It survived review because it is correct locally: redirecting to a + # file disables vitest's colour, so the pattern matches on a developer + # machine and only misfires under CI. + # + # Replaced with the JSON reporter — structured and colourless. A/B + # measured on real reports, BOTH directions: + # all-skipped (0 passed / 99 pending) → RED (protection preserved) + # 945 passed + 184 skipped → GREEN (false red removed) - name: Integration tests against LIVE OpenSearch + ClickHouse (blocking) run: | set -eu - # Guard: env MUST be present here — the suites self-skip without it, - # and a skipped suite must never masquerade as a green integration - # gate (Master-DoD §56 status honesty). + # Env MUST be present here — the suites self-skip without it, and a + # skipped suite must never masquerade as a green integration gate + # (Master-DoD §56 status honesty). [ -n "${OPENSEARCH_URL:-}" ] && [ -n "${CLICKHOUSE_URL:-}" ] - pnpm vitest run tests/integration 2>&1 | tee /tmp/itest.log - if grep -qE "skipped" /tmp/itest.log && ! grep -qE "Tests [0-9]+ passed" /tmp/itest.log; then - echo "FATAL: integration suites skipped instead of running"; exit 1 - fi + pnpm vitest run tests/integration \ + --reporter=default \ + --reporter=json --outputFile.json=/tmp/itest.json + node -e ' + const fs = require("fs"); + const p = "/tmp/itest.json"; + if (!fs.existsSync(p)) { + console.error("FATAL: vitest produced no JSON report"); process.exit(1); + } + const r = JSON.parse(fs.readFileSync(p, "utf8")); + console.log(`integration: ${r.numPassedTests} passed, ${r.numPendingTests} skipped, ${r.numFailedTests} failed, ${r.numTotalTests} total`); + if (r.numFailedTests > 0) { + console.error("FATAL: integration tests failed"); process.exit(1); + } + if (r.numPassedTests === 0) { + console.error("FATAL: no integration test actually executed — every suite skipped"); + process.exit(1); + } + ' - # 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 +217,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 .