Key Takeaways

  • Amazon ECR offers two scanning tiers: basic scanning (OS package CVEs via Clair, on push) and enhanced scanning (continuous rescanning plus OS and programming-language package vulnerabilities, powered by Amazon Inspector).
  • docker container security scanning should run at three points: locally before commit, in CI before merge, and continuously in the registry after push — no single point catches everything.
  • Enhanced scanning re-evaluates images automatically whenever a new CVE is published, not just at push time — a critical difference from basic scanning.
  • Findings can be routed to EventBridge and automatically fail a deployment pipeline when a CRITICAL or HIGH severity finding appears above a defined threshold.
  • Scanning catches known CVEs; it doesn't replace minimal base images, non-root users, and signed images — all layers of the same defense.

A docker image that passed a security review six months ago is not secure today — new CVEs get published against the same OS packages and language dependencies baked into that image every week. docker security scanning on AWS means treating vulnerability scanning as a continuous control, not a one-time gate, and that's exactly the architecture ECR enhanced scanning and Amazon Inspector are built for.

ECR Basic Scanning vs. Enhanced Scanning

FeatureBasic scanningEnhanced scanning
EngineClair (open source)Amazon Inspector
CoverageOS packages onlyOS packages + language packages (npm, pip, gems, Maven, Go modules)
TriggerOn push onlyOn push, plus continuous rescans on new CVE publication
Findings locationECR consoleECR console + Inspector console + EventBridge
CostFreePriced per image scanned

Enabling enhanced scanning at the registry level:

aws ecr put-registry-scanning-configuration \
  --scan-type ENHANCED \
  --rules '[
    {
      "scanFrequency": "CONTINUOUS_SCAN",
      "repositoryFilters": [{ "filter": "*", "filterType": "WILDCARD" }]
    }
  ]'

This turns on continuous scanning for every repository in the registry. For most production environments, continuous scanning on all repos is worth the extra cost — a CVE published against a package in an image you pushed three months ago still needs to surface.

Scanning in the CI/CD Pipeline, Before Push

Waiting until an image lands in ECR to find vulnerabilities means they've already been built, tested, and are one step from deploy. Scan earlier with an open-source scanner like Trivy inside your build job:

# .github/workflows/build.yml (excerpt)
- name: Build image
  run: docker build -t app:${{ github.sha }} .

- name: Scan image with Trivy
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: "app:${{ github.sha }}"
    severity: "CRITICAL,HIGH"
    exit-code: "1"          # fail the job if findings exist
    ignore-unfixed: true    # don't block on CVEs with no available fix

exit-code: "1" is the actual gate — without it, Trivy reports findings but the pipeline continues regardless.

Reacting to ECR Findings Automatically

Enhanced scanning publishes findings to EventBridge, so you can react without polling the console:

{
  "source": ["aws.inspector2"],
  "detail-type": ["Inspector2 Finding"],
  "detail": {
    "severity": ["CRITICAL", "HIGH"]
  }
}

Route that rule to a Lambda function that checks whether the affected image tag is deployed to production, and if so, notifies the on-call channel or automatically rolls back:

import boto3

ecs = boto3.client("ecs")

def lambda_handler(event, context):
    finding = event["detail"]
    image_uri = finding["resources"][0]["id"]
    severity = finding["severity"]

    if severity in ("CRITICAL", "HIGH"):
        # Check if any running ECS service uses this image tag
        services = ecs.list_services(cluster="production")["serviceArns"]
        for service_arn in services:
            task_def = ecs.describe_services(
                cluster="production", services=[service_arn]
            )["services"][0]["taskDefinition"]
            if image_uri in str(ecs.describe_task_definition(taskDefinition=task_def)):
                print(f"ALERT: {severity} finding affects running service {service_arn}")
    return {"status": "processed"}

Beyond Scanning: Reducing What There Is to Find

Scanning tells you what's wrong; it doesn't fix the habits that create findings in the first place.

  • Use minimal base images — distroless or alpine images ship far fewer OS packages than ubuntu:latest, meaning far fewer CVEs to scan for in the first place.
  • Run as non-root — add USER appuser in your Dockerfile so a container compromise doesn't hand over root inside the container.
  • Pin versions — FROM node:20.11.1-alpine instead of FROM node:latest so you know exactly what you're scanning and can reproduce builds.
  • Rebuild regularly — even with no code changes, rebuild base images weekly so OS package patches land before a scanner flags them as outstanding.
FROM node:20.11.1-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm ci --omit=dev
USER appuser
CMD ["node", "server.js"]

Scanning Coverage Across the Pipeline

<svg viewBox="0 0 640 180" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Docker image scanning stages from local build to runtime">
  <style>
    .stage { fill: none; stroke: currentColor; stroke-width: 1.5; }
    .lbl { fill: currentColor; font-family: sans-serif; font-size: 12px; }
    .arrow { stroke: currentColor; stroke-width: 1.5; marker-end: url(#a2); }
  </style>
  <defs>
    <marker id="a2" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto">
      <path d="M0,0 L8,4 L0,8 Z" fill="currentColor"/>
    </marker>
  </defs>
  <rect class="stage" x="10" y="60" width="120" height="50"/>
  <text class="lbl" x="22" y="90">Local build</text>
  <rect class="stage" x="175" y="60" width="120" height="50"/>
  <text class="lbl" x="187" y="82">CI (Trivy)</text>
  <text class="lbl" x="187" y="98">pre-merge</text>
  <rect class="stage" x="340" y="60" width="120" height="50"/>
  <text class="lbl" x="352" y="82">ECR Enhanced</text>
  <text class="lbl" x="352" y="98">on push</text>
  <rect class="stage" x="505" y="60" width="120" height="50"/>
  <text class="lbl" x="517" y="82">Continuous</text>
  <text class="lbl" x="517" y="98">rescans</text>
  <path class="arrow" d="M130,85 L175,85"/>
  <path class="arrow" d="M295,85 L340,85"/>
  <path class="arrow" d="M460,85 L505,85"/>
</svg>

FAQ

Does ECR basic scanning catch vulnerabilities in application dependencies like npm packages? No. Basic scanning (Clair) only covers OS-level packages. Enhanced scanning, powered by Amazon Inspector, adds coverage for npm, pip, Maven, Go modules, and other language ecosystem packages.

Will enhanced scanning re-scan an image I pushed last month if a new CVE comes out today? Yes — that's the core difference from basic scanning. Continuous scan mode automatically re-evaluates stored images against newly published CVEs without requiring a new push.

Should scanning happen in CI, in the registry, or both? Both, plus locally if possible. CI scanning (e.g., Trivy) blocks a build before merge; registry scanning (ECR) catches drift from new CVEs published after the image already shipped.

What should happen when a CRITICAL vulnerability is found in a running production image? Route the Inspector/EventBridge finding to an automated check against your running task definitions or pod specs, and alert or trigger a rollback if the affected image is live — don't rely on someone checking the console.

Build a Continuous Scanning Pipeline

f9infotech helps teams wire ECR enhanced scanning, CI gates, and EventBridge-driven alerting into one pipeline so vulnerable images get caught before — not after — they reach production. Talk to our DevSecOps team about a container security assessment.