# Container CVE Matcher (`codeclouds/container-cve-matcher`) Actor

Matches a dependency list, lockfile, or SBOM against OSV.dev to report

- **URL**: https://apify.com/codeclouds/container-cve-matcher.md
- **Developed by:** [Dennis](https://apify.com/codeclouds) (community)
- **Categories:** Developer tools, Other
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 dependency cve checkeds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Container CVE Matcher

Matches a dependency list, lockfile, or SBOM against [OSV.dev](https://osv.dev) to report exactly
which CVEs/GHSAs apply to **each installed version** — not just "this package has had a CVE at
some point", but a real answer to "does *my* exact version actually fall inside the affected
range?", using the same range-comparison libraries npm and pip themselves use, plus CVSS severity
and the patched version to upgrade to.

### When should an AI agent use this?

- "Is this exact version of `lodash` I have pinned actually vulnerable, or was that CVE fixed
  before this version?"
- "Scan this package-lock.json/requirements.txt for known vulnerabilities before I merge this PR."
- "Given this CycloneDX/SPDX SBOM, which components have unpatched CVEs, and how severe are they?"
- "I know I have `flask==0.12` and `requests==2.28.0` installed — check both against known CVEs."
- "Before recommending this npm package version to a user, confirm it isn't currently flagged for
  a critical vulnerability."

A composite semver range like `>=2.0.0 <2.4.5 || >=3.0.0-alpha <3.0.1` — with pre-release ordering,
multiple disjoint intervals, and an ecosystem-specific version scheme (npm semver vs. Python's
PEP 440, which order versions differently) — is exactly the kind of exact, deterministic
computation a language model cannot reliably "eyeball" from training data. CVE data is also
inherently time-sensitive: new advisories publish daily, so even a very recently trained model
cannot know about a CVE published after its training cutoff. This Actor exists to give an agent a
verifiable, live answer instead of a guess.

### What this Actor does

- Accepts a public git repository URL (shallow-cloned, no `git` binary required), **and/or** one or
  more lockfiles/SBOMs pasted directly as text, **and/or** a direct flat package+version list — any
  combination, so an agent that already knows its own dependency graph can skip file parsing
  entirely
- Parses **npm** `package-lock.json` (both the legacy nested-tree format and the modern flat
  `"packages"` map) and **PyPI** `requirements.txt` / `Pipfile.lock`, plus **CycloneDX** and
  **SPDX** SBOM JSON files (via each component's `purl`)
- Batch-queries [OSV.dev](https://osv.dev)'s free, key-free API (`POST /v1/querybatch`) for every
  dependency, then fetches full advisory details only for the vulnerabilities that actually came
  back — never one request per dependency per advisory
- **Re-verifies every match itself** against the exact installed version, rather than trusting OSV's
  batch response at face value: npm ranges are checked with the [`semver`](https://www.npmjs.com/package/semver)
  package (the same library npm itself uses), PyPI ranges with
  [`@renovatebot/pep440`](https://www.npmjs.com/package/@renovatebot/pep440) (a maintained PEP 440
  implementation used in production by Renovate) — correctly handling composite/disjoint ranges,
  open-ended "still unfixed" ranges, and inclusive-vs-exclusive upper bounds
- Computes a real CVSS base score + severity level from OSV's CVSS vector string (using
  [`ae-cvss-calculator`](https://www.npmjs.com/package/ae-cvss-calculator), supporting CVSS v2/v3/v4),
  falling back to GHSA's own qualitative severity label when no CVSS vector is present
- Reports the exact **patched version** to upgrade to, taken directly from the matching range's
  fix boundary
- Returns clean, flat JSON records: one run-summary record, one record per checked dependency
  (including unsupported-ecosystem and unresolved-version dependencies — never silently dropped),
  and one record per confirmed CVE/GHSA match

### Supported ecosystems (v1)

| Ecosystem | Lockfile/manifest support | Exact-version range matching |
|---|---|---|
| **npm** | `package-lock.json` (v1/v2/v3), CycloneDX/SPDX `pkg:npm/...` | ✅ via the `semver` npm package |
| **PyPI** | `requirements.txt` (pinned `==` lines), `Pipfile.lock`, CycloneDX/SPDX `pkg:pypi/...` | ✅ via `@renovatebot/pep440` |

OSV.dev itself covers many more ecosystems (Go, Maven, RubyGems, crates.io, Packagist, NuGet, ...),
and a CycloneDX/SPDX SBOM can freely describe components from any of them. This Actor **never
drops** a dependency it can't check — a component from an unsupported ecosystem is still reported
as a `"dependency"` record (with `supported: false` and a `lookupError` explaining why), it just
isn't queried against OSV. This is a deliberate, honest v1 scope limit rather than a half-correct
guess: getting exact version ordering right for Go's pseudo-versions, Maven's version-comparison
rules, RubyGems' segment rules, etc. each needs its own verified library, and only npm/PyPI have
one with the maturity this Actor requires (see PROJECTINFORMATIE.txt for the full reasoning).
A dependency with only a version *range* (e.g. an unpinned `requirements.txt` line or a
`package.json`-style caret range) is likewise reported but not checked — this Actor matches an
*exact installed version*, never a range against a range, to avoid a false sense of certainty.

### Input

| Field | Type | Description | Example |
|---|---|---|---|
| `repoUrl` | string | Public git repository to clone (`https://`/`http://` only) | `"https://github.com/expressjs/express.git"` |
| `gitRef` | string | Branch/tag/commit to check out (default: repo's default branch) | `"main"` |
| `manifests` | array of `{filename, content}` | Lockfile/SBOM contents pasted directly | see below |
| `dependencies` | array of `{name, version, ecosystem}` | A direct package+version list, `ecosystem` defaults to `"npm"` | see below |
| `maxDependencies` | integer | Cap on dependencies checked in one run, 1-1000 (default `300`) | `300` |

At least one of `repoUrl`, `manifests`, or `dependencies` is required (any combination is fine).

```json
{
  "dependencies": [
    { "name": "lodash", "version": "4.17.15", "ecosystem": "npm" },
    { "name": "flask", "version": "0.12", "ecosystem": "PyPI" }
  ]
}
```

```json
{
  "manifests": [
    {
      "filename": "requirements.txt",
      "content": "flask==0.12\nrequests==2.31.0\n"
    }
  ]
}
```

### Output

One `"project"` record per run:

```json
{
  "recordType": "project",
  "scanId": "b1e6...",
  "input": "2 direct dependencies",
  "totalDependenciesFound": 2,
  "totalDependenciesChecked": 2,
  "totalUnresolvedVersions": 0,
  "totalUnsupportedEcosystem": 0,
  "totalMatches": 1,
  "highestSeverity": "medium",
  "ecosystemsSupported": ["npm", "PyPI"],
  "error": null
}
```

One `"dependency"` record per reported dependency (checked or not):

```json
{
  "recordType": "dependency",
  "scanId": "b1e6...",
  "name": "lodash",
  "ecosystem": "npm",
  "supported": true,
  "version": "4.17.15",
  "versionRange": null,
  "checked": true,
  "matchCount": 1,
  "highestSeverity": "medium",
  "lookupError": null
}
```

One `"match"` record per confirmed CVE/GHSA finding:

```json
{
  "recordType": "match",
  "scanId": "b1e6...",
  "name": "lodash",
  "ecosystem": "npm",
  "installedVersion": "4.17.15",
  "vulnId": "GHSA-29mw-wpgm-hmr9",
  "cveId": "CVE-2020-28500",
  "aliases": ["CVE-2020-28500"],
  "summary": "Regular Expression Denial of Service (ReDoS) in lodash",
  "severityLevel": "medium",
  "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
  "cvssScore": 5.3,
  "affectedRange": ">=4.0.0 <4.17.21",
  "fixedVersion": "4.17.21",
  "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-28500",
  "published": "2022-01-06T20:30:46Z",
  "modified": "2025-09-29T21:12:31.102523Z"
}
```

`severityLevel` is one of `critical`, `high`, `medium`, `low`, `none` (a score of exactly 0), or
`unknown` (no CVSS vector and no GHSA qualitative label could be found — never guessed).

### Using this Actor as an MCP tool for AI agents

Every public Apify Actor is automatically exposed as a callable "tool" via Apify's MCP server.
This Actor is designed for that use case specifically:

- **Flat, typed output** — no nested objects; every field is a string/number/boolean/array of
  strings, easy for an agent to filter on (`severityLevel === "critical"`) or aggregate
- **Three independent input modes** — an agent that already knows `{name, version}` pairs from its
  own dependency resolution doesn't need to construct a fake lockfile; it can pass `dependencies`
  directly and skip file parsing entirely
- **Never silently drops or guesses** — an unsupported ecosystem, an unpinned version, or a failed
  advisory-detail fetch is always reported as its own record with a `lookupError` explaining why,
  rather than being absent from the output or a false "0 matches"
- **Deterministic, source-cited output** — every match carries the exact `affectedRange` and
  `fixedVersion` that produced it, so an agent (or a human reviewing its work) can verify the claim
  against the linked advisory `url`
- **Bounded cost/runtime** — `maxDependencies` and a per-request timeout on every OSV.dev call
  prevent a single run from becoming unexpectedly large or slow

### Use cases

- Pre-merge CI-style check: does this dependency bump introduce a new, currently-unpatched CVE?
- Triage a legacy/inherited codebase: "what are we actually running, and is any of it flagged?"
- Verify a CycloneDX/SPDX SBOM (e.g. generated by a build pipeline) against live vulnerability data
- Spot-check a single package version before an agent recommends or installs it

### How exact-version matching works (and its limits)

OSV.dev's batch API deliberately returns only candidate vulnerability IDs per package+version —
not a pre-verified "yes this exact version is affected" answer (see PROJECTINFORMATIE.txt for the
live-captured API shape). This Actor treats that as a candidate list only: it fetches each
candidate's full advisory record and re-walks its `affected[].ranges[].events` sequence itself,
using the ecosystem's real version-comparison library, before ever reporting a match. A range with
multiple `introduced`/`fixed`/`last_affected` pairs (e.g. a vulnerability re-introduced in a later
branch) is walked as the sequence of intervals it actually represents, not approximated as one
simple `>=`/`<` pair. OSV's optional explicit `versions` enumeration (when present) is checked too,
as additional confirmation alongside the range walk.

This is not a replacement for a full SCA platform (Snyk, Trivy, Grype, Dependabot) — it is a
narrowly-scoped, verifiable single-purpose tool: given an exact version, tell me precisely which
CVEs apply and why, callable ad-hoc without an account or CI integration. Always have a qualified
professional review a genuine security-critical finding before acting on it.

### Legal

This Actor only clones **public** repositories you provide a URL for, or reads lockfile/SBOM/
dependency text you supply directly — it never accesses private repositories or credentials, and
never performs any exploit or intrusive testing against the packages it reports on. Vulnerability
and version metadata is technical data about software packages, not personal data. Findings are
informational, not a certification of security or a substitute for a full security audit.

### FAQ

**Q: Does this replace Snyk / Trivy / Grype / Dependabot?**
A: No. Those are full CI/CD-integrated SCA platforms. This Actor is a narrowly-scoped, ad-hoc tool
— call it once, get a verified answer, no account or pipeline integration required. That makes it
specifically well-suited to on-demand use by a coding agent.

**Q: Why does OSV say a package is vulnerable but this Actor reports 0 matches for it?**
A: OSV's batch endpoint can return a candidate ID for a package name+version pair loosely, but this
Actor always re-verifies the *exact* range before reporting a match. If your exact version turns
out to be outside every affected interval (e.g. you're on a version released after the fix), it's
correctly excluded — that's the whole point of doing real range matching instead of trusting the
package name alone.

**Q: What happens if a dependency's ecosystem isn't supported (e.g. Maven, Go, RubyGems)?**
A: It's still reported as a `"dependency"` record with `supported: false` and an explanatory
`lookupError` — never silently dropped from the output.

**Q: What happens to an unpinned dependency (e.g. `requests>=2.0`, no exact version)?**
A: It's reported with `version: null`, `checked: false`, and a `lookupError` explaining that no
exact version was available — this Actor never checks a range against a range.

**Q: Can this scan a private repository?**
A: No — only `https://`/`http://` public repository URLs are accepted (no authentication is
performed). For a private repo, paste its lockfile/SBOM/dependency list directly instead.

**Q: Does the CVSS score come from this Actor's own calculation?**
A: The vector string comes directly from OSV.dev; the numeric base score is computed from that
vector using `ae-cvss-calculator`, a maintained third-party CVSS implementation — not a
hand-rolled reimplementation of the CVSS formula.

### Keywords

cve, vulnerability-scanner, osv, sbom, sca, dependency-audit, security, npm, pypi, cyclonedx,
spdx, software-composition-analysis

### Related Actors

- [Legacy Code License Scanner](https://apify.com/CodeClouds/legacy-code-license-scanner) — the
  license-compliance counterpart to this Actor: same git-clone/manifest-parsing approach, but
  checking license conflicts instead of CVEs. Complementary, not overlapping — run both for a full
  pre-dependency-add check (license risk + security risk).

### Changelog

#### 0.1.0

- Initial release: git-clone / inline-manifest-SBOM / direct-dependency-list scanning, OSV.dev
  batch querying, npm-semver and PEP 440 exact-version range re-verification, CVSS scoring.

# Actor input Schema

## `repoUrl` (type: `string`):

Public git repository to clone and scan for lockfiles/SBOMs (https:// or http:// only). Provide this, or fill in "manifests"/"dependencies" below, or any combination. Example: "https://github.com/expressjs/express.git".

## `gitRef` (type: `string`):

Branch, tag, or commit to check out. Leave empty to use the repository's default branch.

## `manifests` (type: `array`):

One or more dependency files pasted directly, instead of (or in addition to) a repository URL. Each item needs a "filename" (used to detect the format: "package-lock.json", "requirements.txt", "Pipfile.lock", or a CycloneDX/SPDX SBOM .json file) and its "content".

## `dependencies` (type: `array`):

A flat package+version list, for callers (e.g. an AI agent) that already know exactly what's installed and don't need lockfile/SBOM parsing. Each item needs a "name" and exact "version"; "ecosystem" defaults to "npm" (also supports "PyPI").

## `maxDependencies` (type: `integer`):

Safety cap on how many dependencies get a live OSV.dev lookup in one run. Example: 300.

## Actor input object example

```json
{
  "manifests": [],
  "dependencies": [],
  "maxDependencies": 300
}
```

# Actor output Schema

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

All results in the default dataset.

# 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 = {
    "manifests": [],
    "dependencies": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("codeclouds/container-cve-matcher").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 = {
    "manifests": [],
    "dependencies": [],
}

# Run the Actor and wait for it to finish
run = client.actor("codeclouds/container-cve-matcher").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 '{
  "manifests": [],
  "dependencies": []
}' |
apify call codeclouds/container-cve-matcher --silent --output-dataset

```

## MCP server setup

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

```

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/SldJptWmiClBVdd40/builds/PXgQnLbQFqdMcbtgS/openapi.json
