API Reference
Every endpoint that powers npmscan.com — public, unauthenticated, and free to call directly.
https://npmscan.com/api— it's the same read-only data that powers the website. Please be a good citizen: cache results client-side and avoid tight polling loops.https://npmscan.com/api/mcp with 23ready-made tools for package, version, and vulnerability lookups. Same data, no API key, one line of config. There's also a plaintext /llms.txt if your agent just wants a map of the site.30 requests / 60s per IP. Going over returns 429 Too Many Requests with X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After headers (plus the same fields in the JSON body) telling you the budget, how many requests are left, and how many seconds to wait. /api/mcp has its own separate limit at the same rate, surfaced as error.data on its JSON-RPC 429 response. Cache responses client-side to stay well under it.Search the npm registry and pull package or version metadata — mirrored with a registry fallback for reliability.
| Name | Type | Required | Description |
|---|---|---|---|
| q | string | required | Search text, 2–64 characters. |
| size | number | optional | Number of results to return. Defaults to 20. |
curl "https://npmscan.com/api/npm/search?q=react-router&size=5"{
"objects": [
{
"downloads": { "monthly": 186579079, "weekly": 40361311 },
"dependents": "8904",
"updated": "2026-09-22T12:12:55.230Z",
"searchScore": 2340.063,
"package": {
"name": "react-router",
"version": "8.4.0",
"description": "Declarative routing for React",
"keywords": ["react", "router", "route", "routing", "history", "link"],
"date": "2026-09-15T15:23:42.214Z",
"links": {
"npm": "https://www.npmjs.com/package/react-router",
"repository": "https://github.com/remix-run/react-router"
},
"maintainers": [{ "username": "brophdawg11", "email": "matt@brophy.org" }, { "username": "mjackson", "email": "mjijackson+npm@gmail.com" }]
},
"score": {
"final": 2340.063,
"detail": { "quality": 1, "popularity": 1, "maintenance": 1 }
}
}
],
"total": 453868,
"time": "2026-09-25T08:02:49.417Z"
}{ "error": "Query parameter \"q\" is required" }- ›Passes through the npm registry's own `-/v1/search` response shape unmodified — including exactly how `score`/`searchScore` are scaled and what fields sit at the object level vs. nested under `package`. That shape is controlled entirely by npm's own API and can change without notice; re-verify against a live call rather than treating this example as a stable contract.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | npm package name (URL-encode scoped names, e.g. %40scope%2Fname). Passed as a path segment. |
curl "https://npmscan.com/api/npm/package/lodash"{
"name": "lodash",
"dist-tags": { "latest": "4.18.1" },
"versions": {
"4.18.1": {
"name": "lodash",
"version": "4.18.1",
"license": "MIT",
"dist": {
"shasum": "ff2b66c1f6326d59513de2407bf881439812771c",
"tarball": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBc..."
}
}
/* ...every other published version */
},
"time": {
"created": "2012-04-23T16:37:11.912Z",
"modified": "2026-09-25T00:57:07.767Z",
"4.18.1": "2026-04-01T21:01:20.458Z"
},
"maintainers": [{ "name": "jdalton", "email": "john.david.dalton@gmail.com" }],
"license": "MIT",
"repository": { "type": "git", "url": "git+https://github.com/lodash/lodash.git" }
}{ "error": "Package not found" }- ›Response is the full npm registry packument — can be large for packages with many published versions.
- ›Verified live — a "latest" dist-tag and `time.modified` are moving targets on an actively-maintained package; re-check before treating either as a fixed reference value.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | npm package name. |
| version | string | required | Exact version or dist-tag (e.g. 1.4.2 or latest). |
curl "https://npmscan.com/api/npm/package/example-package/version/1.4.2"{
"name": "example-package",
"version": "1.4.2",
"description": "Example package for documentation purposes.",
"license": "MIT",
"scripts": {
"preinstall": "node ./scripts/check-platform.js",
"postinstall": "node ./scripts/postinstall.js",
"test": "jest"
},
"dependencies": { "chalk": "^5.3.0" },
"dist": {
"integrity": "sha512-abc123...",
"shasum": "9c1a4d2f...",
"tarball": "https://registry.npmjs.org/example-package/-/example-package-1.4.2.tgz",
"fileCount": 12,
"unpackedSize": 48213
},
"maintainers": [{ "name": "maintainer-handle", "email": "maintainer@example.com" }]
}{ "error": "Package version not found" }- ›`scripts.preinstall` / `scripts.postinstall` are what actually runs on install — check them here rather than trusting the README.
Proxies to OSV.dev, scoped to the npm ecosystem by default. Use the batch endpoint to scan an entire package.json or lockfile in one request — it's the same call npmscan.com/analyze makes under the hood.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | npm package name. |
| version | string | optional | Narrow results to a specific version. Omit for all known advisories. |
| ecosystem | string | optional | Defaults to "npm". |
curl -X POST https://npmscan.com/api/osv/query \
-H "Content-Type: application/json" \
-d '{"name":"lodash","version":"4.17.15"}'{
"vulns": [
{
"id": "GHSA-35jh-r3h4-6jhm",
"summary": "Prototype Pollution in lodash",
"aliases": ["CVE-2020-8203"],
"modified": "2022-01-27T00:00:00Z",
"published": "2020-07-15T00:00:00Z",
"database_specific": { "severity": "HIGH" },
"affected": [
{
"package": { "name": "lodash", "ecosystem": "npm" },
"ranges": [
{ "type": "ECOSYSTEM", "events": [{ "introduced": "0" }, { "fixed": "4.17.19" }] }
]
}
],
"references": [
{ "type": "ADVISORY", "url": "https://github.com/advisories/GHSA-35jh-r3h4-6jhm" }
],
"fixedVersion": "4.17.19"
}
],
"isVulnerable": true,
"highestSeverity": "HIGH"
}- ›`vulns` is empty (not omitted) when the package has no known advisories for the given version.
- ›Unlike the batch endpoint below, `ecosystem` isn't restricted to npm — this passes straight through to OSV.dev, so PyPI, Go, crates.io, Maven, RubyGems, and any other OSV-supported ecosystem work too.
- ›`isVulnerable` and `highestSeverity` are direct top-level answers computed from `vulns` — use them instead of scanning the array yourself. Each entry in `vulns` also carries a merged-in `fixedVersion` (the version that patches it, extracted from `affected[].ranges[].events`, `null` when OSV can't resolve one) — everything else in `vulns` is OSV's own document shape, untouched.
| Name | Type | Required | Description |
|---|---|---|---|
| packages | Array<{ name, version? }> | optional | Up to 1000 items. `version` is optional per item. Use this OR `content`, not both. |
| content | string | optional | Raw package.json, package-lock.json (v1-v3), yarn.lock (classic or Berry), pnpm-lock.yaml, CycloneDX JSON, or SPDX JSON — parsed into a package list for you. Use this OR `packages`, not both. |
| includeDevDependencies | boolean | optional | Only applies when `content` is a manifest/lockfile format that distinguishes dev dependencies. |
| includePeerDependencies | boolean | optional | Only applies when `content` is a package.json. peerDependencies are excluded from scanning by default (see `warnings`) since a peer is often intentionally left unresolved by the consumer. |
curl -X POST https://npmscan.com/api/osv/batch \
-H "Content-Type: application/json" \
-d '{"packages":[{"name":"lodash","version":"4.17.15"},{"name":"minimist","version":"1.2.0"}]}'{
"inputFormat": "packages",
"parsedPackageCount": 2,
"results": [
{
"package": { "name": "lodash", "version": "4.17.15" },
"scanStatus": "scanned",
"vulns": [{ "id": "GHSA-35jh-r3h4-6jhm", "summary": "Prototype Pollution in lodash", "fixedVersion": "4.17.19", "...": "full OSV vuln doc" }],
"isVulnerable": true,
"highestSeverity": "HIGH"
},
{
"package": { "name": "minimist", "version": "1.2.0" },
"scanStatus": "scanned",
"vulns": [{ "id": "GHSA-vh95-rmgr-6w4m", "summary": "Prototype Pollution in minimist", "fixedVersion": "1.2.3", "...": "full OSV vuln doc" }],
"isVulnerable": true,
"highestSeverity": "MODERATE"
}
]
}{ "error": "\"packages\" must contain at most 1000 items" }- ›`results[i]` corresponds positionally to the package list you sent (or, with `content`, the packages npmscan parsed out of it) — same order, one entry per package.
- ›Unlike OSV.dev's own batch endpoint (which only returns bare `{id, modified}` stubs), each `results[i].vulns` entry is enriched with full detail — summary, severity, aliases, `fixedVersion` — via a follow-up fetch per unique advisory; requests over 100 packages are chunked past OSV's own batch cap transparently.
- ›`results[i].scanStatus: "not-scanned"` means this package's spec pointed somewhere other than the registry (git/file/workspace/URL) or never resolved to a version — it was excluded from the OSV query entirely, NOT queried by name alone (which would otherwise attach an unrelated public npm package's entire vulnerability history to it). `vulns: []`/`isVulnerable: false` there is NOT a clean bill of health. `results[i].package` also carries `actualName` (set only for an npm alias — the real package `vulns` attach to) and `declaredSpec` (set when `version` was resolved from a package.json range/tag rather than being an already-exact pin).
- ›Optional response fields, present only when relevant: `ignoredCount`/`warnings` (entries from `content` that couldn't be identified as npm dependencies), `queryFailureCount` (packages an upstream OSV query failed for — treat those as unchecked, not confirmed clean), `enrichmentNote` (advisory detail truncated past this call's enrichment budget).
- ›Unlike the batch_query_vulnerabilities MCP tool this mirrors, this endpoint does not currently return `signals` (popularity/maintenance/typosquat) or `source` (lockfile resolved-URL/integrity tamper detection) — those are MCP-only today.
- ›This is exactly what powers the batch scan on npmscan.com/analyze: parse your package.json or package-lock.json (v1–v3) into a { name, version } list for every dependency, POST it here, then optionally call GET /api/npm/package/:name per package to flag outdated versions. Paste the file directly into /analyze for the full UI with that enrichment built in — or skip the parsing yourself and pass the file content via `content`.
Reviewed GitHub Security Advisories for the npm ecosystem — useful for a "what shipped this week" feed or as a fallback when OSV.dev hasn't ingested a just-published advisory yet.
| Name | Type | Required | Description |
|---|---|---|---|
| page | number | optional | Page number, 30 per page. Defaults to 1. |
| severity | string | optional | Filter: low, medium, high, critical, or all. |
curl "https://npmscan.com/api/advisories/latest?severity=critical&page=1"{
"advisories": [
{
"id": "GHSA-xxxx-xxxx-xxxx",
"cve": "CVE-2024-00000",
"ghsaUrl": "https://github.com/advisories/GHSA-xxxx-xxxx-xxxx",
"summary": "Remote code execution via crafted input",
"severity": "critical",
"publishedAt": "2024-05-01T00:00:00Z",
"updatedAt": "2024-05-02T00:00:00Z",
"packages": [
{ "name": "example-package", "affectedRange": "< 2.0.1", "patchedVersion": "2.0.1" }
]
}
]
}- ›Flattened and simplified from GitHub's advisory schema; scoped to `ecosystem=npm`, `type=reviewed`.
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | required | GHSA id (e.g. GHSA-xxxx-xxxx-xxxx) or CVE id. |
curl "https://npmscan.com/api/advisories/GHSA-xxxx-xxxx-xxxx"{
"id": "GHSA-xxxx-xxxx-xxxx",
"summary": "Remote code execution via crafted input",
"details": "Full advisory description...",
"aliases": ["CVE-2024-00000"],
"modified": "2024-05-02T00:00:00Z",
"published": "2024-05-01T00:00:00Z",
"database_specific": { "severity": "CRITICAL", "cwe_ids": ["CWE-94"] },
"severity": [{ "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" }],
"affected": [
{
"package": { "name": "example-package", "ecosystem": "npm" },
"ranges": [{ "type": "ECOSYSTEM", "events": [{ "introduced": "0" }, { "fixed": "2.0.1" }] }]
}
],
"references": [{ "type": "ADVISORY", "url": "https://github.com/advisories/GHSA-xxxx-xxxx-xxxx" }]
}{ "error": "Advisory not found" }- ›Same response shape as OSV — GitHub publishes immediately, OSV.dev syncs on its own delayed schedule. Query /api/osv/query first and fall back to this for a specific id.
Authoritative NIST NVD data, not OSV.dev — CVSS score/vector, CISA KEV (confirmed active exploitation) and FIRST.org EPSS (30-day exploitation probability) per CVE. Not npm-scoped like the rest of this API: NVD covers every ecosystem, so pass `keywordSearch` to narrow a search to a specific package. Shares its implementation with the get_cve MCP tool.
| Name | Type | Required | Description |
|---|---|---|---|
| cveId | string | optional | Exact CVE ID, e.g. "CVE-2024-12345". Use this OR the search filters below, not both. |
| keywordSearch | string | optional | Free-text search, e.g. a package or product name (1-200 chars). |
| severity | string | optional | Filter by CVSS v3 base severity: CRITICAL, HIGH, MEDIUM, or LOW. |
| cweId | string | optional | Filter by weakness type, e.g. "CWE-79". |
| publishedSince | string | optional | Publication date range start (YYYY-MM-DD). Must be given together with publishedUntil. |
| publishedUntil | string | optional | Publication date range end (YYYY-MM-DD). Range is capped at 120 days. |
| resultsPerPage | number | optional | Max results for a search (default 10, capped at 50). |
| startIndex | number | optional | Pagination offset for a search. |
curl -X POST https://npmscan.com/api/cve \
-H "Content-Type: application/json" \
-d '{"cveId":"CVE-2024-12345"}'{
"id": "CVE-2024-12345",
"npmscanUrl": "https://npmscan.com/vulnerability/CVE-2024-12345",
"vulnStatus": "Analyzed",
"description": "...",
"published": "2024-05-01T00:00:00.000",
"lastModified": "2024-05-02T00:00:00.000",
"cvss": { "version": "3.1", "baseScore": 9.8, "baseSeverity": "CRITICAL", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
"cwes": ["Improper Input Validation"],
"references": [{ "url": "https://example.com/advisory", "source": "cve@mitre.org", "tags": ["Third Party Advisory"] }],
"source": "nvd",
"kev": null,
"epss": { "score": 0.42, "percentile": 0.91, "date": "2024-05-03" }
}{ "error": "When \"cveId\" is provided, omit keywordSearch, severity, cweId, and date-range filters" }- ›A single-`cveId` lookup with no match returns `200 OK` with `{ "cveId": "...", "found": false, "npmscanUrl": "..." }` rather than a 404 — a bare CVE ID isn't guaranteed to exist yet.
- ›If NVD has no record yet (or hasn't scored one), a single-`cveId` lookup falls back to the raw MITRE CVE record automatically — look for `"source": "mitre"` on the result.
- ›A search (any of keywordSearch/severity/cweId/publishedSince+publishedUntil instead of cveId) returns `{ totalResults, startIndex, resultsPerPage, cves: [...] }` — each entry in `cves` has the same shape as the exact-lookup response above.
- ›`kev` is non-null only if this CVE is a confirmed, actively-exploited-in-the-wild vulnerability on CISA's catalog — treat that as an urgent-patch signal regardless of CVSS score.
- ›NVD enforces a strict shared rate limit; a 429 response means retry shortly rather than a genuine failure.
Deeper, heuristic checks beyond raw registry/vulnerability data: install-script scanning, transitive-dependency graphs, publish provenance, maintainer-change history, license policy, before/after diffing, remediation prioritization, and alternative-package suggestions. Each of these mirrors an MCP tool of the same shape — same request/response contract, same underlying logic.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name, e.g. "lodash" or "@scope/name". |
| version | string | optional | Exact version to analyze; omit to use the latest published version. |
curl -X POST https://npmscan.com/api/analysis/install-script \
-H "Content-Type: application/json" \
-d '{"name":"event-stream","version":"3.3.6"}'{
"name": "event-stream",
"version": "3.3.6",
"npmscanUrl": "https://npmscan.com/package/event-stream",
"hasLifecycleScripts": false,
"lifecycleScripts": {},
"filesScanned": [],
"scanNote": null,
"possibleTyposquatOf": null,
"findings": [],
"totalScore": 0,
"riskTier": "none"
}{ "error": "Package \"left-pad-typo\" not found" }- ›This is a heuristic static scan of the published tarball — it does not execute any code and cannot see behavior gated on runtime conditions.
- ›Fetches and gunzips the actual tarball, so it is slower than a plain registry lookup; budget for it accordingly in a CI gate.
- ›Shares its implementation with the analyze_install_script MCP tool.
| Name | Type | Required | Description |
|---|---|---|---|
| packages | Array<{ name, version? }> | required | 1-15 root packages, e.g. a package.json's "dependencies". version accepts an exact version or a semver range; omitted = latest. |
| maxDepth | number | optional | How many levels of transitive dependencies to expand (0 = only the roots). Default 2, max 3. |
curl -X POST https://npmscan.com/api/analysis/transitive-dependencies \
-H "Content-Type: application/json" \
-d '{"packages":[{"name":"express","version":"^4.18.0"}],"maxDepth":2}'{
"summary": "Scanned 62 packages (2 unresolved) up to depth 2; 1 vulnerable package found.",
"roots": [{ "name": "express", "requestedVersion": "^4.18.0" }],
"maxDepth": 2,
"nodes": [ /* full resolved graph: depth, parents, resolutionError, vulnerabilities */ ],
"vulnerablePaths": [
{
"name": "cookie",
"version": "0.4.2",
"highestSeverity": "MODERATE",
"vulnerabilityCount": 1,
"pulledInBy": ["express"],
"npmscanUrl": "https://npmscan.com/package/cookie"
}
],
"totalPackagesScanned": 62,
"unresolvedCount": 2,
"vulnerablePackageCount": 1,
"totalVulnerabilities": 1,
"truncated": false,
"truncationNote": null,
"enrichmentNote": null
}- ›Only the "dependencies" field is followed (not devDependencies/peerDependencies/optionalDependencies), and each range is resolved independently per branch — this does not emulate npm/yarn's actual hoisting/dedup.
- ›The traversal is capped at a total node budget per request; check `truncated`/`truncationNote` rather than assuming a large graph was scanned exhaustively.
- ›Prefer POST /api/osv/batch instead when you already have a flat package@version list — this endpoint is for when you only have the direct/root dependencies and need the graph walked for you.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name. |
| version | string | optional | Exact version to check; omit to use the latest published version. |
curl -X POST https://npmscan.com/api/analysis/provenance \
-H "Content-Type: application/json" \
-d '{"name":"tinycolor2"}'{
"name": "tinycolor2",
"version": "1.6.0",
"npmscanUrl": "https://npmscan.com/package/tinycolor2",
"provenance": { "hasProvenance": false, "predicateType": null, "sourceRepository": null, "workflowPath": null, "builderId": null, "sourceCommit": null, "buildRunUrl": null, "declaredRepository": "github.com/bgrins/TinyColor", "repositoryMatchesBuild": null, "note": null },
"peers": { "orgKind": null, "orgIdentifier": null, "peersChecked": 0, "peersWithProvenance": 0, "peerProvenanceRate": null, "note": null },
"sourceDiff": { "checked": false, "gitRef": null, "refSource": null, "addedInstallScripts": [], "addedDependencies": [], "note": null },
"findings": [],
"totalScore": 0,
"riskTier": "none"
}- ›Most packages do not use `npm publish --provenance` yet, so its bare absence is never scored on its own — only an org-norm anomaly (peers in scope mostly have it) or an actual source mismatch is.
- ›This is a heuristic, structural check: it does not cryptographically re-verify the Sigstore bundle — it trusts npm's registry already rejected any publish that failed that verification.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name. |
curl -X POST https://npmscan.com/api/analysis/maintainer-changes \
-H "Content-Type: application/json" \
-d '{"name":"chalk"}'{
"name": "chalk",
"npmscanUrl": "https://npmscan.com/package/chalk",
"lookbackDays": 180,
"currentMaintainers": [{ "name": "sindresorhus", "email": "sindresorhus@gmail.com" }],
"history": {
"versionsConsidered": 44,
"firstTrackedVersion": { "version": "0.1.0", "publishedAt": "2013-08-03T00:21:59.499Z" },
"latestTrackedVersion": { "version": "6.0.0", "publishedAt": "2026-07-26T14:51:07.269Z" },
"latestVersionPublishedViaTrustedPublisher": false,
"changes": [
{ "version": "1.1.2", "publishedAt": "2016-03-28T23:32:04.003Z", "added": ["qix"], "removed": ["jbnicolai"] },
{ "version": "5.6.2", "publishedAt": "2025-09-08T14:47:54.486Z", "added": [], "removed": ["qix"] }
],
"changesTruncated": false,
"note": null
},
"repository": { "checked": true, "declaredRepository": "git+https://github.com/chalk/chalk.git", "currentFullName": "chalk/chalk", "transferred": false, "archived": false, "reachable": true, "note": null },
"findings": [],
"totalScore": 0,
"riskTier": "none"
}- ›Reconstructed entirely from the packument's per-version maintainer snapshots — no extra API calls beyond the registry doc and one GitHub repo lookup.
- ›A maintainer-list change that happened on npm's site after the latest release (access changed but nothing published with it yet) surfaces as a `changes` entry with `version`/`publishedAt` both null.
- ›`findings: []`/`riskTier: "none"` here does NOT mean chalk has a clean history — it means nothing in `history.changes` falls inside the 180-day `lookbackDays` window right now. qix was REMOVED as a chalk maintainer on 2025-09-08 (the real, well-documented "qix" chalk/debug compromise — access revoked after it was discovered), which is real and visible in `history.changes` above, but is now old enough to no longer score. Always read `history.changes` itself, not just `findings`/`riskTier`, for the full record.
| Name | Type | Required | Description |
|---|---|---|---|
| maintainerUsername | string | required | Exact npm username, e.g. "jaredwray" — as shown at npmjs.com/~username. |
curl -X POST https://npmscan.com/api/analysis/maintainer-blast-radius \
-H "Content-Type: application/json" \
-d '{"maintainerUsername":"jaredwray"}'{
"maintainerUsername": "jaredwray",
"npmscanUrl": "https://npmscan.com/profile/jaredwray",
"npmProfileUrl": "https://www.npmjs.com/~jaredwray",
"avatarUrl": "https://npmscan.com/api/avatar/f08cf036a76de57f0deb190a24970e29",
"totalPackagesFound": 61,
"packagesReturned": 61,
"resultsTruncated": false,
"clusterWindowHours": 72,
"packages": [],
"clusters": [
/* 4 clusters as of this writing — this account keeps publishing, so
both the count and totals below will keep moving; re-verify live */
{
"windowStart": "2026-06-27T18:19:51.509Z",
"windowEnd": "2026-06-27T18:20:33.507Z",
"packageNames": ["@cacheable/utils", "@cacheable/memory", "@cacheable/node-cache", "cache-manager", "cacheable", "@cacheable/net", "flat-cache", "file-entry-cache"],
"packageCount": 8,
"combinedWeeklyDownloads": 278348331,
"stillCurrentMaintainerCount": 8
}
],
"findings": [ /* one tight-publish-cluster finding per cluster, 4 total */ ],
"totalScore": 116,
"riskTier": "critical",
"note": null
}- ›Uses npm's own maintainer:<username> search index (registry.npmjs.org's /-/v1/search) — the same reverse lookup npmjs.com's own site search uses. The public registry API has no dedicated "list packages by maintainer" endpoint otherwise.
- ›A large total package count is not itself a finding — only a tight cluster of packages published within `clusterWindowHours` of each other is scored, weighted by cluster size and by the packages' combined weekly downloads/dependentsCount. But a huge, entirely legitimate account is not automatically safe either: sindresorhus (1000+ packages, see /api/analysis/maintainer-profile below) currently combines 15 separate legitimate clusters accumulated over 5+ years into `riskTier: "critical"` — proof that this field is a prompt to verify with /api/analysis/maintainer-changes, never a standalone verdict, at any tier.
- ›A cluster where most packages share one npm scope (e.g. `@docusaurus/*`) is dampened — that's a project's own monorepo doing one coordinated release, not a compromised account spread across unrelated packages — and multiple distinct clusters on one account combine with diminishing returns rather than a plain sum, so a prolific account with several independent legitimate release clusters doesn't accumulate an unbounded score just from being active over time. A single very large or high-exposure cluster can still reach `riskTier: "critical"` on its own; treat it as a prompt to verify with `/api/analysis/maintainer-changes`, not as a standalone verdict.
- ›Results are capped at one search page (up to 250 packages, ranked by npm's own relevance/popularity scoring, not by recency) — `resultsTruncated`/`totalPackagesFound` say when a maintainer's real footprint exceeds that.
| Name | Type | Required | Description |
|---|---|---|---|
| maintainerUsername | string | required | Exact npm username, e.g. "sindresorhus" — as shown at npmjs.com/~username. |
curl -X POST https://npmscan.com/api/analysis/maintainer-profile \
-H "Content-Type: application/json" \
-d '{"maintainerUsername":"sindresorhus"}'{
"maintainerUsername": "sindresorhus",
"npmscanUrl": "https://npmscan.com/profile/sindresorhus",
"npmProfileUrl": "https://www.npmjs.com/~sindresorhus",
"avatarUrl": "https://npmscan.com/api/avatar/d36a92237c75c5337c17b60d90686bf9",
"totalPackagesFound": 1064,
"packagesReturned": 250,
"resultsTruncated": true,
"currentlyMaintainsCount": 250,
"totalWeeklyDownloads": 17893700000,
"totalDependents": 620500,
"packages": [
{
"name": "chalk",
"version": "6.0.0",
"lastPublished": "2026-07-26T14:51:07.269Z",
"weeklyDownloads": 411482632,
"dependentsCount": 160309,
"isCurrentMaintainer": true,
"npmscanUrl": "https://npmscan.com/package/chalk"
}
],
"note": null
}- ›Shares its business logic with the get_maintainer_profile MCP tool via runGetMaintainerProfile, and the same underlying npm search call as /api/analysis/maintainer-blast-radius (searchMaintainerPackages) — no duplicated fetch between the two endpoints.
- ›This endpoint does not run publish-cluster/compromised-account detection. Use /api/analysis/maintainer-blast-radius when the goal is a security read on whether recent activity looks like a takeover, not just a profile summary.
- ›Results are capped at one search page (up to 250 packages) — `resultsTruncated`/`totalPackagesFound` say when a maintainer's real footprint exceeds that, and the download/dependent totals are summed only over `packages` (packagesReturned), not the full totalPackagesFound.
| Name | Type | Required | Description |
|---|---|---|---|
| packages | Array<{ name, version? }> | required | 1-100 packages to check. |
| policy | { allow?: string[]; deny?: string[] } | optional | SPDX ids, family prefixes (e.g. "GPL"), or category names. Omit for the default policy: only copyleft/network-copyleft/proprietary are violations. |
curl -X POST https://npmscan.com/api/analysis/license-compliance \
-H "Content-Type: application/json" \
-d '{"packages":[{"name":"left-pad"},{"name":"readline-sync"}],"policy":{"deny":["GPL"]}}'{
"policy": { "mode": "deny", "allow": [], "deny": ["GPL"] },
"summary": "2 packages checked: 2 compliant, 0 violations, 0 need review.",
"results": [
{
"package": { "name": "left-pad" },
"npmscanUrl": "https://npmscan.com/package/left-pad",
"resolvedVersion": "1.3.0",
"rawLicense": "MIT",
"category": "permissive",
"isCompliant": true,
"needsReview": false,
"violation": null,
"resolutionError": null
}
],
"totalPackages": 2,
"compliantCount": 2,
"violationCount": 0,
"needsReviewCount": 0,
"unresolvedCount": 0
}- ›`policy.deny` always wins over `policy.allow`. With `policy.allow` set, anything not matching it is treated as a violation (unproven is non-compliant).
- ›Understands simple SPDX expressions: "(MIT OR GPL-3.0)" is compliant if either side is permitted; "MIT AND Apache-2.0" requires both. A nested/mixed expression is reported as `needsReview` rather than guessed at.
- ›Reads only the registry-declared `license` field — it does not fetch or parse LICENSE file contents from the source repository.
| Name | Type | Required | Description |
|---|---|---|---|
| before | string | required | Raw file content: package.json, package-lock.json (npm v1-v3), yarn.lock (classic or Berry), or pnpm-lock.yaml. |
| after | string | required | Raw file content, same format rules as `before`. Format is auto-detected per side; before/after may differ. |
curl -X POST https://npmscan.com/api/analysis/dependency-diff \
-H "Content-Type: application/json" \
-d '{"before":"<package.json before>","after":"<package.json after>"}'{
"summary": "Compared package.json snapshots: 0 added, 0 removed, 1 changed — 1 flagged for a newly introduced install script, source/integrity change, override change, and/or vulnerability.",
"beforeFormat": "package.json",
"afterFormat": "package.json",
"comparisonNote": null,
"added": [],
"removed": [],
"changed": [
{
"name": "ua-parser-js",
"npmscanUrl": "https://npmscan.com/package/ua-parser-js",
"beforeVersion": "0.7.28",
"afterVersion": "0.7.29",
"coexistingVersions": null,
"changeType": "upgrade",
"hasInstallScript": true,
"installScriptIntroduced": true,
"installScriptKeys": ["preinstall"],
"installScriptKeysIntroduced": ["preinstall"],
"sourceIntegrityChanged": null,
"resolvedUrl": null,
"integrity": null,
"isVulnerable": true,
"highestSeverity": "CRITICAL",
"vulnerabilities": [],
"vulnerabilityDelta": "introduced",
"resolutionNote": null
}
],
"totalAdded": 0,
"totalRemoved": 0,
"totalChanged": 1,
"flaggedCount": 1,
"truncated": false,
"truncationNote": null,
"enrichmentNote": null,
"projectLifecycleChanges": null,
"overridesChanges": null
}{ "error": "before: unrecognized file format — expected package.json, package-lock.json, yarn.lock, or pnpm-lock.yaml" }- ›`installScriptIntroduced` and `sourceIntegrityChanged` are the two highest-signal per-package fields here: a routine-looking patch bump that quietly adds a postinstall script is the shape of a compromised-maintainer attack, and a lockfile entry whose resolved tarball URL/integrity hash changed while the version stayed IDENTICAL is a tampered lockfile or compromised mirror a version-only diff would miss.
- ›Every lockfile format (package-lock.json, pnpm-lock.yaml, yarn.lock) reports its FULL resolved graph — direct and transitive dependencies alike — so a transitive-only bump is caught too; a package.json is diffed as its own declared list only, since a manifest has no transitive data at all.
- ›`projectLifecycleChanges` (the scanned project's own root lifecycle scripts) and `overridesChanges` (`overrides`/`resolutions`/`pnpm.overrides`) are reported independently of `added`/`removed`/`changed` — a PR that only touches one of these changes nothing else here would normally flag.
- ›Body size is capped at 8 MiB per snapshot to bound worst-case parse time for a pathological monorepo lockfile.
- ›Ideal for a CI gate reviewing a dependency-changing PR — pass the file content directly, no shell-out to npm/yarn/pnpm required.
| Name | Type | Required | Description |
|---|---|---|---|
| findings | Array<{ packageName, cveId?, severity?, currentVersion?, fixedVersion?, advisoryId?, findingType? }> | required | 1-200 findings, typically straight from POST /api/osv/batch or /api/analysis/transitive-dependencies. findingType: "malware" (or a MAL-* advisoryId) forces tier "remove-now"; omit for an ordinary vulnerability. |
curl -X POST https://npmscan.com/api/analysis/prioritize-remediation \
-H "Content-Type: application/json" \
-d '{"findings":[{"packageName":"lodash","cveId":"CVE-2020-8203","severity":"HIGH","fixedVersion":"4.17.19"}]}'{
"totalFindings": 1,
"uniqueCveCount": 1,
"summary": { "removeNow": 0, "patchNow": 0, "patchSoon": 1, "scheduled": 0, "monitor": 0, "kevListedCount": 0 },
"ranked": [
{
"rank": 1,
"packageName": "lodash",
"cveId": "CVE-2020-8203",
"advisoryId": null,
"currentVersion": null,
"fixedVersion": "4.17.19",
"severity": "HIGH",
"kev": null,
"epss": { "score": 0.045, "percentile": 0.91, "date": "2024-01-15" },
"score": 62,
"tier": "patch-soon",
"findingType": "vulnerability",
"reason": "High EPSS (4.5%) with HIGH severity, not on CISA KEV.",
"npmscanUrl": "https://npmscan.com/package/lodash",
"cveNpmscanUrl": "https://npmscan.com/vulnerability/CVE-2020-8203"
}
]
}- ›Does not re-query OSV/NVD itself — pass in findings other tools already returned; this only adds CISA KEV + FIRST.org EPSS enrichment and ranks the batch.
- ›A CVE id shared by multiple findings in the same call is only looked up once.
- ›A finding with no `cveId` (a GHSA advisory with no CVE alias) is still ranked, using severity alone as the fallback signal.
- ›A confirmed-malware finding (`findingType: "malware"` in the request body, or a `MAL-*` `advisoryId` — auto-detected even without setting `findingType`) forces `tier: "remove-now"` ahead of every other signal, including KEV/EPSS.
| Name | Type | Required | Description |
|---|---|---|---|
| packageName | string | required | Exact npm package name, e.g. "lodash" or "@scope/name". |
| currentVersion | string | required | Currently installed version — exact version, semver range, or dist-tag. |
| targetVersion | string | optional | Version to simulate upgrading to — exact version, range, or dist-tag (e.g. the fixedVersion a prioritize-remediation finding named). Omit for the registry's "latest" dist-tag. |
curl -X POST https://npmscan.com/api/analysis/simulate-dependency-upgrade \
-H "Content-Type: application/json" \
-d '{"packageName":"lodash","currentVersion":"3.10.1","targetVersion":"4.17.21"}'{
"packageName": "lodash",
"npmscanUrl": "https://npmscan.com/package/lodash",
"resolvedCurrentVersion": "3.10.1",
"resolvedTargetVersion": "4.17.21",
"direction": "upgrade",
"semverBump": "major",
"isBreakingBySemver": true,
"majorVersionsSkipped": 0,
"targetDeprecated": null,
"installScriptIntroduced": false,
"engineChange": null,
"currentIsVulnerable": false,
"targetIsVulnerable": false,
"vulnerabilityDelta": "still-clean",
"targetVulnerabilities": [],
"riskTier": "breaking-change-likely",
"reasons": [
"Major version bump — semver signals this release is allowed to contain breaking API changes."
],
"verdict": "Upgrading \"lodash\" from the current to the target version is a major upgrade that is likely to require code changes."
}- ›Does not fetch the package's changelog/release notes or diff the target tarball's source — this is a fast, deterministic pre-check built from registry metadata and OSV, not a substitute for reading the release notes on a flagged major bump.
- ›A minor bump between two pre-1.0 (0.x) versions is treated as breaking-risk, per semver's own convention that the public API isn't stable before 1.0.
- ›An unresolvable version spec (a typo, an unsatisfiable range) comes back as a normal 200 with `resolvedCurrentVersion`/`resolvedTargetVersion: null` and an explanatory note, not a request error — only a nonexistent package name is rejected outright.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name, e.g. "request" or "node-sass". |
| reason | string | optional | One of deprecated/vulnerable/abandoned/typosquat/general — biases filtering/ranking. Auto-inferred when omitted. |
| limit | number | optional | Max suggestions to return. Default 5, max 10. |
curl -X POST https://npmscan.com/api/analysis/suggest-alternative \
-H "Content-Type: application/json" \
-d '{"name":"request","limit":3}'{
"source": { "name": "request", "latestVersion": "2.88.2", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "isLatestVersionVulnerable": false, "highestSeverity": null, "popularityTier": "high", "maintenanceTier": "stale", "possibleTyposquatOf": null, "npmscanUrl": "https://npmscan.com/package/request" },
"reason": "deprecated",
"confidence": "high",
"categoryTokens": ["http", "request", "client"],
"searchedQueries": ["http client"],
"nonPackageAlternatives": ["fetch (built-in since Node 18)"],
"suggestions": [
{
"name": "undici",
"version": "6.19.2",
"description": "An HTTP/1.1 client, written from scratch for Node.js",
"npmscanUrl": "https://npmscan.com/package/undici",
"weeklyDownloads": 42000000,
"dependentsCount": 1200,
"githubStars": 6200,
"hasBuiltInTypes": true,
"deprecated": null,
"isLatestVersionVulnerable": false,
"highestSeverity": null,
"popularityTier": "very-high",
"maintenanceTier": "active",
"topPackagesRank": 42,
"categoryOverlap": ["http", "client"],
"matchedQueries": ["http client"],
"whySuggested": "Actively maintained, very high adoption, and covers the same HTTP-client category."
}
]
}- ›Combines maintainer-provided deprecation hints (parsed from the `deprecated` field) with deterministic npm-search category matching — it is not an LLM call.
- ›Filters out typosquats and stale/weak contenders before ranking, so every suggestion is itself a reasonably healthy package.
| Name | Type | Required | Description |
|---|---|---|---|
| packages | string[] | required | 2-5 exact npm package names, e.g. ["axios", "got", "node-fetch"]. |
curl -X POST https://npmscan.com/api/analysis/compare-packages \
-H "Content-Type: application/json" \
-d '{"packages":["axios","got","node-fetch"]}'{
"candidates": [
{ "name": "axios", "found": true, "weeklyDownloads": 65000000, "popularityTier": "very-high", "maintenanceTier": "active", "hasBuiltInTypes": true, "isLatestVersionVulnerable": false, "installScriptRisk": { "hasLifecycleScripts": false, "riskTier": "none", "scanScope": "lifecycle-scripts-only" }, "installSize": { "unpackedSize": 1983343, "transitive": { "transitiveUnpackedSize": 2271546, "transitiveDependencyCount": 12, "sizeUnknownCount": 1, "truncated": false } }, "score": 41 },
{ "name": "got", "found": true, "weeklyDownloads": 26000000, "popularityTier": "very-high", "maintenanceTier": "active", "hasBuiltInTypes": true, "isLatestVersionVulnerable": false, "installScriptRisk": { "hasLifecycleScripts": false, "riskTier": "none", "scanScope": "lifecycle-scripts-only" }, "installSize": { "unpackedSize": 448958, "transitive": { "transitiveUnpackedSize": 1553040, "transitiveDependencyCount": 19, "sizeUnknownCount": 0, "truncated": false } }, "score": 26 },
{ "name": "node-fetch", "found": true, "weeklyDownloads": 45000000, "popularityTier": "very-high", "maintenanceTier": "aging", "hasBuiltInTypes": true, "isLatestVersionVulnerable": false, "installScriptRisk": { "hasLifecycleScripts": false, "riskTier": "none", "scanScope": "lifecycle-scripts-only" }, "installSize": { "unpackedSize": 107319, "transitive": { "transitiveUnpackedSize": 9236710, "transitiveDependencyCount": 6, "sizeUnknownCount": 0, "truncated": false } }, "score": 20 }
],
"differentiators": {
"mostDownloads": "axios",
"mostGithubStars": "axios",
"hasTypeScriptSupport": ["axios", "got", "node-fetch"],
"hasKnownVulnerabilities": [],
"deprecated": [],
"possibleTyposquat": [],
"installScriptRiskFlagged": [],
"smallestInstallSize": "got",
"largestInstallSize": "node-fetch"
},
"recommendation": {
"pick": "axios",
"runnerUp": "got",
"rationale": "axios: widely used (65,000,000 weekly downloads); actively maintained (published 12d ago); ships built-in TypeScript types; no known vulnerabilities in its latest version.",
"confidence": "high"
}
}- ›Shares its business logic with the compare_packages MCP tool via runComparePackages — same enrichment get_package uses, fanned out across candidates.
- ›installScriptRisk is a lightweight, tarball-free scan of lifecycle script command strings only — call POST /api/analysis/install-script on a specific candidate for the deeper, file-content-aware scan.
- ›installSize.unpackedSize is the candidate's own package size; installSize.transitive sums dist.unpackedSize across its resolved dependency tree (depth 2, capped at 60 nodes per candidate) — a small package can still have a large transitive footprint, so check both. sizeUnknownCount/truncated flag when that sum is partial rather than exact; call POST /api/analysis/transitive-dependencies on a specific candidate for the full graph.
- ›A name that fails to resolve (typo, unpublished, malformed) still appears in `candidates` with `found:false` and `resolutionError` set rather than failing the whole request; duplicate names are rejected with a 400.
| Name | Type | Required | Description |
|---|---|---|---|
| url | string | required | GitHub repository URL, e.g. "https://github.com/owner/repo". |
| ref | string | optional | Branch, tag, or commit SHA to audit; omit to use the repository default branch. |
| includeDevDependencies | boolean | optional | Include package.json devDependencies — root and, for a monorepo, every merged workspace member. Default false; ignored when a pnpm/yarn lockfile is used instead. |
| policy | { allow?: string[], deny?: string[] } | optional | Same shape as /api/analysis/license-compliance. Omit for the default policy (only copyleft/network-copyleft/proprietary are violations). |
curl -X POST https://npmscan.com/api/analysis/audit-github-repository \
-H "Content-Type: application/json" \
-d '{"url":"https://github.com/expressjs/express"}'{
"summary": "Audited 28 dependencies from expressjs/express: 0 with known vulnerabilities, 0 license violation(s), 3 flagged install script(s).",
"owner": "expressjs",
"repoName": "express",
"ref": "master",
"defaultBranchUsed": true,
"manifestPath": "package.json",
"lockfilePath": null,
"inputFormat": "package.json",
"isMonorepo": false,
"workspacePatterns": [],
"workspacePackageCount": 0,
"workspaceNote": null,
"policy": { "mode": "default", "allow": [], "deny": [] },
"findings": [
{
"name": "content-disposition",
"requestedVersion": null,
"resolvedVersion": "3.0.0",
"npmscanUrl": "https://npmscan.com/package/content-disposition",
"deprecated": null,
"possibleTyposquatOf": null,
"isVulnerable": false,
"highestSeverity": null,
"vulnerabilities": [],
"rawLicense": "MIT",
"licenseCategory": "permissive",
"isLicenseCompliant": true,
"licenseNeedsReview": false,
"licenseViolation": null,
"hasLifecycleScripts": true,
"installScriptRiskTier": "low",
"installScriptScore": 3,
"installScriptScanScope": "deep-tarball-scan",
"installScriptFindings": [
{ "rule": "lifecycle-present", "text": "Lifecycle scripts present", "points": 3, "note": "preinstall/install/postinstall/prepare detected in package.json or script files.", "locations": [{ "file": "package.json#scripts", "snippet": "prepare" }] }
],
"resolutionError": null
}
],
"overflowPackages": [],
"totalPackages": 28,
"vulnerablePackageCount": 0,
"licenseViolationCount": 0,
"installScriptFlaggedCount": 3,
"deepScannedCount": 3,
"warnings": [],
"truncationNote": null,
"deepScanNote": null
}{ "error": "No package.json found in \"owner/repo\" at ref \"main\"" }- ›Shares its business logic with the audit_github_repository MCP tool via runAuditGithubRepository.
- ›Tries pnpm-lock.yaml, then package-lock.json, then yarn.lock, in that order — the first one found is used for exact resolved versions; falls back to package.json alone (ranges resolved against the registry, so requestedVersion comes back null and each package is checked at its current latest registry version) when none exist.
- ›A monorepo (package.json#workspaces, Yarn's {packages:[...]} form, or pnpm-workspace.yaml) is detected automatically. pnpm-lock.yaml and yarn.lock already record every workspace member's dependencies directly; for a package-lock.json or no-lockfile repo, the repo's file tree is additionally listed, the declared glob patterns resolved to member directories, and each member's dependencies merged into the audit (capped at 50 member packages). See isMonorepo/workspacePatterns/workspacePackageCount/workspaceNote in the response.
- ›Every dependency (up to 100 per call, across the root and any merged workspace members) gets a vulnerability check, a license-compliance verdict, and a tarball-free install-script signal. Up to 10 packages that actually declare a lifecycle script — prioritized by already-vulnerable, then possible-typosquat — additionally get the full tarball-fetching deep scan (installScriptScanScope: "deep-tarball-scan"); the rest are noted in deepScanNote.
- ›This is the most expensive endpoint in the suite (a repo lookup, a handful of file fetches, up to 100 registry doc fetches, one OSV batch call, up to 10 tarball fetches, and — for a monorepo needing enumeration — one file-tree listing plus up to 50 more manifest fetches) — avoid calling it in a tight loop across many repos.
| Name | Type | Required | Description |
|---|---|---|---|
| packages | Array<{ name, version? }> | optional | 1-1000 items, capped to 100 when includeLicenses is on. Use this OR `content`, not both. |
| content | string | optional | Raw package.json / lockfile / CycloneDX JSON / SPDX JSON content. Use this OR `packages`, not both. |
| format | "cyclonedx" | "spdx" | optional | Default "cyclonedx". |
| includeDevDependencies | boolean | optional | Only applies when `content` is a manifest/lockfile format that distinguishes dev dependencies. Default false. |
| includeVulnerabilities | boolean | optional | Query OSV.dev and embed findings natively. Default true. |
| includeLicenses | boolean | optional | Resolve registry license data and embed it natively. Default true. |
| policy | { allow?: string[]; deny?: string[] } | optional | Same shape as /api/analysis/license-compliance. Only affects the echoed policy/licenseViolationCount, never blocks generation. |
| componentName | string | optional | Name of the SBOM's own root component/document, if known — sets SPDX documentDescribes to the matching package. |
| componentVersion | string | optional | Paired with componentName. |
curl -X POST https://npmscan.com/api/analysis/generate-sbom \
-H "Content-Type: application/json" \
-d '{"packages":[{"name":"minimist","version":"1.2.5"}]}'{
"format": "cyclonedx",
"sbom": {
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:62464a1d-22b7-4c8b-86e3-e7760f344b2f",
"components": [
{ "bom-ref": "pkg:npm/minimist@1.2.5", "type": "library", "name": "minimist", "version": "1.2.5", "purl": "pkg:npm/minimist@1.2.5", "licenses": [{ "expression": "MIT" }] }
],
"vulnerabilities": [
{
"id": "GHSA-xvch-5gv4-984h",
"source": { "name": "GitHub Advisories", "url": "https://npmscan.com/vulnerability/GHSA-xvch-5gv4-984h" },
"references": [{ "id": "CVE-2021-44906", "source": { "name": "NVD" } }],
"ratings": [{ "source": { "name": "OSV" }, "severity": "critical" }],
"recommendation": "Upgrade to 1.2.6 or later.",
"affects": [{ "ref": "pkg:npm/minimist@1.2.5" }],
"analysis": { "state": "in_triage" }
}
]
},
"inputFormat": "packages",
"parsedPackageCount": 1,
"policy": { "mode": "default", "allow": [], "deny": [] },
"totalVulnerabilities": 1,
"packagesWithVulnerabilities": 1,
"licenseViolationCount": 0
}{ "error": "Provide either \"packages\" or \"content\"" }- ›Shares its business logic with the generate_sbom MCP tool via runGenerateSbom.
- ›CycloneDX gets a top-level `vulnerabilities[]` array (one entry per unique advisory id, `affects[]` listing every impacted component, `analysis.state` always "in_triage") and per-component `licenses[]`. SPDX 2.3 has no vulnerabilities array of its own, so each finding becomes a `packages[].externalRefs[]` entry (`referenceCategory: "SECURITY"`, `referenceType: "advisory"`) instead, alongside the native `licenseDeclared`/`licenseConcluded` fields.
- ›A package with no OSV findings gets no vulnerability entry at all — no fabricated "clean" record. The CycloneDX `dependencies[]` transitive graph and any SPDX package hierarchy are intentionally omitted: this only has a flat inventory, not resolved edges between packages.
- ›`sbom` is validated against the official CycloneDX 1.6 / SPDX 2.3 JSON Schemas in npmscan's own test suite.
| Name | Type | Required | Description |
|---|---|---|---|
| content | string | required | Raw `npm audit --json` stdout — either npm 7+'s {"vulnerabilities": {...}} format (auditReportVersion 2) or legacy npm 6's {"advisories": {...}}. |
curl -X POST https://npmscan.com/api/analysis/enrich-npm-audit \
-H "Content-Type: application/json" \
-d '{"content":"{\"auditReportVersion\":2,\"vulnerabilities\":{\"minimist\":{\"name\":\"minimist\",\"severity\":\"critical\",\"isDirect\":true,\"via\":[{\"source\":1179,\"name\":\"minimist\",\"url\":\"https://github.com/advisories/GHSA-xvch-5gv4-984h\",\"title\":\"Prototype Pollution in minimist\",\"severity\":\"critical\"}],\"fixAvailable\":true}}}"}'{
"inputFormat": "npm-audit-v2",
"totalFindings": 1,
"uniqueCveCount": 1,
"ghsaResolvedToCveCount": 1,
"summary": { "removeNow": 0, "patchNow": 0, "patchSoon": 1, "scheduled": 0, "monitor": 0, "kevListedCount": 0 },
"ranked": [
{
"rank": 1,
"packageName": "minimist",
"cveId": "CVE-2021-44906",
"advisoryId": "GHSA-xvch-5gv4-984h",
"advisoryTitle": "Prototype Pollution in minimist",
"currentVersion": null,
"fixedVersion": null,
"severity": "CRITICAL",
"kev": null,
"epss": { "score": 0.021, "percentile": 0.88, "date": "2024-01-15" },
"score": 12.6,
"tier": "scheduled",
"findingType": "vulnerability",
"reason": "Low predicted exploitation probability (EPSS 2.1%), CRITICAL severity.",
"npmscanUrl": "https://npmscan.com/package/minimist",
"cveNpmscanUrl": "https://npmscan.com/vulnerability/CVE-2021-44906",
"isDirect": true,
"fixAvailable": true,
"fixTarget": null
}
],
"warnings": [],
"skippedCount": 0
}{ "error": "No advisory-bearing packages found in this npm audit report — metadata.vulnerabilities.total may be 0 (a clean audit has nothing to rank)." }- ›Shares its business logic with the enrich_npm_audit MCP tool via runEnrichNpmAudit — which itself composes parseNpmAuditJson with runPrioritizeRemediation, the same function POST /api/analysis/prioritize-remediation calls directly.
- ›npm audit JSON almost never includes a CVE id itself, only a GHSA advisory URL — each GHSA is resolved to a CVE alias via OSV.dev first (ghsaResolvedToCveCount reports how many); a GHSA with no CVE alias falls back to severity-only ranking, same as prioritize-remediation's own documented behavior for a missing cveId.
- ›A package whose `via` array is only chain pointers to another package's own advisory (npm v2 format) contributes no separate finding — counted in `skippedCount`, not dropped silently.
- ›`fixTarget` can name a different package than the vulnerable one (e.g. bumping a parent to pull in a patched transitive dependency) — its version is only ever surfaced as `fixedVersion` when it matches the vulnerable package itself.
- ›`yarn audit --json` and `pnpm audit --json` use different report shapes and are not supported — parse those as a lockfile instead via /api/analysis/... endpoints that accept `content`.
- ›A `MAL-*`-shaped advisory id in the audit report is auto-detected as malware and forces `tier: "remove-now"` ahead of every other signal — same behavior as /api/analysis/prioritize-remediation's `findingType`.
Live XML feeds for anyone who'd rather subscribe in a feed reader, Slack/Discord RSS bridge, or SIEM than poll a REST endpoint. These live off the site root, not under /api.
curl "https://npmscan.com/latest-vulnerabilities/rss.xml"<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>NPMSCan - Latest NPM Vulnerabilities</title>
<link>https://npmscan.com/latest-vulnerabilities</link>
<atom:link href="https://npmscan.com/latest-vulnerabilities/rss.xml" rel="self" type="application/rss+xml" />
<description>Live feed of reviewed security advisories affecting the npm ecosystem (GitHub Advisory Database).</description>
<ttl>60</ttl>
<item>
<title>GHSA-xxxx-xxxx-xxxx (CVE-2024-00000): Remote code execution via crafted input</title>
<link>https://npmscan.com/vulnerability/GHSA-xxxx-xxxx-xxxx</link>
<atom:link href="https://npmscan.com/vulnerability/GHSA-xxxx-xxxx-xxxx/rss.xml" rel="related" type="application/rss+xml" />
<guid isPermaLink="false">GHSA-xxxx-xxxx-xxxx</guid>
<pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate>
<description>severity: critical | packages: example-package | rss: https://npmscan.com/vulnerability/GHSA-xxxx-xxxx-xxxx/rss.xml | source: https://github.com/advisories/GHSA-xxxx-xxxx-xxxx</description>
</item>
</channel>
</rss>- ›`<ttl>60</ttl>` — feeds are effectively live; a reader polling once a minute won't miss anything.
- ›Each item links to its own per-vulnerability feed via an atom:link rel="related", so a reader can offer "subscribe to just this CVE" from the main feed.
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | required | An OSV-recognized id (GHSA ids reliably resolve). This hits OSV.dev's /v1/vulns/:id directly — unlike /api/advisories/:id, it doesn't go through GitHub's API or accept a bare CVE id. |
curl "https://npmscan.com/vulnerability/GHSA-35jh-r3h4-6jhm/rss.xml"<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>NPMSCan - GHSA-35jh-r3h4-6jhm</title>
<link>https://npmscan.com/vulnerability/GHSA-35jh-r3h4-6jhm</link>
<ttl>1440</ttl>
<item>
<title>GHSA-35jh-r3h4-6jhm: Prototype Pollution in lodash</title>
<link>https://npmscan.com/vulnerability/GHSA-35jh-r3h4-6jhm</link>
<guid isPermaLink="false">GHSA-35jh-r3h4-6jhm</guid>
<pubDate>Wed, 15 Jul 2020 00:00:00 GMT</pubDate>
<category>high</category>
<category>CVE-2020-8203</category>
<description>id: GHSA-35jh-r3h4-6jhm
aliases: CVE-2020-8203
severity: high
published: 2020-07-15T00:00:00Z
affected:
- lodash (< 4.17.19)
references:
- ADVISORY: https://github.com/advisories/GHSA-35jh-r3h4-6jhm</description>
</item>
</channel>
</rss>Repo and maintainer signals pulled live from the GitHub API — stars, recent activity, and account age are useful trust signals alongside the package data above.
| Name | Type | Required | Description |
|---|---|---|---|
| url | string | required | Full GitHub repo URL, e.g. https://github.com/owner/repo. |
curl "https://npmscan.com/api/github/stars?url=https://github.com/lodash/lodash"{ "stars": 59892 }{ "error": "Invalid GitHub repository URL" }| Name | Type | Required | Description |
|---|---|---|---|
| url | string | required | Full GitHub repo URL. |
curl "https://npmscan.com/api/github/repo/commits?url=https://github.com/lodash/lodash"{
"commits": [
{
"sha": "a1b2c3d",
"html_url": "https://github.com/owner/repo/commit/a1b2c3d",
"message": "Fix regression in X",
"author_login": "octocat",
"author_name": "The Octocat",
"author_avatar_url": "https://avatars.githubusercontent.com/u/1?v=4",
"date": "2024-05-01T12:00:00Z"
}
]
}- ›Last 10 commits.
| Name | Type | Required | Description |
|---|---|---|---|
| url | string | required | Full GitHub repo URL. |
curl "https://npmscan.com/api/github/repo/contributors?url=https://github.com/lodash/lodash"{
"contributors": [
{ "login": "octocat", "contributions": 482, "html_url": "https://github.com/octocat", "avatar_url": "https://avatars.githubusercontent.com/u/1?v=4" }
]
}- ›Top 10 by contribution count.
| Name | Type | Required | Description |
|---|---|---|---|
| url | string | required | Full GitHub repo URL. |
curl "https://npmscan.com/api/github/repo/issues?url=https://github.com/lodash/lodash"{
"issues": [
{
"number": 123,
"title": "Memory leak on large payloads",
"html_url": "https://github.com/owner/repo/issues/123",
"state": "open",
"labels": ["bug"],
"author_login": "someuser",
"author_avatar_url": "https://avatars.githubusercontent.com/u/2?v=4",
"created_at": "2024-04-20T09:00:00Z",
"updated_at": "2024-04-25T09:00:00Z"
}
]
}- ›Up to 10 issues, sorted by most recently updated.
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | optional | GitHub username. Provide this or `url`. |
| url | string | optional | A repo URL to derive the owner from, e.g. https://github.com/owner/repo. |
curl "https://npmscan.com/api/github/user?username=sindresorhus"{
"login": "sindresorhus",
"name": "Sindre Sorhus",
"html_url": "https://github.com/sindresorhus",
"avatar_url": "https://avatars.githubusercontent.com/u/170270?v=4",
"created_at": "2010-03-01T12:00:00Z",
"public_repos": 1000,
"followers": 50000,
"following": 50,
"blog": "https://sindresorhus.com",
"twitter_username": "sindresorhus",
"company": null,
"location": "Oslo, Norway",
"bio": null
}- ›A brand-new account with one popular package is a very different risk profile than a maintainer active for a decade — this endpoint is what powers that check.