# PolinRider Scanner and Cleaner > Check a machine, a GitHub account or a whole organization for PolinRider malware, and clean it up. Free and open source. Changes nothing until you say yes. Everything on this site is public. People, search engines, AI assistants and agents may read, quote and cite it; please link to the page you quote. The indicator set and the code are MIT licensed. Every section below names the page it comes from. Dates are YYYY-MM-DD. --- # Scan for PolinRider: quick start Source: https://polinriderscanner.com/docs/quick-start/ polinrider is one free program that checks this computer, a folder, a GitHub account or a whole organization for the PolinRider malware. Install it, run it with no arguments and answer its questions. It changes nothing until you type `yes`, and everything else on this site can wait until it has told you what it found. ## Install it and run it ```bash brew install meSingh/tap/polinrider-scanner # macOS and Linux scoop bucket add mesingh https://github.com/meSingh/scoop-bucket scoop install polinrider-scanner # Windows ``` The package managers put the program and its indicator set in place for you. ## Or download it and verify it yourself 1. Download the archive for your computer from the [latest release](https://github.com/meSingh/polinrider-scanner/releases/latest): macOS (Apple silicon or Intel), Linux (x86_64 or arm64) or Windows (x86_64). 2. Check that it is genuine: ```bash sha256sum -c SHA256SUMS --ignore-missing gh attestation verify polinrider-v3.0.0-macos-arm64.tar.gz --repo meSingh/polinrider-scanner ``` For a release from before 3.0.0, see [Verifying releases from before 3.0](https://polinriderscanner.com/docs/reference/verifying-before-3/). 3. Unpack it and run it with no arguments: ```bash tar -xzf polinrider-v3.0.0-macos-arm64.tar.gz cd polinrider-v3.0.0-macos-arm64 ./polinrider ``` Keep the `ioc/` folder beside the program. It is the indicator set, and the program refuses to scan without it. On macOS, Gatekeeper stops the first run, because the program is not notarised. Once you have verified it, run `xattr -d com.apple.quarantine ./polinrider`. On Debian and Ubuntu, releases since 2.1.0 also carry a `.deb` for x86_64 and arm64. Verify it the same way, then run `sudo apt install ./` followed by its file name. ## The first question It asks what to check. Answer with a word, never a number. | Answer | What it checks | |---|---| | `computer` | This computer: your whole home folder, what starts by itself, shell startup files, git and npm settings, running programs and open connections | | `folder` | One folder of code. It suggests the code folders it finds | | `organization` | Every repository, branch and tag of a GitHub organization | | `account` | The same for your own GitHub account | | `everything` | This computer first, then GitHub | Type `q` to leave at any point. ## If it finds something **Start with this computer, not your repositories** Cleaning GitHub while an infected computer still holds a valid token puts you back where you started within minutes. That is how this campaign behaves, not bad luck. [Order matters](https://polinriderscanner.com/docs/guides/order/) Not sure what a result means? [Is it PolinRider or a false alarm?](https://polinriderscanner.com/news/2026-11-04-polinrider-or-false-alarm/) --- # Verify polinrider before you run it Source: https://polinriderscanner.com/docs/reference/verifying/ You are being asked to run unknown code on a machine you may believe is compromised. That deserves more than a promise. | Signal | What it proves | |---|---| | GPG-signed commits | Every commit was made by the key holder. This is the direct counter to the campaign's own backdated-amend technique | | Attested releases | Each release carries a sigstore `.intoto.jsonl` provenance attestation covering every archive, the Windows zip and the `.deb` packages, so you can verify the file came from this repository's CI and not from someone's laptop | | `SHA256SUMS` on every release | The file you downloaded is the file that was built | | OpenSSF Scorecard | 18 automated checks on the repository's security posture, published so the badge resolves | | CodeQL and Semgrep in CI | Static analysis on every push | ## Verify a release The same two checks for any asset: an archive, the Windows zip or a `.deb`. ```bash sha256sum -c SHA256SUMS --ignore-missing gh attestation verify polinrider-v3.0.0-macos-arm64.tar.gz --repo meSingh/polinrider-scanner ``` The Homebrew formula and the Scoop manifest carry the checksums from that same `SHA256SUMS`, so a package manager install is checked against it too. ## Releases before 3.0.0 In 3.0.0 the repository was renamed from `meSingh/polinrider-cleaner` to `meSingh/polinrider-scanner`. Releases up to 2.3.0 were built and attested under the old name. Their `SHA256SUMS` and their signed tags check exactly as before. For their build attestation, `--repo meSingh/polinrider-scanner` does not work, but this one command checks a release from any version: ```bash gh attestation verify polinrider-v2.3.0-macos-arm64.tar.gz \ --owner meSingh \ --cert-identity-regex '^https://github\.com/meSingh/polinrider-(cleaner|scanner)/\.github/workflows/release\.yml@refs/tags/v' ``` The old name, the offline check and how to prove an attestation came from this repository: [Verifying releases from before 3.0](https://polinriderscanner.com/docs/reference/verifying-before-3/). ## Verify the commits ```bash git log --show-signature -5 ``` ## The honest part None of this proves the tool is correct, only that it is the code this repository published. The source is deliberately readable for that reason: the binary is one Rust crate with no dependencies, the indicator set is plain text files, and every behaviour is pinned by a case in the conformance corpus. --- # Verifying releases from before 3.0 Source: https://polinriderscanner.com/docs/reference/verifying-before-3/ In 3.0 the repository was renamed from `meSingh/polinrider-cleaner` to `meSingh/polinrider-scanner`. Releases up to and including 2.3.0 were built and attested under the old name, and their attestations record that name. 3.0.0 is the first release built under the new one. ## What the rename does not change **The checksums.** Every release still carries its `SHA256SUMS`, and checking a download against it works as before: ```bash sha256sum -c SHA256SUMS --ignore-missing ``` **The signed tags.** Each release's tag is signed, and the signature belongs to the tag, not to the repository's name: ```bash git verify-tag v2.3.0 ``` ## The build attestation ### One command for every release This checks a release from any version, before or after 3.0. It accepts an attestation made by this project's release workflow, from a release tag, under either name: ```bash gh attestation verify polinrider-v2.3.0-macos-arm64.tar.gz \ --owner meSingh \ --cert-identity-regex '^https://github\.com/meSingh/polinrider-(cleaner|scanner)/\.github/workflows/release\.yml@refs/tags/v' ``` `--owner` limits the lookup to this account, and `--cert-identity-regex` requires that the file was signed by `release.yml` in this repository, under its old or its new name, while building a tag. ### With the old name For a release from before 3.0, the command that worked before the rename still works. GitHub redirects the old name to the renamed repository: ```bash gh attestation verify polinrider-v2.3.0-macos-arm64.tar.gz --repo meSingh/polinrider-cleaner ``` The new name does not work for these releases. Their attestations record the old name, so `--repo meSingh/polinrider-scanner` fails with `Error: verifying with issuer "sigstore.dev"`. That error on a release from before 3.0.0 means the wrong name was used, not that the file is fake. ### Offline Every release ships its attestation as `polinrider-vX.Y.Z.intoto.jsonl`. `--bundle` reads it from that file instead of asking GitHub: ```bash gh attestation verify polinrider-v2.3.0-macos-arm64.tar.gz \ --bundle polinrider-v2.3.0.intoto.jsonl \ --repo meSingh/polinrider-cleaner ``` ### Proof that it came from this repository A name can change hands; a repository's ID does not change when it is renamed. To be sure the attestation was made in this repository, and not in some later repository given the old name, print the ID it records: ```bash gh attestation verify polinrider-v2.3.0-macos-arm64.tar.gz \ --owner meSingh \ --cert-identity-regex '^https://github\.com/meSingh/polinrider-(cleaner|scanner)/\.github/workflows/release\.yml@refs/tags/v' \ --format json --jq '.[].verificationResult.signature.certificate.sourceRepositoryIdentifier' ``` It must print `1358099890`, this repository's ID, for every attestation it finds. All of the above was tested on 4 November 2026 with `gh` 2.100.0, on 2.3.0 and 2.0.0, after the rename. ## Or use a current release Unless you need a particular old version, download the latest release and verify it the usual way ([Verifying this repository](https://polinriderscanner.com/docs/reference/verifying/)). --- # Exit codes Source: https://polinriderscanner.com/docs/reference/exit-codes/ Every scanner in this repository uses the same four codes. They are a contract: scripts and CI can depend on them. | Code | Meaning | |---|---| | `0` | Clean against the current indicator set | | `1` | Review items only; nothing confirmed | | `2` | A confirmed indicator hit | | `3` | **The scan could not run** | ## Why 3 exists `2` used to mean both "confirmed infection" and "could not run", which meant pointing the tool at a path that did not exist printed the full compromise playbook. A tool that reports a compromise it did not find is worse than one that reports nothing. `3` is the separate answer for "I was unable to tell you", and it covers a missing dependency, an unreadable path, an empty indicator set and an unauthenticated GitHub CLI. A clean result is not proof. ## A clean result is not proof `0` means the current indicator set is absent. Signatures rotate; an older or rotated variant may have been here and left. Rotate your GitHub tokens, SSH keys and cloud keys regardless of the exit code. --- # PolinRider scanner false positives Source: https://polinriderscanner.com/docs/reference/false-positives/ Each of these is harmless, and each was met during a real cleanup or while testing the tool. Precision matters more than reach here: a false `INFECTED` in a tool people run during an incident costs everyone real time. | What you see | Why | What to do | |---|---|---| | Your own detection tooling reported: a scan workflow, its scripts, a copy of `ioc/` | It contains the indicator strings because it searches for them | Already handled for the usual paths, listed under each check below. Anything else of yours: in the CI scanner, add it with `--exclude` | | `.vscode/settings.json` matched `folderOpen` | Only `.vscode/`**`tasks`**`.json` runs commands | The folder and computer checks skip `.vscode/` in their review-level scan. On GitHub it can appear as a review line, never as confirmed | | A `.vscode/tasks.json` that runs on folder open, "verify the command by hand" | Teams use folder-open tasks for real work | Read the command. With a known indicator in it, it is reported as confirmed instead | | Every `.woff2` in a repo flagged as "not a font" | Git LFS stores a text pointer instead of the font, and a zero-byte placeholder has no font bytes | Already handled: the folder, computer and GitHub checks skip LFS pointers and empty files | | A config file flagged for "content after module end" | Flat configs legitimately open with `export default [` on line 1 and run long | A review line, never confirmed, and only when the remainder also looks like a payload | | A README or incident write-up that names the campaign's indicators | Documenting the campaign means naming its indicators | Depends on the check; see below | | Rendered documentation (HTML, a built docs site) reported | The documentation rules key off `.md`, `docs/` and README, which built output no longer matches | In the CI scanner, exclude your build output: `--exclude '(^\|/)site/'`. This repository's own self-scan excludes its built site, `docs-site/dist/` | ## Documentation, check by check Each check treats documentation differently. This is what the code does today. - **The folder and computer checks** read indicators in particular kinds of file: `tasks.json`, build configs, fonts, git hooks and editor extensions. A `.md` file is none of these. Their wider, review-level scan of other files skips the paths treated as the user's own detection tooling: any `.md` file, anything under `docs/`, any README, `ioc/*.txt`, the tool's own scripts and workflows with `polinrider` in their name. - **The GitHub check** never confirms a finding in those same paths, even when one contains an indicator string. It can still list such a file as a review line if it mentions a weak signal, such as `folderOpen`. - **The CI scanner** (`ci/scan-workspace.sh`) skips `.md` files by default. `--scan-docs` includes them. It also always skips `.git/`, `node_modules/`, `.github/polinrider/`, `ioc/` and workflows with `polinrider` in their name, and `--exclude` adds more. None of these exemptions covers `.mdx`, HTML or other formats. A page in one of those that quotes a confirmed indicator can be reported as one. Found a new one? [Open an issue](https://github.com/meSingh/polinrider-scanner/issues/new?template=false-positive.md). A missed detection is [a different template](https://github.com/meSingh/polinrider-scanner/issues/new?template=missed-detection.md), and both are worth filing. --- # Removing PolinRider: do it in this order Source: https://polinriderscanner.com/docs/guides/order/ If more than one of these applies to you, the order is fixed and it is not a style preference. 1. **Check the machines.** Every machine that has touched the affected repositories, before you touch GitHub at all. 2. **Rotate every credential.** Assume everything reachable from the affected account is in someone else's hands. 3. **Get the payload out of the repositories.** Restore where push events survive, clean where they do not. 4. **Scan every future push**, so a reinfection is caught by CI rather than by a stranger. Cleaning a remote while an infected machine still holds a valid token puts you back where you started within minutes. This is documented behaviour of the campaign, and it is the single most common way a cleanup fails. ## Why Cleaning the remote while an infected machine still holds a valid token puts you back where you started within minutes. The payload's whole purpose is to propagate: it amends your latest commit and force-pushes the current branch to `origin` with `-uf --no-verify` ([why your history looks clean](https://polinriderscanner.com/threat/#why-your-git-history-looks-clean)). A clean repository plus an infected laptop is a clean repository for about as long as it takes the next commit to land. This is documented behaviour of the campaign, and it is the single most common way a cleanup fails. ## The guided flow does this for you Running `polinrider` and answering `everything` walks the sequence: this computer first, then GitHub. Rotating credentials is yours to do between the two. The per-track guides are next, and [Remove PolinRider: the full cleanup checklist](https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/) puts every step on one page, with the checks still done by hand and when to rebuild. What has moved since OpenSourceMalware's August cleanup guide is in [Cleaning up PolinRider: what has changed since August](https://polinriderscanner.com/guides/developers-guide-getting-rid-of-polinrider/). --- # Remove PolinRider: the full cleanup checklist Source: https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/ The whole cleanup on one page, in the order from [Order matters](https://polinriderscanner.com/docs/guides/order/). Each step links the guide with the detail. What has moved since OpenSourceMalware's August cleanup guide is in [Cleaning up PolinRider: what has changed since August](https://polinriderscanner.com/guides/developers-guide-getting-rid-of-polinrider/). ## Before you start From a computer you trust, before the long work: 1. Turn off automatic tasks in VS Code for every folder: search the settings for "task" and set *Allow Automatic Tasks* to off. 2. On every repository the account can reach, block force-pushes, require signed commits and pause automatic deploys. 3. Suspend any OAuth App or GitHub App you do not recognise. 4. Tell your collaborators and anyone who installs your package. 5. Do not open, build or `git pull` any affected project until it is checked. These come from OpenSourceMalware's [developer's guide](https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider). ## 1. Every computer On every computer that has pushed to the affected repositories: 1. Run `polinrider` and answer `computer`. It reads and reports; nothing changes yet. [A machine](https://polinriderscanner.com/docs/guides/machine/) 2. Read each `[HIT]` and `[review]` line. [Is it PolinRider or a false alarm?](https://polinriderscanner.com/guides/polinrider-or-false-alarm/) 3. Type `yes` to move confirmed files into quarantine. 4. Run each command it prints for what it does not change itself: a startup entry, a crontab line, a shell startup file, a line in `~/.ssh/authorized_keys`, a process still running. 5. If npm's own program files were confirmed, reinstall Node.js before you run `npm` again. 6. Decide whether to rebuild (below). What `yes` does and does not do: | It does | It does not | |---|---| | Moves confirmed files into a new folder in your home directory, with a manifest and `RESTORE.txt` | Delete anything | | Cuts a payload appended to a build config, keeping the infected original | Stage, commit or reset anything in git | | Prints the command for each startup entry and SSH key | Edit a startup entry, a crontab, a profile or `authorized_keys` | ### Checks by hand | Check | Why | |---|---| | Every line of `~/.ssh/authorized_keys` | The tool names a published key; the next build can use another ([ADR-0049](https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0049-the-computer-check-reads-authorized-keys.md)) | | Windows Run values and scheduled tasks named `MicrosoftCLROptimization`, including under the SYSTEM profile | Seen starting the malware at logon; the tool knows the script by its file name ([2026-10-05 review](https://polinriderscanner.com/indicator-reviews/2026-10-05/)) | | Repeated macOS Keychain prompts for GitHub access | A detached process retrying ([OpenSourceMalware](https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider)) | | The Windows side of a WSL2 setup | The loader starts `node.exe` from inside the Linux guest ([2026-10-05 review](https://polinriderscanner.com/indicator-reviews/2026-10-05/)) | | `git reflog` in each project | Amended commits you do not remember ([OpenSourceMalware](https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider)) | ### Rebuild or not | Found | Do | |---|---| | Proof the payload ran here: a login item, startup file, git hook, editor extension, implant, process or connection. The tool says "This machine cannot be trusted until it is rebuilt" | Rebuild | | Files inside projects only, and you know nothing infected was built or opened | Quarantine, delete every clone, rotate, keep scanning | | Files inside projects only, and you are not sure | Rebuild | A rebuild is still the safer choice on a computer with production or financial access. ([ADR-0034](https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0034-the-verdict-is-followed-by-what-to-do-and-rebuild-is-said-only-on-proof.md); [README](https://github.com/meSingh/polinrider-scanner#step-1-check-the-machines)) ## 2. Every credential Yours to do, from a computer you trust, after step 1 and before step 3. In this order: 1. **Repository write:** personal access tokens, SSH and signing keys, deploy keys on every repository, OAuth and GitHub App grants, Actions secrets and variables, self-hosted runner tokens. 2. **Publish rights:** npm tokens with a 2FA reset, then Packagist, PyPI, Docker Hub and Chrome Web Store. 3. **Everything else the stealer could read:** cloud keys, every `.env`, database passwords, connected platforms such as Vercel and Netlify, browser-saved passwords and a password manager vault that was unlocked on the computer. 4. **Cached Git credentials**, which survive a password change. 5. **A wallet or seed phrase** on that computer: move the funds first. The full list, with the commands to clear cached credentials, is in the [README](https://github.com/meSingh/polinrider-scanner#step-2-rotate-every-credential). ## 3. Every repository 1. Run `polinrider` and answer `account` or `organization`. It needs `gh`, signed in. [A personal GitHub account](https://polinriderscanner.com/docs/guides/account/) ยท [An organization](https://polinriderscanner.com/docs/guides/organization/) 2. Read the summary. A familiar name in the push record means that person's computer needs step 1 too. 3. Pick a fix for each repository: | The branch was | Fix | |---|---| | Force-pushed to a rewritten history | `restore`, offered only after the earlier commit is fetched and checked clean | | Committed to normally | `remove` (one new commit) or `erase` (rewrite history) | | Not in use | `archive` | 4. Type `yes` after reading each dry run. 5. Delete every local clone and clone again. 6. Check forks. If you publish a Go module, release a fixed version: the module proxy keeps the infected one ([OpenSourceMalware](https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider)). 7. When every restore is done, run `polinrider --purge-evidence` to delete the copies it made. Run this step early. GitHub serves the commit a branch was moved off only until it collects its garbage. ## 4. Every future push 1. Vendor the CI scanner into each repository. [Block PolinRider in CI](https://polinriderscanner.com/docs/guides/ci/) ```bash ./ci/install-workflow.sh /path/to/your/repo ``` 2. Keep force-pushes blocked and signed commits required. 3. Scan weekly for a month, then monthly. ([README](https://github.com/meSingh/polinrider-scanner#keep-scanning)) --- # Check a computer for PolinRider Source: https://polinriderscanner.com/docs/guides/machine/ > The machine check is the `polinrider` binary. macOS, Linux and Windows are > checked by the same code. **Read-only unless you say otherwise.** Check every machine that has touched the affected repositories, before you touch GitHub at all. ```bash polinrider ``` That is the guided flow: it asks what to check, one question at a time, and changes nothing unless you type `yes`. Choose `computer`. Without the questions: ```bash polinrider check ~/code ``` A check of this computer and of that folder. It moves nothing. Add `--apply` to move confirmed artifacts into quarantine. `--fs-only` checks the folder alone and reads nothing of the machine, which is what you want for a backup drive or a mounted image. ## What it looks at The `MicrosoftSystem64` implant first, malware from the same DPRK-linked group that this tool checks for alongside PolinRider ([why](https://polinriderscanner.com/threat/#related-malware-microsoftsystem64)): its files, its process, and on Windows its scheduled task and Run entry. Then editor extensions, editor tasks that run on folder open, build configs, fake fonts, the script the payload spreads with, known-bad packages, Python startup files, what starts by itself, shell startup files and PowerShell profiles, git hooks, npm configuration and npm's own program files, interpreters running inline code and live connections. Last, an inventory of the keys and credential files that would have to be changed if anything else is confirmed. They are counted and named, and never opened. "What starts by itself" depends on the system: | System | Read | |---|---| | Linux | systemd units, autostart entries, the user crontab, system cron | | macOS | LaunchAgents and LaunchDaemons, the user crontab | | Windows | registry Run and RunOnce values, both Startup folders, scheduled tasks | Two kinds of persistence live inside a tool rather than beside it, and both survive cleaning every repository: - **Python startup files.** Python runs the `import` lines of every `.pth` file in `site-packages` each time it starts. These are read in each project's `.venv`, in your own Python installations and in Python installed for every user. What packaging tools write there is ignored; a line that decodes, downloads or starts a process is listed for review. - **npm itself.** The campaign has been seen replacing npm's `lib/cli.js` so that every `npm` and `npx` command starts the payload again. npm's entry files are read in each installation in the usual places, including version managers. If one is confirmed, reinstall Node.js before running npm again. ## What changes, and when Nothing is ever deleted. - `polinrider check --apply` moves confirmed artifacts into a timestamped quarantine directory with a manifest and instructions for putting them back. It never edits a file. - `polinrider clean DIR --apply` does that and one thing more: where the payload was appended to a build config, it cuts the payload out in place and keeps the infected original in quarantine. Your own uncommitted work in that file survives. It does not touch git. - A crontab line, a shell startup file, a PowerShell profile, a Run entry and a scheduled task are never changed for you. The finding carries the command that removes it. ## Large disks `node_modules`, `.git`, caches and trash are never walked. Malicious packages are caught by name from manifests and lockfiles, and the payload lives in the project's own files rather than inside a dependency. Each manifest and lockfile is read for the packages it installs, and each name is compared whole, so a package whose name merely starts with a campaign one is not reported. One that cannot be read is matched as text and listed for review. A long check shows how far it has come. 2.0 does not checkpoint a scan: there is no `--resume`. An interrupted check starts again from the beginning. ## Exit codes `0` clean against today's indicators, `1` something needs a human look, `2` a confirmed indicator, `3` the check could not run. `3` says nothing about whether you are infected. --- # Check your GitHub account for PolinRider Source: https://polinriderscanner.com/docs/guides/account/ Run `polinrider` and answer `account`. It checks every repository of your own account, every branch and every tag. Check every machine and rotate every credential first. [Order matters](https://polinriderscanner.com/docs/guides/order/). ## Before it starts It needs [`gh`](https://cli.github.com/), installed and signed in. It checks that first and says what to run if not. It uses the account `gh` is signed in as. ## What it does 1. **Copies every repository** into an evidence folder outside any git working tree, before anything changes. Those copies hold live malware, so do not open them in an editor. The default folder is temporary and cleared on restart. Once every restore is done, `polinrider --purge-evidence` deletes the copies after asking; add `--evidence DIR` if you chose the folder. 2. **Checks every branch and tag** against the indicator set, and reads GitHub's push record for each repository. 3. **Shows a summary**: which repositories, how many branches, who pushed. `details` lists every branch and file. 4. **Asks how to go through them**: `each` for one repository at a time, `all` for one fix everywhere it applies, or `none` to change nothing. ## The fixes | Choice | What it does | |---|---| | `restore` | Moves each branch back to the newest clean state on GitHub's push record. Offered only where that record shows one, and only after the commit has been fetched and checked clean. Never trusts commit dates, which this malware forges | | `erase` | Rewrites history so the payload is in no commit, force-pushes, then shows how to reset every existing clone. Every commit ID on the rewritten branches changes | | `remove` | One new commit that takes the payload out. History is left as it is | | `archive` | Puts a large infected notice at the top of the README without removing anything, rewrites the description and makes the repository read-only. For a repository nobody uses | | `skip` | Leaves it | Each fix shows a dry run of exactly what would change on GitHub, and pushes only when you type `yes`. Every push carries a lease, so a push that landed since the check is never overwritten. Afterwards it asks GitHub what it now shows and reports that, not what it intended. `all` warns first, says which repositories can take the fix and which will be left alone, and goes ahead only when you type the owner's name. `yes` is not enough there. ## Which fix If the branch was **force-pushed** to a rewritten history, GitHub's push record still holds the earlier commit and `restore` goes back to it. If the payload was **committed normally**, there is no earlier state to return to, so `remove` or `erase` is the fix. GitHub serves the commit a branch was moved off only until it collects its garbage, so run the check early. After any fix, delete every local clone and clone again. A `git pull` into an infected clone can re-infect the remote. --- # Scan a GitHub organization for PolinRider Source: https://polinriderscanner.com/docs/guides/organization/ Run `polinrider` and answer `organization`. It checks every repository of an organization you belong to, every branch and every tag. Check every machine and rotate every credential first. [Order matters](https://polinriderscanner.com/docs/guides/order/). ## Before it starts It needs [`gh`](https://cli.github.com/), installed and signed in. It checks that first and says what to run if not. It lists the organizations your `gh` sign-in can see and asks which one. ## What it does 1. **Copies every repository** into an evidence folder outside any git working tree, before anything changes. Those copies hold live malware, so do not open them in an editor. The default folder is temporary and cleared on restart. Once every restore is done, `polinrider --purge-evidence` deletes the copies after asking; add `--evidence DIR` if you chose the folder. 2. **Checks every branch and tag** against the indicator set, and reads GitHub's push record for each repository. 3. **Shows a summary**: which repositories, how many branches, who pushed. `details` lists every branch and file. 4. **Asks how to go through them**: `each` for one repository at a time, `all` for one fix everywhere it applies, or `none` to change nothing. ## The fixes | Choice | What it does | |---|---| | `restore` | Moves each branch back to the newest clean state on GitHub's push record. Offered only where that record shows one, and only after the commit has been fetched and checked clean. Never trusts commit dates, which this malware forges | | `erase` | Rewrites history so the payload is in no commit, force-pushes, then shows how to reset every existing clone. Every commit ID on the rewritten branches changes | | `remove` | One new commit that takes the payload out. History is left as it is | | `archive` | Puts a large infected notice at the top of the README without removing anything, rewrites the description and makes the repository read-only. For a repository nobody uses | | `skip` | Leaves it | Each fix shows a dry run of exactly what would change on GitHub, and pushes only when you type `yes`. Every push carries a lease, so a push that landed since the check is never overwritten. Afterwards it asks GitHub what it now shows and reports that, not what it intended. `all` warns first, says which repositories can take the fix and which will be left alone, and goes ahead only when you type the owner's name. `yes` is not enough there. ## Which fix If the branch was **force-pushed** to a rewritten history, GitHub's push record still holds the earlier commit and `restore` goes back to it. If the payload was **committed normally**, there is no earlier state to return to, so `remove` or `erase` is the fix. GitHub serves the commit a branch was moved off only until it collects its garbage, so run the check early. After any fix, delete every local clone and clone again. A `git pull` into an infected clone can re-infect the remote. --- # Block PolinRider in CI Source: https://polinriderscanner.com/docs/guides/ci/ > **Being migrated.** The full guide currently lives in > [`ci/README.md`](https://github.com/meSingh/polinrider-scanner/blob/main/ci/README.md) and is authoritative until this page > replaces it. A scanner you copy into your own repository, so every push is checked with no third-party action and nothing leaves your infrastructure. ```bash ./ci/install-workflow.sh /path/to/your/repo ``` This vendors the scanner and a workflow into the target repository. It runs on every push and pull request, and fails the check when a confirmed indicator is found. It reads indicators written in `\uXXXX`, `\u{...}` and `\xNN` escapes as well as plain ones, and reads `package.json` and the three lockfile formats for the packages they install, so a campaign name counts only as a whole dependency name. It uses `perl` and `jq` where they are installed, as they are on GitHub's hosted runners, and says in its output when either is missing. **Update a copy vendored from 2.1.0 or earlier** Those copies match package names as plain substrings and do not decode escape codes. They miss the escaped loader family and report a project that depends on a longer package as infected. Re-run the installer with `--force`: ```bash ./ci/install-workflow.sh /path/to/your/repo --force ``` Because the scanner contains the indicator strings it searches for, it will match itself. The triage filter separates those matches by path; see [false positives](https://polinriderscanner.com/docs/reference/false-positives/). --- # Contributing Source: https://polinriderscanner.com/docs/project/contributing/ The most valuable contributions to this project are not code. ## An indicator we are missing [Open a missed-detection issue](https://github.com/meSingh/polinrider-scanner/issues/new?template=missed-detection.md) with the source you saw it in. New indicators are reviewed against the classification rule on [the indicator set](https://polinriderscanner.com/threat/indicators/): `strong.txt` only when a match means infection with no plausible alternative. ## A false positive [Open a false-positive issue](https://github.com/meSingh/polinrider-scanner/issues/new?template=false-positive.md). These matter more than they look. A tool that cries wolf during an incident wastes the one resource nobody has, and the [known ones](https://polinriderscanner.com/docs/reference/false-positives/) were all found this way. ## A fix to these docs Every page has an edit link in the top right. It opens the Markdown source in GitHub's editor and turns your change into a pull request. No local setup, no toolchain. ## Code **Run everything in the sandbox.** This tool quarantines files, rewrites git history and walks `$HOME`, and its test suite exercises those paths. Testing it directly on your workstation is one bad path expansion away from moving your real files. ```bash ./polinrider-sandbox --all # fmt, clippy, unit tests, the corpus, shell lint ./polinrider-sandbox --beta # the binary installed in the container, with a sample ./polinrider-sandbox # an interactive shell in the sandbox ``` The repository is mounted read-only, `$HOME` belongs to the container, and networking is off unless you ask for it. It needs Docker, which is a development dependency only: the released binary runs on a bare machine with nothing installed. See [ADR-0027](https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0027-development-and-testing-happen-in-a-container.md). One thing the sandbox does not give you is platform coverage. It is Linux with bash 5; macOS paths and bash 3.2 behaviour need CI's macOS runner or a real Mac. [`CONTRIBUTING.md`](https://github.com/meSingh/polinrider-scanner/blob/main/CONTRIBUTING.md) walks through trying the tool in the sandbox, the machine checks and the GitHub screens against a pretend organization. Read [`AGENTS.md`](https://github.com/meSingh/polinrider-scanner/blob/main/AGENTS.md) first: it covers the layout, the exit-code contract, the release process and the rules that are not negotiable. The pre-pull-request checklist is in there, and CI runs the same checks. Commits and tags are GPG-signed and show as Verified. A repository about backdated, force-pushed commits that does not sign its own is not credible, so please do not disable signing. --- # Reporting a vulnerability Source: https://polinriderscanner.com/docs/project/security/ If you find a vulnerability **in this tool**, report it privately through [GitHub Security Advisories](https://github.com/meSingh/polinrider-scanner/security/advisories/new) rather than opening a public issue. That covers, for example: a way to make the scanner miss a known indicator, a path that lets `--apply` delete rather than quarantine, a command injection through a crafted filename, or anything that would make running this tool worse than not running it. ## If you are reporting an infection That is not a vulnerability in this tool and does not need to be private. [Open a missed-detection issue](https://github.com/meSingh/polinrider-scanner/issues/new?template=missed-detection.md), or report the campaign artifacts to [opensourcemalware.com](https://opensourcemalware.com), which maintains the primary dossier. ## If you are mid-incident Nothing here is a support channel with a response time. Start with [order matters](https://polinriderscanner.com/docs/guides/order/) and rotate your credentials first. --- # Disclaimer Source: https://polinriderscanner.com/docs/project/disclaimer/ Read this before running anything in this repository against systems you care about. ## What this is An independent open source project written and maintained by one person. It is not a commercial product. It is not affiliated with, endorsed by, or supported by GitHub, Socket, OpenSSF, JFrog, safedep, OpenSourceMalware, any security vendor, or any employer past or present. Their work is cited because it is the public source of the indicators; that citation is not a partnership and implies no review of this code by them. ## No warranty The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and non-infringement. See [LICENSE](https://github.com/meSingh/polinrider-scanner/blob/main/LICENSE). There is no support commitment and no service level. Issues and pull requests are read when time allows. ## No liability To the maximum extent permitted by applicable law, the authors and contributors accept no liability for any claim, damages or other liability, whether in contract, tort or otherwise, arising from or in connection with the software or its use. That includes, without limitation, data loss, loss of access to repositories, service interruption, and business or financial loss. ## You are responsible for what you run it against - **Authorisation is yours to establish.** Scanning or modifying repositories, organizations or machines that you do not own or administer may breach your employer's policy, a customer contract, or applicable law. Get permission first, in writing if the systems are not yours. - **The destructive operations are your decision.** The GitHub fixes push to GitHub, and `erase` rewrites history. `check --apply` moves files on a machine and `clean --apply` edits a build config in place. Both run with your credentials, at your instruction. Read the dry run first. - **Recovery can lose work.** Restoring a branch to an earlier commit orphans anything pushed to it afterwards. The tool tells you when that will happen. Act on that warning rather than past it. - **Take your own backups.** Keep the `evidence/` mirrors until you are certain the incident is closed. ## What a result does and does not mean A clean result means the indicators in [`ioc/`](https://github.com/meSingh/polinrider-scanner/tree/main/ioc/) were not found. It is not a certificate, an audit, or an assurance that a system is uncompromised. Signatures for this campaign rotate, and a variant this tool does not yet recognise will not be reported. A finding means a known indicator matched. It is evidence to investigate, not a determination. False positives are documented in the README and are expected. ## Not advice Nothing here is legal advice, regulatory advice, or a compliance assessment. This tool cannot tell you whether you have a notifiable breach, what your disclosure obligations are, or whether an incident is over. If those questions are live, involve your own counsel and your own incident response people. ## Reporting Security issues in this tool: see [SECURITY.md](https://github.com/meSingh/polinrider-scanner/blob/main/SECURITY.md). Findings about the campaign itself belong with the researchers tracking it, linked in the README. --- # PolinRider indicators of compromise Source: https://polinriderscanner.com/what-is-polinrider/indicators/ One source of truth, in [`ioc/`](https://github.com/meSingh/polinrider-scanner/tree/main/ioc). Every script reads these files at runtime, so adding an indicator there updates the organization scanner, the account scanner and all three local checks at once. | File | Meaning | Effect on verdict | |---|---|---| | `strong.txt` | Confirmed PolinRider artifacts | `INFECTED` | | `strong-patterns.txt` | Patterns over file contents where a part rotates (extended regex, with a fixed part of 3 or more characters, no `^` or `$`) | `INFECTED` (ADR-0052) | | `bad-packages.txt` | Malicious package names from the campaign and from related malware | `INFECTED` | | `filenames.txt` | Paths that are indicators on their own (extended regex) | `INFECTED` | | `filenames-review.txt` | Paths worth a look that legitimate projects can also have (extended regex) | `review` only (ADR-0048) | | `ssh-keys.txt` | SSH keys that related malware adds to `~/.ssh/authorized_keys`: a key's comment or the key itself | `INFECTED`, removed by hand (ADR-0049) | | `weak.txt` | Strings legitimate projects also use | `review` only | | `network.txt` | Hosts and IP addresses of the campaign and of related malware | `INFECTED` if a live connection matches | | `implant-paths.txt` | Install, persistence and working paths of the related `MicrosoftSystem64` implant | `INFECTED` if the path exists | | `implant-names.txt` | Implant binary and process names | `INFECTED` on an exact process-name match | | `hashes.txt` | SHA-256 of known files of the campaign and of related malware: `MicrosoftSystem64` binaries, and small files such as a poisoned `tasks.json` or a fake font | `INFECTED` on a hash match, whatever the file is named. Files of 10 to 300 MB are hashed, and every `tasks.json`, font file and build config at any size (ADR-0045) | "Related malware" is `MicrosoftSystem64` and the npm packages that install it: malware from the same DPRK-linked group, not part of PolinRider itself. [What PolinRider is](https://polinriderscanner.com/threat/#related-malware-microsoftsystem64) explains the link and its sources. Format: one entry per line. Lines starting with `#` and blank lines are ignored. `strong.txt` and `weak.txt` are matched as fixed strings (`grep -F`), so no regex escaping is needed. `bad-packages.txt` holds names, compared whole: a `package.json`, `package-lock.json`, `yarn.lock` or `pnpm-lock.yaml` is read for the packages it installs, and anywhere else a name counts only where it stands on its own, so `sass-format` is not found inside `sass-formatter` and a description is not a dependency. A manifest that cannot be read is matched as text and reported for review. The CI scanner, `ci/scan-workspace.sh`, does the same. `strong.txt` and `bad-packages.txt` are also matched against a copy of each file with its `\uXXXX`, `\u{...}` and `\xNN` escapes decoded, because one loader family is written entirely in them. Add an indicator as it reads once decoded, never in escapes. `filenames.txt` is extended regex matched against the full path. ## The classification rule Put an indicator in `strong.txt` **only if a match means infection with no plausible alternative.** If a legitimate project could contain the same string, it belongs in `weak.txt`. A false positive in `strong.txt` costs every user of this repository a panic. A miss in `weak.txt` costs one line of review output. When the two are close, choose `weak.txt`. ## Keeping it current The campaign rotates its signatures, so an indicator set that matched in September will not necessarily match in December. The set is reviewed weekly against the published sources, and **every review is recorded, including the weeks where nothing changed**, in [the indicator reviews](https://polinriderscanner.com/news/indicator-reviews/). A scanner that has quietly stopped matching is worse than no scanner, because it returns a clean result somebody believes. The review log is how you check that has not happened. ## Self-matching Any file that *detects* PolinRider contains PolinRider strings by definition. Your own scanners, this repository, and CI workflows built from it will be flagged by a grep-based scan. That is expected, and the triage filter separates those matches by path. --- # How to get rid of PolinRider: a developer's guide Source: https://polinriderscanner.com/guides/developers-guide-getting-rid-of-polinrider/ Published 2026-11-05. PolinRider spreads from the computer it infects. A script there uses the Git credentials already stored on that machine to amend your last commit with its payload, set the clock back so the date still matches and force-push. Nobody logs in from somewhere else. That is why cleaning a repository on GitHub while the computer is still infected does not last: the next push puts it back. So the order matters more than anything else in this guide. Check and clean every computer that pushes to the affected repositories first, then change every password and token from a computer you trust, then clean GitHub, then scan every future push. What follows takes you through that from the first suspicion to the last check, with polinrider doing the scanning. It draws on cleaning up one account of our own, 57 repositories and 304 branches and tags, and on what researchers have published about the campaign since. ## The signs Most people find it in one of a few ways. A config file such as `postcss.config.mjs` or `eslint.config.mjs` has code after its real end, pushed far to the right by a long run of spaces. A file under `public/fonts/` with a Font Awesome name turns out to hold JavaScript, and a `.vscode/tasks.json` you did not write runs it when the folder opens. A `.gitignore` lists `temp_auto_push.bat`. Windows Defender reports `Trojan:NPM/PolinRider.SF` and removes it, and it comes back on the next `npm run dev`. A process called `MicrosoftSystem64` is running, which is related malware from the same DPRK-linked group. Or GitHub shows a branch "updated" hours ago that nobody on the team touched, while `git log` looks perfectly normal. [What is PolinRider?](https://polinriderscanner.com/what-is-polinrider/) explains each of these, and why the history looks clean when it is not. ## The first hour: what not to do Stop pushing from the computer you suspect. Do not open the affected project in VS Code again: a hidden task runs as soon as you trust the folder. If VS Code asks whether to trust it, say no. Turn off automatic tasks for every folder, not just this one. Do not `git pull` into a clone you already have, because a pull into an infected clone can put the payload back on the remote. Do not start cleaning GitHub yet, and do not change your passwords yet either. Both get undone while the computer is still infected: a process that is still running takes the new credentials on its next run. There are three things worth doing now, from a different computer you trust. On every repository the account can reach, turn on branch protection that blocks force-pushes and requires signed commits, and pause any automatic deploys. Revoke any OAuth App or GitHub App you do not recognise. And warn the people who push to the same repositories, because their computers may be infected too. ## Install polinrider and check it is genuine polinrider is one program. On macOS or Linux, `brew install meSingh/tap/polinrider-scanner`; on Windows, add the bucket and `scoop install polinrider-scanner`. Or download the archive for your system from the latest release and keep the `ioc/` folder beside the program. Check it before you run it: ```bash sha256sum -c SHA256SUMS --ignore-missing gh attestation verify polinrider-v3.0.0-macos-arm64.tar.gz --repo meSingh/polinrider-scanner ``` [Verifying polinrider](https://polinriderscanner.com/docs/reference/verifying/) has the details, including releases built before the rename. On macOS the program is not notarised, so Gatekeeper stops the first run until you clear the quarantine flag on the copy you have verified. ## Scan your computer Run it with no arguments: ```bash polinrider ``` It asks "What should I check?" and you answer in words. Type `computer`. It reads your home folder, what starts by itself, shell startup files, git and npm settings, npm's own program files, Python startup files, running programs and open connections. Nothing changes during the scan. Without the questions, `polinrider check ~/code` checks the computer and that folder and changes nothing. You get one of three answers. `VERDICT: COMPROMISED` means it found something only the malware leaves. A box under it says either "This machine cannot be trusted until it is rebuilt.", when something proves the payload ran there, or "The payload is in your project files. See below.", when everything it found is inside your projects. `VERDICT: no confirmed indicator.` comes with `[review]` lines: a file, a process or a setting that a normal project can also have. Read each one and ask whether you put it there. If one is not yours, treat it as confirmed. A line that says a check could not run means that part was not looked at. [False positives you will see](https://polinriderscanner.com/docs/reference/false-positives/) lists the harmless matches. `VERDICT: clean against the current indicator set.` means today's known signs are absent. It is not proof that nothing was ever there. If you had a reason to check, change your credentials anyway. ## Clean your computer If it found something, the next screen says what it can deal with: stripping the payload out of config files and moving files into quarantine. It says "Nothing is deleted." and names the folder where every original is kept. Type `yes` to do it, `details` to see every finding first or `no` to leave everything as it is. A config file is cut in place, so your own uncommitted work in it survives, and the infected original goes to quarantine. Nothing in git is touched. The quarantine folder has a manifest of where each file came from and instructions for putting any of them back. Keep it until the incident is closed. Some things it will not change for you, because they are yours: a crontab line, a shell startup file, a PowerShell profile, a Run entry, a scheduled task and a line in `~/.ssh/authorized_keys`. For each one it prints the command or the edit to make. It also prints the command to stop a process that is already running, because moving a login item into quarantine does not stop what it started. Read every line of `authorized_keys`, not only the one it names: the next build may use a key nobody has published. If npm's own program files were confirmed, remove Node.js and npm, make sure nothing is still running and reinstall before you run `npm` again. OpenSourceMalware found infected computers where npm's `lib/cli.js` had been replaced with a copy about a megabyte in size, so every `npm` and `npx` command started the payload again, even after a reboot. As for reinstalling the whole system: if the tool says the machine cannot be trusted until it is rebuilt, rebuild it. If everything it found is in project files and you know nothing infected was built or opened in an editor, clean up, delete your clones and keep scanning. If you are not sure, rebuild. Quarantine contains the problem. A clean install is what recovers from it, and it is the safer choice on any machine with production or financial access. ## Change every password and token Do this from a computer you trust, once every computer that pushes to the affected repositories is clean. Assume everything that was reachable from that computer is in someone else's hands. Start with what can write to your repositories: personal access tokens, SSH keys and signing keys, deploy keys on every repository, OAuth and GitHub App grants, Actions secrets and self-hosted runner tokens. Then publish rights: npm tokens with a 2FA reset, then Packagist, PyPI, Docker Hub or Chrome Web Store credentials. Then everything else: cloud keys, every value in every `.env`, database passwords, tokens for connected platforms such as Vercel or Netlify, browser-saved passwords and your password manager vault if it was unlocked on that computer. Clear the cached Git credentials too, which survive a password change. If a wallet or seed phrase was on the computer, move the funds first. ## Scan GitHub Run `polinrider` again and answer `account` for your own repositories or `organization` for one you belong to. It needs `gh`, signed in, and checks that first. Before it changes anything, it copies every repository into an evidence folder outside any working tree. Those copies hold live malware, so do not open them in an editor. Then it checks every branch and every tag and reads GitHub's push record for each repository: who pushed what and when, by GitHub's clock. The rewriting script can fake a commit's date and author, but not that record. Type `details` on the summary to see every branch and file. Expect familiar names in the push record. The malware pushes as whoever is logged in, so a colleague's name does not clear anything. It means their computer needs scanning too. Run this soon. GitHub serves the commit a branch was moved off only until it cleans up unreferenced data, and after that there is nothing to go back to. ## Fix each repository For each infected repository it offers what can actually fix it, and you type one word. - `restore` puts each branch back where GitHub's push record says it was before the attack. Nothing is edited and history is not rewritten. It is offered only where that record shows a clean state, and only after the old commit has been fetched and checked, because the commit before an attack is often the previous attack. Two of ten restore points on our own account were. Pick it whenever it is offered. - `remove` adds one commit per branch that takes the payload out. It is easy to undo, and the payload stays in older commits. - `erase` takes the payload out of every commit and pushes the history back. Every commit ID changes and every existing clone has to be reset; it shows you how. - `archive` marks a repository nobody uses as infected and makes it read-only. Nothing is removed. - `skip` leaves it for now. Where there is no clean state to return to, because the payload was committed normally, choose `remove` to keep history as it is or `erase` to have the payload in no commit at all. Every fix shows a dry run of exactly what would change, and pushes only when you type `yes`. Each push is refused if someone pushed after the check, and afterwards it asks GitHub what it now shows. Choosing `all` applies one fix everywhere it can, and goes ahead only when you type the owner's name. When you are done, delete every local clone and clone again. Once every restore has worked, `polinrider --purge-evidence` deletes the copies it made, after asking. ## Tell the people affected Anyone who pushes to these repositories needs to scan their own computer. Without that, a collaborator who is still infected can put the payload back after you have finished. Anyone who forked or cloned a repository while it was infected should check what they have. If you publish packages, look through your release history for versions you did not make, publish fixed versions and use GitHub's security advisories to reach people who installed a bad one. A Go module is a special case: the Go module proxy keeps every version it has fetched, so a tagged infected commit stays downloadable whatever you do to the repository. ## Keep it out Copy the CI scanner into each repository, so a reinfection fails the next push instead of waiting to be noticed: ```bash ./ci/install-workflow.sh /path/to/your/repo ``` It vendors the scanner and the indicator set into the repository and runs on every push and pull request, with no third-party action to trust. It commits nothing; review it, then commit. A copy vendored from 2.1.0 or earlier needs re-running with `--force`. [Block PolinRider in CI](https://polinriderscanner.com/docs/guides/ci/) has the rest. Keep force-pushes blocked and signed commits required, and leave VS Code's automatic tasks off. Consider `npm config set ignore-scripts true` too, with exceptions where a build needs them. Then scan again weekly for a month and monthly after that, because the campaign keeps changing its signatures. ## If you cannot run the tool These are the checks that catch the most, by hand. Do them from a terminal, without opening anything in an editor. Read the first four bytes of every `.woff` and `.woff2` file: a real one starts with `wOF2` or `wOFF`. Search the whole tree for `fa-solid-*` and `.llf` files, since the name keeps changing. Read every `.vscode/tasks.json` as text and look for a task that runs `node` on a font file or runs on folder open. Open each JavaScript config file and scroll to the end of every line. Look for `temp_auto_push.bat` and `config.bat` anywhere on disk. Check what starts by itself: Run keys, both Startup folders and scheduled tasks on Windows, including anything named `MicrosoftCLROptimization` or `MicrosoftSystem64`; LaunchAgents, LaunchDaemons and the crontab on macOS; systemd user units, autostart entries and the crontab on Linux. Compare npm's `lib/cli.js` with a fresh install, read `~/.ssh/authorized_keys` line by line and look for `node -e` or `python -c` processes you did not start. If macOS keeps asking for Keychain access to your GitHub credentials after you deny it, something is looping in the background. On GitHub, look at each repository's activity for force-pushes you did not make, and run `git reflog` in your clones for amended commits you do not remember. The [cleanup checklist](https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/) has every step of this guide on one page. ## Sources - OpenSourceMalware, [Hundreds of GitHub Repos Compromised By DPRK's PolinRider Campaign][osm-march], March 2026 - OpenSourceMalware, [A Developer's Guide to Getting Rid of PolinRider][osm-guide], August 2026, for the replaced `cli.js` and the Keychain prompts - OpenSourceMalware, [PolinRider is A/B Testing its Way Past Your Detections][osm-ab], September 2026 - OpenSourceMalware, [How to Investigate a GitHub Repo's Real History][osm-history], October 2026 - SafeDep, [Inside MicrosoftSystem64][safedep], May 2026 - polinrider-scanner: the [README][readme] and [ADR-0010][adr-0010], on checking restore points before offering them [osm-march]: https://opensourcemalware.com/blog/polinrider-dprk-compromised-hundreds-of-github-repos [osm-guide]: https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider [osm-ab]: https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections [osm-history]: https://opensourcemalware.com/blog/how-to-investigate-a-github-repos-real-history [safedep]: https://safedep.io/microsoftsystem64-binary-payload-analysis/ [readme]: https://github.com/meSingh/polinrider-scanner#readme [adr-0010]: https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0010-restore-targets-are-read-before-they-are-offered.md --- # PolinRider Scanner 3.0: polinrider-cleaner has a new name and a new home Source: https://polinriderscanner.com/announcements/polinrider-scanner-3-0/ Published 2026-11-04. polinrider-cleaner is now PolinRider Scanner and Cleaner. The program itself has not changed: it is still `polinrider`, with the same options, the same output and the same exit codes. What changes is where it lives, what the packages are called and how you check that a download is genuine. That is why this release is 3.0.0 rather than 2.4. ## Where everything is now Bookmark these, and check any link you are sent against them. | What | Address | | --- | --- | | Website | [polinriderscanner.com](https://polinriderscanner.com/) | | Source code | [github.com/meSingh/polinrider-scanner](https://github.com/meSingh/polinrider-scanner) | | Downloads | [github.com/meSingh/polinrider-scanner/releases](https://github.com/meSingh/polinrider-scanner/releases) | | Homebrew tap | [github.com/meSingh/homebrew-tap](https://github.com/meSingh/homebrew-tap) | | Scoop bucket | [github.com/meSingh/scoop-bucket](https://github.com/meSingh/scoop-bucket) | The old addresses still work. GitHub redirects `github.com/meSingh/polinrider-cleaner` to the new repository, for the web, for git and for the API, and links to the site's old GitHub Pages address send you here. ## Upgrade from 2.x Pick the way you installed it. ### Homebrew (macOS and Linux) `brew upgrade` moves an existing `polinrider-cleaner` install to the new name by itself: ```bash brew update brew upgrade ``` ### Scoop (Windows) Scoop does not move you across. Remove the old package and install the new one: ```powershell scoop uninstall polinrider-cleaner scoop update scoop install polinrider-scanner ``` The old name stays at 2.3.0 and `scoop status` reports it as deprecated. ### Debian and Ubuntu Download the new `.deb`, check it and install it. Installing it removes the old `polinrider-cleaner` package. On ARM, use `linux-arm64` in place of `linux-x86_64`. ```bash gh release download v3.0.0 --repo meSingh/polinrider-scanner --pattern 'polinrider-v3.0.0-linux-x86_64.deb' gh attestation verify polinrider-v3.0.0-linux-x86_64.deb --repo meSingh/polinrider-scanner sudo apt install ./polinrider-v3.0.0-linux-x86_64.deb ``` ### A downloaded archive Download the archive for your system from the [releases page](https://github.com/meSingh/polinrider-scanner/releases), check it as shown below and replace your old folder with the new one. Keep the `ioc/` folder beside the binary. ### winget A winget package is being prepared as `meSingh.PolinRiderScanner`. It is not in the winget repository yet. ### The CI scanner in your repositories Nothing to do. The `ioc/` folder has the same layout, and the scan you vendored never named the repository. ### Check the upgrade ```bash polinrider --version ``` The version it shows should be 3.0.0. ## New to it? Install 3.0 ```bash brew install meSingh/tap/polinrider-scanner ``` ```powershell scoop bucket add mesingh https://github.com/meSingh/scoop-bucket scoop install polinrider-scanner ``` Every other way to install it is in the [README](https://github.com/meSingh/polinrider-scanner#run-it). ## Check that a download is genuine Every release from 3.0.0 on is checked against the new name: ```bash gh attestation verify polinrider-v3.0.0-macos-arm64.tar.gz --repo meSingh/polinrider-scanner ``` Releases up to 2.3.0 were attested while the repository was still called `polinrider-cleaner`, and their attestations record that name. The new name fails for them, with an error that looks like a fake file but is not one. Use the old name, which GitHub still follows: ```bash gh attestation verify polinrider-v2.3.0-macos-arm64.tar.gz --repo meSingh/polinrider-cleaner ``` Or use one command that works for a release of any version, and also proves the file was built by this repository's release workflow: ```bash gh attestation verify polinrider-v2.3.0-macos-arm64.tar.gz \ --owner meSingh \ --cert-identity-regex '^https://github\.com/meSingh/polinrider-(cleaner|scanner)/\.github/workflows/release\.yml@refs/tags/v' ``` We tested every form on the day of the rename. [Verifying releases from before 3.0](https://polinriderscanner.com/docs/reference/verifying-before-3/) has the rest, including how to confirm the attestation came from this repository and not a later holder of the old name. ## What else is new The documentation site has been rebuilt. Updates, where this post lives, carries news on the campaign and our own guides and analysis. It also points to articles by other researchers that are worth your time. Every weekly indicator review is still public. A new page, [What is PolinRider?](https://polinriderscanner.com/what-is-polinrider/), explains the malware from the start for a developer who has never heard of it: how it gets in, what it takes, why your Git history looks clean and the signs to look for. The docs are now kept for each release line, so someone running an older version can still read instructions for the program they actually have. There is also a cleanup checklist and a longer guide to getting rid of PolinRider, written from what we learned cleaning up our own account and from what researchers have published since OpenSourceMalware's August guide. The repository itself now keeps a daily record of release downloads on its public `traffic` branch. GitHub keeps only a running total per file, so until now there was no way to tell whether anyone came after a release or a post. Our own test downloads are recorded and taken off, so the weekly figure leaves our own out. ## What has not changed 3.0.0 adds no new detection. It finds exactly what 2.3.0 found, and a clean result still means that today's known signs are absent, not that nothing was ever there. The indicators are still checked against published reports every week. The other limits are the same too. The macOS binaries are not notarised, so Gatekeeper stops the first run until you clear the quarantine flag on a binary you have verified. Restoring a branch on GitHub still depends on GitHub serving the commit from before the attack, which it does only until it clears out unreferenced data, so check early. And it is still one person's open source project, with no warranty and no support commitment. If you are here because something is wrong on your machine, start with the [cleanup checklist](https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/). ## Sources - [Release notes for 3.0.0](https://polinriderscanner.com/releases/v3.0.0/) and the [README](https://github.com/meSingh/polinrider-scanner#run-it) - [ADR-0057][adr-0057], the rename, with the verification tests - [ADR-0056][adr-0056], docs kept per release line - [ADR-0053][adr-0053], the download history [adr-0057]: https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0057-the-repository-is-polinrider-scanner-from-3-0-0.md [adr-0056]: https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0056-the-docs-are-versioned-by-release-line.md [adr-0053]: https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0053-release-downloads-are-counted-daily-with-our-own-kept-apart.md --- # MicrosoftSystem64: what it is and how to remove it Source: https://polinriderscanner.com/analysis/microsoftsystem64/ Published 2026-11-03. A program, process or folder called `MicrosoftSystem64` has nothing to do with Windows or any Microsoft product. It is a remote-access trojan and information stealer that malicious npm packages install, and it runs on Windows, macOS and Linux. [SafeDep][safedep], which took the program apart in May, says the name is chosen to look like a system service in a process list, and that a computer running it should be treated as fully compromised. In October Island found it being pulled into working open source projects as well, so you do not need to have picked a suspicious package to end up with it. ## What it does It is a Node.js single executable: the runtime and the malicious JavaScript packed into one native file, so it runs on computers without Node.js installed. The Linux build SafeDep analysed is 81 MB, and JFrog found the Windows, Mac and Linux builds all carry the same JavaScript. Its job is to collect. SafeDep lists credentials from 15 browser families, data from more than 80 cryptocurrency wallet extensions, Telegram Desktop sessions and SSH keys. It also logs keystrokes, watches the clipboard, takes a screenshot every 60 seconds, runs commands it is sent and updates itself. What it takes goes to private datasets on Hugging Face, so the traffic looks like ordinary HTTPS to a well-known site. ## How it gets onto a computer Through npm, at install time. The first packages SafeDep and JFrog described were fake logging libraries. `js-logger-pack` appeared in April 2026, and after npm took it down on 22 April the same dropper came back as `terminal-logger-utils` and several packages around it. In `js-logger-pack` a `postinstall` script downloads the build for your system and leaves it running in the background, so `npm install` finishes as normal. Later packages copied real libraries. The OpenSSF malicious-packages database records `void-ulid`, a copy of the legitimate `ulid` package down to its repository link, which installs the agent as a JavaScript file rather than the packed program. A later one, `pino-ulid`, sets up the same kind of agent under another name. [Island][island] has since followed this cluster to at least 88 public GitHub repositories, many of them presented as AI and agent tools. They contained real, working code and pulled the dropper in through a dependency such as `ioredis-xyz`, later `oscar-redis` or `oracle-redis`. Their histories were copied or generated and forks arrived almost exactly every three minutes. One of them came up unprompted in both ChatGPT and Google's AI Overview. A project you cloned because an assistant recommended it can bring the trojan in the first time you run `npm install`. ## Where it hides On Windows the program sits in your local application data folder as `MicrosoftSystem64.exe`, started by a scheduled task and a small script of the same name, with a registry Run value as a fallback. On macOS it lives under Application Support and starts from a LaunchAgent with `com.launchkeeper` in its name. On Linux it sits under `~/.local/share` and starts from a systemd user service or an autostart entry. On every system it keeps its upload queue in folders called `.pcl-data` and `.pcl-state` and leaves a `.registered` file, as JFrog describes. One build also leaves a way back in. According to the OpenSSF record for the `eo-terminal` package, it appends the attacker's own SSH key, with the comment `bink@DESKTOP-N8JGD6T`, to `~/.ssh/authorized_keys`. Removing the program does not remove that line, and until you do, the computer can still be entered over SSH. ## Is it PolinRider? Not by name, as far as the published reports go. None of the reports on `MicrosoftSystem64` mentions PolinRider, and none of the PolinRider reports we have read mentions `MicrosoftSystem64`. They are related through the group behind them. SafeDep places these packages in a cluster that kmsec.uk and OX Security attribute to Famous Chollima, also tracked as Contagious Interview, a DPRK-linked group. OpenSourceMalware describes PolinRider as a parallel or sub-campaign of Contagious Interview. We check for both because they target the same developers and need the same response. A different Microsoft-looking name, `MicrosoftCLROptimization`, has been seen starting malware at logon on Windows. It is not this program; we wrote about it in [why PolinRider came back after Defender removed it](https://polinriderscanner.com/guides/polinrider-keeps-coming-back/). ## What to do Assume everything on the computer has been taken. Stop it from starting again first: remove the scheduled task and Run value, the LaunchAgent or the systemd unit and autostart entry, then the program and its folders. Read `~/.ssh/authorized_keys` and remove any line you did not add. Then remove the package: delete `node_modules` and the lockfile, clear the npm cache and take the package out of `package.json` before you install anything again. From a clean computer, change everything: browser-saved passwords, SSH keys, API and cloud keys, npm tokens and database passwords. If a wallet was on the computer, its seed phrase is exposed, which means moving the funds to a new wallet. Consider reinstalling the operating system too: removing files contains the problem, and only a clean install recovers from it. polinrider looks for the program by its names, its install and startup paths and its published file hashes, so a renamed copy is still caught. It reads the process table too, and since 2.3.0 it also catches the JavaScript version running under `node` and names the attacker's key in `authorized_keys` without editing the file. The [cleanup checklist](https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/) covers the rest of the cleanup in order. ## Sources - SafeDep, [Inside MicrosoftSystem64: A Supply Chain RAT Exfiltrating to HuggingFace][safedep], 28 May 2026 - JFrog Security Research, [js-logger-pack Operator Turns Hugging Face into a Malware CDN and Exfiltration Backend][jfrog], 23 April 2026 - Island, [How Attackers Built Software an AI Would Recommend][island], 13 October 2026 - OpenSSF malicious-packages: [MAL-2026-4345][mal-4345] (`eo-terminal`), [MAL-2026-5341][mal-5341] (`void-ulid`) and [MAL-2026-16154][mal-16154] (`pino-ulid`) - OpenSourceMalware, [PolinRider Blast Radius Grows][osm-blast], 15 July 2026 - Our indicator reviews of [12 October](https://polinriderscanner.com/indicator-reviews/2026-10-12/) and [2 November](https://polinriderscanner.com/indicator-reviews/2026-11-02/) [safedep]: https://safedep.io/microsoftsystem64-binary-payload-analysis/ [jfrog]: https://research.jfrog.com/post/hugging-face-exfil/ [island]: https://www.island.io/blog/how-attackers-built-software-an-ai-would-recommend [mal-4345]: https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/eo-terminal/MAL-2026-4345.json [mal-5341]: https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/void-ulid/MAL-2026-5341.json [mal-16154]: https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/pino-ulid/MAL-2026-16154.json [osm-blast]: https://opensourcemalware.com/blog/polinrider-blast-radius-grows --- # Is it PolinRider or a false alarm? Reading a scan result Source: https://polinriderscanner.com/guides/polinrider-or-false-alarm/ Published 2026-11-03. Since 2.3.0, polinrider reads every text file up to 1 MB in a folder or on a computer for strings that a normal project can also contain, and for file names that a legitimate tool might also write. So a project that 2.2.0 called clean can now come back with a few `[review]` lines and exit code 1. That is not a verdict of infection. It is the tool asking you a question. This is how to read each of its answers, and which of the usual matches are harmless. ## The three answers `VERDICT: COMPROMISED`, with `[HIT]` lines, means it found at least one thing that only the malware leaves. Under the verdict, a box says one of two things. "This machine cannot be trusted until it is rebuilt" means something proves the payload ran on this computer: the implant, a login item, a git hook, an editor extension, a startup file, a running process or a live connection. "The payload is in your project files" means everything it found sits inside your projects. The payload runs when an infected project is built or opened in an editor, so if that happened, or you are not sure, treat the computer as compromised anyway. Below the box, a list called WHAT TO DO NEXT gives a step for each finding with the exact command. Follow it in order, and check the computer before you touch GitHub. `VERDICT: no confirmed indicator.`, with `[review]` lines, means nothing is confirmed but a person has to decide about each line. The tool prints three steps in its own words. Read each line and ask whether you put that file, process or setting there. Treat a line that says a check could not run, or was skipped, as a part that was not looked at, and fix whatever stopped it. If any line turns out not to be yours, treat it as confirmed: disconnect from the network and change your credentials from a different machine. `VERDICT: clean against the current indicator set.` means none of today's indicators is on what was checked. It is not proof that nothing was ever there, because the campaign changes its signatures. If you had a reason to check in the first place, change your credentials anyway. A fourth outcome, exit code 3, means the check could not run, for example because a path does not exist or `gh` is not signed in. It says nothing either way. ## How it decides what to confirm There is one rule. A match may confirm an infection only if it has no plausible innocent explanation. Anything a legitimate project could also contain stays at review, and when the two are close, it goes to review. A false confirmation costs every user a panic, while keeping a real indicator at review costs one line of output. So take a `[HIT]` at its word and read a `[review]`. ## Matches that are usually harmless A `.vscode/tasks.json` that runs on folder open is the most common review line. Teams use folder-open tasks for real work, so the tool asks you to check the command by hand. It is real if the command runs `node` on a font file, or downloads something and runs it. If the task carries a known indicator, you will see a `[HIT]` instead. `folderOpen` or `allowAutomaticTasks` in another file, such as `.vscode/settings.json`, is harmless on its own, because only `tasks.json` runs commands. A long build config can match too: a tail after `export default` or a line longer than 4,000 characters. Some configs legitimately run long, and a flat ESLint config opens with `export default [`. It is real if there is code you did not write after the end of the config, often after a long run of spaces. `public/fonts/fa-solid-900.woff2` is a genuine Font Awesome file name, so the name alone proves nothing. OpenSourceMalware has seen a fake use it, and the tool settles it by reading the first bytes: a fake font is a `[HIT]` whatever it is called. `spellright.dict` belongs to a real VS Code extension, and `branch_structure.json` or `clr_init.vbs` are names a legitimate tool could write. They matter if nobody on the team uses the tool that writes them, or if they sit beside other campaign files. Two things the tool never reports are a Git LFS pointer standing in for a font and an empty font file. Neither has font bytes, and neither is malware. ## Two that are not false alarms `temp_auto_push.bat` listed in a committed `.gitignore` can look harmless, since the repository itself may hold no payload. It means a contributor's computer is infected; we explain why in [Found temp_auto_push.bat?](https://polinriderscanner.com/analysis/temp-auto-push-bat-polinrider/) An antivirus detection named for PolinRider, such as Windows Defender's `Trojan:NPM/PolinRider.SF`, means something ran on that computer. The other direction does not hold. A clean result from another scanner is a first pass, which OpenSourceMalware says plainly of its own scanner, built against the campaign's original signature. ## If it got it wrong Both directions are worth reporting. A harmless file reported as `[HIT]` or a review line that should never fire goes in a [false-positive issue](https://github.com/meSingh/polinrider-scanner/issues/new?template=false-positive.md). Something real that was missed goes in a [missed-detection issue](https://github.com/meSingh/polinrider-scanner/issues/new?template=missed-detection.md), with the source you saw it in. If the result is real, the [cleanup checklist](https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/) has every step. ## Sources - polinrider-scanner: [`src/scan.rs`][scan-rs] for the verdicts and next steps, [`src/checks.rs`][checks-rs] for the tasks.json, config and font checks, [ADR-0034][adr-0034], [ADR-0048][adr-0048] and [`ioc/weak.txt`][weak] - Our [release notes for 2.3.0](https://polinriderscanner.com/releases/v2.3.0/) and [false positives reference](https://polinriderscanner.com/docs/reference/false-positives/) - OpenSourceMalware, [PolinRider is A/B Testing its Way Past Your Detections][osm-ab], 26 September 2026 - OpenSourceMalware, [A Developer's Guide to Getting Rid of PolinRider][osm-guide], August 2026 [scan-rs]: https://github.com/meSingh/polinrider-scanner/blob/main/src/scan.rs [checks-rs]: https://github.com/meSingh/polinrider-scanner/blob/main/src/checks.rs [adr-0034]: https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0034-the-verdict-is-followed-by-what-to-do-and-rebuild-is-said-only-on-proof.md [adr-0048]: https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0048-a-review-level-filename-list.md [weak]: https://github.com/meSingh/polinrider-scanner/blob/main/ioc/weak.txt [osm-ab]: https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections [osm-guide]: https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider --- # Why PolinRider keeps coming back after you remove it Source: https://polinriderscanner.com/guides/polinrider-keeps-coming-back/ Published 2026-10-31. On 30 October a developer asked for help on the [BleepingComputer forums][bc]. Windows Defender kept detecting `Trojan:NPM/PolinRider.SF` and removing it, and it came back the moment they ran `npm run dev` or `node server` in their project. A volunteer read their logs and found two logon entries they had missed. If you are stuck in the same loop, something that starts the malware is still on your computer or in your project, and cleaning harder in the places you have already looked will not find it. ## What the logs showed The developer had done more than most people would. They ran a full offline scan, removed a suspicious Defender exclusion for their project folder, checked the scheduled tasks and the registry Run keys, ran `npm audit` and confirmed VS Code's automatic tasks were off. They attached logs from the Farbar Recovery Scan Tool. Defender's section lists seven detections, and none of them is a file. Each one is a command line: Node.js started with a long block of JavaScript passed to it directly. Defender was catching the payload as it ran, not a file it could remove. The same log lists a Run value named `MicrosoftCLROptimization`, which starts a script called `clr_init.vbs` with Windows Script Host, plus a file of the same name in the user's Startup folder. So the Run keys the developer had checked did hold an entry. Its name is borrowed from a real Microsoft .NET component, which makes it easy to read past. Windows runs both each time the user logs on. The helper quoted the two entries back and called them the source of the repeated infection. At the time of writing the thread has no reply after that, so we do not know whether removing them fixed it. The name is not new. In September [Deception.Pro][dp] described a PolinRider operator working by hand on a decoy workstation, between 27 August and 3 September. The operator planted a scheduled task, a Run key and a Startup script, all called `MicrosoftCLROptimization`, under the SYSTEM profile so they survive a user logging off. ## Four places it comes back from The first is the project itself. The payload is appended to a config file such as `postcss.config.js` or `eslint.config.mjs`, so it runs whenever that file loads: the next dev server, the next build, the next CI run. As OpenSourceMalware puts it in its [cleanup guide][osm-guide], killing the process buys you a clean moment, not a clean project. That fits the forum case exactly. Until the config file is cleaned, no detection can last. The second is something on the computer that starts it. Logon entries like the ones above are one kind. Another, from the same guide, is npm itself: on several infected computers npm's own `lib/cli.js` had been replaced with a copy of about a megabyte, so every `npm` or `npx` command starts the malware again, even after a reboot. The fix there is to remove Node.js, make sure nothing is still running and reinstall. A detached process left over from the first infection will also keep pushing, with whatever credentials are valid on the computer, including ones you have just changed. The third is GitHub. Each time the payload runs, it looks for other JavaScript projects on the computer, injects itself into them and pushes them with the stored Git credentials, including repositories you have not opened in months. A common way back in is to clean the computer and then clone the infected branches straight back onto it. The fourth is other people. OpenSourceMalware traced one small agency's organization through three cleanups in 2026. Each time, a force-push from a different contributor's account put the payload back, the fastest about five hours later, under a commit message the team already trusted. A collaborator who is still infected can reinfect you after you have finished. ## What antivirus can and cannot tell you In most variants the payload is decrypted from a blockchain transaction into memory and never written to disk as a program, so scanning again and again does not find much. Defender does flag some variants by name: OpenSourceMalware lists `Trojan:JS/PolinRider.DB!MTB`, and the forum case shows `Trojan:NPM/PolinRider.SF`. A detection tells you something ran. A clean scan does not tell you that you are clear. The campaign also changes its signatures, so not finding last month's marker does not mean you are clean. ## What stops it Do it in order. Turn off VS Code's automatic tasks for every folder and tell your collaborators first. Then check every computer that pushes to the affected repositories, including Run keys, Startup folders and scheduled tasks on Windows, and launch agents and the crontab on macOS. Change your credentials only once those computers are clean, because a process that is still running simply takes the new ones. Then clean the repositories on GitHub, using its record of who pushed what rather than commit dates, which the malware forges. polinrider reads the Windows Run and RunOnce values, both Startup folders, the scheduled tasks and npm's own program files, and it knows the `MicrosoftCLROptimization` script by its file name. Look through your Run values and scheduled tasks for that name yourself as well. Our [cleanup checklist](https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/) has every step on one page. ## Sources - BleepingComputer forums, [PolinRider/NPM infection keeps returning, FRST logs attached][bc], 30 October 2026 - Deception.Pro, [Hands-on-Keyboard Activity from the DPRK "PolinRider" Supply Chain Attack][dp], 9 September 2026 - OpenSourceMalware, [A Developer's Guide to Getting Rid of PolinRider][osm-guide], August 2026 - OpenSourceMalware, [PolinRider is A/B Testing its Way Past Your Detections][osm-ab], 26 September 2026 [bc]: https://www.bleepingcomputer.com/forums/t/819751/polinridernpm-infection-keeps-returning-frst-logs-attached/ [dp]: https://blog.deception.pro/blog/hok-dprk-polinrider-sep-2026 [osm-guide]: https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider [osm-ab]: https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections --- # PolinRider's fake woff2 font that is really JavaScript Source: https://polinriderscanner.com/analysis/fake-woff2-font-javascript/ Published 2026-10-23. One of PolinRider's ways in is a file such as `public/fonts/fa-solid-400.woff2` that is not a font at all. It holds JavaScript, and a VS Code task runs it with Node.js the moment someone opens the folder and trusts it. This year the file has turned up under at least four names, so searching for one name will miss it. The first four bytes of a real font never change, though, and you can check them without opening the project. If you find a fake, treat every computer that opened the project as infected. ## How the trick works It takes two files. The "font" sits where web fonts usually live, under `public/fonts/`, named like a Font Awesome file. Next to it, `.vscode/tasks.json` holds a task, usually labelled `eslint-check`, set to run when the folder opens with its output hidden. The task's command runs the font with Node.js, and Node does not care what a file is called. It works on people as well as editors. A fonts folder looks normal in a web project, and a font reads as a binary file, which most scanners and most reviewers skip. [OpenSourceMalware][osm-jan] first described the trick in January 2026, in fake job-interview repositories handed to developers as coding tests. In April the same researchers found the PolinRider loader inside `public/fonts/fa-solid-400.woff2` in a repository with no `tasks.json` at all, and concluded that the fake font was now part of PolinRider's standard kit. It has since turned up in packages too. As OpenSourceMalware reports it, JFrog found two npm packages with a task that ran a file of that name, and Nextron found 16 Go modules carrying the same file. Neither of those reports named PolinRider; linking them to the campaign is OpenSourceMalware's own reading. ## The name keeps changing In September OpenSourceMalware followed the fake through three repositories of one organization across 2026. It started as `fa-solid-400.woff2` in April, became `fa-solid-500.woff2` in early September and `fa-solid-900.woff2` on 25 September, and three days later appeared as `fa-solid-300.llf`. The variants did not neatly replace each other: at one point the 500 and 900 versions were running side by side. Three details make a name search even less reliable. `fa-solid-900.woff2` is a genuine Font Awesome file name, and a real one sat in the same folder as the earlier fakes, so the name alone proves nothing either way. `.llf` is not a font extension, so a check that only reads `.woff2` files misses it. And on 25 September the fake moved several levels deep into existing folders, under a different path in each repository. The contents vary as well. Many builds start with a long run of whitespace, 421 tab characters in one case, so a quick look at the file shows nothing. ## Copied by another group In October OpenSourceMalware found the GhostAction campaign using a copy of the same launcher to run a credential stealer. It keeps the `eslint-check` label, the folder-open trigger and a Font Awesome name under `public/fonts/`, but the "font" is 38 lines of plain Node.js. The researchers found no shared indicators with any DPRK malware they had seen and, for now, call it a copycat. The same check finds both, and both need the same response. ## Telling a real font from a fake Do this from a terminal, without opening the project in an editor. The WOFF 2.0 specification requires every font file to begin with the signature `wOF2`, and WOFF 1.0 with `wOFF`: ```bash head -c 4 public/fonts/fa-solid-400.woff2; echo ``` Anything else in a `.woff2` or `.woff` file means it is not that font. Running `file public/fonts/*` helps too: on the January sample it reported JavaScript source text with very long lines. Then read `.vscode/tasks.json` as text. A task that runs `node` on a font file, or anything set to run on folder open that you did not write, is the launcher. Finally, search the whole tree for `fa-solid-*` and `.llf` files. Two harmless cases look like fakes. A Git LFS pointer is a short text stub standing in for a font, and some projects ship an empty placeholder. Neither is malware. Never run the file to see what it does: running it is the infection. ## If you find one Do not open the folder in VS Code. If it asks whether to trust the folder, say no, and if it asks about the folder's automatic tasks, choose Disallow. Treat any computer that opened the project as infected and check it before anything else. Then look for the rest. The same repository can also carry a payload appended to a config file such as `eslint.config.mjs`, which runs without VS Code at all, whenever a lint run, a commit hook or a CI job loads it. Remove the fake font, the task and the appended payload together, then clean up in order: computers, credentials, repositories. The [cleanup checklist](https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/) has each step. polinrider reads every `.woff` and `.woff2` file and confirms one whose first bytes are not a font's, skipping LFS pointers and empty files. It also knows published fakes by their hash, confirms `.llf` files by name and confirms a task that runs `node` on one of these fonts. A Font Awesome name it cannot confirm, such as a weight Font Awesome does not ship, is listed for you to review, because a name alone is not proof. If a result is unclear, the [false positives reference](https://polinriderscanner.com/docs/reference/false-positives/) explains the usual harmless matches. ## Sources - OpenSourceMalware, [DPRK Contagious Interview "Fake Font" Abuses VS Code Tasks][osm-jan], 28 January 2026 - OpenSourceMalware, [PolinRider DPRK Attack Expands Across GitHub][osm-april], 12 April 2026 - OpenSourceMalware, [PolinRider Caused Dozens of npm, Go, PHP Compromises][osm-go], 31 July 2026 - OpenSourceMalware, [PolinRider is A/B Testing its Way Past Your Detections][osm-ab], 26 September 2026 - OpenSourceMalware, [GhostAction Copycats PolinRider by Hiding a Stealer in a Fake Font File][osm-ghostaction], October 2026 - W3C, [WOFF File Format 2.0][w3c-woff2] and [WOFF File Format 1.0][w3c-woff] - Our indicator reviews of [28 September](https://polinriderscanner.com/indicator-reviews/2026-09-28/) and [5 October](https://polinriderscanner.com/indicator-reviews/2026-10-05/) [osm-jan]: https://opensourcemalware.com/blog/contagious-code-fake-font [osm-april]: https://opensourcemalware.com/blog/polinrider-rides-again-north-korean-attack-expands-across-github [osm-go]: https://opensourcemalware.com/blog/polinrider-caused-dozens-of-npm-and-go-compromises [osm-ab]: https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections [osm-ghostaction]: https://opensourcemalware.com/blog/ghostaction-copycats-polinrider-by-hiding-a-stealer-in-a-fake-font-file [w3c-woff2]: https://www.w3.org/TR/WOFF2/ [w3c-woff]: https://www.w3.org/TR/WOFF/ --- # Found temp_auto_push.bat in your repository? Source: https://polinriderscanner.com/analysis/temp-auto-push-bat-polinrider/ Published 2026-10-18. `temp_auto_push.bat` is the script PolinRider uses to push its payload into repositories. If you have found it in a repository or listed in a `.gitignore` nobody on your team wrote, a computer that pushes to that repository has very likely been infected. Check that computer before you touch the repository. Cleaning the repository first does not last, because the same computer will push the payload straight back. ## What the script does [OpenSourceMalware][osm-march] published the script in full in its first report on the campaign, in March 2026. It is a Windows batch file, and run inside a Git repository it does something careful. It reads the current branch and the last commit's date, time, message and author. It sets the Windows clock back to that commit's date and time, and sets the repository's Git name and email to that commit's author. It stages every change in the folder and amends the last commit with the same message, skipping any commit hooks. Then it puts the clock right and force-pushes the branch to `origin`, again skipping hooks. By the time it runs, another part of the malware has already appended its loader to a config file such as `postcss.config.mjs`, `tailwind.config.js` or `eslint.config.mjs`. The script folds that change into a commit that already existed and pushes it over the original. ## Why your history still looks clean Because the clock was wound back, Git records the amended commit with the old date. The author, the message and the branch are all unchanged. Reading `git log`, the commit looks as if nobody touched it. Two things do change. The commit gets a new ID, since a commit's hash covers everything in it, but that only helps if you still have a copy from before. The more useful one is GitHub's record of the push itself. As OpenSourceMalware showed in its October guide to investigating a repository's real history, the activity feed lists every push and force-push with the account that made it and GitHub's own time. None of that comes from the commit, so a changed clock or a changed Git name cannot forge it. Our page on [why your Git history looks clean](https://polinriderscanner.com/what-is-polinrider/#why-your-git-history-looks-clean) shows where to find it. ## In the repository, or only in .gitignore You can meet the name in two ways. The first is the file itself, committed. OpenSourceMalware uses the name to find victims: a GitHub code search for it in April found 101 repositories, 22 of which also carried a malicious `.vscode/tasks.json`. The second is quieter. In September OpenSourceMalware reported that infected computers add this block to `.gitignore`: ```text config.bat temp_auto_push.bat temp_interactive_push.bat branch_structure.json ``` These are the push tool's working files on the developer's own computer, and the entries keep them out of `git status` so nobody notices them. The team found the block in every infected organization it examined. Where it appears, at least one contributor's computer is infected, even if no payload has reached that repository yet. One repository traced in October shows how it looks in practice: its history dated a commit April 2025, but a force-push in June 2026 had replaced it with a rewritten copy that added the script to `.gitignore`. ## Not only a Windows problem The script found so far is a batch file, and changing the Windows clock needs administrator rights. OpenSourceMalware has also reported evidence of the same history rewriting on other operating systems, though no tool was left behind there. Not finding the `.bat` file on a Mac or a Linux machine does not clear it. ## How it spreads between people The script runs on the infected computer with the credentials already stored there, and it runs again whenever its trigger fires: a scheduled task, a background process or someone opening an infected project again. A teammate who opens one of the rewritten repositories in VS Code and trusts the folder is infected in turn, and their computer starts doing the same. VS Code's Workspace Trust prompt is the main thing standing in the way. If you publish a Go module or a Composer package straight from the repository, the rewritten commit is the release. OpenSourceMalware has reported compromised Go modules spread this way. ## What to do Do not open the repository in an editor, and do not install or build it, until it has been checked. If VS Code asks whether to trust the folder, say no. Then find the computer. In the repository's activity feed, look for the force-pushes and the account that made each one. The feed records whose credentials pushed, not who was at the keyboard, so treat that account's owner as a probable victim. If the name only appears in `.gitignore`, every contributor's computer is a candidate. Check that computer before anything on GitHub. Look for `temp_auto_push.bat` and `config.bat` anywhere on its disk, then check the rest of it. polinrider reports `temp_auto_push.bat` and `temp_interactive_push.bat` as confirmed wherever it finds them. On GitHub it flags a `.gitignore` that lists them for review instead, because that points at a contributor's computer rather than a payload in the repository. Once the computer is clean, change every credential from a clean machine, then clean the repositories and start signing your commits. The [cleanup checklist](https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/) has each step. ## Sources - OpenSourceMalware, [Hundreds of GitHub Repos Compromised By DPRK's PolinRider Campaign][osm-march], 8 March 2026 - OpenSourceMalware, [PolinRider DPRK Attack Expands Across GitHub][osm-april], 12 April 2026 - OpenSourceMalware, [PolinRider Jumps the Fence to Go, Packagist, npm, PyPI][osm-fence], 8 July 2026 - OpenSourceMalware, [PolinRider Caused Dozens of npm, Go, PHP Compromises][osm-go], 31 July 2026 - OpenSourceMalware, [A Developer's Guide to Getting Rid of PolinRider][osm-guide], August 2026 - OpenSourceMalware, [PolinRider is A/B Testing its Way Past Your Detections][osm-ab], 26 September 2026 - OpenSourceMalware, [How to Investigate a GitHub Repo's Real History][osm-history], 6 October 2026 - OpenSourceMalware, [GhostAction Copycats PolinRider by Hiding a Stealer in a Fake Font File][osm-ghostaction], October 2026 - Our [indicator review of 28 September](https://polinriderscanner.com/indicator-reviews/2026-09-28/), on where these names sit in the scanner [osm-march]: https://opensourcemalware.com/blog/polinrider-dprk-compromised-hundreds-of-github-repos [osm-april]: https://opensourcemalware.com/blog/polinrider-rides-again-north-korean-attack-expands-across-github [osm-fence]: https://opensourcemalware.com/blog/polinrider-jumps-the-fence [osm-go]: https://opensourcemalware.com/blog/polinrider-caused-dozens-of-npm-and-go-compromises [osm-guide]: https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider [osm-ab]: https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections [osm-history]: https://opensourcemalware.com/blog/how-to-investigate-a-github-repos-real-history [osm-ghostaction]: https://opensourcemalware.com/blog/ghostaction-copycats-polinrider-by-hiding-a-stealer-in-a-fake-font-file