Skip to content

Malware Scanning

PentestPad can scan every uploaded file for malware before it is stored or served — project attachments, finding and editor images, report template and PDF replacements, and whitelabel logos. Scanning is handled by a ClamAV clamd sidecar container that the application streams uploads to.

On PentestPad Cloud this is always on. On self-hosted instances it is opt-in: you add the ClamAV container and set a single environment variable.

Add the clamav service to your docker-compose.yml, alongside the existing PentestPad services:

services:
# ... existing app, postgres, redis services ...
clamav:
image: clamav/clamav:1.4-debian
container_name: clamav
restart: unless-stopped
volumes:
# Persist downloaded signatures so the slow first pull only happens once.
- clamav_db:/var/lib/clamav
- ./clamd.conf:/etc/clamav/clamd.conf:ro
healthcheck:
# Image-provided helper: PINGs clamd, exits non-zero until ready.
test: ["CMD", "clamdcheck.sh"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
networks:
- app_network
volumes:
clamav_db:

Then declare the dependency on the app service so it starts after ClamAV, and point it at the sidecar:

services:
app:
depends_on:
clamav:
condition: service_started
environment:
CLAMAV_HOST: clamav

clamd listens on TCP port 3310 and is reached only over the internal app_network — there is no need to publish a host port.

The mounted clamd.conf raises clamd’s size limits so large attachments (up to ~700 MB) can be scanned. The stock image defaults to only 25 MB per stream and 400 MB per file, which would silently leave large uploads unscanned — so this file is required.

Create clamd.conf next to your docker-compose.yml:

LogFile /var/log/clamav/clamd.log
LogTime yes
# Bind all interfaces (dual-stack) so the image's clamdcheck.sh healthcheck,
# which PINGs IPv6 localhost, keeps working. Do not pin TCPAddr.
LocalSocket /tmp/clamd.sock
TCPSocket 3310
User clamav
# Raise the limits well above the ~700 MB attachment cap. The application
# enforces a matching ceiling via CLAMAV_MAX_STREAM_BYTES.
StreamMaxLength 768M
MaxScanSize 768M
MaxFileSize 768M

Set the master switch in your .env and restart the app:

SCANNING_ENABLED=true

When scanning is disabled (the self-hosted default), the application behaves exactly as before — no scan records are created and no jobs are queued.

The remaining variables have sensible defaults that match the container above; override them only for a non-standard setup (for example an external clamd):

Environment variableDefaultPurpose
SCANNING_ENABLEDfalse (self-hosted)Master on/off switch.
CLAMAV_HOSTclamavHostname of the clamd sidecar.
CLAMAV_PORT3310clamd TCP port.
CLAMAV_TIMEOUT120Socket timeout, in seconds, for a single scan.
CLAMAV_MAX_STREAM_BYTES805306368 (768 MB)Largest file streamed to clamd; bigger files are recorded as unscannable. Must stay clamd’s StreamMaxLength.
CLAMAV_SYNC_MAX_BYTES134217728 (128 MB)Largest file scanned in-request at upload time. Bigger files are accepted and scanned in the background.
CLAMAV_STALE_AFTER_MINUTES60How long a scan may sit pending before the recovery sweep re-runs or escalates it.

Files larger than CLAMAV_SYNC_MAX_BYTES (128 MB by default), plus any uploads accepted while the scanner was briefly unreachable, are scanned in the background on a dedicated scans queue. The standard PentestPad image runs Laravel Horizon, which already drains this queue — just make sure Horizon is running.

  1. Start the stack: docker compose up.
  2. Confirm the sidecar is healthy: docker compose ps should show clamav as healthy (allow ~2 minutes on first boot).
  3. Upload a normal file in a project — it should attach without any warning badge.
  4. To safely confirm detection works, upload an EICAR test file — a harmless standard antivirus test string. The upload should be blocked by malware scanning.

Once verified, see Attachment Scanning for what your team sees day to day — the scan badges, the blocked-upload flow, and downloading flagged files safely.