What it detects
Looking for the families PushWarden covers rather than the rules? See What it covers.
PushWarden looks for the artefacts and behaviour of the PolinRider / Contagious Interview campaign in two places: your repositories and the computer itself. It also has generic rules for the tricks this kind of malware uses, so a new campaign with different strings is still caught. Every finding has a severity, a category, a threat name, the evidence and a one-sentence reason.
Severities
| Severity | Meaning | Automatic action |
|---|---|---|
| CRITICAL | A confirmed malware artefact or activity: a PolinRider indicator, or code that runs by itself and downloads or hides what it runs | Yes, according to the action policy |
| HIGH | A strong indicator that needs a human decision, for example a compromised package version or exposed keys on an infected host | No, listed for review |
| WARNING | Context worth checking, for example a suspicious git reflog, a secrets file in an infected repository, or files that arrived in the same commit as a loader | No |
| INFO | Informational | No |
Scan commands exit with code 1 when anything HIGH or CRITICAL is found.
In repositories
Every script, config, font, image and .vscode/*.json file in a project is checked, not only files with well-known names. node_modules and vendor are skipped unless --deep or deep=true is set.
| What | Examples | Severity |
|---|---|---|
| Campaign signatures and build tags in any script file | Loader marker strings, obfuscator fingerprints and build tags found in .js, .ts, .json, .py, .sh, .bat, .ps1 and similar files |
CRITICAL |
| Payload injected into a config file | Code appended, prepended or inserted into postcss.config.mjs, eslint.config.mjs, next.config.js, tailwind.config.js and other framework configs |
CRITICAL |
| Entry-file hooks | A loader call added to an application entry file | CRITICAL |
| Known keys and wallets | The campaign’s XOR payload keys; TRON, Aptos and Ethereum wallet addresses used as dead drops; blockchain RPC hosts next to a marker | CRITICAL |
| C2 references | Known C2 IP addresses, first-stage hosts, C2 URL paths and the payload header | CRITICAL |
| Disguised payloads | A font, image or dictionary file whose bytes are code, with or without whitespace padding; files matching known loader hashes | CRITICAL |
| Editor autorun | .vscode/tasks.json with runOn: folderOpen together with a loader or C2 host; settings that force automatic tasks on |
CRITICAL or HIGH |
| Propagation scripts | The batch scripts the malware uses to commit and force-push itself, and the helper files they create | CRITICAL or HIGH |
.gitignore tampering |
Entries that hide the malware’s files, or the .gitignore hiding itself |
CRITICAL or HIGH |
| Git hooks and git config | Malicious hooks; settings in .git/config that make git run a command (core.fsmonitor, core.pager, core.sshCommand, credential.helper, filter.*, aliases that start with !) when the command downloads or runs code. Ordinary values such as less, code --wait or git-lfs are left alone |
CRITICAL |
| Compromised packages | Known bad versions of npm packages, Go modules and a Packagist package, including typosquats; suspicious lifecycle scripts | CRITICAL or HIGH |
| Git history | Backdated commits, force-push traces in the reflog, commits that touch payload code on any branch. With scan --history, every old file version that carries an indicator |
HIGH or WARNING |
| Camouflage | Files that arrived in the same commit as a loader: decoy fonts, a decoy README, a .vscode set |
WARNING |
Beyond PolinRider
Campaigns change their strings; their tricks stay the same. These rules look for the trick itself, whatever the campaign. They act on their own only when the code runs by itself and the evidence is strong; everything else is an alert for you to review. On the same file, a PolinRider finding takes precedence and keeps its PolinRider name.
| What | Examples | Severity | Automatic action |
|---|---|---|---|
| A task that downloads and runs code when a folder is opened | .vscode/tasks.json with runOn: folderOpen running curl … \| sh, iwr … \| iex, powershell -enc … |
CRITICAL | Quarantined |
| A git hook that downloads and runs code | .husky/*, .git/hooks/* or a core.hooksPath folder fetching and running a script |
CRITICAL | Quarantined |
| Code hidden off-screen | 150 or more spaces or tabs on a line, then code that can run code (eval, Function(, child_process, require(, exec(, atob() |
CRITICAL | Pro removes only the hidden code; the free version quarantines the file |
| An install script that downloads and runs code | preinstall, install, postinstall or prepare in package.json running curl … \| sh and similar |
CRITICAL | None: package.json is never moved. Install with --ignore-scripts until it is reviewed |
| An obfuscated loader | A config file, entry file or the script an install hook runs that looks machine-obfuscated (_0x names, while(!![]), packed strings, escape runs) and can run code. Minified bundles are not judged |
HIGH | None |
| An install or container step that downloads and runs code | setup.py that executes downloaded or decoded code; .devcontainer commands, including initializeCommand, which runs on your computer |
HIGH | None |
| Invisible characters (GlassWorm style) | A dozen or more zero-width or hidden characters in a row in a code file. Critical when the same file also decodes text and runs code; otherwise a warning. Emoji joiners are normal and ignored | CRITICAL or WARNING | The file is quarantined |
| The Shai-Hulud npm worm | setup_bun.js, bun_environment.js, .truffler-cache and its planted workflow; an install script that runs them |
CRITICAL or HIGH | None for the files; install with --ignore-scripts |
| An AI coding tool run with its safety checks off | An install script that starts claude, gemini, codex and similar with --dangerously-skip-permissions, --yolo or --full-auto, the way the s1ngularity attack hunted for secrets |
CRITICAL | None: package.json is never moved |
| Fake job-interview spyware | A keylogger and a screen grabber (node-global-key-listener with screenshot-desktop) in one project |
HIGH | None |
| AI assistant config | .claude/settings.json hooks, MCP files and rules files (CLAUDE.md, AGENTS.md) that download and run code, start a script hidden in an editor folder, or hide text in invisible characters |
CRITICAL, HIGH or WARNING | None |
| GitHub Actions workflows | A step that sends every secret to a network command, installs a self-hosted runner, or runs a pull request’s own code with pull_request_target |
HIGH or WARNING | None |
“Downloads and runs” means a command that fetches code and executes it in one go, such as piping curl or wget into a shell, Invoke-WebRequest into Invoke-Expression, an encoded PowerShell command, node -e or python -c with decoding or network code, or certutil, mshta and bitsadmin downloads. Downloading a file without running it is not flagged. The patterns are in the indicator file (download_exec_regexes, exec_sink_regexes, hidden_padding_min), so they are tuned through the daily indicator update. The newer family rules (worm_files, ai_cli_abuse_regexes, suspicious_dep_sets) work the same way. A file too large to read whole still has its last megabyte checked, because a payload is appended to the end.
On the host
Host checks run in scan (unless --no-system) and in the guard’s full sweep. Process and network checks also run every 5 seconds.
| What | Examples | Severity |
|---|---|---|
| Malicious processes | A running process whose command line carries a campaign marker, or a known payload process name | CRITICAL |
| C2 connections | An open socket to a known C2 address | CRITICAL |
| Remote access tool footprint | The directories, files and environment variables the campaign’s remote access tools leave behind; portable Node or Python runtimes dropped by a later stage | CRITICAL or HIGH |
| Persistence | Entries in cron, systemd user units, XDG autostart, LaunchAgents, Windows scheduled tasks, the Startup folder and the HKCU\...\Run registry key |
CRITICAL or HIGH |
| Shell start-up injection | Lines added to shell rc files or the PowerShell profile | HIGH |
| Editor and tool injection | Modified files inside VS Code, Cursor, VSCodium, Windsurf, Discord or GitHub Desktop; a backdoored or oversized global npm/lib/cli.js |
CRITICAL or HIGH |
| Hosts-file redirects | Package registries redirected in the hosts file | HIGH |
| Exposed credentials on an infected host | Plain-text tokens in .npmrc, .git-credentials, the gh, AWS and Docker configs, and SSH keys. Reported only when the host shows other signs of infection |
HIGH |
Threat names
Notifications, dialogs, reports and the history name threats in the style of Windows Defender, so an alert is recognisable at a glance.
| Threat name | What it is |
|---|---|
Trojan:JS/PolinRider.ConfigInject |
Loader code injected into a framework config file, or a known XOR key |
Trojan:JS/PolinRider.EntryHook |
Loader call added to an application entry file |
Trojan:JS/PolinRider.FakeFont |
A script disguised as a font file |
Trojan:JS/PolinRider.Disguised |
Code hidden in another non-code file type |
Trojan:Script/PolinRider.TaskJacker |
An editor task that runs the loader when a folder is opened |
Trojan:BAT/PolinRider.AutoPush |
The propagation script that commits and force-pushes the backdoor |
Trojan:JS/PolinRider.DeadDrop |
A blockchain wallet or RPC reference used to fetch the payload location |
Trojan:JS/PolinRider.C2 |
A reference to a command-and-control server |
Trojan:JS/OmniStealer.Exfil |
Credential exfiltration through a messaging API |
Trojan:Script/PolinRider.GitHook |
A malicious git hook |
Trojan:JS/PolinRider.Package |
A compromised package version |
Trojan:JS/PolinRider.EditorInject |
Code injected into an editor or desktop tool |
Trojan:Script/PolinRider.Hide |
.gitignore entries that hide the malware |
Behavior:Node/PolinRider.Payload |
A running payload process |
Behavior:Net/PolinRider.C2 |
A live connection to a C2 server |
Backdoor:JS/RuntimeDevLink |
The remote access tool and its runtime |
Backdoor:Script/PolinRider.Persist |
A persistence entry |
Trojan:Script/PolinRider |
Any other injected loader |
Trojan:Script/Generic.AutoRun |
A task that runs by itself and downloads code (editor task, dev container) |
Trojan:Script/Generic.Downloader |
A git hook or install script that downloads and runs code |
Trojan:JS/Generic.HiddenCode |
Code hidden off-screen behind whitespace |
Trojan:JS/Generic.Obfuscated |
An obfuscated loader in a hand-written file |
Trojan:JS/GlassWorm.Invisible |
Code hidden in invisible characters |
Worm:JS/ShaiHulud |
A file or install script used by the Shai-Hulud npm worm |
Trojan:Script/Generic.AIAbuse |
An install script that starts an AI coding tool with its safety checks off |
Trojan:JS/ContagiousInterview.Spy |
The keylogger and screen grabber used by fake job-interview projects |
Trojan:Script/Generic.AgentHook |
An AI assistant config or rules file that runs or hides a command |
Trojan:Script/Generic.CIWorkflow |
A workflow that sends secrets out or installs a runner |
Windows Defender itself names the family Trojan:JS/PolinRider.DB!MTB.
Reasons on every finding
Every finding has a why line: one sentence that says what the evidence means and why it has that severity. Every response has a response line: what was done and why (killed, stripped, quarantined, or left for you to decide).
These lines appear in scan output, dialogs, pushwarden alerts, pushwarden history, desktop notifications, the webhook JSON (reasons array) and guard.log.
Indicators are data
All indicators live in one file, iocs.json: signature strings and regular expressions, XOR keys, wallet addresses, C2 IPs and hostnames, URL paths, loader hashes, process markers and compromised package versions.
- The program carries an embedded copy, so it works offline from the first run.
- The guard downloads a newer copy from the project repository every 24 hours. The download is parsed and validated before it replaces the previous copy: every regular expression must compile, every IP and hash must be well formed, and the version must be newer.
pushwarden update-iocsforces a download.--urland theioc_update_urlsetting point it at a mirror or a fork.- New variants are usually covered by adding an indicator, with no new release.
The current list is in pushwarden/iocs.json in the repository.
Kill list versus report list
A process is killed only when its command line holds a strict marker that legitimate tools never use. A broad indicator alone is reported but not killed, and the reason says so. This is what keeps ordinary node processes safe.
Files that legitimately contain signatures
Rule sets, tests and indicator databases contain the same strings the scanner looks for. Such a file opts out by carrying the token pushwarden:allow-signatures in its first 512 bytes.
The token is never honoured for config files, entry files, asset files or JavaScript and TypeScript files, so malware cannot use it to hide.
False positives
If a legitimate file is flagged:
pushwarden history --allow PATH --note "what the file really is"
This restores the file, stops flagging that exact content for 30 days, and records the note as feedback. Use exclude in the configuration to skip a directory entirely. See Response and recovery.