Find ECR Repositories Without Image Scanning and Enable It

Stacks of colorful shipping containers at a port terminal under a clear sky

Photo by taro ohtani on Unsplash

To enable ECR image scanning for every repository, call PutRegistryScanningConfiguration with a rule whose repositoryFilters is * (type WILDCARD): SCAN_ON_PUSH for basic scanning, or SCAN_ON_PUSH or CONTINUOUS_SCAN with scanType: "ENHANCED" for Amazon Inspector. BatchGetRepositoryScanningConfiguration shows which repositories aren’t scanned automatically today.

Container images carry an operating system and language packages, and new CVEs are published against them every week. Amazon ECR can scan images for you, but only for repositories that match your registry’s scanning rules; everything else is set to manual scanning and quietly never scanned. This example is for engineers who want to find the gaps and enable ECR image scanning across every Region without clicking through each registry.

The TypeScript script uses the AWS SDK for JavaScript v3, reports by default and changes the registry only with --apply. It pairs with the script to set an ECR lifecycle policy to delete old images: fewer stale images means fewer findings nobody will fix and, with enhanced scanning, fewer images to pay for.

Basic or enhanced scanning?

Scanning is configured per registry, which means per account and Region, with up to two rules of up to 100 repository filters each.

Basic scanning Enhanced scanning (Amazon Inspector)
Finds Operating system package CVEs OS and programming language package vulnerabilities
Frequencies Scan on push, or manual Scan on push, or continuous
Repositories not matching a filter Manual: you start each scan Off: not scanned, and manual scans aren’t supported
Limits Each image once per 24 hours; up to 100,000 images per 24 hours per registry Only images pushed in the 14 days before you turn it on are picked up; push older ones again
Cost No additional charge Billed by Amazon Inspector per image

Filters behave in a way that surprises people: a filter without a wildcard matches every repository whose name contains it, so prod also matches repo-prod-repo. A lone * matches everything, which is what the script uses. With enhanced scanning, a continuous filter wins over a scan-on-push filter for the same repository. Switching the registry between basic and enhanced also hides the scans you had under the old type until you switch back.

The repository-level scanOnPush flag still shows up in DescribeRepositories, but AWS is deprecating PutImageScanningConfiguration in favor of registry-level rules, so the script reads the effective frequency from BatchGetRepositoryScanningConfiguration instead.

What does enhanced scanning cost?

From the Amazon Inspector pricing page and the AWS Price List for US East (N. Virginia), checked September 2026:

Item Price
Initial scan of an image pushed to ECR $0.09 per image
Rescan in continuous mode $0.01 per image per rescan
New Inspector accounts 15-day free trial

Rescans happen when Inspector’s vulnerability database changes, so their number varies. A team pushing 200 images a month pays 200 × $0.09 = $18 for initial scans. If 300 images stay in continuous scanning and each is rescanned 10 times that month, that adds 300 × 10 × $0.01 = $30. Scan on push avoids rescan charges; a lifecycle policy and Inspector’s re-scan duration setting cap how many images stay covered.

What does the script do?

  1. Lists RegionsDescribeRegions, or --regions=.
  2. Reads the registry configurationGetRegistryScanningConfiguration returns the scan type and rules.
  3. Reads each repository’s effective frequencypaginateDescribeRepositories, then BatchGetRepositoryScanningConfiguration in batches of 25, the API maximum.
  4. Checks the newest imagepaginateDescribeImages finds the latest push; DescribeImageScanFindings returns its scan status and findingSeverityCounts. A ScanNotFoundException means it was never scanned.
  5. Fixes on requestWith --apply, and only in Regions with unscanned repositories: PutRegistryScanningConfiguration with a * rule, keeping existing rules that stay valid.

Prerequisites

Which IAM permissions does it need?

Replace the account ID. Registry-level actions and DescribeRepositories are granted on *. The last three statements are for --apply; the Inspector and service-linked role permissions come from the ECR guide for enabling enhanced scanning, and basic scanning doesn’t need them.

ecr-scanning-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RegistryAndRegions",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeRegions",
        "ecr:GetRegistryScanningConfiguration",
        "ecr:DescribeRepositories"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ReadRepositoriesAndFindings",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchGetRepositoryScanningConfiguration",
        "ecr:DescribeImages",
        "ecr:DescribeImageScanFindings"
      ],
      "Resource": "arn:aws:ecr:*:111122223333:repository/*"
    },
    {
      "Sid": "PutRegistryScanningApplyOnly",
      "Effect": "Allow",
      "Action": "ecr:PutRegistryScanningConfiguration",
      "Resource": "*"
    },
    {
      "Sid": "EnhancedScanningApplyOnly",
      "Effect": "Allow",
      "Action": [
        "inspector2:Enable",
        "inspector2:ListAccountPermissions",
        "inspector2:ListFindings",
        "inspector2:ListCoverage"
      ],
      "Resource": "*"
    },
    {
      "Sid": "InspectorServiceLinkedRoleApplyOnly",
      "Effect": "Allow",
      "Action": "iam:CreateServiceLinkedRole",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "iam:AWSServiceName": "inspector2.amazonaws.com"
        }
      }
    }
  ]
}

The script to enable ECR image scanning

enable-ecr-image-scanning.ts

// enable-ecr-image-scanning.ts
// Reports the ECR registry scanning type (BASIC or ENHANCED) in each Region, the effective scan frequency of
// every private repository, and the scan status and CRITICAL/HIGH counts of each repository's newest image.
// Dry run by default: with --apply it adds a registry scanning rule that matches every repository
// (scan on push for basic; add --enhanced for Amazon Inspector, --continuous for continuous scanning).
// Usage: npx tsx enable-ecr-image-scanning.ts [--regions=us-east-1] [--apply [--enhanced [--continuous]]]
import { EC2Client, DescribeRegionsCommand } from "@aws-sdk/client-ec2";
import {
  ECRClient,
  GetRegistryScanningConfigurationCommand,
  PutRegistryScanningConfigurationCommand,
  BatchGetRepositoryScanningConfigurationCommand,
  DescribeImageScanFindingsCommand,
  paginateDescribeRepositories,
  paginateDescribeImages,
  type ImageDetail,
  type RegistryScanningRule,
  type ScanFrequency,
} from "@aws-sdk/client-ecr";

const args = process.argv.slice(2);
const apply = args.includes("--apply");
const enhanced = args.includes("--enhanced");
const continuous = args.includes("--continuous");
const regionArg = args.find((a) => a.startsWith("--regions="))?.split("=")[1]?.split(",").map((s) => s.trim()).filter(Boolean);
if (continuous && !enhanced) {
  console.error("--continuous needs --enhanced (basic scanning supports scan on push and manual scans only)");
  process.exit(1);
}

interface Row {
  Region: string;
  Repository: string;
  Frequency: string;
  LatestImage: string;
  ScanStatus: string;
  Critical: number | string;
  High: number | string;
}

async function listRegions(): Promise<string[]> {
  if (regionArg) return regionArg;
  const out = await new EC2Client({}).send(new DescribeRegionsCommand({}));
  return (out.Regions ?? []).map((r) => r.RegionName ?? "").filter(Boolean).sort();
}

async function newestImage(ecr: ECRClient, repositoryName: string): Promise<ImageDetail | undefined> {
  let newest: ImageDetail | undefined;
  for await (const page of paginateDescribeImages({ client: ecr }, { repositoryName })) {
    for (const img of page.imageDetails ?? []) {
      if (!newest || (img.imagePushedAt?.getTime() ?? 0) > (newest.imagePushedAt?.getTime() ?? 0)) newest = img;
    }
  }
  return newest;
}

async function scanSummary(ecr: ECRClient, repositoryName: string, img: ImageDetail): Promise<Pick<Row, "ScanStatus" | "Critical" | "High">> {
  try {
    const out = await ecr.send(new DescribeImageScanFindingsCommand({ repositoryName, imageId: { imageDigest: img.imageDigest }, maxResults: 1 }));
    const counts = out.imageScanFindings?.findingSeverityCounts ?? {};
    return { ScanStatus: out.imageScanStatus?.status ?? "?", Critical: counts.CRITICAL ?? 0, High: counts.HIGH ?? 0 };
  } catch (err) {
    const name = err instanceof Error ? err.name : String(err);
    return { ScanStatus: name === "ScanNotFoundException" ? "NEVER SCANNED" : name, Critical: "-", High: "-" };
  }
}

// Keep the existing rules that stay valid, and make the target frequency match every repository ("*").
function newRules(current: RegistryScanningRule[], target: ScanFrequency): RegistryScanningRule[] {
  const valid: ScanFrequency[] = enhanced ? ["SCAN_ON_PUSH", "CONTINUOUS_SCAN"] : ["SCAN_ON_PUSH"];
  const kept = current.filter((r) => r.scanFrequency && valid.includes(r.scanFrequency) && r.scanFrequency !== target);
  return [...kept, { scanFrequency: target, repositoryFilters: [{ filter: "*", filterType: "WILDCARD" }] }];
}

async function scanRegion(region: string): Promise<Row[]> {
  const ecr = new ECRClient({ region });
  const config = (await ecr.send(new GetRegistryScanningConfigurationCommand({}))).scanningConfiguration;
  const scanType = config?.scanType ?? "BASIC";
  const names: string[] = [];
  for await (const page of paginateDescribeRepositories({ client: ecr }, {})) {
    for (const repo of page.repositories ?? []) if (repo.repositoryName) names.push(repo.repositoryName);
  }
  if (!names.length) return [];
  console.log(`${region}: ${scanType} scanning, ${config?.rules?.length ?? 0} rule(s), ${names.length} repository(ies)`);

  const frequency = new Map<string, string>();
  for (let i = 0; i < names.length; i += 25) {
    const out = await ecr.send(new BatchGetRepositoryScanningConfigurationCommand({ repositoryNames: names.slice(i, i + 25) }));
    for (const c of out.scanningConfigurations ?? []) frequency.set(c.repositoryName ?? "", c.scanFrequency ?? "?");
  }

  const rows: Row[] = [];
  for (const name of names) {
    const img = await newestImage(ecr, name);
    const base = { Region: region, Repository: name, Frequency: `${scanType}/${frequency.get(name) ?? "?"}` };
    if (!img?.imageDigest) {
      rows.push({ ...base, LatestImage: "(empty)", ScanStatus: "-", Critical: "-", High: "-" });
      continue;
    }
    const pushed = img.imagePushedAt?.toISOString().slice(0, 10) ?? "?";
    rows.push({ ...base, LatestImage: `${img.imageTags?.[0] ?? img.imageDigest.slice(7, 19)} (${pushed})`, ...(await scanSummary(ecr, name, img)) });
  }

  const unscanned = names.filter((n) => !["SCAN_ON_PUSH", "CONTINUOUS_SCAN"].includes(frequency.get(n) ?? ""));
  if (apply && unscanned.length) {
    const target: ScanFrequency = continuous ? "CONTINUOUS_SCAN" : "SCAN_ON_PUSH";
    const rules = newRules(config?.rules ?? [], target);
    await ecr.send(new PutRegistryScanningConfigurationCommand({ scanType: enhanced ? "ENHANCED" : "BASIC", rules }));
    console.log(`${region}: registry set to ${enhanced ? "ENHANCED" : "BASIC"} with ${target} for "*" (${unscanned.length} repository(ies) were not scanned automatically)`);
  }
  return rows;
}

async function main(): Promise<void> {
  if (apply && enhanced) console.log("Enhanced scanning is billed by Amazon Inspector per image scanned and rescanned.");
  const rows: Row[] = [];
  for (const region of await listRegions()) {
    try {
      rows.push(...(await scanRegion(region)));
    } catch (err) {
      console.error(`${region}: ${err instanceof Error ? err.name : String(err)}`);
    }
  }
  const auto = (r: Row) => /SCAN_ON_PUSH|CONTINUOUS_SCAN/.test(r.Frequency);
  rows.sort((a, b) => Number(auto(a)) - Number(auto(b)) || (Number(b.Critical) || 0) - (Number(a.Critical) || 0));
  console.table(rows);
  const manual = rows.filter((r) => !auto(r)).length;
  console.log(`${rows.length} repository(ies); ${manual} without automatic scanning.`);
  if (!apply && manual) {
    console.log("Dry run. Re-run with --apply (basic scan on push) or --apply --enhanced (Amazon Inspector).");
    process.exitCode = 2;
  }
}

main().catch((err) => {
  console.error(err instanceof Error ? `${err.name}: ${err.message}` : err);
  process.exit(1);
});

How do you run it?

Terminal

npm install @aws-sdk/client-ecr @aws-sdk/client-ec2
npm install --save-dev tsx typescript

# Report every enabled Region (dry run)
AWS_PROFILE=security-audit npx tsx enable-ecr-image-scanning.ts

# Basic scan on push for every repository in two Regions
AWS_PROFILE=platform-admin npx tsx enable-ecr-image-scanning.ts --regions=us-east-1,eu-west-1 --apply

# Amazon Inspector, continuous scanning for every repository
AWS_PROFILE=platform-admin npx tsx enable-ecr-image-scanning.ts --regions=us-east-1 --apply --enhanced --continuous

The dry run exits with code 2 when any repository has no automatic scanning. The script reads every image in each repository to find the newest, so very large registries take a while; limit Regions while you test.

Sample output

Output

eu-west-1: BASIC scanning, 0 rule(s), 2 repository(ies)
us-east-1: BASIC scanning, 1 rule(s), 3 repository(ies)
┌─────────┬─────────────┬──────────────────┬──────────────────────┬─────────────────────────────┬─────────────────┬──────────┬──────┐
│ (index) │ Region      │ Repository       │ Frequency            │ LatestImage                 │ ScanStatus      │ Critical │ High │
├─────────┼─────────────┼──────────────────┼──────────────────────┼─────────────────────────────┼─────────────────┼──────────┼──────┤
│ 0       │ 'eu-west-1' │ 'billing-worker' │ 'BASIC/MANUAL'       │ 'v2.14.0 (2026-09-21)'      │ 'NEVER SCANNED' │ '-'      │ '-'  │
│ 1       │ 'eu-west-1' │ 'legacy/cron'    │ 'BASIC/MANUAL'       │ 'latest (2025-11-03)'       │ 'COMPLETE'      │ 2        │ 9    │
│ 2       │ 'us-east-1' │ 'web-frontend'   │ 'BASIC/SCAN_ON_PUSH' │ '3f9c1e7a2b40 (2026-09-26)' │ 'COMPLETE'      │ 1        │ 4    │
│ 3       │ 'us-east-1' │ 'api'            │ 'BASIC/SCAN_ON_PUSH' │ 'v5.2.1 (2026-09-27)'       │ 'COMPLETE'      │ 0        │ 2    │
│ 4       │ 'us-east-1' │ 'sandbox'        │ 'BASIC/SCAN_ON_PUSH' │ '(empty)'                   │ '-'             │ '-'      │ '-'  │
└─────────┴─────────────┴──────────────────┴──────────────────────┴─────────────────────────────┴─────────────────┴──────────┴──────┘
5 repository(ies); 2 without automatic scanning.
Dry run. Re-run with --apply (basic scan on push) or --apply --enhanced (Amazon Inspector).

The names are illustrative. Both eu-west-1 repositories fall outside every scanning rule. legacy/cron was scanned once by hand and still has 2 CRITICAL findings; billing-worker has never been scanned at all. After --apply, new pushes are scanned, but existing images need a manual scan (basic) or a fresh push (enhanced).

ECR takes each CVE’s severity from the upstream distribution when it can, otherwise from its CVSS score; the NVD vulnerability severity ratings explain how those scores map to CRITICAL, HIGH and the rest.

Troubleshooting

  • ScanNotFoundException on every image. The repository has never matched a rule. Run --apply, then push again or start a basic scan with aws ecr start-image-scan.
  • Status SCAN_ELIGIBILITY_EXPIRED. With enhanced scanning, the image is older than the 14-day window at enablement or past Inspector’s re-scan duration. Push it again if it’s still deployed.
  • Status UNSUPPORTED_IMAGE. The image’s operating system isn’t one the scanner supports. Check the supported list and rebuild on a supported base.
  • BlockedByOrganizationPolicyException. The account’s scanning configuration is managed by an organization policy; change it there.

Scanning finds vulnerable packages, not secrets or misconfiguration. The script to find secrets in ECS task definitions covers what the containers are given at run time, and finding EKS clusters with a public endpoint covers where they run. For the infrastructure around those images, the script to detect CloudFormation drift across all stacks catches configuration someone changed outside the template.

Ask ChatWithCloud instead

ChatWithCloud turns a plain-English question into AWS SDK for JavaScript v2 code, runs it on your machine with your profile and sends the JSON result to the AI model for the answer. “Which ECR repositories in us-east-1 aren’t scanned on push?” or “How many CRITICAL findings does the newest api image have?” read the same data. It uses one Region per session. When you’re ready to enable ECR image scanning, the script’s dry run followed by --apply lets you review the change first. Generated code runs without a confirmation step, so use a read-only AWS profile with ChatWithCloud; the ChatWithCloud security page explains what leaves your machine.

Frequently asked questions

How do I enable scan on push for all ECR repositories?

Run aws ecr put-registry-scanning-configuration --scan-type BASIC --rules '[{"scanFrequency":"SCAN_ON_PUSH","repositoryFilters":[{"filter":"*","filterType":"WILDCARD"}]}]' in each Region. The call replaces the existing rules.

Is ECR basic scanning free?

Yes, basic scanning has no additional charge. Enhanced scanning is billed by Amazon Inspector per image scanned and rescanned.

Why doesn’t DescribeImages show scan results?

The current version of basic scanning doesn’t fill imageScanFindingsSummary in DescribeImages. Use DescribeImageScanFindings.

Can I scan images that are already in ECR?

With basic scanning, start a manual scan once per image per 24 hours. With enhanced scanning, images pushed before the 14-day window must be pushed again.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud