Find the North-Korean "Contagious Interview" malware anywhere on your machine. Fast, and without ever running it.
PolinRider Scanner walks your whole computer, reads every file as plain data, and
flags the ones that carry fingerprints of the PolinRider malware. It never opens,
runs, installs, or require()s anything it looks at, so checking an infected file
cannot infect you. It reads and it reports. The only file it writes is a JSON
report of what it found.
Indicators shown in this README are "defanged" (
evil[.]com,1.2.3[.]4) so the document is safe to keep around and will not set off the scanner itself.
PolinRider is malware run by a North-Korean crew. It is publicly tracked as BeaverTail, InvisibleFerret, and OtterCookie, part of the "Contagious Interview" operation. It goes after developers:
- A fake recruiter or client sends you a "coding challenge," a demo repo, or an npm package to try.
- Buried inside is a booby-trapped file. Usually a config file
(
next.config.mjs,tailwind.config.js, and so on), a fake.woff2font that is really JavaScript, or a.vscode/tasks.jsonrigged to run the instant you open the folder. - The moment you run the project, or just open it in VS Code, the payload steals your saved passwords, browser sessions, crypto wallets, SSH keys, and GitHub tokens, then phones home.
- With your GitHub token it logs in as you and force-pushes across every repo you can write to. That is how one infected laptop becomes a whole team's problem.
The original research, the tracked victim and repo lists, and the incident-response playbook are kept here: OpenSourceMalware/PolinRider.
You need Rust. Then:
git clone https://github.com/zer0Tokens/PolinRiderScanner.git
cd PolinRiderScanner
cargo build --releaseScan the whole machine:
# macOS / Linux, scans /
./target/release/polinrider-scanner
# Windows, scans every drive it finds
.\target\release\polinrider-scanner.exeOr point it at a folder you are worried about:
./target/release/polinrider-scanner ~/Downloads/that-coding-challengeEvery run prints a live summary and drops a polinrider-report.json next to you
with the full details. The exit code is 1 if anything HIGH or MEDIUM turned up
and 0 if you are clean, which is handy for CI or a cron job.
β Ή 138204 files Β· 22.1 GB Β· 41233/s Β· 1 flagged Β· β¦/Downloads/cool-demo/next.config.mjs
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PolinRider scan complete
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Scanned 412883 files (61.4 GB in 94.2s, 4383/s)
Unreadable 1204 (permission denied / IO errors)
Too large 7 (> 1024 MiB, contents skipped)
β οΈ 2 indicator(s) in 1 file(s): 2 HIGH Β· 0 MEDIUM Β· 0 LOW
[HIGH] content /Users/you/Downloads/cool-demo/public/fonts/fa-solid-400.woff2 @778
PolinRider obfuscator v1 signature marker (obf-v1-marker)
β ...split(x)})("rmcej%oβ¦",2857687);global[_$_1e4x[0]]= require;if(...
Report written to polinrider-report.json
A clean machine just says β
CLEAN. No PolinRider indicators found.
polinrider-report.json groups everything so you can see which files are hit at a
glance, alongside the raw findings:
{
"tool": "PolinRider Scanner",
"scanned_files": 412883,
"infected_file_count": 1,
"high": 2, "medium": 0, "low": 0,
"infected_files": [
{
"path": "/Users/you/Downloads/cool-demo/public/fonts/fa-solid-400.woff2",
"worst_severity": "HIGH",
"indicators": 2,
"signatures": ["obf-v1-marker", "obf-decoder"]
}
],
"findings": [ { "path": "β¦", "severity": "HIGH", "signature_id": "obf-decoder", "offset": 343 } ]
} roots (/, C:\, or a folder you name)
β
ββββββββββββββΌββββββββββββββ
β parallel directory walk β all CPU cores at once. hidden files,
β (skips pseudo-FS, self) β dotfiles and .gitignore'd files included
ββββββββββββββ¬ββββββββββββββ
β one file at a time, read-only
ββββββββββββββΌββββββββββββββ
β name check + byte scan β Aho-Corasick: every indicator matched
β (single pass per file) β in one pass. small files slurped,
ββββββββββββββ¬ββββββββββββββ big files streamed with flat memory
β
ββββββββββββββΌββββββββββββββ
β rank HIGH / MEDIUM / LOW β β live progress, console summary,
β group by file, report β and polinrider-report.json
βββββββββββββββββββββββββββββ
A few choices worth calling out:
- One pass, many patterns. All of the indicators are compiled into a single Aho-Corasick automaton, so each byte of a file is looked at once no matter how many indicators exist. That is what lets a full-disk scan finish in minutes.
- No blind spots. The walk ignores
.gitignoreand hidden-file rules on purpose and descends intonode_modules. That is exactly where these payloads like to hide, whether fake fonts, trojaned config files, or malicious deps. - Big files cannot hide anything. There is no cap on how many matches we read through, so an attacker cannot bury the real indicator behind megabytes of noise. Files over the size limit are streamed, so memory stays flat.
- The scanner will not cry wolf on itself. Its indicator list is XOR-obfuscated at rest, so the raw malware strings never sit in this repo's source or the built binary. A full-disk scan will not flag the scanner, its own report, or its checkout, and other antivirus will not quarantine it over the strings.
| Category | Examples (defanged) |
|---|---|
| Obfuscator markers | rmcej%oβ¦, Cot%3tβ¦, the _$_1e4x decoder |
| Command-and-control | default-configuration[.]vercel[.]app and other bootstrap hosts, 166.88.54[.]158, lianxinxiao[.]com |
| Dead-drop wallets | the crew's hard-coded TRON and Aptos addresses |
| XOR keys | the payload decryption keys |
| Malicious npm packages | tailwindcss-style-β¦, tailwind-mainanimβ¦ and 4 more (the real tailwindcss-animate is not flagged) |
| Weaponized VS Code template | the StakingGame tasks.json UUID and template string |
| Drop / propagation files | temp_auto_push[.]bat, the Windows lock file, credential-dump names |
Only known indicators are reported. The tool skips fuzzy heuristics on purpose,
so it will not panic you over a normal tasks.json or a legit config.
polinrider-scanner [ROOT]... folders/drives to scan (default: whole system)
--exclude <PATH> skip a directory (repeatable), e.g. a backup drive
--no-default-excludes also scan noise-only spots (.claude data, saved reports)
--json <FILE> report path (default: ./polinrider-report.json)
--no-report do not write the JSON report
-t, --threads <N> worker threads (0 = one per CPU core)
--max-file-mb <MiB> do not read contents of files bigger than this (default 1024)
--no-snippet omit the context snippet in the console
--no-progress turn off the live progress line
-h, --help / -V, --version
The scanner matches the malware's strings, so it will flag any file that merely quotes an indicator, not just real payloads: security notes, chat logs, IDE search caches, and saved scan reports. To keep results meaningful:
- Scan where code lives (
~/dev, a project folder) rather than all of$HOME, which is full of logs and caches that reference indicators. - A few noise-only spots are skipped automatically,
.claude/(Claude Code data) and this tool's ownpolinrider-report.json. Pass--no-default-excludesto include them. - Use
--excludefor anything else that's just records, e.g.--exclude ~/Library/Caches --exclude ~/notes. - The scanner also skips its own binary, and its indicator database is obfuscated, so it never flags its own source or checkout.
If a hit's path is a log, a cache, a report, or your own security notes, it's a
record of the indicator, not an infection. Real payloads show up in code and
asset files inside projects (config files, fake fonts, package.json, etc.).
A HIGH or MEDIUM hit means you should treat that machine as compromised. Do not panic-delete the file. Instead, in order:
- Do not open, run,
npm install, orrequire()the flagged files. - Pull the machine off the network.
- Rotate every credential it touched: GitHub tokens and SSH keys, cloud keys, browser-saved passwords, wallet seed phrases. Assume they are gone.
- Back up your data, then reimage. With this malware, deleting one file is not enough.
- Follow the full playbook in the OpenSourceMalware/PolinRider incident-response guide.
A LOW hit, like a generically-named config.bat, is worth a look but is often
benign, and does not by itself set the non-zero exit code.
- Never executes anything. No
exec, no shell-out, noeval, no dynamic loading. The only syscalls it makes on your files are open read-only, read, and close. - Never modifies your files. Nothing is written, moved, quarantined, or
deleted. The only output is the JSON report, which you can turn off with
--no-report. - Does not follow symlinks, so it cannot be walked into a loop or out of the area you asked it to scan.
- Will not trigger cloud downloads. Windows "Files On-Demand" placeholders are detected and skipped instead of being pulled down.
This scanner matches known, exact indicators. It does not yet flag the
generic mechanisms the campaign abuses, for example a .vscode/tasks.json set
to auto-run on folder-open ("runOn": "folderOpen" / allowAutomaticTasks), or
a font-extension file whose bytes aren't actually a font. Those gaps, and how to
close them safely, are written up in docs/DETECTIONS.md.
Contributions go through pull requests, see CONTRIBUTING.md.
main is protected: branch off it, open a PR, and let CI run.
Big thanks to the team behind OpenSourceMalware/PolinRider, who are actively researching this campaign, tracking compromised accounts and repositories, and publishing updated indicators and incident-response guidance as it evolves. This scanner is only possible because of their work. The indicators it looks for come straight from their reporting, cross-referenced with public threat intelligence from Unit 42 (Palo Alto), Microsoft Security, Silent Push, Cisco Talos, and NVISO.
If you find a new variant or indicator, please contribute it upstream to the OpenSourceMalware project so everyone's detection improves.
MIT, see LICENSE. This is a defensive tool. It contains no malicious code, only the fingerprints needed to detect it.