Guide · AWS ECR · Container Security
ECR Enhanced Scanning for MCP Servers — Amazon Inspector, SBOM, EventBridge Findings
ECR Enhanced Scanning uses Amazon Inspector to scan not just OS packages but also application-layer dependencies — Node.js package.json, Python requirements.txt, Java POMs — that BasicScanning (Clair) entirely misses. Three things catch teams: Enhanced Scanning costs $0.11/image/month per actively stored image, so a repository with 200 images incurs $22/month on scanning alone — use lifecycle policies to cap image count before enabling; continuous scanning can re-flag a previously-clean image without a new push as new CVEs are published, which means a CI pipeline that only checks findings at push time may deploy a later-vulnerable image; and the Inspector2:FindingCreated EventBridge event fires for new findings on stored images, not just on-push — wire it to SNS or a Lambda to get proactive alerts when a zero-day hits a dependency used in production.
TL;DR
Enable Enhanced Scanning at the registry level via put-registry-scanning-configuration --scan-type ENHANCED. Set scan frequency to CONTINUOUS_SCAN (not just SCAN_ON_PUSH) for production repositories. In CI, poll describe-image-scan-findings until imageScanStatus.status == "ACTIVE", then fail the build on CRITICAL severity or CRITICAL with EPSS > 0.1. Subscribe to Inspector2:FindingCreated EventBridge events to detect new CVEs in already-deployed images. Export SBOM via export-sbom-for-image for compliance audit trails.
BasicScanning vs Enhanced Scanning comparison
| Feature | BasicScanning (Clair) | Enhanced Scanning (Inspector) |
|---|---|---|
| OS packages (rpm, deb, apk) | Yes | Yes |
| Application dependencies (npm, pip, Maven, Gem) | No | Yes |
| Programming language packages | No | Yes (Node, Python, Java, .NET, Go, Ruby) |
| Scan frequency | On-push only | On-push + continuous (re-scans stored images as new CVEs published) |
| SBOM export | No | Yes (CycloneDX, SPDX2) |
| EPSS score | No | Yes |
| EventBridge findings events | Limited (ScanCompleted only) | Rich (FindingCreated, FindingUpdated, FindingClosed) |
| Security Hub integration | No | Yes (if Security Hub enabled) |
| Cost | Free | $0.11/image/month per image actively stored |
For MCP servers, the npm dependency scan is the critical differentiator. A typical Node.js MCP server pulls 50-200 transitive npm dependencies. BasicScanning misses all of them. Enhanced Scanning catches vulnerabilities in node_modules lockfile-resolved packages even when the OS image (node:22-alpine) is clean.
Enabling Enhanced Scanning
Enhanced Scanning is configured at the registry level (per account per region), not per-repository. A repository filter pattern determines which repositories get Enhanced Scanning — others continue with BasicScanning or no scanning.
## Enable Enhanced Scanning on all repositories matching "mcp-server*"
aws ecr put-registry-scanning-configuration \
--scan-type ENHANCED \
--rules '[
{
"repositoryFilters": [
{ "filter": "mcp-server*", "filterType": "WILDCARD" }
],
"scanFrequency": "CONTINUOUS_SCAN"
},
{
"repositoryFilters": [
{ "filter": "*", "filterType": "WILDCARD" }
],
"scanFrequency": "SCAN_ON_PUSH"
}
]'
## Verify scanning configuration
aws ecr get-registry-scanning-configuration
## For a specific repository: check its scan type
aws ecr describe-repositories \
--repository-names mcp-server \
--query 'repositories[0].imageScanningConfiguration'
Scan frequency options: SCAN_ON_PUSH scans once when an image is pushed; CONTINUOUS_SCAN scans on push and then re-scans all images in the repository as new CVEs are published (typically within 24 hours of NVD/VulnDB publication); MANUAL disables automatic scanning. Use CONTINUOUS_SCAN for production repositories and SCAN_ON_PUSH for CI/dev repositories where cost matters more than continuous coverage.
CI pipeline: block on CRITICAL findings
Enhanced Scanning is asynchronous — findings are not available immediately after push. The scan starts within seconds but may take 2-5 minutes to complete for a large image. CI pipelines must poll for scan completion before acting on findings.
#!/usr/bin/env bash
# check-ecr-scan.sh — block CI deploy if CRITICAL CVEs found
# Usage: ./check-ecr-scan.sh mcp-server v1.2.3
REPO=$1
TAG=$2
MAX_WAIT=300 # 5 minutes
echo "Waiting for ECR Enhanced Scan to complete for $REPO:$TAG..."
ELAPSED=0
while true; do
STATUS=$(aws ecr describe-image-scan-findings \
--repository-name "$REPO" \
--image-id imageTag="$TAG" \
--query 'imageScanStatus.status' \
--output text 2>/dev/null)
if [ "$STATUS" = "ACTIVE" ]; then
echo "Scan complete."
break
elif [ "$STATUS" = "FAILED" ]; then
echo "Scan failed — check Inspector configuration"
exit 1
fi
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
echo "Scan timed out after ${MAX_WAIT}s — proceeding without block"
exit 0
fi
sleep 15
ELAPSED=$((ELAPSED + 15))
done
# Parse findings severity counts
CRITICAL=$(aws ecr describe-image-scan-findings \
--repository-name "$REPO" \
--image-id imageTag="$TAG" \
--query 'imageScanFindings.findingSeverityCounts.CRITICAL' \
--output text)
HIGH=$(aws ecr describe-image-scan-findings \
--repository-name "$REPO" \
--image-id imageTag="$TAG" \
--query 'imageScanFindings.findingSeverityCounts.HIGH' \
--output text)
CRITICAL=${CRITICAL:-0}
HIGH=${HIGH:-0}
echo "Findings: CRITICAL=$CRITICAL HIGH=$HIGH"
if [ "$CRITICAL" != "None" ] && [ "$CRITICAL" -gt 0 ]; then
echo "BLOCKING DEPLOY: $CRITICAL CRITICAL vulnerabilities found"
# Print the CVE IDs and fixed versions for the build log
aws ecr describe-image-scan-findings \
--repository-name "$REPO" \
--image-id imageTag="$TAG" \
--query 'imageScanFindings.enhancedFindings[?severity==`CRITICAL`].[title,packageVulnerabilityDetails.vulnerabilityId,packageVulnerabilityDetails.vulnerablePackages[0].fixedInVersions[0]]' \
--output table
exit 1
fi
echo "No CRITICAL findings — deploy approved"
exit 0
EPSS-gated blocking: a CVE can be CRITICAL by CVSS score but have a near-zero probability of being exploited. The EPSS (Exploit Prediction Scoring System) score (0.0–1.0) indicates the probability that a CVE will be exploited in the wild within 30 days. For security-conscious teams, block only on CRITICAL CVEs with EPSS > 0.1 (10% exploit probability) to reduce false-positive deploy blocks.
// Node.js: block on CRITICAL+EPSS threshold using AWS SDK
import { ECRClient, DescribeImageScanFindingsCommand } from "@aws-sdk/client-ecr";
const ecr = new ECRClient({ region: process.env.AWS_REGION });
async function checkScanFindings(repo: string, tag: string): Promise {
const result = await ecr.send(new DescribeImageScanFindingsCommand({
repositoryName: repo,
imageId: { imageTag: tag },
}));
const findings = result.imageScanFindings?.enhancedFindings ?? [];
const blockers = findings.filter(f => {
if (f.severity !== "CRITICAL") return false;
// Block if no EPSS data (conservative) or EPSS > 0.1
const epss = f.epss?.score ?? 1.0;
return epss > 0.1;
});
if (blockers.length > 0) {
console.error(`BLOCKING: ${blockers.length} CRITICAL+EPSS findings:`);
blockers.forEach(f => {
console.error(` ${f.packageVulnerabilityDetails?.vulnerabilityId} — EPSS: ${f.epss?.score?.toFixed(3)}`);
});
process.exit(1);
}
}
EventBridge: proactive alerts on new CVEs in stored images
Enhanced Scanning continuously monitors stored images and fires EventBridge events when new CVEs are discovered in images already deployed. This is the key advantage over scan-on-push: a zero-day that hits a package used in your production image triggers an alert even if you haven't pushed a new image in weeks.
// EventBridge rule: alert on new CRITICAL Inspector findings in ECR
{
"source": ["aws.inspector2"],
"detail-type": ["Inspector2 Finding"],
"detail": {
"severity": ["CRITICAL"],
"resources": {
"type": ["AWS_ECR_CONTAINER_IMAGE"]
},
"status": ["ACTIVE"]
}
}
// The event detail includes:
// - findingArn: unique ARN for the finding
// - severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFORMATIONAL" | "UNTRIAGED"
// - title: human-readable CVE description
// - packageVulnerabilityDetails.vulnerabilityId: CVE-YYYY-NNNNN
// - packageVulnerabilityDetails.vulnerablePackages[].name: package name
// - packageVulnerabilityDetails.vulnerablePackages[].fixedInVersions: if a fix exists
// - epss.score: 0.0-1.0 exploit probability
// - resources[0].details.awsEcrContainerImage.repositoryName: which repo
// - resources[0].details.awsEcrContainerImage.imageTags: which tag(s)
// CDK: wire Inspector2 ECR findings to SNS for Slack/PagerDuty alerts
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as sns from "aws-cdk-lib/aws-sns";
const alertTopic = new sns.Topic(this, "CriticalCveTopic");
new events.Rule(this, "CriticalEcrFinding", {
eventPattern: {
source: ["aws.inspector2"],
detailType: ["Inspector2 Finding"],
detail: {
severity: ["CRITICAL"],
status: ["ACTIVE"],
resources: {
type: ["AWS_ECR_CONTAINER_IMAGE"],
},
},
},
targets: [new targets.SnsTopic(alertTopic)],
});
SBOM export for compliance audit
Enhanced Scanning generates a Software Bill of Materials (SBOM) for each scanned image. Export the SBOM in CycloneDX or SPDX2 format for compliance requirements (FedRAMP, SOC 2, supply-chain attestation).
## Export SBOM in CycloneDX format for a specific image
aws ecr create-sbom-export \
--repository-name mcp-server \
--image-id imageTag=v1.2.3 \
--output-format CYCLONEDX_1_4
## The response includes a downloadUrl (pre-signed S3 URL, valid 24h):
# {
# "downloadUrl": "https://ecr-sbom-export.s3.amazonaws.com/...",
# "outputFormat": "CYCLONEDX_1_4"
# }
curl -o sbom-v1.2.3.json "$(aws ecr create-sbom-export \
--repository-name mcp-server \
--image-id imageTag=v1.2.3 \
--output-format CYCLONEDX_1_4 \
--query 'downloadUrl' --output text)"
## Available output formats:
## CYCLONEDX_1_4 — CycloneDX JSON (most tool-compatible)
## SPDX_2_3 — SPDX JSON format
The SBOM includes all detected packages, their versions, and licenses. It does not require any build-time instrumentation — Inspector derives it from the image layers after push. Store the SBOM alongside the image tag in an S3 bucket as part of the release artifact bundle.
Suppression rules for accepted risks
Some CVEs have no available fix or are not relevant to your deployment context (a CVE in a GUI library doesn't matter in a headless server). Inspector suppression rules (called "suppression filters") let you hide accepted findings without disabling scanning.
## Create a suppression filter via the Inspector2 API
aws inspector2 create-filter \
--name "AcceptedNodeCVEs" \
--description "CVEs accepted as low-risk for MCP probe worker" \
--action SUPPRESS \
--filter-criteria '{
"findingStatus": [{ "comparison": "EQUALS", "value": "ACTIVE" }],
"resourceType": [{ "comparison": "EQUALS", "value": "AWS_ECR_CONTAINER_IMAGE" }],
"vulnerabilityId": [
{ "comparison": "EQUALS", "value": "CVE-2023-XXXXX" }
],
"resourceTags": [
{ "comparison": "EQUALS", "key": "env", "value": "staging" }
]
}'
## Suppressed findings still appear in Inspector but with status SUPPRESSED,
## not ACTIVE — they do not trigger EventBridge FindingCreated events
## and are excluded from severity counts in the ECR console
AliveMCP: scanning probe worker images
AliveMCP's probe worker image is the most security-sensitive image in the deployment — it runs in every customer's AWS region, makes outbound HTTP connections to thousands of MCP endpoints, and handles response data from untrusted servers. Enhanced Scanning is enabled on the alivemcp/probe-worker repository with CONTINUOUS_SCAN frequency. The CI pipeline blocks on CRITICAL + EPSS > 0.05 (5%) — a lower threshold than the default because the probe worker has a larger attack surface than typical internal services. New CRITICAL findings on stored production images trigger a PagerDuty alert via the Inspector2:FindingCreated EventBridge rule, escalating to on-call within 5 minutes. SBOMs are exported and stored in S3 on every release for supply-chain attestation.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
imageScanStatus.status stays IN_PROGRESS indefinitely | Inspector2 may not be enabled in this region; or the image was pushed before Enhanced Scanning was configured | Check aws inspector2 list-delegated-admin-accounts; ensure Inspector2 is active; re-push image to trigger new scan |
| No findings returned even for known-vulnerable images | BasicScanning (not Enhanced) is active for this repository — imageScanFindings.enhancedFindings is only populated for Enhanced Scanning | Verify registry scanning configuration with get-registry-scanning-configuration; check that the repository name matches the filter pattern |
| CI blocks on HIGH finding that has no fix | Blocking on all HIGH severity regardless of fix availability | Filter on packageVulnerabilityDetails.vulnerablePackages[].fixedInVersions — only block when a fix is available |
| EventBridge does not fire for re-scanned images | Using SCAN_ON_PUSH instead of CONTINUOUS_SCAN — events only fire at push time | Change scan frequency to CONTINUOUS_SCAN for repositories that need ongoing alerts |
| SBOM export fails: "image not scanned" | SBOM requires Enhanced Scanning; BasicScanning does not generate SBOM data | Enable Enhanced Scanning for the repository and re-scan the image before exporting |
| Inspector2 findings appear in Security Hub but ECR console shows no findings | Enhanced Scanning findings are Inspector2 findings — they appear in Security Hub if integration is enabled; the ECR console shows BasicScanning findings separately | Check both ECR console (basic) and Inspector2 console (enhanced) for complete findings coverage |