Scan Salesforce Attachments for Malware with Apex

Use AttachmentScanner 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);
        }
    }
}