Data & Insight

npm Supply Chain Monitoring Is Live on Attestd

RobertUpdated May 8, 20266 min read
Dark terminal-style feature image. White text reads: PyPI. Now npm. Same call. Below it: supply_chain.compromised: true in teal monospace. Attestd branding bottom left.

npm Supply Chain Monitoring Is Live on Attestd#

Attestd now monitors npm packages for supply chain compromise. The same /v1/check endpoint, the same response shape, the same two independent signals. You do not specify an ecosystem in the API call. The endpoint infers it automatically from the package name.

45 npm packages are monitored in phase 1. Two of them, @bitwarden/cli and @checkmarx/kics, already have confirmed compromised versions in the database from the April 22 2026 TeamPCP campaign.


The API#

The call shape is identical to any other Attestd check. Scoped package names are URL-encoded in the query string:

curl "https://api.attestd.io/v1/check?product=%40bitwarden%2Fcli&version=2026.4.0" \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "product": "@bitwarden/cli",
  "version": "2026.4.0",
  "supported": true,
  "risk_state": "none",
  "supply_chain": {
    "compromised": true,
    "sources": ["registry", "osv"],
    "malware_type": "backdoor",
    "description": "TeamPCP supply chain attack via compromised GitHub Actions CI/CD pipeline. Credential stealer targets SSH keys, cloud credentials, Claude Code auth tokens, and MCP configs.",
    "advisory_url": "https://www.bleepingcomputer.com/news/security/bitwarden-cli-npm-package-compromised-to-steal-developer-credentials/",
    "compromised_at": "2026-04-22T17:57:00Z",
    "removed_at": "2026-04-22T19:30:00Z"
  }
}

risk_state is none because there is no CVE for this attack. supply_chain.compromised is true because the package was a malicious publish. These are independent signals and you need both. A package can pass a CVE check and fail a supply chain check at the same time. That is exactly what happened with every real attack Attestd has covered this year.

A clean version of the same package:

{
  "product": "@bitwarden/cli",
  "version": "2026.3.0",
  "supported": true,
  "risk_state": "none",
  "supply_chain": {
    "compromised": false,
    "sources": []
  }
}

compromised: false on a monitored package is a meaningful signal. It means all three ingestion sources checked this version and found nothing.


The JavaScript SDK#

import { Client } from "@attestd/sdk";
 
const client = new Client({ apiKey: process.env.ATTESTD_API_KEY });
 
const result = await client.check("@bitwarden/cli", "2026.4.0");
 
if (result.supplyChain?.compromised) {
  console.log(`BLOCKED: ${result.product}@${result.version}`);
  console.log(`Malware type: ${result.supplyChain.malwareType}`);
  console.log(`Window: ${result.supplyChain.compromisedAt}${result.supplyChain.removedAt}`);
  process.exit(1);
}

The JS SDK already URL-encodes scoped package names automatically. Pass the package name as-is: @bitwarden/cli, not %40bitwarden%2Fcli.


Checking both ecosystems in one loop#

The Python SDK handles npm packages the same way it handles PyPI packages. You can check dependencies from both ecosystems in a single loop:

from attestd import Client
 
client = Client(api_key="YOUR_API_KEY")
 
dependencies = {
    # npm packages
    "@langchain/core": "0.3.0",
    "openai": "4.67.0",
    "@bitwarden/cli": "2026.4.0",   # this one will fire
    # PyPI packages
    "langchain": "0.3.0",
    "litellm": "1.83.0",
}
 
for package, version in dependencies.items():
    result = client.check(package, version)
    if result.supply_chain and result.supply_chain.compromised:
        print(f"SUPPLY CHAIN ALERT: {package}@{version}")
        print(f"  {result.supply_chain.description}")

The endpoint infers ecosystem from the package name. You do not need to specify it.


What is in the database already#

Two confirmed incidents from the April 22 2026 TeamPCP campaign are seeded:

@bitwarden/cli@2026.4.0

Live on npm for 90 minutes: 5:57 PM to 7:30 PM ET on April 22. The same TeamPCP campaign as the LiteLLM attack in March. The payload harvested SSH keys, AWS/GCP/Azure credentials, GitHub tokens, npm auth tokens, .env files, Claude Code tokens (~/.claude.json), and MCP server configurations. It also functioned as a self-propagating worm: if npm publish tokens were found on the compromised system, the malware injected itself into every package the victim could publish and republished them with an incremented version number.

@checkmarx/kics@2.1.21

Live for 84 minutes: 14:17 to 15:41 UTC on April 22. Same campaign. A malicious Docker image and VS Code extension delivered a credential-stealing MCP addon targeting GitHub tokens, cloud credentials, and Claude Code configurations.

Both are confirmed by multiple security researchers and flagged with sources: ["registry", "osv"].


The 45 monitored packages#

The watchlist covers packages that sit deepest in CI/CD pipelines and have the broadest access to credentials and infrastructure secrets.

LLM and AI frameworks: @langchain/core, langchain, openai, @anthropic-ai/sdk, llamaindex, @huggingface/transformers, ai (Vercel AI SDK), @google/generative-ai, groq-sdk, cohere-ai, @mistralai/mistralai, @modelcontextprotocol/sdk

Web frameworks: express, fastify, next, nuxt, @nestjs/core, hono

Database and ORM: @prisma/client, mongoose, typeorm, pg, drizzle-orm

Auth and security: jsonwebtoken, bcryptjs, passport, helmet, @bitwarden/cli, @checkmarx/kics

HTTP and core: axios, got, node-fetch, undici, ky

Cloud SDKs: @aws-sdk/client-s3, firebase, @google-cloud/storage, @azure/storage-blob

Dev toolchain: typescript, eslint, prettier, jest, vite

Real-time: socket.io, mqtt


Three data sources#

OSV GCS dataset. Google's open-source vulnerability database publishes a full zip of every MAL- prefixed advisory for npm at https://storage.googleapis.com/osv-vulnerabilities/npm/all.zip. Attestd sweeps this on every 6-hour ingestion cycle. The MAL- prefix specifically means malicious code was injected into a published package, which is distinct from CVEs.

npm Deprecation API. The npm registry exposes deprecation messages per version. Attestd fetches the full version manifest for each watchlist package and checks every version's deprecated field for targeted-attack language: malicious, compromised, backdoor, credential, stolen, supply chain, malware, trojan, injected. A deduplication rule applies: if the same deprecation message appears on more than three versions of a package it is treated as a generic maintenance notice and skipped. Targeted supply chain events are always version-specific.

Manual registry. A human-curated YAML file for incidents where OSV indexing lags. This is how @bitwarden/cli and @checkmarx/kics were seeded. The human curation path gets compromised versions into the database faster than waiting for OSV to publish. Registry entries carry a confidence score of 1.0.


Confidence scoring#

SourceConfidence
registry (manual YAML)1.0
osv0.95
npm_deprecation0.80

When multiple sources agree on the same compromised version, the highest confidence wins and all source names appear in the sources array. For @bitwarden/cli@2026.4.0, both registry and osv flagged the version, so sources: ["registry", "osv"] and confidence: 1.0.


What pip audit and npm audit do not catch#

pip audit and npm audit check for known CVEs. They scan your dependency manifest against the NVD and the npm advisory database. They are useful tools.

They would not have caught any of the four supply chain attacks Attestd has covered since March:

  • litellm==1.82.7 (March 24): no CVE
  • elementary-data==0.23.3 (April 24): no CVE
  • pytorch-lightning==2.6.3 (April 30): no CVE
  • @bitwarden/cli@2026.4.0 (April 22): no CVE All four had risk_state: none at time of attack. All four had supply_chain.compromised: true. CVE monitoring and supply chain monitoring cover different threat categories and you need both.

The full monitored package list is at attestd.io/docs/supply-chain.

Get an API key at api.attestd.io/portal/login. Free tier, 1,000 calls a month, no credit card required.