Scan uploads for malware at ingest (§164.308(a)(5)(ii)(B))

FeatureConversionService
Shipped
August 4, 2026 at 3:50 AM UTC
Author
Kamo
Commit
b111065

The document vault accepted arbitrary uploads with no malware scanning anywhere on the platform, and it is multi-tenant — a file one org uploads is opened by another org's users through the viewer, the WOPI editor and the binder. ClamAV over INSTREAM, spoken directly to clamd (no client library; the protocol is a socket and a length prefix). The gate runs in ImagingUploadController on /upload and /templates/upload, before the member lookup and before any bytes reach MinIO — scanning after storage leaves the object retrievable for the width of the race. Scanner-unreachable behaviour is FAIL OPEN by default, and that is a decision rather than an accident. clamd is not deployed yet: this is the application half, and the deployment is a separate operational act (KlusterServices auto-applies on push and is not this change's to touch). Shipping fail-closed would take document upload down for every tenant the moment the image rolls, with no staging in which that would have been caught. Fail-open here is not "no protection", it is the status quo of the last several years, now logged at ERROR every time it happens. Every knob — including fail-closed — lives in kamowsconversion-config, so tightening it once clamd is stable is a configmap edit and a restart, not a release. A circuit breaker bounds the cost of an absent scanner: after 3 consecutive failures the guard stops dialling for 60s, so uploads do not each pay a connect timeout for a control that is effectively off. On detection: 422 naming the signature (not a 5xx — retrying will not help), the bytes are preserved in a dedicated quarantine bucket rather than dropped, and the rejection is recorded through PhiAccessRecorder. The audit row carries identifiers only; the uploaded filename stays out of it because filenames here routinely contain borrower names. PhiAccessKind has no UPLOAD member and adding one would alter an @Enumerated(STRING) column's constraint in the shared library, so DOWNLOAD + permitted=false is used and the reason is documented at the call site. Also switches this service's PHI trail to the durable phi_access_log writer. The matching com.kamo.z.shared.phi.audit entry is in the same commit's @EnableJpaRepositories list, and PhiAuditRepositoryScanTest fails the build if the two ever drift — that pairing is what took every login down on 2026-08-03. Retro-scan (POST **************** sweeps documents already in the vault, tenant-scoped and paged. It reports and preserves, never blocks or deletes: making a stored object unretrievable needs a persisted quarantine flag, i.e. a shared-library column applied by a hand-run initializer while service CI builds against main, and deleting on a false positive would destroy a tenant's record with no recovery. Tests: 36 new. Pre-existing **************** at_or_below_1080 still fails on a clean tree; untouched. Not covered, deliberately: RecordingIngestController (internal-auth Asterisk push) and DocsService's WOPI PutFile are second ingest paths that want the same gate, but the scanner lives in this service and WOPI has no *** token to attribute an audit row to. Both are follow-ups, not oversights.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing