feat(domain): virus-scan and PDF-check uploaded diplomas with ClamAV (closes #192) #194

Open
not wants to merge 13 commits from feat/192-scan-uploads into main
Contributor

Uploaded diplomas are now virus-scanned and type-checked before they reach OpenZaak (ADR-0036). Builds on #193 (clamd infra, merged).

What changes

  • Domain: a new IDocumentScanner port, implemented by ClamdDocumentScanner (clamd INSTREAM over TcpClient, no NuGet package). ProvideDocuments checks ownership, then scans, then checks for %PDF-, and only then stores the file and completes the wait. New outcomes: Infected, NotAPdf, ScannerUnavailable. The API answers 422 {reason} or 503.
  • Fail closed: an unreachable clamd, a timeout or an ERROR reply all count as Unavailable. The upload is refused, nothing is stored and the wait stays open.
  • The scan runs before the PDF check: clamd only matches EICAR at the start of a file (verified against a real clamd: %PDF-1.4\n + EICAR scans OK), so a PDF check in front of the scan would report malware as "not a PDF".
  • BFF: relays 422 with the reason and 503. openapi.json and the generated client are regenerated.
  • Self-service portal: a specific, accessible role="alert" message for infected, not-a-PDF and scanner-unavailable.
  • Acceptance: EenDiplomaAanleveren.feature has four scenarios (clean, infected, not a PDF, scanner down).

Verified locally: every .NET unit and acceptance suite is green, as are the self-service vitest + axe tests and dotnet format. The adapter was run against a real clamd 1.4.6: clean gives Clean, EICAR gives Infected, a closed port gives Unavailable. The existing e2e and local-flow uploads begin with %PDF- and are clean, so the happy path is unchanged.
Not run: the full verify-stack (main only) and a Playwright e2e for the infected path. That path is covered by unit, BFF, portal and acceptance tests, plus verify-clamav for the live engine.

closes #192

🤖 Generated with Claude Code

Uploaded diplomas are now virus-scanned and type-checked before they reach OpenZaak (ADR-0036). Builds on #193 (clamd infra, merged). **What changes** - **Domain:** a new `IDocumentScanner` port, implemented by `ClamdDocumentScanner` (clamd INSTREAM over `TcpClient`, no NuGet package). `ProvideDocuments` checks ownership, then scans, then checks for `%PDF-`, and only then stores the file and completes the wait. New outcomes: `Infected`, `NotAPdf`, `ScannerUnavailable`. The API answers 422 `{reason}` or 503. - **Fail closed:** an unreachable clamd, a timeout or an ERROR reply all count as `Unavailable`. The upload is refused, nothing is stored and the wait stays open. - **The scan runs before the PDF check:** clamd only matches EICAR at the start of a file (verified against a real clamd: `%PDF-1.4\n` + EICAR scans `OK`), so a PDF check in front of the scan would report malware as "not a PDF". - **BFF:** relays 422 with the reason and 503. `openapi.json` and the generated client are regenerated. - **Self-service portal:** a specific, accessible `role="alert"` message for infected, not-a-PDF and scanner-unavailable. - **Acceptance:** `EenDiplomaAanleveren.feature` has four scenarios (clean, infected, not a PDF, scanner down). **Verified locally:** every .NET unit and acceptance suite is green, as are the self-service vitest + axe tests and `dotnet format`. The adapter was run against a real clamd 1.4.6: clean gives Clean, EICAR gives Infected, a closed port gives Unavailable. The existing e2e and local-flow uploads begin with `%PDF-` and are clean, so the happy path is unchanged. **Not run:** the full verify-stack (main only) and a Playwright e2e for the infected path. That path is covered by unit, BFF, portal and acceptance tests, plus `verify-clamav` for the live engine. closes #192 🤖 Generated with [Claude Code](https://claude.com/claude-code)
not added this to the Iteration 6 — Production Posture milestone 2026-10-02 07:30:31 +00:00
not added the type:slicearea:domainarea:portal-self-servicearea:bff labels 2026-10-02 07:30:33 +00:00
not changed target branch from build/191-clamav to main 2026-10-02 07:53:33 +00:00
not added 12 commits 2026-10-02 07:53:33 +00:00
Red: ProvideDocuments takes the new IDocumentScanner port but ignores it, so
infected, non-PDF and scanner-unavailable uploads are still Accepted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Red: the stub adapter never connects and always answers Clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Verified against a real clamd 1.4.6: clean → Clean, EICAR → Infected,
closed port → Unavailable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
openapi.json and the generated portal client regenerated from the served spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
clamd matches EICAR only at the start of a file, so a %PDF- check ahead of the
scan made the infected path unreachable for the EICAR test file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
not closed this pull request 2026-10-02 07:53:50 +00:00
not reopened this pull request 2026-10-02 07:53:50 +00:00
not added 1 commit 2026-10-02 10:54:45 +00:00
test(domain): cover clamd timeout, caller cancellation, null content and defaults (refs #192)
CI / k8s (pull_request) Successful in 8s
CI / lint (pull_request) Successful in 1m47s
CI / build (pull_request) Successful in 1m38s
CI / docs (pull_request) Successful in 55s
CI / unit (pull_request) Successful in 1m13s
CI / frontend (pull_request) Successful in 2m24s
CI / mutation (pull_request) Successful in 4m42s
CI / verify-stack (pull_request) Skipped
254b1796dc
Domain mutation score 89.87% → 91.98%, back above the 90% break.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
not force-pushed feat/192-scan-uploads from e39a46febc to 254b1796dc 2026-10-02 10:54:45 +00:00 Compare
All checks were successful
CI / k8s (pull_request) Successful in 8s
CI / lint (pull_request) Successful in 1m47s
Required
Details
CI / build (pull_request) Successful in 1m38s
Required
Details
CI / docs (pull_request) Successful in 55s
CI / unit (pull_request) Successful in 1m13s
Required
Details
CI / frontend (pull_request) Successful in 2m24s
Required
Details
CI / mutation (pull_request) Successful in 4m42s
Required
Details
CI / verify-stack (pull_request) Skipped
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/192-scan-uploads:feat/192-scan-uploads
git checkout feat/192-scan-uploads
Sign in to join this conversation.