# Container Image SBOM and Vulnerability Scanner (`jetlaunch/container-image-scanner`) Actor

Scan public images from Docker Hub, GHCR, Quay and Amazon ECR Public without a Docker daemon. Get an SBOM (CycloneDX or SPDX) and known vulnerabilities with severities and fixed versions, plus the database build date.

- **URL**: https://apify.com/jetlaunch/container-image-scanner.md
- **Developed by:** [Chris O'Byrne](https://apify.com/jetlaunch) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Container Image SBOM and Vulnerability Scanner

Scan public container images straight from their registry and get two things back for each image: a software bill of materials (SBOM) in CycloneDX or SPDX format, and a list of known vulnerabilities found in the packages inside the image. No Docker daemon is needed and no image is run. The Actor reads the image layers over the registry API, identifies the operating system and language packages, and matches them against a vulnerability database that is built into the Actor.

Results are decision support, not a security guarantee. Read the limitations below before you act on them.

### What does it do?

For every image reference you give, the Actor:

1. Resolves the tag to an exact image digest (for the platform you choose) and checks the image size.
2. Reads the image from the registry anonymously and builds an SBOM of every operating system package (apk, deb, rpm) and language package it can identify (for example npm, PyPI, Maven, Go, Cargo, RubyGems).
3. In full mode, matches those packages against the vulnerability database and returns each finding with its identifier, severity, package, installed version and fixed version.
4. Saves the SBOM file in the run's key-value store and returns a download link.
5. Tells you when the vulnerability database was built, so you can judge how current the result is.

The scanner is [Trivy](https://github.com/aquasecurity/trivy) 0.75.0 (Apache-2.0), built from source for this Actor. This Actor is not affiliated with or endorsed by Aqua Security.

### Who is it for?

- Developers and DevOps teams who want a quick check of a base image or a third-party image before they use it.
- Security and compliance teams who need an SBOM file for an image they do not build themselves.
- Anyone who wants to run the same scan from a script, a schedule or an integration without installing a scanner.

### Supported registries

Public images on Docker Hub (docker.io), GitHub Container Registry (ghcr.io), Quay (quay.io) and Amazon ECR Public (public.ecr.aws). Images are pulled anonymously. Private images and registry credentials are not supported in this version, and credentials in an image reference are rejected.

### Input

| Field | Meaning |
| --- | --- |
| Image references | Up to 20 images as `name:tag` or `name@sha256:digest`, for example `alpine:3.19`, `ghcr.io/org/app:1.2`, `quay.io/org/app:latest`, `public.ecr.aws/docker/library/alpine:3.19`. |
| Mode | `full` (SBOM and vulnerabilities) or `sbom-only`. |
| Minimum severity to list | all, low, medium, high or critical. Summary counts always cover every severity. |
| Only list vulnerabilities with a fix | Leaves out findings that have no fixed version yet. Summary counts still include them. |
| SBOM format | CycloneDX JSON or SPDX JSON. |
| Platform | `linux/amd64` or `linux/arm64`, used when an image is published for several architectures. |
| Maximum packages per image | Safety limit, default 3000. An image above the limit is reported as an error and is not charged. |

Example input:

```json
{
  "images": ["public.ecr.aws/docker/library/alpine:3.19"],
  "mode": "full",
  "minSeverity": "all",
  "sbomFormat": "cyclonedx"
}
```

### Output

One dataset item per image, plus a `SUMMARY` record and the SBOM files in the key-value store. Main fields: `image`, `status`, `digest`, `os`, `packageCount`, `packageTypes`, `vulnerabilities[]` (id, severity, package, installedVersion, fixedVersion, status, packageType, purl, cvssScore, title, url, source), `summary` (counts by severity), `sbomKey`, `sbomUrl`, `dbBuildDate`, `toolVersions`, `notes`.

Sample (shortened) result for `alpine:3.19`:

```json
{
  "image": "alpine:3.19",
  "status": "ok",
  "imageReference": "docker.io/library/alpine:3.19",
  "digest": "sha256:b58899f069c47216f6002a6850143dc6fae0d35eb8b0df9300bbe6327b9c2171",
  "os": "alpine 3.19.9",
  "osEndOfLife": true,
  "packageCount": 15,
  "vulnerabilities": [
    {
      "id": "CVE-2026-40200",
      "severity": "high",
      "package": "musl",
      "installedVersion": "1.2.4_git20230717-r5",
      "fixedVersion": "1.2.4_git20230717-r6",
      "status": "fixed",
      "cvssScore": 7.8
    }
  ],
  "summary": { "critical": 0, "high": 2, "medium": 5, "low": 3, "unknown": 0, "total": 10, "withFix": 10 },
  "sbomFormat": "CycloneDX 1.7",
  "sbomUrl": "https://api.apify.com/v2/key-value-stores/.../records/sbom_docker.io_library_alpine_3.19.cdx.json?signature=...",
  "dbBuildDate": "2026-10-21T19:18:22.876313144Z",
  "toolVersions": { "trivy": "0.75.0", "actor": "1.0.0" }
}
```

`status` is `ok` for a scanned image, `no-packages` when the image contains no packages the scanner can identify (for example an empty scratch image), and `error` when the image could not be scanned. The reason is in `errorCode` and `errorMessage`. Error codes include `RATE_LIMITED`, `NOT_PUBLIC`, `IMAGE_NOT_FOUND`, `UNSUPPORTED_REGISTRY`, `INVALID_REFERENCE`, `PLATFORM_NOT_AVAILABLE`, `MAX_PACKAGES_EXCEEDED`, `IMAGE_TOO_LARGE` and `SCAN_TIMEOUT`.

The SBOM link is a signed link to the file in the run's key-value store. It works for as long as Apify keeps that run's storage.

### Pricing

This Actor uses pay per event. You are charged once per image that is scanned successfully, and nothing for images that fail.

| Event | Price | When it is charged |
| --- | --- | --- |
| `image-scan` | $0.10 | One image scanned in full mode (SBOM and vulnerabilities). |
| `sbom-only` | $0.04 | One image scanned in SBOM-only mode. |

Not charged: images that return an error (rate limited, private, not found, too large, over your package limit, timeout), images with no identifiable packages, and invalid input. Apify platform usage is covered by the Actor owner.

### How current is the vulnerability data?

The vulnerability database and the Java index database are downloaded when the Actor is built and are stored inside the Actor image. Every full-scan result includes `dbBuildDate`, the build time of the database that was used, and the result notes warn when that date is more than 3 days old. The Actor is meant to be rebuilt daily to pick up a new database. If `dbBuildDate` looks old, say so in the Issues tab.

### Data sources and licences

- Scanner: Trivy by Aqua Security, Apache-2.0. The licence texts of Trivy and the Go modules it contains are kept in the Actor source under `licenses/`.
- Vulnerability data: the Trivy vulnerability database (`aquasecurity/trivy-db`) and Java index database (`aquasecurity/trivy-java-db`), published by Aqua Security and documented by them for offline use and mirroring. They combine advisory data from sources such as the National Vulnerability Database, the GitHub Advisory Database and distribution security trackers (Alpine, Debian, Ubuntu, Red Hat and others), each under its own terms. The Actor returns advisory identifiers, severities, versions and links only.
- Registries: Docker Hub, GitHub Container Registry, Quay and Amazon ECR Public, accessed anonymously through their public registry APIs with a descriptive User-Agent. Names of these products are used only to describe what the Actor reads. This Actor is not affiliated with or endorsed by Docker, GitHub, Red Hat, Amazon or Aqua Security.

### FAQ

**Why did my Docker Hub image fail with RATE_LIMITED?**
Docker Hub limits anonymous pulls per IP address, and Apify runs share IP addresses. When Docker Hub answers with HTTP 429 the Actor stops asking Docker Hub for the rest of the run, reports `RATE_LIMITED` with an explanation, and does not charge. Try again later, or use the same image from another registry, for example `public.ecr.aws/docker/library/alpine:3.19`. The other registries have their own limits, and the same handling applies to them.

**Can I scan a private image?**
No. This version only reads public images and refuses credentials.

**Does it run the image?**
No. It only downloads layers and reads package metadata files.

**Why does an image show no vulnerabilities?**
Either nothing known matches its packages in the database, or the operating system is not supported for matching. Unsupported operating systems (CentOS Stream is one example) still get a complete package list, and a note tells you that only language packages were matched.

**Why are there many findings with no fixed version?**
The database includes advisories that the distribution has not fixed yet. Use "Only list vulnerabilities with a fix" if you want to see just the actionable ones.

**What is the largest image I can scan?**
Images whose layers total more than 1.5 GB compressed are refused. Each image has a 4 minute time limit.

### Limitations

- Results are decision support, not a security guarantee. The scan lists known vulnerabilities that match the packages the scanner could identify. It can miss problems (unknown flaws, software copied into the image without a package manager entry, vendor-specific patches) and can list problems that do not affect how you use the image.
- Only packages are checked. Secrets, misconfigurations and license problems are not scanned.
- Public images on four registries only, platforms linux/amd64 and linux/arm64 only.
- Images from end-of-life operating systems may have incomplete vulnerability data, because their vendors no longer publish fixes. Results say so in `notes`.
- The vulnerability database is as current as its build date. Newer advisories are not included.
- At most 20 images per run, 1.5 GB compressed per image, 4 minutes per image.

### Support

Questions, bug reports or feature requests: open an issue in the Issues tab of this Actor. Please include the run ID and the image reference.

# Changelog

This Actor's version history is a separate document: https://apify.com/jetlaunch/container-image-scanner/changelog.md

# Actor input Schema

## `images` (type: `array`):

Public images as name:tag or name@sha256:digest. Examples: alpine:3.19, ghcr.io/org/app:1.2, quay.io/org/app@sha256:..., public.ecr.aws/docker/library/alpine:3.19. Supported registries: Docker Hub, GitHub Container Registry, Quay and Amazon ECR Public. Up to 20 per run. Private images and credentials are not supported.

## `mode` (type: `string`):

Full scan returns the SBOM plus the vulnerability list (event image-scan, $0.10 per image). SBOM only returns the package list without matching vulnerabilities (event sbom-only, $0.04 per image).

## `minSeverity` (type: `string`):

Only vulnerabilities at or above this severity are listed in the vulnerabilities array. The summary counts always cover every severity so you can see what was filtered out. Ignored in SBOM-only mode.

## `onlyFixable` (type: `boolean`):

When on, vulnerabilities for which no fixed package version is known are left out of the list. The summary counts still include them.

## `sbomFormat` (type: `string`):

Format of the SBOM file saved in the key-value store.

## `platform` (type: `string`):

Which variant to scan when an image is published for several CPU architectures.

## `maxPackages` (type: `integer`):

Safety limit. An image that contains more packages than this is reported as an error item and is not charged. Raise it for very large images.

## Actor input object example

```json
{
  "images": [
    "public.ecr.aws/docker/library/alpine:3.19"
  ],
  "mode": "full",
  "minSeverity": "all",
  "onlyFixable": false,
  "sbomFormat": "cyclonedx",
  "platform": "linux/amd64",
  "maxPackages": 3000
}
```

# Actor output Schema

## `results` (type: `string`):

One row per image.

## `summary` (type: `string`):

Counts, charges, tool versions, database dates, notices and the disclaimer.

## `sboms` (type: `string`):

All SBOM files saved by this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "images": [
        "public.ecr.aws/docker/library/alpine:3.19"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jetlaunch/container-image-scanner").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "images": ["public.ecr.aws/docker/library/alpine:3.19"] }

# Run the Actor and wait for it to finish
run = client.actor("jetlaunch/container-image-scanner").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "images": [
    "public.ecr.aws/docker/library/alpine:3.19"
  ]
}' |
apify call jetlaunch/container-image-scanner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jetlaunch/container-image-scanner"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/F9VFC9fzUcd9z6wsi/builds/NyU77oKYEu5zr1bK0/openapi.json
