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_FOUNDorUNSUPPORTED) is never scanned again. - When
$scansends a scan, it tags the S3 objectMedplumMalwareScanRequestedwith 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
FAILEDorACCESS_DENIEDresult 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.
| Code | Severity | Issue code | Meaning |
|---|---|---|---|
NO_THREATS_FOUND | information | informational | The scan finished and found no threats |
THREATS_FOUND | error | security | The scan finished and found a potential threat |
UNSUPPORTED | warning | not-supported | GuardDuty cannot scan this object, for example a password-protected archive or an oversized file |
FAILED | error | exception | GuardDuty couldn't finish the scan. The next call sends a new scan |
ACCESS_DENIED | error | forbidden | GuardDuty couldn't read the object. The next call sends a new scan |
SCAN_REQUESTED | information | informational | The 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
guardDutyMalwareProtectionEnabledinfra config option creates one. It also grants the servers3:GetObjectTagging,s3:PutObjectTaggingandguardduty: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 taggedNO_THREATS_FOUND. Binaries that haven't been scanned yet, or that returned any other result, are not served. - Scan on demand (
guardDutyMalwareProtectionEnabledandguardDutyMalwareProtectionOnDemandOnly): for deployments with many existing, unscanned Binaries. Only$scansends scans, and CloudFront blocks only Binaries taggedTHREATS_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 theguardduty-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.
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.