The wshu.net npm Campaign Delivers a Multi-Stage Infostealer
Table of Contents
TL;DR
A single actor published 15 npm packages across 13 throwaway scopes on June 4, 2026, most of them in a roughly 40 minute window (07:27 to 08:05 UTC), then expanded the version ranges over the following two weeks (64 malicious versions in total). Each package looks like a small developer utility (a JWT helper, a retry wrapper, a logger, a date library) but ships a roughly 260 to 282 KB obfuscated blob that a postinstall hook executes the moment the package is installed. That blob is a downloader. It fetches a roughly 10.6 MB Rust-compiled binary from GitHub Releases and runs it, and that binary is the infostealer. The packages share one publisher infrastructure (@wshu.net emails), one obfuscation toolchain, and one execution wrapper. Amazon Inspector’s security research team independently documented the same cluster as Operation Friday Harvest on June 19, 2026, including the second-stage binary that our static pass could not recover from the encrypted payload strings.
Impact:
- Installing any malicious version runs an obfuscated downloader at install time, before any of your code executes
- The downloader pulls and runs a multi-platform (Linux, macOS, Windows) Rust infostealer that targets 30+ crypto wallets, browser credentials (Chrome, Brave, Firefox, Edge), cloud tokens (AWS, GCP, Azure, Kubernetes), SSH keys, Discord and Telegram sessions, developer credentials (npm,
.env,.npmrc, GitHub and PyPI tokens), and database client connection strings - The binary installs persistence as a systemd user service disguised as a normal daemon (
colord,haveged), and exfiltrates over the Telegram Bot API (Windows) or multipart HTTP POST (Linux and macOS) @petitcode/eb-retryand@briskforge/envcheckfire the payload at runtime too, the first time you call their exported function, which survivesnpm install --ignore-scripts- The
latesttag of every package was scrubbed to hide the hook, so a quick metadata check is not a safe signal
Indicators of Compromise (IoC):
Every affected version across the 15 packages is listed below, one row each, with its postinstall, payload file, blob SHA256, publisher email, and a status column. Of the 64 versions, 44 still carry an active payload hook, 12 are scrubbed latest tags, 4 are intermediate releases with no hook, 3 are scope-reservation stubs, and 1 was unpublished. The SHA256 is filled only for the versions we downloaded and analyzed. Use the search box to filter (try a SHA256, a scope, or payload) and the download button to pull the raw CSV.
| ecosystem | name | version | postinstall | payload_file | payload_sha256 | publisher_email | status | notes | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | npm | @apexcraft/nano-key | 1.2.4 | node lib/seed.js | lib/seed.js | [email protected] | payload | ||
| 2 | npm | @apexcraft/nano-key | 1.2.5 | node lib/seed.js | lib/seed.js | [email protected] | payload | ||
| 3 | npm | @apexcraft/nano-key | 1.3.2 | node ./dist/cjs/seed.cjs | dist/cjs/seed.cjs | [email protected] | payload | ||
| 4 | npm | @apexcraft/nano-key | 1.3.3 | node ./dist/cjs/seed.cjs | dist/cjs/seed.cjs | [email protected] | payload | ||
| 5 | npm | @apexcraft/nano-key | 1.3.4 | node ./dist/cjs/seed.cjs | dist/cjs/seed.cjs | [email protected] | payload | ||
| 6 | npm | @apexcraft/nano-key | 1.3.5 | node ./dist/cjs/seed.cjs | dist/cjs/seed.cjs | [email protected] | payload | ||
| 7 | npm | @apexcraft/nano-key | 1.3.6 | node ./dist/cjs/seed.cjs | dist/cjs/seed.cjs | [email protected] | payload | ||
| 8 | npm | @apexcraft/nano-key | 1.3.7 | node ./dist/cjs/seed.cjs | dist/cjs/seed.cjs | 618dfffb6829356c131fded9f4c6528b73b4f9d7ff1fc1d3b457599a12584e29 | [email protected] | payload | analyzed (root lineage) |
| 9 | npm | @apexcraft/nano-key | 1.3.8 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 10 | npm | @briskforge/envcheck | 0.5.2 | node lib/preflight.js | lib/preflight.js | [email protected] | payload | ||
| 11 | npm | @briskforge/envcheck | 0.5.3 | node lib/preflight.js | lib/preflight.js | [email protected] | payload | ||
| 12 | npm | @briskforge/envcheck | 0.5.4 | node lib/preflight.js | lib/preflight.js | 26ddfae644673e0ad65b63caaaf67c0f7dc6c2b2b4127bb5271f8d03fb62091a | [email protected] | payload | analyzed; dual-trigger (require-time) |
| 13 | npm | @briskforge/envcheck | 0.5.5 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 14 | npm | @bytemend/mfebus | 1.4.0 | node dist/bootstrap.js | dist/bootstrap.js | [email protected] | payload | ||
| 15 | npm | @bytemend/mfebus | 1.4.1 | node dist/bootstrap.js | dist/bootstrap.js | [email protected] | payload | ||
| 16 | npm | @bytemend/mfebus | 1.4.2 | node dist/bootstrap.js | dist/bootstrap.js | a9a28f2e9f7e0348092682940b3bf63d47a63afc18eb8bfe628e5ddab0d73b47 | [email protected] | payload | analyzed |
| 17 | npm | @bytemend/mfebus | 1.4.3 | (none) | - | [email protected] | no-hook | ||
| 18 | npm | @bytemend/mfebus | 1.4.4 | (none) | - | [email protected] | no-hook | ||
| 19 | npm | @bytemend/mfebus | 1.4.5 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 20 | npm | @chunklab/hexparse | 1.0.7 | node lib/prelude.js | lib/prelude.js | [email protected] | payload | ||
| 21 | npm | @chunklab/hexparse | 1.1.4 | node ./script/prelude.cjs | script/prelude.cjs | [email protected] | payload | ||
| 22 | npm | @chunklab/hexparse | 1.1.5 | node ./script/prelude.cjs | script/prelude.cjs | [email protected] | payload | ||
| 23 | npm | @chunklab/hexparse | 1.1.6 | node ./script/prelude.cjs | script/prelude.cjs | 24c8f9b8ac17c2f88cc01d44543963206472112510962b68cf5f74d598b3b065 | [email protected] | payload | analyzed |
| 24 | npm | @chunklab/hexparse | 1.1.7 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 25 | npm | @frostnode/probe | 0.0.1 | (none) | - | [email protected] | stub | scope-reservation, no payload | |
| 26 | npm | @frostnode/waitfor | 0.9.0 | node lib/tickinit.js | lib/tickinit.js | [email protected] | payload | ||
| 27 | npm | @frostnode/waitfor | 0.10.3 | node ./dist/cjs/tickinit.cjs | dist/cjs/tickinit.cjs | [email protected] | payload | ||
| 28 | npm | @frostnode/waitfor | 0.10.4 | node ./dist/cjs/tickinit.cjs | dist/cjs/tickinit.cjs | [email protected] | payload | ||
| 29 | npm | @frostnode/waitfor | 0.10.5 | node ./dist/cjs/tickinit.cjs | dist/cjs/tickinit.cjs | 2de602e6422a991346aaf0b74ed6bd525215f5177b9f7f267ccb4d82e919273d | [email protected] | payload | analyzed; require(_0x2cb1b0['UGeLH']) |
| 30 | npm | @frostnode/waitfor | 0.10.6 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 31 | npm | @gleamkit/probe | 0.0.1 | (none) | - | [email protected] | stub | scope-reservation, no payload | |
| 32 | npm | @glitchpad/throttler | 2.1.1 | node dist/primer.js | dist/primer.js | [email protected] | payload | ||
| 33 | npm | @glitchpad/throttler | 2.2.1 | node ./primer.cjs | primer.cjs | [email protected] | payload | ||
| 34 | npm | @glitchpad/throttler | 2.2.2 | node ./primer.cjs | primer.cjs | [email protected] | payload | ||
| 35 | npm | @glitchpad/throttler | 2.2.3 | node ./primer.cjs | primer.cjs | 68b4fe54a4c05cd0115535ebd4aa8d3cccb03ea5a685f440314814ba1b89e875 | [email protected] | payload | analyzed; blob == @lazyutil/[email protected] |
| 36 | npm | @glitchpad/throttler | 2.2.4 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 37 | npm | @lazyutil/dater | 0.8.1 | node lib/tzinit.js | lib/tzinit.js | [email protected] | payload | ||
| 38 | npm | @lazyutil/dater | 0.9.2 | node ./dist/lib/tzinit.cjs | dist/lib/tzinit.cjs | [email protected] | payload | ||
| 39 | npm | @lazyutil/dater | 0.9.3 | node ./dist/lib/tzinit.cjs | dist/lib/tzinit.cjs | [email protected] | payload | ||
| 40 | npm | @lazyutil/dater | 0.9.4 | node ./dist/lib/tzinit.cjs | dist/lib/tzinit.cjs | 68b4fe54a4c05cd0115535ebd4aa8d3cccb03ea5a685f440314814ba1b89e875 | [email protected] | payload | analyzed; blob == @glitchpad/[email protected] |
| 41 | npm | @lazyutil/dater | 0.9.5 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 42 | npm | @nullzero/urlcat | 1.4.3 | (none) | - | [email protected] | no-hook | ||
| 43 | npm | @nullzero/urlcat | 1.4.4 | (none) | - | [email protected] | no-hook | ||
| 44 | npm | @nullzero/urlcat | 1.4.5 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 45 | npm | @nullzero/urlcat | 1.4.2 | node lib/encoder.js | lib/encoder.js | [email protected] | unpublished | removed before capture; payload unverified | |
| 46 | npm | @petitcode/eb-retry | 1.3.3 | node lib/warmup.js | lib/warmup.js | [email protected] | payload | ||
| 47 | npm | @petitcode/eb-retry | 1.3.4 | node lib/warmup.js | lib/warmup.js | [email protected] | payload | ||
| 48 | npm | @petitcode/eb-retry | 1.3.5 | node lib/warmup.js | lib/warmup.js | 32d02f806d58a6670f7cc9b93f1d85b22e0e0f535e1f90a62d86918033896f54 | [email protected] | payload | analyzed; dual-trigger (require-time) |
| 49 | npm | @petitcode/eb-retry | 1.3.6 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 50 | npm | @thymelab/logfx | 2.15.3 | node dist/bootstrap.js | dist/bootstrap.js | [email protected] | payload | ||
| 51 | npm | @thymelab/logfx | 2.15.4 | node dist/bootstrap.js | dist/bootstrap.js | [email protected] | payload | ||
| 52 | npm | @thymelab/logfx | 2.15.5 | node dist/bootstrap.js | dist/bootstrap.js | 4e927f22ad04f4ac9b487ae11412fc2a55210188789ac29f3a47ad77931907a5 | [email protected] | payload | analyzed; decoy logger |
| 53 | npm | @thymelab/logfx | 2.15.6 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 54 | npm | @tinyfox/shapecheck | 0.7.4 | node lib/bootstrap.js | lib/bootstrap.js | [email protected] | payload | ||
| 55 | npm | @tinyfox/shapecheck | 0.8.5 | node dist/bootstrap.cjs | dist/bootstrap.cjs | [email protected] | payload | ||
| 56 | npm | @tinyfox/shapecheck | 0.8.6 | node dist/bootstrap.cjs | dist/bootstrap.cjs | [email protected] | payload | ||
| 57 | npm | @tinyfox/shapecheck | 0.8.7 | node dist/bootstrap.cjs | dist/bootstrap.cjs | 0d27ca72b6f02faf4db95effb18347a7e2fa2def2034707bf9e56fa217879a3b | [email protected] | payload | analyzed; decoy type validator |
| 58 | npm | @tinyfox/shapecheck | 0.8.8 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 59 | npm | @zynkit/jwtbytes | 0.4.3 | node lib/prelude.js | lib/prelude.js | [email protected] | payload | ||
| 60 | npm | @zynkit/jwtbytes | 0.5.1 | node dist/prelude.cjs | dist/prelude.cjs | [email protected] | payload | ||
| 61 | npm | @zynkit/jwtbytes | 0.5.2 | node dist/prelude.cjs | dist/prelude.cjs | [email protected] | payload | ||
| 62 | npm | @zynkit/jwtbytes | 0.5.3 | node dist/prelude.cjs | dist/prelude.cjs | d06ee17d30ebb333ab2e5b6e8a1324fcf95edaaae17b6793ec0f3647338efda1 | [email protected] | payload | analyzed; decoy base-N encoders |
| 63 | npm | @zynkit/jwtbytes | 0.5.4 | (none) | - | [email protected] | scrubbed | latest, payload removed | |
| 64 | npm | @zynkit/probe | 0.0.1 | (none) | - | [email protected] | stub | scope-reservation, no payload | |
| No matching rows | |||||||||
@zynkit/[email protected],@frostnode/[email protected], and@gleamkit/[email protected]are payload-free stubs by the same actor, used to reserve their scopes- Second-stage delivery and exfiltration infrastructure: GitHub Releases under
github[.]com/angelmaybeth21-oss/test(repo removed, account live, created 2026-06-03), secondary accountsmilingdusty233, and the Telegram Bot API (api[.]telegram[.]org,149[.]154[.]166[.]110) - Second-stage binary SHA256: Linux ELF
2457b2e775a5fe7a9e022ba77074a1b9aacb41b4fc0cc1d8a3dc66546599c5de, macOS Mach-Ob1c7b17f31a84e2596250121c3610ae5e0d592651940dd6c0dd74506f0f38313, Windowswin.js11fe3a47333f63fd0e0a32ea16351eb302659aba983c07e4ea3dc9b09b618509 - Host artifacts:
/tmp/_installer-0/,~/.local/bin/<daemon>,~/.config/systemd/user/<daemon>.service,~/.local/state/<daemon>/{install.nonce,machine.id} @nullzero/[email protected]was published fromnullzero-rlnozk@wshu[.]netbut has since been unpublished, so its payload could not be statically inspected. The versions that remain (1.4.3to1.4.5) carry no install hook@lazyutil/[email protected]and@glitchpad/[email protected]ship the byte-identical payload blob (SHA25668b4fe54a4c05cd0115535ebd4aa8d3cccb03ea5a685f440314814ba1b89e875)- Publisher npm emails:
<scope>-<6 random chars>@wshu[.]net(for exampleapexcraft-8uiljr@wshu[.]net,briskforge-5psyxc@wshu[.]net,frostnode-gk8pbf@wshu[.]net) - Author identity in every
package.json:<scope>@pm[.]me, homepagegithub[.]com/<scope> - Obfuscated bootstrap filenames seen so far:
seed,bootstrap,prelude,warmup,primer,preflight,tzinit,tickinit,encoder(.cjs/.js) - All scopes created June 4, 2026, between 07:27 and 08:05 UTC
Analysis
One actor, many scopes
These packages share a publisher even where they share no code. Every malicious version came from an npm account whose email follows one template, the scope name, a hyphen, six random characters, then @wshu[.]net.
Each package.json carries a matching author block pointing at a per-scope identity:
// @zynkit/jwtbytes package.json"author": { "name": "zynkit", "url": "https://github.com/zynkit"}The npm registry time.created timestamps put every scope inside a tight window on June 4, 2026, the earliest at 07:27 UTC and the latest just after 08:05. One scope per package keeps each account looking like an independent small project, which is why a maintainer based search does not connect them. The shared signal is the wshu[.]net publisher domain. wshu[.]net is a public disposable-email provider, so the domain alone also catches unrelated throwaway accounts, and npm’s public search does not index publisher emails anyway. The tighter fingerprint is the shared obfuscation template plus the 2026-06-04 publish burst. Investigators keep finding more scopes, so treat this member list as a lower bound.
Execution trigger
Every package declares a postinstall hook that runs its payload file directly:
// @zynkit/jwtbytes package.json"scripts": { "postinstall": "node dist/prelude.cjs"}The payload file is structured so that running it as a script fires the payload immediately. The obfuscated logic is wrapped in a guarded function, and the file ends by calling that function when it is the entry module:
// @zynkit/jwtbytes dist/prelude.cjs'use strict';let _fired = false;function runPrepare() { if (_fired) return; _fired = true;(function(_0x4b6d34,_0x558c8c){var _0x2dc988={_0x3f5b57:'$Dz^', /* ...~282KB of obfuscation... */ _0x175f();}function onInstall() { runPrepare(); }exports.runPrepare = runPrepare;exports.onInstall = onInstall;exports.default = runPrepare;module.exports.runPrepare = runPrepare;if (require.main === module) onInstall();node dist/prelude.cjs makes require.main === module true, so onInstall() runs runPrepare() and the payload executes. The same wrapper, byte for byte in its readable parts, appears in every payload file we inspected, renamed per scope: seed.cjs, bootstrap.js, bootstrap.cjs, prelude.cjs, warmup.js, primer.cjs, preflight.js, tzinit.cjs, and tickinit.cjs.
@petitcode/eb-retry adds a second trigger. Its main entry, a near verbatim copy of the legitimate retry wrapper used as a decoy, calls into the payload on its first public function:
// @petitcode/eb-retry lib/index.jsvar retrier = require('retry');var warmup = require('./warmup');
function retry(fn, opts) { warmup.runPrepare(); // ...legitimate retry logic copied from the real package...}So even if postinstall is skipped with --ignore-scripts, the payload still runs the first time application code calls retry(). @briskforge/envcheck does the same, and even documents it in a comment above the hook:
// @briskforge/envcheck lib/preflight.js// Internal preflight hook ... runs once per process from the library entry// point, and once again as a standalone script during postinstall.Its lib/index.js calls require('./preflight') and preflight.runPrepare(), giving the same install-time plus runtime double trigger.
The decoy surface
Each package ships real, working utility code so it passes a glance. @zynkit/jwtbytes includes genuine base32, base58, base64, hex, and ascii85 encoders. @tinyfox/shapecheck ships a runtime type validator. @glitchpad/throttler ships a real throttler. The malicious file sits beside that code and dwarfs it. In @zynkit/jwtbytes, the largest legitimate source file is about 13 KB while dist/prelude.cjs is 282 KB. The blob is always the single largest file in the package, and the gap is wide: from roughly 4x the largest real file in @glitchpad/throttler to over 200x in @petitcode/eb-retry. When you spot one large opaque file next to a handful of small readable modules, check the size gap first.
Payload
The payload is a javascript-obfuscator artifact. It uses hex named identifiers, a while (!![]) array rotation IIFE that reorders a string array at load time, and a base64 plus RC4 string decoder (the _0x25ff function, indexing into a 1267 entry array with a 0x184 offset). Each string is individually RC4 keyed, and the keys come from control flow flattened proxy objects rather than literals at the call site.
The blobs fall into a handful of build clusters, each with its own obfuscator seed, string-array function, and re-entrancy guard variable. The string-array function we decoded differs by scope (_0x175f in @zynkit/jwtbytes, _0xe119 in @briskforge/envcheck, _0x15fd in @frostnode/waitfor), which lines up with the five clusters (A through E) that Amazon Inspector mapped. Packages in the same cluster can carry a byte-identical blob: @lazyutil/[email protected] and @glitchpad/[email protected] (both cluster B) ship the exact same compiled payload (SHA256 68b4fe54...), a byte-level link between two otherwise unrelated scopes.
The payload also resolves modules by a name decrypted at runtime, keeping them out of a static module graph. The same obscured require appears, with the same variable name, across several of the packages:
// resolved at runtime from a decrypted string, identical var name across packagesrequire(_0x45af03['GyrZN']);Because every string literal is RC4 encrypted behind that proxy machinery, the concrete URLs and file paths are not readable from a static pass, and we did not execute the payload to recover them. Running the obfuscated control flow, even inside a Node vm context, is not a safe boundary for code we already know to be malicious, so we did not do it.
Amazon Inspector ran the payload in a controlled environment and recovered what the encrypted strings hide. The obfuscated JavaScript is a loader. It spawns a detached child process behind environment-variable guards, downloads a roughly 10.6 MB Rust-compiled binary from a GitHub Releases URL (github[.]com/angelmaybeth21-oss/test/releases/download/v1.0.0/{linux,mac,win.js}), and executes it. The repository is now removed, but the publishing account angelmaybeth21-oss (created 2026-06-03, the day before the npm burst) and a second account smilingdusty233 are still live.
The Rust binary is the infostealer. Per Amazon Inspector’s analysis it sweeps 30+ cryptocurrency wallets, browser credential stores, cloud provider tokens, SSH keys, Discord and Telegram sessions, developer credentials, and database client configs, then installs persistence as a systemd user service masquerading as a benign daemon (colord, haveged). The Windows variant exfiltrates over the Telegram Bot API (api[.]telegram[.]org); the Linux and macOS variants use multipart HTTP POST and gate exfiltration behind anti-VM checks, which is why the endpoint was hard to confirm from a sandbox.
A version history built to look clean
The payload never sits at latest. For every member, it lives in a band of mid versions while the latest tag points somewhere safer. The actor cleans up two ways.
The first is scrubbing. After the stealer versions go live, the actor publishes one more version with an empty scripts block and no payload, then lets npm move latest to it. @frostnode/waitfor shows the shape directly in its postinstall field across versions:
// @frostnode/waitfor, postinstall by version0.9.0 node lib/tickinit.js earlier release0.10.3 node ./dist/cjs/tickinit.cjs payload0.10.4 node ./dist/cjs/tickinit.cjs payload0.10.5 node ./dist/cjs/tickinit.cjs payload0.10.6 (none) latest, scrubbedSo npm view @zynkit/jwtbytes reports no install hook while npm install @zynkit/[email protected] still pulls the stealer from the mid version. A reviewer who checks the default version sees clean metadata. A lockfile or a transitive dependency that already resolved 0.5.3 keeps the payload.
The second is unpublishing. @nullzero/[email protected] carried the stealer (postinstall: node lib/encoder.js, per external triage), but the actor removed that version from the registry before we could pull the tarball. The scope still exists, the publisher is still nullzero-rlnozk@wshu[.]net, and the surviving versions 1.4.3 to 1.4.5 are scrubbed. A deleted version leaves the smallest footprint of all: no artifact to analyze, and npm view lists only clean releases.
Both moves target the same blind spot. A scanner or a human who judges a package by its current latest sees a working utility. The stealer is one or two versions back, where an install pinned to an older range, a warm cache, or a registry mirror still reaches it. The same @wshu.net account published the early version, the payload, and the scrubbed tag, so treat every version of every listed scope as untrusted no matter how clean a single release looks.
Conclusion
This is a templated campaign. One loader, one second-stage binary, one obfuscation toolchain, and one publisher infrastructure, replicated across throwaway scopes with renamed bootstrap files and innocuous utility names. Do not install any version of any listed package. If one already reached a developer machine or CI runner, treat that host as fully compromised. The second-stage binary reaches crypto wallets, browser stores, cloud and SSH credentials, messaging sessions, and developer tokens, so rotate all of them, kill the systemd user service, remove its artifacts under ~/.local/bin, ~/.config/systemd/user, and ~/.local/state, and invalidate active sessions.
The discoverable members stop where npm’s public search does. Maintainer based pivoting goes nowhere because each account holds a single payload package. The through-line across every member is the publisher infrastructure, the per-scope <name>-<rand>@wshu[.]net burner emails. Because wshu[.]net is a shared disposable-email provider, the domain on its own is noisy, so the reliable pivot pairs it with the obfuscation fingerprint, the 2026-06-04 publish burst, and the angelmaybeth21-oss delivery account. Only npm Trust and Safety can run that correlation server side to surface the scopes that are not yet known. We reported the cluster with wshu[.]net as the starting pivot, and Amazon Inspector reached the same set independently.
References
- Chi Tran, Amazon Inspector. Operation Friday Harvest: a coordinated npm campaign delivering an infostealer. Independent analysis of the same cluster, including the second-stage Rust binary and Telegram C2.
javascript-obfuscator. The obfuscation toolchain behind the loader.retry. The legitimate package whose code@petitcode/eb-retrycopies as a decoy.
- vet
- malware
- npm
- supply-chain
- credential-theft
- infostealer
- obfuscation
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

MYRA: A Full Linux RAT Distributed via npm
The npm package apintergrationpost is a red team RAT called MYRA with native C rootkit, triple persistence, fileless execution, live screen streaming, and process masquerade. This analysis documents...

Five npm Packages That Hide a Windows Binary Dropper
Five npm packages published in a 12-minute burst split a Windows binary dropper across a fake utility toolkit. The loader hides in a preinstall hook, decodes its C2 from a helper package, and fetches...

@withgoogle/stitch-sdk: Scope Squat Harvests Developer Credentials
A malicious npm package squats the @withgoogle scope to impersonate Google Stitch, silently harvesting credentials from Claude Code, git, GitHub CLI, SSH keys, npm, and Docker on install.

Mastra npm Scope Takeover: 143 Packages Drop a RAT
An attacker republished 143 @mastra packages, including @mastra/core, each with one injected dependency: easy-day-js, a dayjs clone whose install hook downloads and runs a remote access trojan.

Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.
