Virus scanning of uploads (ClamAV)
What it covers: files a client uploads on the company payroll link (SIGN-4, 26 Oct 2026), and since 28 Oct (stage review SX-45) every attachment that arrives by chat (Telegram, WhatsApp) or email through Chatwoot. Helpdesk mailbox attachments, bookkeeping uploads and staff uploads are not scanned yet. Production status: not running. Kiran's agreement to run ClamAV beside the worker is blocker B8 in the payroll link work plan. Until it runs, link uploads wait unread.
How it works
- The client uploads a file on the link. The API refuses a wrong type, a renamed program or a file over 20 MB before storing anything.
- A file that passes is stored in the tenant's
_inboxarea, marked waiting for the virus check (files.scan_state = PENDING), and the worker jobfile.scanis queued. - The worker sends the bytes to ClamAV (
clamd, INSTREAM protocol, port 3310).- Clean →
scan_state = CLEAN, the file is filed, and later steps (onboarding, the monthly cycle) may read it. - Infected → the object is moved to
<tenant>/_quarantine/…,scan_state = INFECTEDwith the signature inscan_detail, an audit rowfile.quarantined, a bell and email to the Payroll Leads, and the client sees "Could not be accepted: it did not pass the virus check". - The check could not run (scanner down, timeout) →
scan_state = ERROR; the hourlyfile.scan-sweepsends it again.
- Clean →
- Nothing reads a file that has not passed: the document reader skips it, and the storage service refuses to read or link to anything under
_quarantine/whichever route asks. - Nobody opens one either (stage review S2-L01, decisions R41 and R42). The staff download (
GET /ops/entities/:entityId/files/:fileId/download: the company's Files tab and a request card's Open the file) answers 409 This file is still being virus-checked forPENDINGorERROR, and 409 This file failed the virus check: it is kept in quarantine forINFECTED, before anything is signed. A request card answered with a file says so under the card, has no Open the file button and cannot be Confirmed until the check passes (confirmCardrefuses with 409 too); a quarantined one is sent back to ask for the file again. The client's own Open on a change read from their upload follows the same rule. Files that were never scanned (scan_stateempty: older files and channels that do not scan yet) are not affected.
Chat and email attachments (SX-45)
- The attachment is stored and recorded on its message as before, marked waiting for the virus check (
scan_state = PENDING), andfile.scanis queued. It is not filed and not read yet; its document row reads as being classified. - The payroll link's file rule is applied when it lands (PDF, JPG, PNG, HEIC, WEBP, DOCX, XLSX, XLS, CSV; 20 MB; type judged by the first bytes as well as the name). A refused file is kept, its document row is marked
CLASSIFY_REFUSED_FILE(staff see Not classified: it is not a type we read… Set its company and its type by hand), an audit rowdocument.attachment_refusedis written, and nothing reads it. Classify again is refused for it. It is still virus-checked, so staff can open it safely. - When the check passes, the file is filed and its classification (
document.classify) starts, unless the file rule refused it. - An infected attachment is quarantined as above; the Payroll Leads' bell says it was sent by chat or email and that the client has not been told. An attachment not yet matched to a company raises no bell (only the audit row).
Because there is no scanner in production yet, chat and email attachments wait unread there until ClamAV runs (B8), exactly as link uploads do. In development set FILE_SCAN_MODE=skip (or run ClamAV), or chat attachments are never classified.
Settings
| Setting | Meaning |
|---|---|
CLAMAV_HOST, CLAMAV_PORT | Where clamd listens. Set → files are scanned. |
FILE_SCAN_MODE=skip | Development only: files pass as "not scanned" (SKIPPED). The worker refuses to start with it in production. |
| neither | No scanner: link uploads and chat/email attachments wait unread. |
The worker reads these once at start-up: restart it after changing them.
Run it locally
docker compose --profile clamav up -d clamav # first start downloads signatures: a few minutes
printf 'zPING\0' | nc -q1 127.0.0.1 3310 # answers PONG when ready
# .env: CLAMAV_HOST=127.0.0.1Test with the standard EICAR test file (harmless, recognised by every scanner as a test virus). ClamAV recognises it only when it is the whole file, so save the 68-character EICAR string alone as eicar.csv (the upload rule accepts it as plain text) and upload that; it must end up Could not be accepted. Hidden inside a PDF it is not recognised: that is how the test file is defined, not a gap in the scanner.
Run it in production (when B8 is agreed)
- A
clamav/clamav:1.4container beside the worker, on the internal network only (never published), with about 1.5 GB of memory; signatures update themselves (freshclam inside the image). CLAMAV_HOST=<container name>,CLAMAV_PORT=3310in the worker's environment; restart the worker.- Files that waited while there was no scanner are picked up by the hourly sweep.
When something goes wrong
- Files stuck "being checked":
SELECT id, scan_state, scan_detail FROM files WHERE scan_state IN ('PENDING','ERROR') ORDER BY created_at;—ERRORrows say why. Check the scanner answersPONG, then wait for the sweep (minute 17 of each hour). - A quarantined file must be examined: it is in the bucket under
<tenant>/_quarantine/YYYY/MM/DD/<file id>.<ext>. The platform will not open it; download it from the bucket with infrastructure credentials onto an isolated machine only.