sineflow/clamav
Laravel integration for ClamAV virus scanning. Scan uploaded files and local paths through clamd using a simple API or validation rule, with configurable Unix socket or TCP connections for reliable malware detection.
Architecture fit
sineflow/clamav is a Laravel-oriented wrapper around the ClamAV daemon (clamd). It fits the framework's idioms: a service provider, a publishable config file, a facade, and container-bound scanner instances. This is a low-friction fit for an existing Laravel codebase — no bespoke bootstrapping required.Scanner::scanStream() is architecturally significant beyond a convenience method: it allows scanning a PHP stream resource rather than requiring a fully materialized file on disk. That unlocks three patterns we currently handle awkwardly — (a) large uploads where writing to a temp file is wasteful or disallowed, (b) objects streamed from S3/object storage before persistence, and (c) php://input-style request body inspection. clamd's INSTREAM command is the underlying primitive, so this is the correct abstraction level.Integration feasibility
composer require + vendor:publish + config step against the clamav connection settings (socket path or TCP host/port). The real work is infrastructure, not code: a reachable clamd with current signatures.scanStream() (does it mirror scanFile(), and does it distinguish "clean" from "scan error"?) and whether the stream is fully buffered or chunked to clamd.Technical risk
try { scan() } catch { /* continue */ }. If clamd is unreachable, the safe default is to reject the upload. We must own that policy explicitly in our own adapter layer, not inherit the package's default.clamd.conf's StreamMaxLength (commonly 25 MB) applies to INSTREAM. scanStream() will surface a clamd error for anything larger unless the daemon is tuned. This is a silent mismatch waiting to happen between app-level upload limits and daemon-level limits.clamd holds a worker for minutes. Under concurrency this is a availability incident, not a performance nit. Needs a size cap, a timeout, and ideally a queue path.composer.json against our current runtime before scheduling work.Key questions
StreamMaxLength on the target clamd?scanStream() chunk the stream, and does it rewind/close the resource? Can we reuse the stream afterwards (e.g. to persist the file after a clean verdict)?Stack fit
Storage::readStream() yields a resource, which is precisely what scanStream() wants).clamd deployed as a sidecar (Kubernetes) or a dedicated internal service, with freshclam running alongside or as an init/scheduled job.Migration path
exec('clamscan ...') or a homegrown wrapper: the migration is not purely mechanical. clamscan (one-shot CLI) and clamd (daemon) differ in startup cost, timeout behavior, and error output. Expect to rewrite error handling and to re-baseline expected latencies. Keep the old path behind a flag until parity is demonstrated.scanStream() specifically: treat as an additive path, not a replacement. Keep scanFile() for anything already on local disk; use scanStream() only for large or remote objects. This bounds the blast radius of a new code path.Compatibility
2.1.x) rather than floating. Verify transitive dependency constraints don't conflict with the framework version.Sequencing
clamd in Docker, run a clean file, an EICAR test file, and a >StreamMaxLength file through scanStream(). Capture exact exception types and messages.freshclam update schedule, health/readiness probe, resource requests (signature DB needs roughly 1–1.5 GB RAM resident), log shipping.scanFile() into the existing upload path first. Lowest risk, immediate value.scanStream() for the S3/large-object path behind a feature flag.clamd unreachability. A silent scanner is indistinguishable from a working one without this.clamd replica count and PHP-FPM pool from the result.Maintenance
freshclam, typically several updates per day), daemon restarts to pick up major signature DB reloads, and RAM headroom.INSTREAM behavior and memory profile.Support
Scaling
clamd is multithreaded but memory-bound; it scales vertically well and horizontally with replicas behind a load balancer or per-node sidecars. It will become the bottleneck before the Laravel app does for scan-heavy workloads.clamd replicas exist. Mitigate with strict size caps, per-request timeouts, and a queue-based path for anything above a threshold.clamd service.Failure modes
clamd down or socket unreachable → must fail closed; otherwise the scanner becomes a no-op that silently approves everything. This is the failure that matters and the one most likely to be introduced by careless exception handling.StreamMaxLength exceeded → clamd error surfaced as a scan failure; must map to a clear 4xx ("file too large to scan"), not a 500.max_execution_time.Ramp-up
scanStream() itself adds little effort on top of scanFile(); its value is avoiding a temp-file round trip and enabling the object-storage path, not reducing integration work.How can I help you explore Laravel packages today?