Supply Chain Attack Detection: How to Hunt Compromised npm and Python Packages

Supply Chain Attack Detection: How to Hunt Compromised npm and Python Packages

Augusto Barros
Augusto Barros
November 4, 2026

Supply chain attack detection is how a security team finds out whether software it trusts, such as an open source package, a vendor update or a build tool, was turned against it, and what that software did once it was running.

On October 21, 2026, a malicious release of LiteLLM, a Python library for calling AI models, sat on PyPI for about two hours. The same release also went out in the project's container images. It added files that ran a credential stealer every time Python started in that environment, even on machines where no code imported LiteLLM. The stealer collected cloud keys, SSH keys, package registry tokens and saved browser passwords, and deleting the package directory did not remove it, according to Datadog Security Labs.

Many npm and Python package compromises follow the same pattern. The package runs code during install or at startup, collects the credentials the machine can reach, and uses them to spread or to get into cloud accounts. So hunting these compromises means checking four places: developer laptops and servers, CI runners, source control, and the cloud accounts the stolen keys open. It also means answering the exposure question on the day a compromise is published, and changing the settings that give the next one less time to reach you. This guide covers open source packages and the CI pipelines that install them. Tampered vendor updates and compromised SaaS integrations leave different evidence and are not covered here.

Each hunt below starts from a named compromise and the systems it could reach, which is the directed form of proactive threat hunting.

{{ebook-cta}}

How does a compromised package run code during install?

Many npm supply chain attacks deliver their payload through lifecycle scripts. In npm v11 and earlier, npm install runs each package's preinstall, install and postinstall scripts by default, as the npm documentation describes. For example, the malicious Axios versions published on March 31, 2026 added a dependency called plain-crypto-js. Its install hook ran node setup.js, which fetched a remote access trojan built for the victim's operating system. Our write-up of the Axios compromise lists the indicators.

The second Shai-Hulud wave in November 2025 worked the same way, running only during the preinstall phase. npm v12, generally available since July 2026, changed that default: dependency lifecycle scripts now run only when a project allows them through the allowScripts setting. Machines still on earlier releases keep the old behavior unless the ignore-scripts setting is on. A project that allowlists a package's scripts may keep running them if that package is later compromised.

Besides running when code imports them, Python packages have two other common ways to run code. The pip documentation warns that installs can involve running arbitrary code from distributions, and it recommends refusing source distributions with the --only-binary :all: option for a more secure install. A wheel installs mostly by unpacking files, as the wheel format allows, but those files can still run later. Python executes lines that begin with import in .pth files by default at every interpreter start, whether or not the related module is used. The malicious LiteLLM release used exactly that path. It placed cook.pth, cook.py and sitecustomize.py in site-packages, so any Python process using that environment ran the stealer (Datadog).

What does a poisoned package go after?

Many of these payloads go after credentials first. The first Shai-Hulud worm, in September 2025, ran the TruffleHog secret scanner on infected machines and collected GitHub tokens and cloud keys. Next, it published what it found to a public GitHub repository named Shai-Hulud. Then it used any npm tokens it found to publish malicious versions of other packages it could reach, and CISA counted more than 500 compromised packages.

CI systems often hold publishing tokens and deployment keys, so attackers go after them directly too. In the GhostAction campaign, attackers with access to maintainer accounts pushed a workflow named Github Actions Security to 817 repositories. Each run read the repository's secrets and sent them to an attacker's server with curl. In total, the campaign took 3,325 secrets, most often Docker Hub credentials, GitHub tokens and npm tokens, according to GitGuardian.

Several payloads also leave persistence behind. The Axios dropper copied PowerShell to a file named wt.exe and added a Run key so the trojan came back after a reboot (Microsoft). Similarly, the loader in the Mastra attack wrote persistence as a LaunchAgent on macOS, a user service on Linux and a Run key on Windows. As a result, a hunt that only checks which packages are installed can miss machines that are still compromised.

How to hunt compromised packages on laptops and servers

For supply chain attack detection on endpoints, start with process lineage in your EDR telemetry, the same parent and child view analysts use to investigate EDR alerts. During an install, a malicious package usually runs as a child of the package manager: npm, pnpm, yarn or node for JavaScript, and pip or python for Python. A startup-file payload like LiteLLM's is different: it runs under any Python process that uses that environment, long after the install. The useful question is what those processes started.

  • Shells and script hosts. node or python starting sh, bash, cmd.exe, powershell.exe or cscript.exe during an install. By default, npm runs install scripts through sh or cmd.exe, so that shell alone is normal. Look at what it starts next. The Axios chain ran cscript, then a hidden cmd.exe, then PowerShell with its execution policy bypassed (Microsoft).
  • Download tools. curl, wget or certutil started by an install, or an install that connects to a domain other than the package registry.
  • Renamed binaries. A system binary running under another name, such as PowerShell copied to wt.exe.
  • New persistence. Run keys, LaunchAgents or systemd user services written by node or python processes.
  • New startup files. A .pth file or sitecustomize.py in site-packages that no expected package install explains.
  • Credential file reads. Processes launched by an install script reading .npmrc, cloud credential files, SSH keys or .env files outside a normal build. npm itself reads .npmrc and cloud SDKs read credential files routinely, so the parent process matters. Many EDRs also skip file-read events by default.

Some of this activity is normal. Packages that compile native code run build tools during install, and npm v11 and earlier runs node-gyp by default when a package ships a binding.gyp file (npm documentation). Others, such as Puppeteer and Electron, download large binaries from hosts other than the registry during install. So baseline the parent and child pairs and install-time hosts each team produces, and work the new pairs first.

How to hunt CI runners and build pipelines

CI runners often hold publishing tokens, cloud keys and deployment secrets, so baseline their network traffic. A workflow that builds the same project every day usually connects to the same short list of hosts. For instance, the second Shai-Hulud wave reached the CNCF Backstage project in November 2025. StepSecurity's Harden-Runner then flagged one workflow's first-ever connections to bun.sh and oss.trufflehog.org (StepSecurity). The project had no lockfile, so that workflow had installed the newest versions of its packages, including the poisoned ones. By default, GitHub-hosted runners don't log their network connections for you, so this hunt usually needs an agent on the runner, as in the Backstage case.

Watch the workflow definitions too. GhostAction added a new workflow file to each repository it reached (GitGuardian). The second Shai-Hulud wave went further: it registered infected machines as self-hosted runners named SHA1HULUD and added a workflow file called discussion.yaml (Wiz). GitHub's organization audit log records when a new self-hosted runner is registered and when an Actions secret is created (GitHub documentation). Shai-Hulud registered its runners to new repositories it created with the stolen token, usually outside the victim's organization, so the organization audit log may not show them. On endpoints, look for GitHub Actions runner software in unexpected places, such as the hidden .dev-env folder Shai-Hulud used (Sysdig).

In practice, that gives four CI hunts:

  • New outbound destinations from install and test steps, compared with each workflow's own history.
  • Workflow files added or changed outside normal review, especially ones that read secrets and send data with curl.
  • Self-hosted runners registered from machines you do not recognize, and runner software installed on developer laptops.
  • Builds that ran without a lockfile during an exposure window.

How to hunt source control and cloud accounts for stolen tokens

Attackers usually use the tokens they steal, and GitHub and cloud providers log much of that activity outside the infected machine. For the first Shai-Hulud wave, Wiz suggested looking for a repository creation event followed quickly by the repository being made public, along with anomalous API activity in GitHub audit logs (Wiz). The second wave published stolen data to new repositories whose descriptions referenced Shai-Hulud (Wiz). Searching GitHub for repositories with that description also catches ones created under employees' personal accounts, which the organization audit log may not show. If your company publishes npm or PyPI packages, also check for versions your team did not release, since Shai-Hulud spread by publishing with stolen tokens.

The same goes for cloud keys. An attacker who takes keys from a laptop or runner has to call cloud APIs with them, and most of those calls land in the provider's audit log, though data reads such as S3 object access are logged only if you turned that on. So look for those identities calling from addresses or networks they have not used before, and making API calls their owners rarely make. For AWS identities, review CloudTrail and any GuardDuty findings for the same identities across the exposure window.

Supply chain attack detection on the day a compromise is published

The first question from leadership is usually some version of whether the compromise reached the company and whether anything happened. You can answer it in three passes, in order:

  • Inventory. Search lockfiles and installed packages for the affected versions, in files such as package-lock.json, yarn.lock and pnpm-lock.yaml for JavaScript, and requirements files, uv.lock or poetry.lock for Python. CISA's Shai-Hulud guidance also points to artifact repositories and dependency caches, which can keep copies of a version after the registry removes it (CISA). Include container images too, since LiteLLM 1.107.0 also shipped as images under tags including latest (Datadog).
  • Execution. For every machine that installed an affected version, check whether the payload ran, using the process, file and persistence hunts above, scoped to the exposure window. For payloads that persist, such as LiteLLM's startup files, that window runs until the files are removed, not until the registry pulled the package.
  • Credential use. Treat every secret the machine could read as exposed, rotate it, and check whether anyone used it.

A clean result is useful too. In the Mastra campaign, a hunt scoped to the attack's indicators returned only developers who had installed Mastra packages for ordinary work. That told the team it was not affected, and it showed how much benign activity the hunt produces in that environment.

Prophet AI Threat Hunter handles this side of supply chain attack detection through emerging-threat hunts. It ships hunts for compromises such as LiteLLM 1.107.0 and Shai-Hulud. When a threat is severe, those hunts run automatically in the background across customer environments, against the telemetry each team has connected. Teams can also paste a public write-up and get a hunt plan from it. Suspicious leads go to Prophet AI SOC Analyst for a full investigation, and findings that hold up become candidates for new detections, the handoff covered in our post on detection engineering.

Responding to supply-chain compromise attacks

The response steps for LiteLLM fit most compromises of this kind: rotate the exposed credentials from a clean machine, then audit logs for use of those credentials during the exposure window (Datadog Security Labs). For startup-file payloads like LiteLLM's, don't run python or pip from the infected environment during cleanup, since that runs the stealer again.

Removing the package is rarely the end of cleanup, because the payload often outlives it. After launching its trojan, the Axios dropper deleted its own setup.js and swapped in a clean package.json, so a node_modules folder inspected afterward looked normal (Microsoft). The trojan stayed behind in other files: %PROGRAMDATA%\wt.exe, %PROGRAMDATA%\system.bat and a Run key named MicrosoftUpdate on Windows, /Library/Caches/com.apple.act.mond on macOS, and /tmp/ld.py on Linux. Microsoft’s guidance went beyond uninstalling the package. It said to roll back to the last clean Axios versions, use npm overrides to pin copies pulled in by other packages, clear the npm cache, and stop dependency bots from bringing the bad versions back. That trojan could also run arbitrary PowerShell and load payloads into memory, so deleting its files doesn’t prove the machine is clean. Where a remote access payload actually ran, reimaging is the safer choice. Isolate the machine before you rotate credentials, since a trojan that is still running can steal the new ones too.

Shai-Hulud showed that cleanup also has to cover source control. The second wave registered infected machines as self-hosted runners named SHA1HULUD and added a .github/workflows/discussion.yaml file. That file lets anyone run commands on those machines by opening a GitHub discussion. So cleanup means deleting the workflow, deregistering the runners and removing the runner software from the machines. The worm also pushed a short-lived workflow to a branch named add-linter-workflow- followed by a timestamp. It used that workflow to collect every GitHub Actions secret in the repository, then deleted the workflow and the branch. As a result, the secrets to rotate include repository and organization Actions secrets, not only the credential files on the infected laptop.

The same wave reached into cloud accounts. It read secrets from AWS Secrets Manager, Google Secret Manager and Azure Key Vault, and it tried to assume privileged roles and change IAM policies (Wiz). That means you also have to rotate the secrets stored in those vaults and review IAM changes made during the exposure window. On Linux hosts, also check for a sudoers file at /etc/sudoers.d/runner, which the malware tried to plant through a privileged Docker container to get passwordless root. Don’t rely on platform cleanup either. GitHub revoked leaked tokens and made the attacker’s repositories private, but Wiz still saw stolen AWS keys and GitHub tokens being used two days after the first malicious packages appeared. Because the worm sometimes published one victim’s secrets in another victim’s repository, your credentials may have leaked from an account you don’t control.

How to give the next compromise less time to reach you

A release-age cooldown tells the package manager to skip versions published too recently. The LiteLLM release was pulled within about two hours (Datadog), so a one-day cooldown would have kept it off machines that use the setting. Several package managers now offer one:

  • npm: min-release-age, set in days (npm config).
  • pnpm: minimumReleaseAge, set in minutes (pnpm settings).
  • uv: exclude-newer, which accepts relative durations such as 7 days (uv documentation).
  • pip: --uploaded-prior-to, which accepts a duration such as P3D, on indexes that report upload times (pip documentation).

Install-time controls help as well. Leave npm's dependency scripts off unless a package needs them, and use prebuilt wheels in Python where the project allows it. For container images, pin by digest rather than tags like latest, since the package manager cooldowns above don't cover image pulls. Still, none of these controls replaces hunting. A cooldown does nothing against a compromise that goes unnoticed for longer than the delay, and the LiteLLM payload ran at Python startup rather than from an install script. A cooldown also delays urgent security fixes, so keep a way to override it.

Much of the evidence for supply chain attack detection is already in telemetry you collect: which processes an install started, which hosts a build contacted, and which tokens were used and from where. Prophet AI Threat Hunter runs these hunts across that telemetry, and you can see it work in your own environment.

Table of contents
Add as Google Preferred Sources

Insights

Prophet Security Quarterly Threat Report

The attack patterns seen most across customer environments, and why some of those attacks succeeded where others were blocked.

Download Report
Augusto Barros

Augusto Barros

As Principal Product Manager at Prophet Security, Augusto applies his hands-on experience and critical thinking to help push forward the new capabilities of Prophet AI SOC platform