Key points and observations
- On March 24, 2026, two PyPI releases of LiteLLM,
1.82.7and1.82.8, were published with malicious code as a result of a supply chain compromise. PyPI later quarantined the project. - Datadog Security Research's investigation determines that the LiteLLM compromise is the latest event in a five-day campaign that began with the March 19 Trivy compromise, continued with CanisterWorm on npm, and then reached Checkmarx KICS and related artifacts.
- Defenders should treat any host or CI job that installed
litellm1.82.7or1.82.8as a full-credential exposure event and investigate for persistence, outbound traffic, and Kubernetes activity, not just package presence.
What happened
LiteLLM is a widely used Python library and proxy layer for talking to many LLM providers. It is important to note that this was not a fake package and not a typo-squatted name. This was a compromise of the real litellm project on PyPI as a result of an ongoing supply chain campaign that had already moved through Trivy, npm, Aqua Security's internal GitHub, and Checkmarx.
The following image provides an overview of how the whole attack is unfolding:
Before getting into the payload details, it helps to understand the timeline of events. In our analysis, the operator moved from project to project, siphoning credentials and using them to expand the campaign. Each stage reused access or tradecraft from the one before it.
March 19: Trivy is compromised
On March 19, 2026, a threat actor used compromised credentials to:
- Publish a malicious
trivyv0.69.4release - Force-push 76 of 77
aquasecurity/trivy-actiontags to malicious commits - Replace all seven
aquasecurity/setup-trivytags.
The v0.69.4 tag triggered Trivy's standard release machinery, which then pushed the compromised build through GHCR, ECR Public, Docker Hub, deb and rpm packages, and get.trivy.dev.
The malicious trivy-action and setup-trivy commits dumped Runner.Worker memory, scraped common credential locations, encrypted the results with AES and RSA, and exfiltrated the data to scan.aquasecurtiy[.]org (a look alike domain which may avoid suspicion). If direct exfiltration failed, and a usable GitHub token was present, the malware created a public tpcp-docs repository and uploaded the stolen data there instead. This would allow the threat actors to access the exfiltrated data via this public repo.
March 19 was also not a single release-and-leave event. Additional malicious workflow injections took place into aquasecurity/tfsec, aquasecurity/traceeshark, and aquasecurity/trivy-action. By the end of the day, the campaign had already moved from one poisoned release to active reuse of a compromised identity across other repositories and workflows.
March 20 through March 22: The attacker starts spending stolen access
By March 20, the incident had already moved beyond a poisoned GitHub Action. The attacker was pushing a self-propagating npm worm across multiple publisher scopes: 28 packages in @EmilGroup, 16 in @opengov, plus @teale.io/eslint-config, @airtm/uuid-base32, and @pypestream/floating-ui-dom.
Our assessment is that this marks the first major expansion of the campaign. The worm stole npm tokens from compromised environments, resolved which packages each token could publish, bumped patch versions, fetched the original READMEs to preserve appearances, and republished the packages with the malicious payload. At that point, the attacker was automating the next compromise and broadening the campaign.
By March 22, the infrastructure behind the npm activity was also being used for something more aggressive. The same callback domain used in the npm worm was serving a Kubernetes-focused script that split victims into destructive and non-destructive paths.
The script checked timezone and locale to determine if the system was Iranian. On Iranian systems it deployed a host-provisioner-iran DaemonSet, mounted the host root filesystem, and ran a container named kamikaze that deleted the host filesystem and force-rebooted the node. On non-Iranian Kubernetes targets, it deployed host-provisioner-std, mounted / from the host, and installed persistent backdoor logic instead.
Later on March 22, the GitHub side of the campaign became more explicit. A GitHub account called Argon-DevOps-Mgt created and deleted a ghost branch on aquasecurity/trivy-plugin-aqua, then roughly seven hours later the same access was used to deface Aqua Security's internal aquasec-com GitHub organization. In total, 44 repositories were renamed, all with a tpcp-docs- prefix and the description "TeamPCP Owns Aqua Security."
March 23: the same pattern reaches Checkmarx and OpenVSX
By March 23, the campaign had moved into another vendor's release chain. The attacker(s) compromised Checkmarx/kics-github-action, Checkmarx/ast-github-action, and two OpenVSX extensions, ast-results 2.53.0 and cx-dev-assist 1.7.0.
This phase reused the same fallback pattern seen earlier in the campaign. The KICS payload used a setup.sh stealer tied to checkmarx[.]zone and fell back to creating a docs-tpcp repository with the victim's GITHUB_TOKEN if direct C2 failed.
March 23 is the clearest bridge into the LiteLLM compromise. By then, the attacker had already demonstrated a repeated pattern against GitHub Actions, package registries, and developer tooling in more than one vendor environment.
March 24: LiteLLM is published to PyPI with malicious code
litellm 1.82.7 and 1.82.8 were both released on March 24, 2026. These packages feature the same malicious payload with different execution mechanisms.
LiteLLM technical analysis
The Payload
The payload is easiest to follow as a sequence:
- Collect secrets and credentials: The malware gathers environment variables, SSH keys, cloud credentials, Kubernetes data, Docker configs, shell history, database credentials, wallet files, and CI/CD secrets.
- Encrypt the haul locally: It then uses a hybrid scheme for encryption: an AES-256 session key for the data, then RSA-4096 for the session key.
- Exfiltrate it: Once encrypted on the host, the data is then sent to
models.litellm[.]cloudusing the headerX-Filename: tpcp.tar.gz. - Install persistence: The payload writes
~/.config/sysmon/sysmon.pyand installs a user systemd unit calledsysmon.service. - Beacon for follow-on payloads: After this, the malware polls
https://checkmarx[.]zone/raw, downloads/tmp/pglog, and executes whatever the attacker serves there next. - Spread in Kubernetes if it can: The payload can create privileged
node-setup-*pods when it finds a usable Kubernetes service-account token. That turns a package compromise into a cluster compromise.
Version 1.82.7
litellm 1.82.7 carried an injected payload in litellm/proxy/proxy_server.py. That means the malicious logic lived in package code rather than in a one-off install hook.
A victim that installed 1.82.7, and then subsequently used the vulnerable package in their application, would execute the attackers payload.
Version 1.82.8
Version 1.82.8 is the more dangerous story. This version includes a new litellm_init.pth file inside the wheel. From the Python site documentation, executable lines in .pth files run during interpreter startup.
This means that if the package is installed, it will launch the malicious payload when the Python interpreter is started.
Responding to the incident if you're affected
Any environment with litellm 1.82.7 or 1.82.8 installed should be treated as potentially compromised, not just exposed to a bad dependency. The first question is whether the payload had an opportunity to execute. For 1.82.8, that bar is low because the malicious .pth file can run when Python starts. For 1.82.7, prioritize systems that imported the affected proxy module, especially LiteLLM proxy deployments.
Responders should focus on four things:
- Scope affected systems: Identify every host, container, CI job, and developer workstation that installed either version. Separate "package present" from "likely executed" where you can, but do not assume a system is safe if you cannot prove the payload never ran.
- Map reachable credentials: Review what the affected process could access locally and what it could fetch on demand. Prioritize cloud credentials, GitHub and package publishing tokens, CI secrets, SSH keys, Kubernetes service account tokens,
.envfiles, and access to centralized secret stores such as AWS Secrets Manager or SSM Parameter Store. - Rotate based on blast radius: Revoke and reissue any credential that was reachable from an affected runtime, starting with credentials that can publish code, access CI/CD, read shared secrets, or create infrastructure. If an exposed token touched a build or release pipeline, review downstream artifacts published from that environment as well.
- Hunt for follow-on activity and persistence: Look for outbound traffic to
models.litellm[.]cloud,checkmarx[.]zone, related ICP or Cloudflare infrastructure, and for filesystem artifacts such as litellm_init.pth,~/.config/sysmon/sysmon.py,~/.config/systemd/user/sysmon.service,/tmp/pglog, and/tmp/.pg_state. In Kubernetes, review audit logs for unusual secret access, privileged pod creation, or pod names matchingnode-setup-*.
Do not treat reverting the LiteLLM package as a complete remediation. The recovery bar should be higher and requires removing the compromised versions, rotating reachable credentials, reviewing recent artifacts published from exposed environments, and rebuilding critical systems from known-good images and pinned dependencies.
Protecting against known malicious packages with SCFW
The Supply-Chain Firewall (SCFW) is an open-source project that transparently wraps 'pip install' and 'npm install' and automatically blocks known malicious packages such as the compromised LiteLLM ones.
We use SCFW extensively at Datadog, as it's a low-friction way to secure developer workstations against similar threats.
To try SCFW, head over to the GitHub repository.
How Datadog can help
If you're a Code Security customer, the Datadog Security Research Feed can help you easily identify if your environment is affected by this campaign, including by the latest LiteLLM compromised packages.
Datadog Code Security can also identify hosts, containers, and build environments where litellm 1.82.7 or 1.82.8 were installed. That gives you the first scope boundary.
library_name:litellm status:Open library_version:(1.82.7 OR 1.82.8)
Because the compromise primarily impacts services with floating dependencies, you may want to broaden the search to library_version:(1.82.6 OR 1.82.7 OR 1.82.8) to catch services whose data hasn’t yet been refreshed, even though 1.82.6 isn’t vulnerable on its own.
Then move to follow-on activity. Hunt for the linked domains, filesystem paths, and Kubernetes behavior across logs, workload telemetry, and cloud data. A short starting set:
@dns.question.name:(models.litellm[.]cloud OR checkmarx[.]zone OR *.icp0[.]io OR *aquasecurtiy[.]org OR *trycloudflare[.]com)
Note: Cloudflare Tunnels are a legitimate tool for exposing services to the internet. If your environment already uses them, narrow the query to the specific subdomains from the IoC list below instead of treating the whole service as suspicious.
source:kubernetes.audit @objectRef.resource:pods (@objectRef.name:*node-setup-* OR @requestObject.spec.containers.name:(kamikaze OR provisioner))
source:kubernetes.audit @http.method:(get OR list) @objectRef.resource:secrets @userAgent:Python-urllib*
source:cloudtrail @evt.name:(GetSecretValue OR ListSecrets OR DescribeParameters) @http.useragent:Python-urllib*
@file.path:(*litellm_init.pth OR */.config/sysmon/sysmon.py OR */.config/systemd/user/sysmon.service OR /tmp/pglog OR /tmp/.pg_state)
Conclusion
The LiteLLM incident is the latest episode in an ongoing supply-chain compromise that has involved numerous projects across millions of downloads. This is a case study in how stolen CI/CD credentials from an initial attack of one repository had cascading impact that can be turned into fresh compromises across multiple ecosystems in a matter of days.
Defenders should monitor their infrastructure for suspicious activity related to these recent compromises, and evaluate their prevention, detection, and response playbooks to determine preparedness for future attacks.
IOCs
Affected packages
| Type | Name | Affected versions |
|---|---|---|
| Python package | `litellm` | 1.82.7, 1.82.8 |
| Docker image | `aquasec/trivy` | 0.69.4, 0.69.5, 0.69.6 |
| Docker image | `ghcr.io/aquasecurity/trivy` | 0.69.4, 0.69.5, 0.69.6 |
| Docker image | `docker.io/aquasec/trivy:0.69.4` | 0.69.4, 0.69.5, 0.69.6 |
| Docker image | `public.ecr.aws/aquasecurity/trivy` | 0.69.4, 0.69.5, 0.69.6 |
| GitHub action | `aquasecurity/setup-trivy` | 0.2.0 to 0.2.6 |
| GitHub action | `aquasecurity/trivy-action` | All tags not starting with v, except `0.35.0` |
| GitHub action | `Checkmarx/kics-github-action` | v1.1 |
| GitHub action | `Checkmarx/ast-github-action` | v2.3.28 |
| OpenVSX extension | `ast-results` | 2.53.0 |
| OpenVSX extension | `cx-dev-assist` | 1.7.0 |
| npm package | @pypestream/floating-ui-dom | 2.15.1 |
| npm package | @leafnoise/mirage | 2.0.3 |
| npm package | @opengov/ppf-backend-types | 1.141.2 |
| npm package | eslint-config-ppf | 0.128.2 |
| npm package | react-leaflet-marker-layer | 0.1.5 |
| npm package | react-leaflet-cluster-layer | 0.0.4 |
| npm package | react-autolink-text | 2.0.1 |
| npm package | opengov-k6-core | 1.0.2 |
| npm package | jest-preset-ppf | 0.0.2 |
| npm package | cit-playwright-tests | 1.0.1 |
| npm package | eslint-config-service-users | 0.0.3 |
| npm package | babel-plugin-react-pure-component | 0.1.6 |
| npm package | @opengov/form-renderer | 0.2.20 |
| npm package | @opengov/qa-record-types-api | 1.0.3 |
| npm package | @opengov/form-builder | 0.12.3 |
| npm package | @opengov/ppf-eslint-config | 0.1.11 |
| npm package | @opengov/form-utils | 0.7.2 |
| npm package | react-leaflet-heatmap-layer | 2.0.1 |
| npm package | @virtahealth/substrate-root | 1.0.1 |
| npm package | @airtm/uuid-base32 | 1.0.2 |
| npm package | @emilgroup/setting-sdk | 0.2.3,0.2.2,0.2.1 |
| npm package | @emilgroup/partner-portal-sdk | 1.1.3,1.1.2,1.1.1 |
| npm package | @emilgroup/gdv-sdk-node | 2.6.3,2.6.2,2.6.1 |
| npm package | @emilgroup/docxtemplater-util | 1.1.4,1.1.3,1.1.2 |
| npm package | @emilgroup/accounting-sdk | 1.27.3,1.27.2,1.27.1 |
| npm package | @emilgroup/task-sdk | 1.0.4,1.0.3,1.0.2 |
| npm package | @emilgroup/setting-sdk-node | 0.2.3,0.2.2,0.2.1 |
| npm package | @emilgroup/task-sdk-node | 1.0.4,1.0.3,1.0.2 |
| npm package | @emilgroup/partner-sdk | 1.19.3,1.19.2,1.19.1 |
| npm package | @emilgroup/numbergenerator-sdk-node | 1.3.3,1.3.2,1.3.1 |
| npm package | @emilgroup/customer-sdk | 1.54.5,1.54.4,1.54.3,1.54.2,1.54.1 |
| npm package | @emilgroup/commission-sdk | 1.0.3,1.0.2,1.0.1 |
| npm package | @emilgroup/process-manager-sdk | 1.4.2,1.4.1 |
| npm package | @emilgroup/changelog-sdk-node | 1.0.3,1.0.2 |
| npm package | @emilgroup/document-sdk-node | 1.43.6,1.43.5,1.43.4,1.43.3,1.43.2,1.43.1 |
| npm package | @emilgroup/commission-sdk-node | 1.0.3,1.0.2,1.0.1 |
| npm package | @emilgroup/document-uploader | 0.0.12,0.0.11,0.0.10 |
| npm package | @emilgroup/discount-sdk | 1.5.3,1.5.2,1.5.1 |
| npm package | @emilgroup/discount-sdk-node | 1.5.2,1.5.1 |
| npm package | @teale.io/eslint-config | 1.8.16,1.8.15,1.8.14,1.8.13,1.8.12,1.8.11,1.8.10,1.8.9 |
| npm package | @emilgroup/insurance-sdk | 1.97.6,1.97.5,1.97.4,1.97.3,1.97.2,1.97.1 |
| npm package | @emilgroup/account-sdk | 1.41.2,1.41.1 |
| npm package | @emilgroup/account-sdk-node | 1.40.2,1.40.1 |
| npm package | @emilgroup/accounting-sdk-node | 1.26.2,1.26.1 |
| npm package | @emilgroup/api-documentation | 1.19.2,1.19.1 |
| npm package | @emilgroup/auth-sdk | 1.25.2,1.25.1 |
| npm package | @emilgroup/auth-sdk-node | 1.21.2,1.21.1 |
| npm package | @emilgroup/billing-sdk | 1.56.2,1.56.1 |
| npm package | @emilgroup/billing-sdk-node | 1.57.2,1.57.1 |
| npm package | @emilgroup/claim-sdk | 1.41.2,1.41.1 |
| npm package | @emilgroup/claim-sdk-node | 1.39.2,1.39.1 |
| npm package | @emilgroup/customer-sdk-node | 1.55.2,1.55.1 |
| npm package | @emilgroup/document-sdk | 1.45.2,1.45.1 |
| npm package | @emilgroup/gdv-sdk | 2.6.2,2.6.1 |
| npm package | @emilgroup/insurance-sdk-node | 1.95.2,1.95.1 |
| npm package | @emilgroup/notification-sdk-node | 1.4.2,1.4.1 |
| npm package | @emilgroup/partner-portal-sdk-node | 1.1.2,1.1.1 |
| npm package | @emilgroup/partner-sdk-node | 1.19.2,1.19.1 |
| npm package | @emilgroup/payment-sdk | 1.15.2,1.15.1 |
| npm package | @emilgroup/payment-sdk-node | 1.23.2,1.23.1 |
| npm package | @emilgroup/process-manager-sdk-node | 1.13.2,1.13.1 |
| npm package | @emilgroup/public-api-sdk | 1.33.2,1.33.1 |
| npm package | @emilgroup/public-api-sdk-node | 1.35.2,1.35.1 |
| npm package | @emilgroup/tenant-sdk | 1.34.2,1.34.1 |
| npm package | @emilgroup/tenant-sdk-node | 1.33.2,1.33.1 |
| npm package | @emilgroup/translation-sdk-node | 1.1.2,1.1.1 |
Network
| Indicator | Context |
|---|---|
models.litellm[.]cloud |
LiteLLM exfiltration endpoint |
checkmarx[.]zone/raw |
Follow-on C2 polling endpoint referenced in LiteLLM public analysis and KICS reporting |
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io |
CanisterWorm ICP canister C2 from related campaign reporting |
aquasecurtiy[.]org |
Trivy typosquat from the earlier March 2026 compromise |
championships-peoples-point-cassette.trycloudflare[.]com |
Related campaign Cloudflare tunnel |
investigation-launches-hearings-copying.trycloudflare[.]com |
Related campaign Cloudflare tunnel |
souls-entire-defined-routes.trycloudflare[.]com |
Related campaign Cloudflare tunnel |
Filesystem and persistence
| Indicator | Context |
|---|---|
litellm_init.pth |
Malicious startup hook in litellm 1.82.8 |
~/.config/sysmon/sysmon.py |
LiteLLM persistence script path |
~/.config/systemd/user/sysmon.service |
LiteLLM user systemd unit |
sysmon.service |
Service name used for persistence |
/tmp/pglog |
Downloaded and executed second-stage payload |
/tmp/.pg_state |
State file used by the beacon |
X-Filename: tpcp.tar.gz |
Exfiltration header observed in public analysis |
Kubernetes
| Indicator | Context |
|---|---|
node-setup-* |
Privileged pod naming pattern in LiteLLM analysis |
kamikaze |
Related campaign container name from TeamPCP reporting |
provisioner |
Related campaign container name from TeamPCP reporting |
Packages
| Indicator | Context |
|---|---|
litellm==1.82.7 |
Confirmed compromised release |
litellm==1.82.8 |
Confirmed compromised release |
Sources:
- https://research.jfrog.com/post/canister-worm/
- https://www.stepsecurity.io/blog/canisterworm-how-a-self-propagating-npm-worm-is-spreading-backdoors-across-the-ecosystem
- https://ramimac.me/trivy-teampcp
- https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23
- https://checkmarx.com/blog/checkmarx-security-update/
- https://github.com/pypa/advisory-database/blob/main/vulns/litellm/PYSEC-2026-2.yaml