Skip to main content

Malware scan

fremforge runs a ClamAV-based scanner in front of the upload surfaces listed under Surfaces. If a signature matches, the upload is rejected with HTTP 422 and a finding row lands in the per-tenant malware-scan log.

The scanner runs without Cisco Talos and without abuse.ch. Both gate commercial use behind a paid licence and we don’t want a sub-processor on the scanning path that we can’t fully attribute to.

Signature stack

FeedWhat it coversCadenceLicence
SaneSecurityCommodity email and macro malware, phishing, JS droppersPulled every 4h from an EU mirrorFree, donation-supported
YARA-Forge CoreThreat-actor TTPs, packers, web shells, native payloads. Aggregator of 45+ vetted YARA-rule repos (Florian Roth signature-base, DidierStevens, etc.)Pulled every 4h, upstream releases weeklyDRL 1.1, commercial use OK with attribution
Linux Malware Detect (LMD)Linux server-side payloads: web shells, cryptominers, backdoors. rfxn.com static datasetPulled every 4h, upstream dailyGPLv2
EICAR test signatureThe standard malware-scan smoke-test signature, re-seeded on every refreshStatic, ConfigMap-mountedPublic domain

Feed freshness is monitored by fremverk operators; customers don’t see signature freshness directly. The SLA covers scanner availability.

Surfaces

Scanned, fail-closed (upload rejected if scanner unreachable):

  • LFS object PUT
  • Issue and PR attachment POST
  • Package registry PUT / POST, every package type (npm, Maven, PyPI, NuGet, Cargo, container layers and the rest), including publishes from fremforge-hosted runner jobs to an organisation’s registry

An upload larger than 4 GiB cannot be fully scanned and is refused with HTTP 413.

Scanned only if your organisation turns it on (off by default):

NOT scanned:

  • Git protocol pushes (git objects). Push-time secret detection is handled separately by push protection.
  • Workflow artifacts. Those are short-lived, scoped to the tenant, and policy says “don’t accept artifacts you don’t trust” already. Model files among them are checked by model scanning, as findings only.

Dependency proxy fetches

What the dependency proxy fetches from an upstream registry (npm, PyPI, Maven, NuGet, Go, Cargo, Helm and RPM package files, and container image layers) is not ClamAV-scanned by default. This matches GitHub and GitLab, which do not virus-scan package downloads either: public registries catch malware when a package is published and publish it as a malware advisory.

What still applies to every organisation, with ClamAV scanning on or off:

  • Known-malicious packages. Every package version the proxy serves is matched against the OSV MAL- malware advisories under your package policy. A match is refused for every organisation, whatever the policy’s mode (since 2026-10-11; for fremforge’s own internal organisations since 2026-10-10): the download answers 403 with the reason, and the version is left out of the package metadata the proxy serves, so package managers resolve to a version that is not malicious. See Malicious packages.
  • Content ClamAV has already found infected stays blocked. A file or image layer that a scan found infected is refused to every organisation, whether or not that organisation scans.
  • Uploads and pushed images are always scanned, as listed under Surfaces.

To have the proxy scan what it fetches with ClamAV, turn on Scan fetched packages and images for viruses (ClamAV) under Org admin → Build & deploy → Dependency proxy, or set malware_scan: true with PUT /orgs/{org}/dependency-proxy. With it on, every package file and image layer the proxy fetches for your organisation is streamed through ClamAV, and one it finds infected or cannot fully scan is withheld. That includes a file larger than 4 GiB, except a safetensors or GGUF model layer that passes its format check. An image-layer scan that runs out of time is not a verdict: the layer is withheld and queued for fremforge’s background layer scanner, and a later pull is served once that scan passes. A file the proxy cached for another organisation without a scan is fetched and scanned again before your organisation is served it.

Hold new images until the malware scan is finished (block_until_scanned) waits for that scan, so it needs ClamAV scanning on. A request that would leave block_until_scanned on with malware_scan off is refused with 400 scan-pair.

What happens when a match fires

  1. The proxy rejects the upload with HTTP 422 and a body that names the signature, the SHA-256 of the rejected bytes, and a pointer to the override flow.
  2. A finding row is written to malware_scan_findings via the api’s /internal/malware-scan/ingest endpoint (mutual auth, bearer + IP-allowlist).
  3. The finding appears immediately in tenant admin, Security, Malware scan with the upload path, the matched signature, who attempted the upload, and the file size.

Overrides (false positives)

Some signatures fire on legitimate test fixtures, security-research samples, or harmless artefacts that happen to match an old pattern. Tenant admins can add an override that whitelists a specific SHA-256 hash + repository pair.

  1. Tenant admin → Security › Findings › Malware. Expand Active overrides and click Add override.
  2. Paste the SHA-256 of the rejected file (shown in the 422 response).
  3. Pick the scope: this repo only, or all repos in the org.
  4. Set an expiry (default 90 days; max 1 year).
  5. Add a justification (free text, audited in the org audit log).

An override only short-circuits the matched-by-hash decision. If a different signature matches the same bytes the upload is still rejected. fremverk operators review overrides quarterly as part of the security review.

Via REST API

Malware-scan findings and overrides are exposed on the public API surface. Use this when wiring upload-rejection alerts into an external ticketing system, scripting override lifecycle, or driving from an AI agent.

  • List findings GET /_app/api/v1/orgs/:slug/malware-scan/findings
  • Dismiss a finding POST /_app/api/v1/orgs/:slug/malware-scan/findings/:id/dismiss
  • List overrides GET /_app/api/v1/orgs/:slug/malware-scan/overrides
  • Create override POST /_app/api/v1/orgs/:slug/malware-scan/overrides body: { "kind": "file", "signature": "sha256:...", "reason": "test fixture", "expires_at": "2027-01-01T00:00:00Z" }
  • Revoke override DELETE /_app/api/v1/orgs/:slug/malware-scan/overrides/:id

Listing requires the findings:read scope and dismissing a finding requires findings:write. Creating or revoking an override requires policy:write, because an override exempts uploads from the scan; a CI job’s runner token never carries it. Use a Personal Access Token (PAT):

curl -X POST \
  -H "Authorization: Bearer ${FREMFORGE_PAT}" \
  -H "Content-Type: application/json" \
  -d '{"kind":"file","signature":"sha256:abc123...","reason":"known-clean test artefact","expires_at":"2027-01-01T00:00:00Z"}' \
  https://frem.sh/_app/api/v1/orgs/acme/malware-scan/overrides

See the public REST API reference for the full OpenAPI specification.

SLA and fail-closed posture

If the ClamAV daemon is unreachable, uploads are rejected with HTTP 503 and a Retry-After. That’s the fail-closed decision per our Acceptable Use Policy. The alternative (fail-open during a scanner outage) opens a window where the AUP commitment is silently bypassed; we don’t think that trade is worth it. Operators are paged on scanner-down within 5 minutes.

EICAR smoke test

A canary CronJob uploads the standard EICAR test file every 30 minutes and verifies the upload is rejected. If three consecutive canary runs succeed (i.e. the rejection stops happening), operators are paged. That covers the case where the signature DB shipped clean but failed to load.