Skip to content

Latest commit

 

History

History
143 lines (109 loc) · 5.88 KB

File metadata and controls

143 lines (109 loc) · 5.88 KB

How scoring works

Every number this tool prints comes from one file, src/analysis/assess.ts. Everything upstream reports facts; everything downstream renders. This page documents the arithmetic so that a verdict you disagree with is a verdict you can argue with.

The pipeline

signals  ──▶  category  ──▶  risk 0–100  ──▶  severity  ──▶  recommendation

Signals are found first and are independent of category. The category is then decided from the signals plus the script text. Only then is risk computed — and the category is an input to that, because context changes what a signal is worth.

Signals

A signal is one observed behaviour, with the matched evidence attached. A detector answers a narrow mechanical question and never decides whether the answer is bad.

Signal Weight What it observes
pipe-to-shell 45 a download piped straight into a shell
credential-path 35 a reference to a file that normally holds credentials
dynamic-eval 30 eval, new Function, vm.runIn*
obfuscated-source 28 source that cannot be reviewed by eye
base64-blob 25 a large embedded base64 payload
shell-profile-write 25 a shell profile or scheduler — how install-time code persists
process-spawn-detached 18 a process that outlives the install
network 15 any network access
home-escape 12 reaching outside the package into the user home
shell-out 10 starting another process
env-read 6 reading environment variables
native-toolchain 0 node-gyp, prebuild-install, cmake-js
git-hook-install 0 husky, simple-git-hooks, lefthook

The scale is calibrated so that any single signal above 30 is on its own enough to demand a human read the script, and so that the ordinary shape of a legitimate build lands in the low tens.

The last two are worth zero deliberately. They describe what a script is, not whether it is dangerous, and they earn their keep by driving classification rather than by moving the number.

Each detector contributes at most one signal per package. A script that calls fetch eleven times is not eleven times more interesting than one that calls it once.

Contextual discounts

A signal means different things in different scripts. Reaching the network is unremarkable in prebuild-install, whose entire job is downloading a prebuilt binary; it is a strong signal in a script whose purpose is unclear.

Category Discounted signals
native-build network ×0.2, shell-out ×0.2, env-read ×0.3
binary-download network ×0.6, shell-out ×0.5
codegen shell-out ×0.3
dev-tooling shell-out ×0.4, home-escape ×0.5

Discounts are multipliers, not suppressions. The signal is still reported with its evidence, and it still moves the score — a native build that reaches the network is marginally more interesting than one that does not, just not alarmingly so.

Combination bonuses

Some pairs mean more together than apart.

Combination Bonus Why
network + credential-path +25 touches credential files and contacts the network
network + env-read +20 the shape of every registry credential-exfiltration incident
dynamic-eval + base64-blob +20 decodes an embedded payload and evaluates it
network + shell-out +10 downloads and then executes

A bonus is suppressed when either of its signals is heavily discounted in that category. prebuild-install reading npm_config_* to pick a mirror is not an exfiltration pattern, and scoring it as one would teach people to ignore the finding that matters.

Category base risk

Two categories carry risk before any signal is counted.

Category Base Why
binary-download 25 the bytes that execute are not the bytes the lockfile hashed, and it breaks in an offline build for reasons that look nothing like the cause
unknown 10 by definition, nobody has attributed a purpose to it

Without these, a tidily written downloader scored info while being recommended for review — a contradiction a reader is right to distrust.

Severity

risk is capped at 100 and mapped to a band:

Risk Severity
≥ 70 critical
≥ 45 high
≥ 25 medium
≥ 10 low
< 10 info

One override: a script classified unknown that also reaches the network is never below medium, however the arithmetic lands.

Recommendation

Category Recommendation
native-build, codegen allow
dev-tooling, telemetry, notice, no-op deny
binary-download, unknown review

With one rule above all of them: nothing at high or critical is ever recommended for automatic approval, whatever its category. A node-gyp build that also pipes a download into a shell is not an ordinary native build.

scriptgate allowlist writes the allow and deny recommendations and leaves review out.

What is deliberately not scored

A dev-only dependency is not discounted. It would be convenient and it would be wrong. Install scripts from dev dependencies run on developer laptops, which hold the npm tokens, the SSH keys and the cloud credentials that production build agents do not. The dev flag is reported as context so you can weigh deployment risk separately; it never moves the number.

Package popularity is not an input. Download counts measure how many people would be affected by a compromise, not how likely one is. Weighting by popularity would systematically under-rate exactly the packages an attacker would most want to compromise.

Publisher identity is not an input. It would require the network, and maintainer-account takeover — the Axios incident of March 2026, among others — is precisely the case where publisher reputation is worth nothing.