You checked the wrong forty minutes
On August 14, 2026, SOCRadar published a reattribution of the LiteLLM supply-chain compromise: 95% of the affected organizations had already been harvested days earlier, by Aqua Security's own Trivy scanner. Five months of incident response were scoped to the wrong window, because every control in the chain needed someone to name the bad package first. Bromure Agentic Coding's supply-chain and credential layers don't need the name.
You were told to check whether you installed a Python package during a forty-minute window on March 24. Five months later, SOCRadar found that 95% of the victims had already been emptied out five days earlier, by their vulnerability scanner.
On August 14, SecurityWeek's Ionut Arghire published SOCRadar's reattribution of one of the year's largest developer-infrastructure compromises. SOCRadar held the record-level dataset, with per-organization collection records for 2,188 entities, and asked a question nobody had asked: when did each organization's data leave?
For 2,085 of them, collection had ended before the malicious packages everyone was warned about were published. That is 95%. SOCRadar's conclusion runs to one sentence:
"That timing lines up with the upstream Trivy compromise rather than the LiteLLM install window. The 40 minutes everyone reported was the closing act, not the whole play."
Researchers took the payload apart back in March. SOCRadar changed the timeline. For five months the industry ran incident response against the wrong window, using controls that could do nothing until somebody named the bad thing.
The closing act
The famous forty minutes belong to LiteLLM. On March 24, versions 1.82.7 and 1.82.8 went up on PyPI at 10:39 UTC and lasted about forty minutes before PyPI quarantined them. Guidance at the time told you to treat any install through 16:00 UTC that day as suspect.
The payload deserves a moment for what it defeats. It is a .pth file,
litellm_init.pth, dropped into site-packages. Python executes .pth files
at interpreter startup whether or not anything imports the package that
installed them. There is no setup.py to inspect and no install script to
strip, so --ignore-scripts and every control built on the install-script
execution model sail past it. The Hacker News
covered the mechanism
on August 12 and noted the ecosystem-level identifier, CVE-2026-33634. The
harvest went to models.litellm[.]cloud.
The attackers pushed those wheels with a stolen PyPI publishing token, past LiteLLM's own release pipeline. So where did that token come from?
The whole play
Trivy. Aqua Security's open-source vulnerability scanner, which tens of thousands of pipelines run to find this kind of problem.
Aqua's advisory,
GHSA-69fq-xp46-6x23,
does not soften the root cause. A threat actor tracked as TeamPCP (UNC6780 to
Google) lifted a personal access token out of Trivy's GitHub Actions
environment through a pull_request_target workflow in late February. Aqua
disclosed that on March 1 and rotated credentials. The advisory then says the
rotation was "not atomic (not all credentials were revoked simultaneously)."
The attacker sat inside the rotation and collected the new secrets as Aqua
minted them.
Nineteen days later they spent them. On March 19 they force-pushed 76 of the
77 version tags in aquasecurity/trivy-action, and all seven tags in
aquasecurity/setup-trivy, onto malicious commits, so a workflow pinned to
@v0.28.0 fetched new code with nothing in the repository appearing to change.
Using the compromised aqua-bot service account, they published Trivy
v0.69.4 to GHCR, ECR Public, Docker Hub, deb and rpm. Docker Hub images
v0.69.5 and v0.69.6 followed on March 22–23 on a separately compromised
credential.
StepSecurity's teardown describes what the injected stealer did once a pipeline ran it:
- Read every runner process's environment through
/proc/*/environ. - Dumped the GitHub Actions runner worker's memory via
/proc/<pid>/mem, using base64-encoded Python. That recovers the secrets a runner masks in its logs, since masking is a log filter and the process memory holds the unmasked values. - Swept the filesystem for SSH private keys, git credentials, AWS, GCP and Azure tokens, Kubernetes secrets, Docker configs, database credentials, Terraform state and cryptocurrency wallets.
- Sealed all of it under a hardcoded RSA-4096 public key with hybrid
encryption and shipped it to
scan.aquasecurtiy.org.
Read that hostname again. It is the vendor's own domain with two letters
transposed. To a domain-reputation feed, or to an analyst skimming egress at
2am, a scanner talking to something that looks like aquasecurity.org is the
least remarkable line on the page.
If the upload failed, the stealer created a public repository named
tpcp-docs on the victim's own GitHub account and attached the loot as
release assets.
SOCRadar clocked the first collection eighteen minutes after the malicious build went up.
What left the pipelines
Hudson Rock obtained the archive and Help Net Security reported its size on August 13: 153 GB, 433,909 files, 118,829 CI runner dumps mapped to 2,488 corporate domains. CloudSEK's independent count of roughly 434,000 files put exposure at close to 2,500 organizations and more than 430,000 pipelines. Hudson Rock CTO Alon Gal called it a "global ethical disclosure effort" and said the magnitude "pushes us into a completely new world regarding the type of response required." Kevin Beaumont's read: "It's a massive supply chain breach due to poor AI security."
SOCRadar's per-organization breakdown goes further. More than a thousand organizations lost JWTs and authentication tokens. Hundreds lost private keys, AWS access keys, GitLab tokens, OpenAI API keys, Slack webhooks, GitHub Actions tokens and Google API keys. One organization lost roughly 3,477 individual secrets. Eleven hundred exposed their committers' email addresses. The runners were GitHub Actions, GitLab CI, Jenkins, Bitbucket, CircleCI and Buildkite. Germany, Brazil and France took the worst of it, and the collections are now being brokered on Telegram.
Every control in that chain needed a name
Line up the defenses that existed in March, and note what each one needed before it could act.
The advisory needed Aqua to identify the compromise. The yank needed PyPI to
identify the package. The IoC list needed someone to read
scan.aquasecurtiy.org as hostile rather than as a typo of a vendor everybody
trusts. The "did you install between 10:39 and 16:00 UTC" checklist needed the
right five hours. Domain reputation needed the domain to have a reputation. The
reattribution that told 2,085 organizations they had been looking at the wrong
week needed a leaked archive to surface and a research team to sit down with
it, five months later.
Each of those is an identity control: it acts on a thing after somebody names the thing. Naming is how the ecosystem shares knowledge and it works, so this is not a complaint about the people who build advisories. It is a point about order of operations. An identity control arrives after the naming, and in this campaign each artifact was live for three to twelve hours. The eighteen minutes between publication and first collection answer the question of whether that ordering was good enough.
The controls that change the outcome here are the ones that do not need the name.
A clock, not an oracle
Bromure Agentic Coding runs every package fetch through the host-side MITM proxy before a byte reaches the VM, and the age gate is the only supply-chain layer that is on by default. It refuses any package version younger than a cutoff. Two days, out of the box.
It performs no lookup. It holds no opinion about the maintainer, the publisher, the signature or the name. It reads the publish timestamp and applies a floor, on the theory that a fresh release is the one most likely to have been hijacked an hour ago, and that waiting out that window costs a working developer close to nothing.
Against this campaign, that is the entire fight. LiteLLM 1.82.7 and 1.82.8
existed for forty minutes. A two-day floor means they never existed at all from
inside the VM. The proxy strips too-fresh versions out of the registry metadata
and re-aims latest and the other dist-tags at the newest surviving version,
so pip install litellm and every semver range resolve to 1.82.6 with no
error and nothing for an agent to route around. Ask for a poisoned version by
exact pin and the artifact-fetch backstop returns a 451 whose body states the
package's real age against the required minimum. Pip prints it verbatim:
Bromure Supply-Chain Security blocked this request: pypi package
[email protected] published 34 minutes ago — policy requires 2 days minimum
The gate covers npm, PyPI, Cargo, RubyGems and Packagist with full publish-time data. For pip it performs an on-demand lookup against PyPI's JSON API, since the default PEP 503 index carries no timestamps. Every decision lands in the Security Log (Window → Supply Chain Log…) as it happens.
What a memory dump gets
The stealer's best move was reading the CI runner worker's memory to recover secrets that log masking had hidden. It works because masking is cosmetic and the process holds the values.
Run it against a Bromure workspace and it recovers placeholders, because the process holds placeholders. The wire boundary is the product's central mechanism: each credential you configure appears inside the VM as a structure-preserving fake, while the real value stays encrypted on your Mac. The host proxy substitutes it onto the wire after the request has left the VM, and only when the request is bound for the host that credential was minted for.
Walk the stealer's own list against a Bromure VM:
/proc/*/environ
ANTHROPIC_API_KEY is sk-ant-api03-brm-…. OPENAI_API_KEY is sk-brm-….
GH_TOKEN is ghp_ plus 36 characters. Structure-preserving, so claude
and gh accept them without complaint, and worth nothing to anybody else.
/proc/<pid>/mem
Dumping the process recovers the same placeholders the environment holds. No privileged copy hides deeper in memory: the host proxy performs the substitution outside the VM, after the bytes have left.
SSH private keys
There are none. SSH_AUTH_SOCK points at an ssh-agent bridge over vsock
port 8444. The private key bytes never enter the guest and cannot be read
out of it. Bromure leaves your macOS login agent unexposed by design.
Cloud, k8s, Docker, database
~/.aws/config, ~/.kube/config, ~/.docker/config.json,
~/.git-credentials, ~/.config/doctl/config.yaml. All present, all
populated, all fake: brm-k8s-…, brm-docker-…, brm-db-…, glpat-
plus 20, dop_v1_ plus hex.
Bromure derives each fake from the real value plus a per-install 32-byte salt through HKDF-SHA256, so a tool that fingerprints its own key (Claude Code caches a key hash) never sees the credential rotate between sessions. There is also no switch to forget. The proxy is the VM's only route to the network, so a request that bypasses it carries a placeholder and fails upstream authentication. The boundary fails closed by construction.
The typosquat nobody had ever seen
Now the exfiltration. On March 19, scan.aquasecurtiy.org had no reputation,
no history, and no reason to sit on anybody's blocklist. That is why TeamPCP
chose it.
Bromure's compromise detector reads none of that. It watches your credentials rather than the destination's reputation. An Aho-Corasick automaton built from the workspace's own minted fakes sweeps every outbound request, headers and body. When one of those fakes turns up in a request bound for a host outside the scope it was minted for, the proxy treats the request as attempted exfiltration:
- The proxy refuses the request with HTTP 451 and forwards not one byte to the destination.
- Bromure pauses the VM on the spot.
- An alert names what happened: an outbound attempt to leak a session credential to a host it was not minted for.
Bromure then marks the workspace compromised, and the next launch requires
wiping the VM disk image and the persistent home before it will boot. Your
tokens, SSH keys and workspace settings survive that. The Trace Inspector
(⇧⌘I) and bromure-cli trace leaks show the offending host and the exact
request, so you can tell within a minute whether a dependency or a
prompt-injected instruction was responsible.
An AKIA-shaped mint scoped to amazonaws.com, appearing in a POST to
scan.aquasecurtiy.org, trips that automaton on the first attempt. The
detector has never heard of the domain and has no need to. It knows that this
credential has one legitimate destination family, and that this is a different
one. There is nothing to enable; it runs on every request.
The timeline you already own
The record-keeping problem here outlasts the incident.
Researchers reconstructed the timeline from the outside, from a leaked 153 GB archive that had to surface, be obtained, and be sifted before anyone could say which window mattered. The victims could not answer from their own records, because those records left out the two facts that decided it: which artifacts their pipelines fetched and when, and where the resulting processes sent traffic.
A Bromure workspace produces that record as a side effect of how it works.
Every fetch crosses the host proxy, so the Security Log holds each age-gate,
OSV, socket.dev and 451 decision as a live tail. Every request crosses the same
proxy, so bromure-cli trace ls gives you host, method, status, latency and
swap×N / LEAK×N flags per workspace, trace hostnames lists each distinct
host a session contacted, and trace summary aggregates the lot. All of it
stays encrypted at rest on your Mac under the vault master key.
So the question SOCRadar answered in August, was I collected, and when?, becomes a two-minute lookup you run yourself in March, against your own data, without waiting for anyone to publish the right name.
That is the argument. Nobody can make the ecosystem faster at naming things: March showed the vendor's own incident response leaving a nineteen-day tail and a wrong attribution behind it. What you can change is whether your outcome depends on the name. Put a clock in front of the registry, put placeholders in the machine that runs the code, and keep your own log of what it fetched and where it talked.
Sources: SecurityWeek, "Trivy, Not LiteLLM Behind the 2,500 Org Compromise" (Aug 14, 2026) · Aqua Security advisory GHSA-69fq-xp46-6x23 · StepSecurity, "Trivy Compromised a Second Time" · The Hacker News (Aug 12, 2026) · Help Net Security (Aug 13, 2026) · CrowdStrike, "From Scanner to Stealer"