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

FeatureBasicScanning (Clair)Enhanced Scanning (Inspector)
OS packages (rpm, deb, apk)YesYes
Application dependencies (npm, pip, Maven, Gem)NoYes
Programming language packagesNoYes (Node, Python, Java, .NET, Go, Ruby)
Scan frequencyOn-push onlyOn-push + continuous (re-scans stored images as new CVEs published)
SBOM exportNoYes (CycloneDX, SPDX2)
EPSS scoreNoYes
EventBridge findings eventsLimited (ScanCompleted only)Rich (FindingCreated, FindingUpdated, FindingClosed)
Security Hub integrationNoYes (if Security Hub enabled)
CostFree$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

SymptomCauseFix
imageScanStatus.status stays IN_PROGRESS indefinitelyInspector2 may not be enabled in this region; or the image was pushed before Enhanced Scanning was configuredCheck aws inspector2 list-delegated-admin-accounts; ensure Inspector2 is active; re-push image to trigger new scan
No findings returned even for known-vulnerable imagesBasicScanning (not Enhanced) is active for this repository — imageScanFindings.enhancedFindings is only populated for Enhanced ScanningVerify 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 fixBlocking on all HIGH severity regardless of fix availabilityFilter on packageVulnerabilityDetails.vulnerablePackages[].fixedInVersions — only block when a fix is available
EventBridge does not fire for re-scanned imagesUsing SCAN_ON_PUSH instead of CONTINUOUS_SCAN — events only fire at push timeChange 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 dataEnable Enhanced Scanning for the repository and re-scan the image before exporting
Inspector2 findings appear in Security Hub but ECR console shows no findingsEnhanced Scanning findings are Inspector2 findings — they appear in Security Hub if integration is enabled; the ECR console shows BasicScanning findings separatelyCheck both ECR console (basic) and Inspector2 console (enhanced) for complete findings coverage