SBOM scan (API)
Scan a whole lockfile or SBOM for known vulnerabilities - by URL or by POSTing it - no browser required.
The /scan page parses your lockfile in the browser
and checks every dependency against OSV. GET/POST /api/v2/scan is the same
scanner, reachable from a script or CI: hand it a lockfile by URL or as the
request body, and it parses 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.
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}]} |
format (query or body) forces the parser; the default auto sniffs from the
URL/filename then the content. Detected: npm (package-lock.json), cargo
(Cargo.lock), pip (requirements.txt, == pins), composer
(composer.lock), gem (Gemfile.lock), go (go.sum), CycloneDX SBOM
(JSON, via purl), and mise (mise.lock).
Scan by URL
curl -s "https://vuln.mlab.sh/api/v2/scan?url=https://raw.githubusercontent.com/rust-lang/cargo/master/Cargo.lock"Scan a local lockfile
POST the file straight from disk - the format is auto-detected:
curl -s -X POST "https://vuln.mlab.sh/api/v2/scan" --data-binary @Cargo.lockForce the format for an ambiguously-named file:
curl -s -X POST "https://vuln.mlab.sh/api/v2/scan?format=npm" --data-binary @my-lock.jsonResponse
{
"hash": "8a1564f1d99d2afa",
"cached": false,
"count": 2,
"truncated": false,
"ttl": 21600,
"packages": [ { "ecosystem": "crates.io", "name": "time", "version": "0.1.44" } ],
"results": [ { "ok": true, "vulns": [ /* OSV vuln objects */ ] } ]
}results[i]lines up with the scanned coordinatei. For the URL / raw / inline modes apackagesarray echoes those coordinates in the same order, so the result is self-describing.results[i].ok == falsemeans that coordinate could not be scanned (upstream outage, or an unqueryable coordinate). It is not a clean result - never treat it as "no vulns".truncated: truemeans the manifest exceeded the 512-package ceiling and only the first 512 were scanned.
No auth is required, but for CI use an API token - generate one at
/me/tokens and pass it as
Authorization: Bearer <token> (or X-API-Key). A token gets 25 scans / hour;
anonymous callers share only 8 / hour per IP, which is unreliable behind
shared CI runner egress IPs. A signed-in browser session is unlimited.
Use in CI/CD
The official GitHub Action wraps all
of this - auto-detects lockfiles, applies a severity threshold, writes a job
summary. Store your token as a repo secret (VULN_MLAB_TOKEN):
- uses: mlab-sh/vuln-scan-action@v1
with:
fail-on: high
token: ${{ secrets.VULN_MLAB_TOKEN }}Prefer to scan from your editor? The VS Code extension runs the same scan on demand on any lockfile, with results shown inline.
Or roll your own gate with curl - fail the build when any dependency has a known
vulnerability:
curl -s -X POST "https://vuln.mlab.sh/api/v2/scan" \
-H "Authorization: Bearer $VULN_MLAB_TOKEN" --data-binary @Cargo.lock \
| jq -e '[.results[].vulns // [] | length] | add == 0' > /dev/null \
|| { echo "Vulnerable dependencies found"; exit 1; }The URL modes fetch a URL you supply, so an SSRF guard applies: http/https
only, ports 80/443 only, and the target must resolve to a public address
(private, loopback, link-local and cloud-metadata ranges are refused). Redirects
are followed (≤ 4) with the same check, and the body is capped at 8 MiB.
See also
- OSV integration - single-coordinate lookups and using vuln.mlab.sh as a blocking CVE gate.
- API Reference - full schema for every endpoint.
- GitHub Action and VS Code extension - the same scan in CI and in your editor.