feat(ci): #314 — authenticated black-box security scan + Microsoft.OpenApi HIGH-vuln pin #331

Merged
timothy merged 3 commits from fix/314-security-scan into main 2026-07-13 08:42:43 +02:00
Owner

The #197 exit-criterion security scan (#314). Runs the out-of-ecosystem, black-box pass all our other (white-box) checks can't, now that #295 auth + #286 /api/v1 have settled the surface.

What's here

  • scripts/security-scan.sh — repeatable authenticated scan: boots a throwaway container from the image under test (fresh config volume; never the deployed prod/test container — the active scan attacks write endpoints), reads its machine key, runs OWASP ZAP api-scan importing /openapi/v1.json (all 160 /api/v1 ops) with X-Api-Key injected on every request (reaches the authenticated surface, not just the /app shell the unauth baseline saw), then a semgrep SAST cross-check. Wrapped in timeout (zap-api-scan can hang in post-scan cleanup after the report is written). Runs on the docker host, per-release, like migration-smoke.sh — not a per-PR CI job (docker + booted image + slow + noisy).
  • Microsoft.OpenApi 2.0.0 → 2.7.5 (GHSA-v5pm-xwqc-g5wc, High — stack overflow parsing a circular $ref), direct-pinned in CPM + ErsatzTV.csproj over the transitive from Microsoft.AspNetCore.OpenApi/Scalar.AspNetCore (the #8 SQLitePCLRaw override pattern). v1.json regenerates byte-identical; ErsatzTV + Tests both vuln-clear.
  • docs/ci-cd.md — new Security-scanning section + the pin note in dependency management.

Scan result (authenticated, vs :latest=682dceec)

0 High / 0 Medium / 0 Critical across all 160 endpoints. 2 Low (CORP header missing → filed #330; Unexpected Content-Type on SPA HTML routes = ZAP noise), 2 Informational (687× "Client Error" = the auth gate + validation correctly rejecting fuzzed/unauth requests — a good signal; Non-Storable ×5). semgrep: 0 findings on shipped code. → the exit criterion (no go-live-blocking black-box finding) is met.

Verification

.NET build clean, 1583 tests green (incl. 198 OpenAPI/contract/serializer tests — the ones sensitive to the OpenApi bump). Scan report retained on the host (~/etv-secscan/zap-api-report.*).

Independent review to follow on this head.

fixes #314
refs #197 #8 #319 #330

🤖 Generated with Claude Code

The #197 exit-criterion security scan (#314). Runs the **out-of-ecosystem, black-box** pass all our other (white-box) checks can't, now that #295 auth + #286 `/api/v1` have settled the surface. ## What's here - **`scripts/security-scan.sh`** — repeatable authenticated scan: boots a **throwaway** container from the image under test (fresh config volume; never the deployed prod/test container — the active scan attacks write endpoints), reads its machine key, runs **OWASP ZAP `api-scan`** importing `/openapi/v1.json` (all 160 `/api/v1` ops) with `X-Api-Key` injected on every request (reaches the authenticated surface, not just the `/app` shell the unauth baseline saw), then a **semgrep** SAST cross-check. Wrapped in `timeout` (zap-api-scan can hang in post-scan cleanup after the report is written). Runs on the docker host, per-release, like `migration-smoke.sh` — **not** a per-PR CI job (docker + booted image + slow + noisy). - **`Microsoft.OpenApi 2.0.0 → 2.7.5`** (GHSA-v5pm-xwqc-g5wc, **High** — stack overflow parsing a circular `$ref`), direct-pinned in CPM + `ErsatzTV.csproj` over the transitive from `Microsoft.AspNetCore.OpenApi`/`Scalar.AspNetCore` (the #8 SQLitePCLRaw override pattern). **`v1.json` regenerates byte-identical**; ErsatzTV + Tests both vuln-clear. - **`docs/ci-cd.md`** — new Security-scanning section + the pin note in dependency management. ## Scan result (authenticated, vs `:latest`=`682dceec`) **0 High / 0 Medium / 0 Critical** across all 160 endpoints. 2 Low (CORP header missing → filed **#330**; `Unexpected Content-Type` on SPA HTML routes = ZAP noise), 2 Informational (687× "Client Error" = the auth gate + validation correctly *rejecting* fuzzed/unauth requests — a good signal; Non-Storable ×5). semgrep: **0 findings** on shipped code. → the exit criterion (no go-live-blocking black-box finding) is **met**. ## Verification `.NET` build clean, **1583 tests** green (incl. 198 OpenAPI/contract/serializer tests — the ones sensitive to the OpenApi bump). Scan report retained on the host (`~/etv-secscan/zap-api-report.*`). Independent review to follow on this head. fixes #314 refs #197 #8 #319 #330 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 2 commits 2026-07-13 08:30:09 +02:00
Microsoft.AspNetCore.OpenApi 10.0.2 + Scalar.AspNetCore pull Microsoft.OpenApi
2.0.0 transitively — High severity (stack overflow parsing a circular $ref;
fixed in 2.7.5). Direct-pin 2.7.5 in CPM + a direct ErsatzTV PackageReference so
the override actually resolves (the #8 SQLitePCLRaw transitive-override pattern).

Build clean (transformers compile unchanged against 2.7.5), v1.json byte-identical
(no OpenAPI-output change), ErsatzTV + ErsatzTV.Tests both vuln-clear, 198
OpenAPI/contract/security tests green.

refs #314 #8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(ci): #314 — repeatable black-box security scan (authenticated ZAP + semgrep)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 15s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 10m1s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m55s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
366d5d316c
scripts/security-scan.sh: boots a THROWAWAY container from the image under test
(fresh config volume; never the deployed prod/test container — the active scan
attacks write endpoints), reads its machine key, and runs an authenticated OWASP
ZAP api-scan that imports /openapi/v1.json (all 160 /api/v1 ops) and injects
X-Api-Key on every request via a replacer rule so it reaches the
[RequiresAuthentication] + RequireKeyForReads surface — then a semgrep SAST
cross-check. Wrapped in `timeout` because zap-api-scan can hang in post-scan
cleanup after the report is already written.

docs/ci-cd.md: new 'Security scanning' section (out-of-ecosystem black-box gate,
run on the docker host per-release like migration-smoke, not a per-PR CI job) +
the Microsoft.OpenApi 2.7.5 pin note in dependency management.

refs #314 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
timothy added 1 commit 2026-07-13 08:36:34 +02:00
fix(ci): #314 review — reap ZAP container in the cleanup() trap; warn on missing semgrep
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 16s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m34s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m40s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m34s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m39s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m35s
a042623a53
Cold review of PR #331:
- Move the ZAP-container reap from an inline post-scan line into cleanup() (the
  EXIT trap) so a SIGINT/timeout kill mid-scan can't leave it running.
- semgrep absent now prints a stderr WARN instead of silently skipping the SAST
  pass (no false confidence for a release gate).

refs #314

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Review complete

  • Cold review (base diff 366d5d31): MERGEABLE-WITH-NITS. Independently verified the Microsoft.OpenApi pin resolves to 2.7.5 (no lingering transitive 2.0.0, no NU1605, CPM-correct) and that doc-generation is exercised by the OpenAPI contract tests; verified the scan script cannot mis-target prod/test (fixed private name + fresh config volume + own port, set -e aborts if the port is taken) and has no key-injection bug (the machine key is lowercase hex, not base64). Two nits: ZAP-container reap was inline rather than in the cleanup() trap (leaks on mid-scan interrupt); semgrep silently skipped if absent.
  • Both fixed in a042623a: reap moved into the cleanup() EXIT trap (inline removed, no double-reap; $ZAP_IMAGE in scope); semgrep-absent now prints a stderr WARN.
  • Fix-commit re-review: verified correct + exhaustive, no new bug.

Live authenticated scan result already posted in the PR body (0 High/Med/Crit; CORP Low → #330; semgrep 0). Independent-review skip rubric satisfied (security diff got a full pass + fix re-review).

Review-verdict: MERGEABLE @ a042623a

## Review complete - **Cold review** (base diff `366d5d31`): MERGEABLE-WITH-NITS. Independently verified the `Microsoft.OpenApi` pin **resolves to 2.7.5** (no lingering transitive 2.0.0, no NU1605, CPM-correct) and that doc-generation is exercised by the OpenAPI contract tests; verified the scan script **cannot mis-target** prod/test (fixed private name + fresh config volume + own port, `set -e` aborts if the port is taken) and has **no key-injection bug** (the machine key is lowercase hex, not base64). Two nits: ZAP-container reap was inline rather than in the `cleanup()` trap (leaks on mid-scan interrupt); semgrep silently skipped if absent. - **Both fixed in `a042623a`**: reap moved into the `cleanup()` EXIT trap (inline removed, no double-reap; `$ZAP_IMAGE` in scope); semgrep-absent now prints a stderr WARN. - **Fix-commit re-review**: verified correct + exhaustive, no new bug. Live authenticated scan result already posted in the PR body (0 High/Med/Crit; CORP Low → #330; semgrep 0). Independent-review skip rubric satisfied (security diff got a full pass + fix re-review). Review-verdict: MERGEABLE @ a042623a
timothy merged commit a042623a53 into main 2026-07-13 08:42:43 +02:00
Sign in to join this conversation.