NPM SUPPLY CHAIN SECURITY

Know what a package does before it reaches production.

Cohen installs every npm package in an isolated, disposable sandbox and records what it actually does: file access, network connections and spawned processes, each traced to the code responsible.

ANATOMY OF A FINDING

Every step is ordinary. The sequence is not.

polymarket-stake-mathss@3.5.2 is two kilobytes of readable JavaScript with a single postinstall hook. No obfuscation, no packing, nothing a reviewer would catch by opening the tarball. This is what the sandbox recorded while it ran.

polymarket-stake-mathss@3.5.2 · 2 KB · ONE POSTINSTALL HOOK REVIEW
Nothing was taken. The container holds nothing real and is destroyed moments later; everything the package collected exists nowhere else.
Accessed by package
/root/.ssh/id_rsa /root/.aws/credentials /root/.config/gh/hosts.yml /root/.docker/config.json /root/.electrum/wallets/default_wallet
Archives written
ssh.gzaws.gztoken.gz
Executed by package
/usr/bin/tar/usr/bin/gzip
Package connections
216.198.79.1:44364.29.17.1:443

INSTRUMENTATION

Some payloads wait to be called.

So every package runs twice: once as it installs, once as it is imported. Two records, weighed equally.

01 · INSTALL

Scripts left on.

Every lifecycle hook runs, traced. It is where most payloads live.

install_phase

02 · IMPORT

Then loaded.

Loaded the way your code would load it. Nothing to load is still a result.

import_phase

One event is noise.
Two, in order, is a finding.

secret read→archive named after it
chmod +x→execve, same path

ATTRIBUTION

Every action has an author.

npm reading your .npmrc is npm. A postinstall reading your SSH key is not.

PROCESS TREE · SINGLE RUNPID
npm install                                            1
├── npm                                                12
│   ├── openat    /root/.npmrc
│   ├── stat      /usr/lib/node_modules
│   ├── write     ./node_modules/.package-lock.json
│   └── connect   registry.npmjs.org:443
└── postinstall → node                              31
    ├── openat    /root/.ssh/id_rsa
    ├── openat    /root/.aws/credentials
    ├── openat    /root/.electrum/wallets/default_wallet
    ├── execve    /usr/bin/tar
    ├── execve    /usr/bin/gzip
    ├── write     /tmp/.x/ssh.gz
    └── connect   216.198.79.1:443
npm and build tooling: expected, discarded package code: kept, then correlated

An indicator says what was seen.
Attribution says who.

216.198.79.1:443←PID 31postinstall → nodeinstall_phase

DYNAMIC ENGINE

Three questions, every run.

Where did it call? What did it reach for? What did it leave behind? The answers set the verdict.

NETWORK

Where it called.

A hostname can be the payload.

216.198.79.1:443a1b2c3.dnslog.cnwebhook.site

CANARIES

What it reached for.

The secrets are planted. If one leaves, it was sent.

AKIA…SEEDED/root/.ssh/id_rsa/root/.config/gh/hosts.yml

FILESYSTEM

What it left behind.

Written, then run. Neither half matters alone.

…/calc-mapping.bin/tmp/.x/ssh.gz/usr/bin/tar

Silence is recorded too.

tmp_snooped: [ ]→watched, nothing happened
Every field, explained

STATIC ENGINE

Reads what shipped. Never decides.

The tarball is examined without running a line. It adds context to the verdict, never the verdict itself.

THREATS

Scored.

A severity, a tactic and the exact line.

threat-*highexecution

CAPABILITIES

Noted, never scored.

Holding a capability is not an accusation.

capability-*networkprocess spawn

EVERY FILE

Measured.

A hash and an entropy score, file by file.

SHA 256entropy

A rule that cannot show its work is worth nothing.

So every match comes with the file, the line and the source around it. This one is a false positive, shown on purpose. It takes seconds to dismiss.

threat-process-download-exec high execution
Detects download-and-execute patterns: fetching a remote file then executing it
package/packages/cli/dist/index.cjs:43609
console.log( "\nFoundry (forge) was not found on your PATH.\nInstall it with:\n curl -L https://foundry.paradigm.xyz | bash && foundryup\n" );
match curl -L https://foundry.paradigm.xyz | bash

Entropy

Text sits near 0.6. Packed or encrypted data sits near 1. Anything at or above 0.94 is flagged.

FILENORMALISED ENTROPY · 0 → 1
dist/internal/calc-mapping.bin 0.995
dist/index.mjs 0.642
types/index.d.ts 0.552
package.json 0.547
README.md 0.524
Over the size cap means not measured. Missing is not the same as low.

Two engines, one story.

That 0.995 file is a 19 KB binary. The dynamic engine watched it get dropped, made executable and run.

How the static engine works

READING THE FEED

Two verdicts. By design.

A third would start arguments about shades. Two leaves one question: look, or move on.

REVIEW

Worth a human's eyes.

A lead, not a conviction.

CLEAN

Nothing notable observed.

Published too. A feed of only hits has no baseline.

No coverage is not the same as nothing found.

run cut short→REVIEW
How verdicts are assigned

The feed is public. See what the registry shipped today.

Every verdict on this page is published at trail.snappyfeet.org. It is live, searchable, and carries the full record behind every row.

contact@snappyfeet.org