Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
Malware scan

Malware scan

fremforge runs a ClamAV-based scanner in front of every binary-upload surface. The scanner sits in a sidecar proxy (forgejo-upload-proxy) that Bunny routes binary-upload paths to before they reach Forgejo storage. 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

The freshness of each feed is visible to operators on the /_app/_admin/malware-scan-fleet page. Customers don’t see signature freshness directly; the SLA covers scanner availability and the operator console covers signal health.

Surfaces

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

  • LFS object PUT
  • Issue and PR attachment POST
  • Package registry PUT / POST (npm, Maven, container, generic)

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.

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, Malware scan, Overrides, 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. Overrides are reviewable cross-tenant by operators (/_app/_admin/malware-scan-overrides) as part of the quarterly security review.

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 AUP §7. 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 hour 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.