Skip to main content

Binary $scan

Checks the contents of a Binary for malware using Amazon GuardDuty Malware Protection for S3.

$scan returns the Binary's scan result if it has one. Otherwise it asks GuardDuty for an on-demand scan and waits for the result, which usually takes seconds. The operation doesn't rely on GuardDuty scanning uploads automatically, and works with automatic scanning turned off.

$scan waits up to 30 seconds. If the result isn't ready by then, it returns SCAN_REQUESTED, and calling it again waits for the same scan. With Prefer: respond-async, $scan returns 202 Accepted immediately, with a Content-Location header pointing at an AsyncJob. The job waits up to 15 minutes, then completes with the result in its output Parameters as the return parameter. A THREATS_FOUND result still completes the job; the job fails only if the scan can't be requested.

GuardDuty bills every on-demand scan, so $scan avoids sending repeat scans:

  • A Binary with a final result (NO_THREATS_FOUND, THREATS_FOUND or UNSUPPORTED) is never scanned again.
  • When $scan sends a scan, it tags the S3 object MedplumMalwareScanRequested with the request time. Until a result arrives, later calls wait for that scan instead of sending another one. If no result arrives within an hour, the next call sends a new scan. Two calls made at the same moment can still both send a scan.
  • A FAILED or ACCESS_DENIED result is returned, and retried on the next call.

Invocation​

POST [base]/Binary/[id]/$scan

Add the Prefer: respond-async header to run the scan as an AsyncJob.

Any user with read access to the Binary can call this operation.

Output​

The operation returns an OperationOutcome with a single issue. issue.details.coding uses the system https://medplum.com/fhir/CodeSystem/malware-scan-status.

CodeSeverityIssue codeMeaning
NO_THREATS_FOUNDinformationinformationalThe scan finished and found no threats
THREATS_FOUNDerrorsecurityThe scan finished and found a potential threat
UNSUPPORTEDwarningnot-supportedGuardDuty cannot scan this object, for example a password-protected archive or an oversized file
FAILEDerrorexceptionGuardDuty couldn't finish the scan. The next call sends a new scan
ACCESS_DENIEDerrorforbiddenGuardDuty couldn't read the object. The next call sends a new scan
SCAN_REQUESTEDinformationinformationalThe scan is still in progress. Call $scan again for the result

When the result comes from a scan that $scan requested, the issue has a https://medplum.com/fhir/StructureDefinition/malware-scan-requested extension whose valueDateTime is when the scan was requested. Results from automatic scans of new uploads don't have it. GuardDuty doesn't report when a scan of an S3 object finishes, so no completion time is available.

Example​

Request​

POST /fhir/R4/Binary/[id]/$scan

Response​

{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "informational",
"extension": [
{
"url": "https://medplum.com/fhir/StructureDefinition/malware-scan-requested",
"valueDateTime": "2026-09-25T16:44:55.296Z"
}
],
"details": {
"coding": [{ "system": "https://medplum.com/fhir/CodeSystem/malware-scan-status", "code": "NO_THREATS_FOUND" }],
"text": "No threats found"
}
}
]
}

Requirements​

  • Binary storage must be S3 (binaryStorage: "s3:<bucket>").
  • The storage bucket needs a GuardDuty Malware Protection plan with tagging enabled. The guardDutyMalwareProtectionEnabled infra config option creates one. It also grants the server s3:GetObjectTagging, s3:PutObjectTagging and guardduty:SendObjectMalwareScan.
  • SSE-C encryption (sseCustomerKey) is not supported, because GuardDuty cannot read SSE-C objects.

Serving scanned Binaries​

GuardDuty Malware Protection supports two setups:

  • Scan every upload (guardDutyMalwareProtectionEnabled): GuardDuty scans each new object, and CloudFront only serves Binaries tagged NO_THREATS_FOUND. Binaries that haven't been scanned yet, or that returned any other result, are not served.
  • Scan on demand (guardDutyMalwareProtectionEnabled and guardDutyMalwareProtectionOnDemandOnly): for deployments with many existing, unscanned Binaries. Only $scan sends scans, and CloudFront blocks only Binaries tagged THREATS_FOUND. Unscanned Binaries and Binaries with a scan in progress are still served. GuardDuty has no on-demand-only mode, so this setup limits automatic scanning to the guardduty-on-demand-only/ prefix, which Medplum never writes to. On-demand scans ignore the prefix.

When the storage bucket and CloudFront distribution are in different regions, apply the read gate with medplum aws update-bucket-policies. Pass --guardduty-malware-protection, and add --guardduty-on-demand-only for on-demand scanning.

note

The gate only applies when CloudFront reads from the bucket. CloudFront can cache a Binary for up to a day, so a Binary served before GuardDuty tags it THREATS_FOUND may stay available from the cache until that entry expires.