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.
- Threat actor
- TeamPCP
- Detected by
- Socket · Wiz · Aikido
- Also known as
- CanisterWorm
- Ecosystems
- npm
- Packages tracked
- 29
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.
Affected packages (29)
- npm
@emilgroup/account-sdk1.41.11.41.2 - npm
@emilgroup/account-sdk-node1.40.11.40.2 - npm
@emilgroup/accounting-sdk-node1.26.11.26.2 - npm
@emilgroup/api-documentation1.19.11.19.2 - npm
@emilgroup/auth-sdk1.25.11.25.2 - npm
@emilgroup/auth-sdk-node1.21.11.21.2 - npm
@emilgroup/billing-sdk1.56.11.56.2 - npm
@emilgroup/billing-sdk-node1.57.11.57.2 - npm
@emilgroup/claim-sdk1.41.11.41.2 - npm
@emilgroup/claim-sdk-node1.39.11.39.2 - npm
@emilgroup/customer-sdk1.54.11.54.2 - npm
@emilgroup/customer-sdk-node1.55.11.55.2 - npm
@emilgroup/document-sdk1.45.11.45.2 - npm
@emilgroup/document-sdk-node1.43.11.43.2 - npm
@emilgroup/gdv-sdk2.6.12.6.2 - npm
@emilgroup/insurance-sdk1.97.11.97.2 - npm
@emilgroup/insurance-sdk-node1.95.11.95.2 - npm
@emilgroup/notification-sdk-node1.4.11.4.2 - npm
@emilgroup/partner-portal-sdk-node1.1.11.1.2 - npm
@emilgroup/partner-sdk-node1.19.11.19.2 - npm
@emilgroup/payment-sdk1.15.11.15.2 - npm
@emilgroup/payment-sdk-node1.23.11.23.2 - npm
@emilgroup/process-manager-sdk-node1.13.11.13.2 - npm
@emilgroup/public-api-sdk1.33.11.33.2 - npm
@emilgroup/public-api-sdk-node1.35.11.35.2 - npm
@emilgroup/tenant-sdk1.34.11.34.2 - npm
@emilgroup/tenant-sdk-node1.33.11.33.2 - npm
@emilgroup/translation-sdk-node1.1.11.1.2 - npm
@teale.io/eslint-config1.8.91.8.10
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
- 1Revert
@emilgroup/*and@teale.io/eslint-configto last clean versions and rotate developer credentials - 2Hunt for
~/.local/share/pgmon/service.pyand apgmonsystemd user service - 3Block egress to
*.raw.icp0.iocanister endpoints; specificallytdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io