# CanisterWorm: @emilgroup and @teale.io npm publisher compromise (29+ packages)

> An attacker compromised the @emilgroup and @teale.io npm namespaces, replacing 58 package-versions with a Python backdoor that polls an Internet Computer Protocol (ICP) canister for follow-on payloads. The implant persists via user-level systemd and includes worm-style republishing via deploy.js. Wiz later linked the tradecraft to TeamPCP; Socket declined firm attribution.

- Published: 2026-03-20
- Severity: critical
- Kind: Compromise - A legitimate, already-used package, maintainer account, CI pipeline, or release tag was hijacked and a malicious version was published under a name people already depend on.
- Ecosystems: npm
- Scope: 29 packages, 58 compromised versions
- Tags: worm, maintainer-takeover, credential-theft, infostealer
- Threat actor: TeamPCP
- Also known as: CanisterWorm
- Detected by: Socket, Wiz, Aikido
- Incident ID: npm-2026-03-20-canisterworm-emilgroup-teale

## Affected packages (29)

Only the exact versions listed were named by the source advisories. Other versions are not implicated.

| Package | Ecosystem | Compromised versions |
|---|---|---|
| [@emilgroup/account-sdk](https://dependencywatch.io/package/npm/@emilgroup/account-sdk) | npm | 1.41.1, 1.41.2 |
| [@emilgroup/account-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/account-sdk-node) | npm | 1.40.1, 1.40.2 |
| [@emilgroup/accounting-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/accounting-sdk-node) | npm | 1.26.1, 1.26.2 |
| [@emilgroup/api-documentation](https://dependencywatch.io/package/npm/@emilgroup/api-documentation) | npm | 1.19.1, 1.19.2 |
| [@emilgroup/auth-sdk](https://dependencywatch.io/package/npm/@emilgroup/auth-sdk) | npm | 1.25.1, 1.25.2 |
| [@emilgroup/auth-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/auth-sdk-node) | npm | 1.21.1, 1.21.2 |
| [@emilgroup/billing-sdk](https://dependencywatch.io/package/npm/@emilgroup/billing-sdk) | npm | 1.56.1, 1.56.2 |
| [@emilgroup/billing-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/billing-sdk-node) | npm | 1.57.1, 1.57.2 |
| [@emilgroup/claim-sdk](https://dependencywatch.io/package/npm/@emilgroup/claim-sdk) | npm | 1.41.1, 1.41.2 |
| [@emilgroup/claim-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/claim-sdk-node) | npm | 1.39.1, 1.39.2 |
| [@emilgroup/customer-sdk](https://dependencywatch.io/package/npm/@emilgroup/customer-sdk) | npm | 1.54.1, 1.54.2 |
| [@emilgroup/customer-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/customer-sdk-node) | npm | 1.55.1, 1.55.2 |
| [@emilgroup/document-sdk](https://dependencywatch.io/package/npm/@emilgroup/document-sdk) | npm | 1.45.1, 1.45.2 |
| [@emilgroup/document-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/document-sdk-node) | npm | 1.43.1, 1.43.2 |
| [@emilgroup/gdv-sdk](https://dependencywatch.io/package/npm/@emilgroup/gdv-sdk) | npm | 2.6.1, 2.6.2 |
| [@emilgroup/insurance-sdk](https://dependencywatch.io/package/npm/@emilgroup/insurance-sdk) | npm | 1.97.1, 1.97.2 |
| [@emilgroup/insurance-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/insurance-sdk-node) | npm | 1.95.1, 1.95.2 |
| [@emilgroup/notification-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/notification-sdk-node) | npm | 1.4.1, 1.4.2 |
| [@emilgroup/partner-portal-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/partner-portal-sdk-node) | npm | 1.1.1, 1.1.2 |
| [@emilgroup/partner-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/partner-sdk-node) | npm | 1.19.1, 1.19.2 |
| [@emilgroup/payment-sdk](https://dependencywatch.io/package/npm/@emilgroup/payment-sdk) | npm | 1.15.1, 1.15.2 |
| [@emilgroup/payment-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/payment-sdk-node) | npm | 1.23.1, 1.23.2 |
| [@emilgroup/process-manager-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/process-manager-sdk-node) | npm | 1.13.1, 1.13.2 |
| [@emilgroup/public-api-sdk](https://dependencywatch.io/package/npm/@emilgroup/public-api-sdk) | npm | 1.33.1, 1.33.2 |
| [@emilgroup/public-api-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/public-api-sdk-node) | npm | 1.35.1, 1.35.2 |
| [@emilgroup/tenant-sdk](https://dependencywatch.io/package/npm/@emilgroup/tenant-sdk) | npm | 1.34.1, 1.34.2 |
| [@emilgroup/tenant-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/tenant-sdk-node) | npm | 1.33.1, 1.33.2 |
| [@emilgroup/translation-sdk-node](https://dependencywatch.io/package/npm/@emilgroup/translation-sdk-node) | npm | 1.1.1, 1.1.2 |
| [@teale.io/eslint-config](https://dependencywatch.io/package/npm/@teale.io/eslint-config) | npm | 1.8.9, 1.8.10 |

## What happened

On `2026-03-20`, an attacker pushed `58` poisoned package-versions across `29` packages in the `@emilgroup` and `@teale.io` npm scopes. `@emilgroup` ships SDKs widely used in European insurance workflows; `@teale.io/eslint-config` is shared internal tooling - both compromises put the implant on developer laptops and CI runners alike.

## Implant

The malicious `postinstall` writes a Python implant to `~/.local/share/pgmon/service.py` and registers a user-level `pgmon.service` via `~/.config/systemd/user/pgmon.service`. The implant polls an **Internet Computer Protocol (ICP)** canister at `tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io` roughly every `3000s`, fetching follow-on payload URLs that are written to `/tmp/pglog` for execution. State tracking lives in `/tmp/.pg_state`.

Using an ICP canister as the C2 hop makes takedown materially harder: there is no DNS or hosting provider to send a notice to. Socket dubbed the family **CanisterWorm**.

## Worm component

A `deploy.js` shipped alongside the implant attempts to re-publish trojanised versions using any npm credentials it finds in `~/.npmrc` or environment, which is how the malware spread from a single takeover to `29` packages in one session.

## Attribution

**Wiz** linked the tradecraft to **TeamPCP** (the same actor behind the `2026-03-19` Trivy cascade), based on shared exfil infrastructure and operator artifacts. Socket and Aikido (Charlie Eriksen) declined firm attribution but documented mutations of the implant inside `@teale.io/eslint-config` that share code with the Trivy-cascade tooling.

## Impact

- Insurance-industry SDKs (@emilgroup) and shared ESLint tooling (@teale.io) backdoored
- Persistent ICP-based C2 evades takedown via decentralised infrastructure
- ~3000s canister polling interval for follow-on payloads

## What to do

1. Revert `@emilgroup/*` and `@teale.io/eslint-config` to last clean versions and rotate developer credentials
2. Hunt for `~/.local/share/pgmon/service.py` and a `pgmon` systemd user service
3. Block egress to `*.raw.icp0.io` canister endpoints; specifically `tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io`

## Check your own dependencies

- Check a package-lock.json against this incident: https://dependencywatch.io/check/package-lock-json

The scan runs entirely in the browser; lockfile contents are never uploaded.

## References

- [CanisterWorm: npm Publisher Compromise Deploys Backdoor Across 29 Packages](https://socket.dev/blog/canisterworm-npm-publisher-compromise-deploys-backdoor-across-29-packages) - Socket
- [TeamPCP deploys CanisterWorm on NPM following Trivy compromise](https://www.aikido.dev/blog/teampcp-deploys-worm-npm-trivy-compromise) - Aikido

---

Canonical page: https://dependencywatch.io/incident/npm-2026-03-20-canisterworm-emilgroup-teale
Markdown version: https://dependencywatch.io/incident/npm-2026-03-20-canisterworm-emilgroup-teale.md
Site index for agents: https://dependencywatch.io/llms.txt
DependencyWatch.io is operated by Precursor Security (https://precursorsecurity.com).
