Data & Insight

Supply chain integrity, now on /v1/check

RobertUpdated Apr 18, 20260 min read
Dark terminal-style blog feature image with a teal "now live · /v1/check" badge. Left panel shows the headline "Two signals. One API call." with a merge diagram showing CVE risk state fields (risk_state, actively_exploited, patch_available) and supply chain integrity fields (compromised, compromise_type, safe_version) converging into a single /v1/check endpoint. Right panel shows a full JSON API response for litellm version 1.82.8 with the new supply_chain object highlighted, containing compromised true, compromise_type malicious_publish, and safe_version 1.82.6. Below the response, four stat pills: 26 monitored packages, 3 data sources, 6h refresh cycle, 0 new endpoints.

Supply chain integrity, now on /v1/check#

The gap CVE data does not cover#

CVE feeds are good at describing vulnerabilities in code that was written honestly. They are not designed to describe code that was written dishonestly.

A malicious publish to PyPI will almost never have a CVE. A typosquat, a hijacked maintainer account, a version pushed in the middle of the night by a compromised CI token: none of these arrive through the CVE pipeline. It will have an incident report, maybe an OSV MAL- advisory a day or two later, maybe a yanked_reason on PyPI. These signals are real, but they live in different places and they arrive at different speeds.

The March 2026 LiteLLM TeamPCP incident is the example everyone is pointing to right now. A single malicious release (litellm==1.82.7) was live on PyPI for several hours. A CVE-only pipeline had no way to flag it during that window.

Today we are shipping the signal that closes that gap.


What is new#

/v1/check now returns a supply_chain object for a curated set of monitored PyPI packages. It is independent from risk_state:

  • risk_state continues to reflect NVD and CVE data only.
  • supply_chain reflects known malicious publishes and security-related yanks. A package can be CVE-clean and supply-chain-compromised at the same time. They are two separate gates and you should treat them that way.

The API#

A monitored, clean version:

curl "https://api.attestd.io/v1/check?product=langchain&version=0.3.0" \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "product": "langchain",
  "version": "0.3.0",
  "supported": true,
  "risk_state": "none",
  "supply_chain": {
    "compromised": false,
    "sources": [],
    "malware_type": null,
    "advisory_url": null,
    "compromised_at": null,
    "removed_at": null
  },
  "last_updated": "2026-04-18T08:00:00Z"
}

A confirmed compromise:

{
  "product": "litellm",
  "version": "1.82.7",
  "supported": true,
  "risk_state": "none",
  "supply_chain": {
    "compromised": true,
    "sources": ["registry", "osv"],
    "malware_type": "backdoor",
    "description": "TeamPCP supply chain attack — credential stealer in proxy_server.py",
    "advisory_url": "https://docs.litellm.ai/blog/security-update-march-2026",
    "compromised_at": "2026-03-24T10:39:00Z",
    "removed_at": "2026-03-24T16:00:00Z"
  },
  "last_updated": "2026-04-18T08:00:00Z"
}

Three things worth noting about the shape:

sources is a list of short source identifiers, not nested objects. If two detectors agree on the same version, you will see both strings. The descriptive fields (malware_type, description, advisory_url, compromised_at, removed_at) are aggregated on the supply_chain object itself, one record per (package, version). If the requested product is not on the monitored list, supply_chain is null. That is not a clean bill of health. It means supply chain monitoring does not apply to that product. Keep checking risk_state in that case.


The Python SDK#

pip install --upgrade attestd

SDK 0.2.0 adds typed access to the supply chain signal via RiskResult.supply_chain:

import attestd
 
client = attestd.Client(api_key="YOUR_API_KEY")
 
result = client.check("litellm", "1.82.7")
 
if result.supply_chain and result.supply_chain.compromised:
    print(f"[BLOCK] {result.product} {result.version}")
    print(f"Detected by: {', '.join(result.supply_chain.sources)}")
    print(f"{result.supply_chain.malware_type}: {result.supply_chain.description}")
    raise SystemExit(1)

result.supply_chain is a SupplyChainSignal frozen dataclass. sources is a tuple[str, ...]. All datetime fields are parsed to timezone-aware datetime objects.


Exact versions, not ranges#

Supply chain compromise is a property of a specific release artifact. litellm==1.82.7 was malicious; 1.82.6 was not. We match on exact (package, version) tuples and we do not expand ranges.

This is the opposite of how CVE handling works, where affected version ranges are normal and expected. For this signal, a pinned dependency either is or is not on a known-bad list. The answer should not change based on how we interpret a range.


Three sources, one object#

The sources array can contain any combination of three strings:

SourceWhat it represents
registryAttestd's human-verified incident registry. The fastest path for incidents under active response, before OSV has indexed them.
osvOSV.dev malicious-package advisories with MAL- IDs.
pypi_yankPyPI releases yanked with a security-related yanked_reason.

When multiple sources agree on the same version, you will see all of them listed. Independent confirmation across sources is reflected in a higher confidence score.


What we are monitoring in phase 1#

26 PyPI packages, chosen because they sit at the top of typical AI and data-platform dependency graphs. A single bad publish in any of them affects a large number of downstream users.

LLM and agent frameworks (11): litellm, langchain, langchain-core, langgraph, langgraph-checkpoint, autogen-agentchat, autogen-core, crewai, transformers, openai, anthropic

Data science (5): numpy, pandas, scikit-learn, polars, dask

Vector DB clients (3): pinecone, qdrant-client, chromadb

Web frameworks (3): fastapi, django, flask

Cloud SDKs (3): boto3, google-cloud-storage, azure-core

HTTP (1): requests

We picked this list deliberately small so we can stand behind every answer. Coverage will expand. Email support@attestd.io with the packages you depend on and we will prioritise based on demand.


Deterministic by design#

There is no ML in the supply chain detection path. Every compromised: true traces back to a specific source: a record in the verified registry with a timestamp and an author, an OSV MAL- advisory linked in advisory_url, or a PyPI release yanked with a security-related reason. The decision is reproducible, explainable, and auditable. When we say a version is compromised, you can check our work.


How fresh is the data#

Ingestion runs on a fixed cadence, pulling OSV, scanning PyPI release metadata for security yanks, and merging the registry. A new incident that shows up in OSV or gets yanked on PyPI is reflected in /v1/check within a handful of hours.

For the incidents that matter most, the active ones still being responded to, the registry path is the shortest. We can seed a compromise record the moment it is confirmed, without waiting for OSV indexing. That is what carried us through TeamPCP.


Using it in CI/CD#

A minimal pre-deploy check that walks pinned dependencies and fails the build on any compromise:

import attestd
 
client = attestd.Client(api_key="YOUR_API_KEY")
 
with open("requirements.txt") as f:
    for line in f:
        line = line.strip()
        if not line or line.startswith("#") or "==" not in line:
            continue
        package, version = line.split("==", 1)
        result = client.check(package.strip(), version.strip())
        if result.supply_chain and result.supply_chain.compromised:
            raise SystemExit(
                f"[BLOCK] {package}=={version} "
                f"compromised via {', '.join(result.supply_chain.sources)}"
            )

A more complete version handling >= constraints, unsupported products, and combining supply chain with CVE risk is in the supply chain docs.


What is not in scope yet#

Phase 1 is exact-version detection for a fixed watchlist. We are being explicit about the edges:

  • No range detection. We flag specific versions, not version families.
  • No transitive risk. Direct dependencies only.
  • PyPI only. npm, RubyGems, and other ecosystems are on the roadmap.
  • Watchlist only. If you depend on a package we do not monitor, supply_chain will be null. We will grow the list based on what customers actually depend on.

Getting started#

If you already have an Attestd API key, the supply_chain field is live on /v1/check right now. No changes required on your side. Upgrade to SDK 0.2.0 to get typed access via RiskResult.supply_chain.

Full field reference, response semantics, and the monitored package list are 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.

Supply chain attacks are not going away. Neither is this signal.