SBOM scan (API)
Scan a whole lockfile or SBOM for known vulnerabilities in one request, by URL or by POSTing the file, from a script or CI.
The /scan page parses your lockfile in the browser and checks every dependency. GET/POST /api/v2/scan is the same scanner for scripts and CI: hand it a lockfile by URL or as the request body, it parses it server-side and returns one result per dependency.
Unlike /api/v2/query (one coordinate at a time), this scans the whole manifest in a single request.
Supported files
| Ecosystem | Files |
|---|---|
| npm | package-lock.json, npm-shrinkwrap.json, yarn.lock (v1 and berry), pnpm-lock.yaml (v5 to v9) |
| crates.io | Cargo.lock |
| PyPI | requirements.txt (== pins only), poetry.lock, uv.lock, pdm.lock, Pipfile.lock |
| Packagist | composer.lock |
| RubyGems | Gemfile.lock |
| Go | go.sum |
| Maven | pom.xml (literal versions), gradle.lockfile |
| NuGet | packages.lock.json |
| Pub | pubspec.lock |
| Hex | mix.lock |
| mise | mise.lock (npm, pipx, cargo, gem, go and dotnet backends) |
| Any | CycloneDX (JSON or XML) and SPDX (JSON 2.x, tag-value, JSON-LD 3.0) |
Detection uses the file name (the filename query parameter, or the URL), then the content. format forces a parser: npm, yarn, pnpm, cargo, pip, poetry, uv, pdm, pipfile, composer, gem, go, maven, nuget, pub, hex, mise, cyclonedx, spdx.
In an SBOM, every component is read, nested ones included: its purl when it has one (OS packages too: deb, apk, with the distro release), otherwise its CPE, which is matched against NVD configurations. Advisories found that way carry database_specific.source: "nvd-cpe".
Input modes
| How | Example |
|---|---|
| URL (GET) | GET /api/v2/scan?url=https://.../Cargo.lock |
| URL (POST) | body {"url":"https://.../Cargo.lock"} |
| Raw body | --data-binary @Cargo.lock |
| Inline text | body {"manifest":"...","format":"cargo"} |
| Pre-parsed | body {"packages":[{ecosystem,name,version} | {purl} | {cpe}]} |
Scan a local lockfile
curl -s -X POST "https://vuln.mlab.sh/api/v2/scan?filename=Cargo.lock" --data-binary @Cargo.lockForce the parser for an unusually named file:
curl -s -X POST "https://vuln.mlab.sh/api/v2/scan?format=npm" --data-binary @my-lock.jsonScan by URL
curl -s "https://vuln.mlab.sh/api/v2/scan?url=https://raw.githubusercontent.com/rust-lang/cargo/master/Cargo.lock"The URL modes fetch a URL you supply, behind an SSRF guard: http/https on port 80 or 443 only, and the target must resolve to a public address (private, loopback, link-local and cloud-metadata ranges are refused). Redirects are followed (at most 4) with the same check, and the file is capped at your tier's request size.
Limits
| Caller | Packages per scan | Request size | Cooldown between scans |
|---|---|---|---|
| Anonymous (per IP) | 512 | 4 MiB | 10 s |
| Free account (session or API token) | 1024 | 8 MiB | 5 s |
| Paid plan | 4096 | 32 MiB | none |
Generate a token at /me/tokens and send it as Authorization: Bearer <token> (or X-API-Key: <token>). It applies your account's limits, and its scans land in your scan history. The cooldown is per account, so a token and your browser session share it.
Every response states your tier and its limits. When a limit is hit, the answer says so in plain words:
- More packages than your tier scans: the manifest is cut,
truncated: true, andtruncationgives the counts and a message. The packages past the cut were not scanned, never clean. - A body over the size limit:
413withmessage. - A scan inside the cooldown:
429withRetry-Afterandretry_after_secs. Wait and send the same request again: nothing was spent.
Below the top tier, truncation, 413 and 429 carry an upgrade block with what the next tier unlocks.
{
"error": "cooldown",
"message": "Anonymous scans are limited to one every 10 seconds. Retry in 7 seconds.",
"retry_after_secs": 7,
"tier": "anonymous",
"limits": { "max_packages": 512, "cooldown_secs": 10, "max_body_bytes": 4194304 },
"upgrade": {
"tier": "free", "max_packages": 1024, "cooldown_secs": 5, "max_body_bytes": 8388608,
"message": "Create a free account to scan up to 1024 packages per scan (8 MiB per request), with a 5 second cooldown.",
"url": "https://vuln.mlab.sh/sso/login?next=/scan"
}
}Response
{
"hash": "8a1564f1d99d2afa",
"cached": false,
"count": 2,
"tier": "anonymous",
"limits": { "max_packages": 512, "cooldown_secs": 10, "max_body_bytes": 4194304 },
"truncated": false,
"outage": false,
"ttl": 10800,
"packages": [ { "ecosystem": "crates.io", "name": "time", "version": "0.1.44" } ],
"results": [ { "ok": true, "vulns": [ /* OSV vulnerability records */ ] } ]
}results[i]matchespackages[i]. The URL, raw and inline modes echo the coordinates inpackages; a pre-parsed request does not.results[i].ok == falsemeans that coordinate could not be scanned (unqueryable, or the database was unavailable andoutageistrue). It is not a clean result.unscanned(server-parsed modes) lists entries that could not become a coordinate at all: a git or path dependency, an unpinned requirement, the Flutter SDK in apubspec.lock, an SBOM component with neither purl nor CPE.- Each vulnerability is an OSV record. Its CVE is in
aliases, or for Debian and Ubuntu records inupstream(or in an id such asDEBIAN-CVE-2024-1234). Several records can describe the same CVE (GHSA and RUSTSEC, for example). - The result is cached for 3 hours, keyed by the set of coordinates (
cached,ttl). An outage is never cached.
A gate in a few lines
Fail the build when any dependency has a known vulnerability, or when the scan did not cover everything:
r=$(curl -sf -X POST "https://vuln.mlab.sh/api/v2/scan?filename=Cargo.lock" \
-H "Authorization: Bearer $VULN_MLAB_TOKEN" --data-binary @Cargo.lock) || exit 2
echo "$r" | jq -e '
([.results[] | select(.ok | not)] | length) == 0
and (.truncated | not)
and ([.results[].vulns // [] | length] | add) == 0' > /dev/null \
|| { echo "Vulnerable or not fully scanned"; exit 1; }For anything more than that, use one of the integrations: they handle the cooldown, severity thresholds, annotations and reports.
See also
- Monitor an SBOM: the same scan, rerun for you, with an email when a new advisory hits one of your dependencies.
- OSV integration: single-coordinate lookups.
- API Reference: the full schema.