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.*).
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)
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>
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>
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>
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 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 main2026-07-13 08:42:43 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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/v1have 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 ZAPapi-scanimporting/openapi/v1.json(all 160/api/v1ops) withX-Api-Keyinjected on every request (reaches the authenticated surface, not just the/appshell the unauth baseline saw), then a semgrep SAST cross-check. Wrapped intimeout(zap-api-scan can hang in post-scan cleanup after the report is written). Runs on the docker host, per-release, likemigration-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.csprojover the transitive fromMicrosoft.AspNetCore.OpenApi/Scalar.AspNetCore(the #8 SQLitePCLRaw override pattern).v1.jsonregenerates 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-Typeon 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
.NETbuild 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
Review complete
366d5d31): MERGEABLE-WITH-NITS. Independently verified theMicrosoft.OpenApipin 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 -eaborts 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 thecleanup()trap (leaks on mid-scan interrupt); semgrep silently skipped if absent.a042623a: reap moved into thecleanup()EXIT trap (inline removed, no double-reap;$ZAP_IMAGEin scope); semgrep-absent now prints a stderr WARN.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