Scan Salesforce Attachments for Malware with Apex

Use the AttachmentScanner antivirus API to scan files and URLs from Salesforce for viruses and malware using Named Credentials and Apex HTTP callouts.

Prerequisites

  1. Create a Named Credential called AttachmentScanner:

    • URL: https://scans.attachmentscanner.com
    • Authentication: Custom Header
    • Header: Authorization = Bearer YOUR_TOKEN
  2. Add a Remote Site Setting for your scanner URL

Scan a URL

The simplest way to scan — pass a URL and get back a result:

public class AttachmentScannerService {

    private static final String NAMED_CREDENTIAL = 'callout:AttachmentScanner';

    public static Map<String, Object> scanUrl(String fileUrl) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(NAMED_CREDENTIAL + '/v1.0/scans');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(new Map<String, String>{ 'url' => fileUrl }));

        HttpResponse res = new Http().send(req);
        return (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
        // result.get('status') => "found" or "ok"
    }
}

Scan a ContentVersion (File Upload)

Scan files uploaded to Salesforce by reading the ContentVersion blob. This must be called from a @future or Queueable context because Salesforce doesn't allow synchronous callouts from triggers.

@future(callout=true)
public static void scanContentVersion(Id contentVersionId) {
    ContentVersion cv = [
        SELECT VersionData, Title, FileExtension
        FROM ContentVersion
        WHERE Id = :contentVersionId
        LIMIT 1
    ];

    String boundary = '----FormBoundary' + String.valueOf(DateTime.now().getTime());
    String filename = cv.Title + '.' + cv.FileExtension;

    // Build multipart body
    String body = '--' + boundary + '\r\n'
        + 'Content-Disposition: form-data; name="file"; filename="' + filename + '"\r\n'
        + 'Content-Type: application/octet-stream\r\n\r\n';
    String footer = '\r\n--' + boundary + '--';

    Blob bodyBlob = EncodingUtil.base64Decode(
        EncodingUtil.base64Encode(Blob.valueOf(body))
        + EncodingUtil.base64Encode(cv.VersionData)
        + EncodingUtil.base64Encode(Blob.valueOf(footer))
    );

    HttpRequest req = new HttpRequest();
    req.setEndpoint(NAMED_CREDENTIAL + '/v1.0/scans');
    req.setMethod('POST');
    req.setHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
    req.setBodyAsBlob(bodyBlob);

    HttpResponse res = new Http().send(req);
    Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());

    if ((String) result.get('status') == 'found') {
        // Malware detected — delete the file
        ContentDocument doc = [
            SELECT Id FROM ContentDocument
            WHERE LatestPublishedVersionId = :contentVersionId
            LIMIT 1
        ];
        delete doc;
    }
}

Async Scan with Callback

Salesforce supports inbound webhooks via Sites or Experience Cloud. Use async: true with a callback to avoid hitting Apex callout time limits on large files:

public static String scanUrlAsync(String fileUrl, String callbackUrl) {
    HttpRequest req = new HttpRequest();
    req.setEndpoint(NAMED_CREDENTIAL + '/v1.0/scans');
    req.setMethod('POST');
    req.setHeader('Content-Type', 'application/json');
    req.setBody(JSON.serialize(new Map<String, Object>{
        'url' => fileUrl,
        'async' => true,
        'callback' => callbackUrl
    }));

    HttpResponse res = new Http().send(req);
    Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
    return (String) result.get('id'); // scan ID for polling
}

Auto-Scan Trigger

Automatically scan every file uploaded to Salesforce:

trigger ContentVersionScanTrigger on ContentVersion (after insert) {
    for (ContentVersion cv : Trigger.new) {
        if (cv.IsLatest) {
            AttachmentScannerService.scanContentVersion(cv.Id);
        }
    }
}