Repository navigation
Scan and Classification
What happens between "last byte received" and a file's end state. Code:
internal/pipeline/pipeline.go, internal/pipeline/classify.go,
internal/clamd/clamd.go, deploy/clamd/clamd.conf.
flowchart TD
Q["Upload in state scanning<br/>put on scan queue"] --> W{"clamd passed the<br/>EICAR self-test?"}
W -- "no" --> W2["Wait, check again<br/>every 5 s"] --> W
W -- "yes" --> F{"incoming/ID exists<br/>with the expected size?"}
F -- "no" --> HOLD1["held:<br/>staged file missing or wrong size"]
F -- "yes" --> SCAN["zSCAN /data/incoming/ID<br/>timeout SCAN_TIMEOUT"]
SCAN -- "cannot connect" --> RETRY["Stay in scanning,<br/>retry after 15 s"] --> W
SCAN -- "reply" --> CL["Classify"]
CL --> REC["Save decision in record,<br/>audit upload.STATE"]
REC --> PLACE["Move the file<br/>see Outbox handoff"]
- A completed upload is put on an in-memory queue (room for 10 000). The same upload is never queued twice.
-
SCAN_CONCURRENCYworkers (default 2) take uploads from the queue. - The janitor re-queues every upload still in
scanningevery 10 seconds, and once at startup. So a full queue or a restart loses nothing.
A worker never scans until clamd has proved it detects the EICAR test file.
sequenceDiagram
participant A as app health loop
participant T as tmp/
participant C as clamd
A->>C: zPING
C-->>A: PONG
A->>C: zVERSION
C-->>A: ClamAV x.y.z/DB version/date
A->>T: write EICAR test file
A->>C: zSCAN /data/tmp/tmp-RANDOM
C-->>A: Eicar signature FOUND
A->>T: remove test file
Note over A: healthy, scanning on. Repeat in 1 h
- If any step fails, scanning pauses. The log says
clamd self-test failed; scanning paused. The test is tried again after 5 seconds, then 10, 20, 40, and then every minute. - On success the log says
clamd self-test passed; scanning enabledwith the database version. The test repeats every hour. - The database version from
VERSION(for exampleClamAV 1.4.1/27800) is stored with each scan result and written into the manifest asclamav_db. It is refreshed at each self-test, so it can be up to an hour behind a signature reload. -
/readyzanswers 200 only while the last self-test passed. See Startup, health and signatures.
Before scanning, the worker checks that incoming/<ID> exists and has the
size the user declared. If not, the file is held ("staged file missing",
or "staged file is N bytes, expected M").
- The app sends
zSCAN <path>over TCP toclamd:3310. clamd opens the file itself from its read-only mount, so clamd's stream size limit does not apply. The path isCLAMD_PATH_PREFIX+/incoming/<ID>. - The scan has a deadline of
SCAN_TIMEOUT(2 hours). - If the app cannot connect to clamd, the file stays in
scanningand is tried again after 15 seconds (log:clamd unreachable; scan postponed). It is not held. - Any other failure (timeout, dropped connection, reply the client cannot parse) is a scan failure, and the file is held.
clamd settings that matter (deploy/clamd/clamd.conf):
| Setting | Value | Effect |
|---|---|---|
MaxFileSize |
2047M | Largest single file scanned. libclamav cannot go above 2 GiB. Must equal CLAMD_MAX_SCAN_SIZE. |
MaxScanSize |
4000M | Most data extracted from one archive. |
AlertExceedsMax |
yes | Hitting a size limit is reported as Heuristics.Limits.Exceeded.*, not OK. |
MaxRecursion / MaxFiles / MaxScanTime
|
17 / 10000 / 1 h | Archive-bomb limits. |
AlertEncrypted |
yes | Encrypted archives and documents are reported as Heuristics.Encrypted.*. |
The app never treats an unexpected or partial result as clean.
| clamd result | Condition | End state | Reason recorded |
|---|---|---|---|
OK |
size ≤ CLAMD_MAX_SCAN_SIZE
|
passed | |
OK |
size > CLAMD_MAX_SCAN_SIZE
|
held (policy hold or reject), unscanned (policy pass) |
file is N bytes, larger than the scanner limit |
FOUND Heuristics.Limits.Exceeded.MaxFileSize or .MaxScanSize
|
held (policy hold or reject), unscanned (policy pass) |
scanner size limit reached | |
FOUND Heuristics.Limits.Exceeded. anything else (MaxRecursion, MaxFiles, MaxScanTime) |
held | scanner limit reached | |
FOUND any other signature, including Heuristics.Encrypted.*
|
QUARANTINE_ENABLED=true |
quarantined | infected: SIGNATURE |
FOUND any other signature |
QUARANTINE_ENABLED=false |
deleted | infected: SIGNATURE |
ERROR |
held | scan error: MESSAGE | |
| timeout, connection lost, unparseable reply | held | scan failed: ... | |
| anything else | held | unrecognized scan result |
With OVERSIZE_POLICY=reject, files over the limit are refused at tus
create (413). A file that still reaches classification as oversize (for
example an archive that expands past MaxScanSize) is held, not passed.
- The decision is saved in the upload record first: state, reason, raw verdict, signature, database version, scan time.
- The audit log gets
upload.<state>fromsystem / scanner, with state, SHA-256, size and reason. - The file is moved. If the process dies between steps 1 and 3, the janitor finishes the move from the record.
| End state | File goes to | Next |
|---|---|---|
passed |
outbox/<name> |
Outbox handoff |
unscanned |
outbox/<name>, flagged in the manifest |
Outbox handoff |
held |
quarantine/<ID> |
Held files |
quarantined |
quarantine/<ID> |
Can only be deleted |
deleted |
removed | Nothing |
Two more states are set elsewhere: expired (cancelled or abandoned, see
Upload flow) and released (see Held files).
Flows
- Upload flow
- Scan and classification
- Outbox handoff and batches
- Held files: operator flow
- Request security
- Startup, health and signatures
Operations
Release v0.5.3-beta