§ 03 Detectors scoring · thresholds · the 10

Three layers. One score. Every package.

phi runs three layers on every package: pattern detectors (regex), AST-validated detectors (parsed JS), and a known-vulnerability check against OSV. Hits from any layer add to the same risk score.

Scoring

SeverityPoints
CRITICAL+35
HIGH+20
MODERATE+10
LOW+5

Score is summed per package, capped at 100. Detections are deduplicated per detector — a noisy codebase doesn't artificially inflate the score.

Verdict thresholds

ScoreVerdictBehavior
0 – 19SAFEInstall silently.
20 – 59REVIEWPrompt the developer (or fail in --json / --frozen mode).
60 – 100BLOCKEDRefuse to install, write report, exit non-zero.
The threshold is 60, not 50: a single CRITICAL + HIGH combination (e.g. eval + new Function in pino's logger) lands in REVIEW for the user to decide. Real malware combines two CRITICAL signals (eval + cred theft = 70) and still trips.

The 11 detectors

01 · Arbitrary Code Execution CRITICAL

Direct execution of arbitrary code: eval(...), child_process.exec/spawn(...).

AST-validated — phi parses .js / .cjs / .mjs files with goja and only fires on real CallExpressions. References inside string literals, comments, or identifier names are suppressed. eslint's source contains eval in regex patterns; AST validation correctly skips those.

02 · Dynamic Code Compilation HIGH

String-to-function compilation: new Function(...).

Legitimately used by validator generators (zod, ajv), JSON serializers (fast-json-stringify), route compilers (find-my-way), formatters (prettier), stack-trace utilities (depd). Demoted to HIGH so a single hit lands in REVIEW; combined with a CRITICAL or another HIGH it can still escalate.

03 · Code Obfuscation CRITICAL

Techniques used to hide malicious payloads.

04 · Credential Theft CRITICAL

Access to API keys, tokens, or credential files.

Smart matcher — knows the package's normalized name and silently skips reads of the package's own env vars. resend reading process.env.RESEND_API_KEY doesn't fire; an unrelated package reading AWS_SECRET_ACCESS_KEY does. Allowlist of well-known third-party credentials includes AWS, Azure, GCP, GitHub, GitLab, npm, Heroku, Docker, Slack, Discord, Stripe, Twilio, Mailgun, Sendgrid.

File-based credential references — .npmrc, .netrc, id_rsa, id_ed25519 — fire unconditionally.

05 · Install Script Abuse CRITICAL

Lifecycle scripts (preinstall / install / postinstall) that pipe remote code into a shell.

Smart matcher — only inspects scripts.{preinstall,install,postinstall} of package.json. Test scripts, prepublish hooks, and build scripts that happen to use node -e or curl | sh don't fire. Real-world false positive on ljharb's utility packages eliminated.

06 · Crypto Mining CRITICAL

Cryptocurrency mining APIs and pool connections: CoinHive, Monero/XMR, stratum+tcp:// URLs, CryptoNight references.

07 · Wallet Drain CRITICAL

Cryptocurrency wallet access or transfer patterns: web3.eth.sendTransaction, ethers.Wallet instantiation, drainTokens / drainWallet function names.

08 · Reverse Shell CRITICAL

Patterns used to open a remote shell back to an attacker: /bin/bash -i, /dev/tcp/, mkfifo, nc -e /bin/.

09 · Network Exfiltration HIGH

Outbound calls to known exfiltration services or hidden domains. Narrowed on purpose:

Generic fetch(...), axios.X(...), and require('http') patterns produce too many false positives on legitimate API clients, so they're not flagged here.

10 · Typosquatting HIGH

Package name within Levenshtein distance == 1 of a popular package (lodash, express, axios, react, vue, …). Distance-2 was tried originally but produced false positives on legitimate short names like fecha matching mocha.

11 · File System Access HIGH

Reads of OS-level sensitive paths: /etc/passwd, /etc/shadow, .aws/credentials, .kube/config, .docker/config.json.

Layer 3 — known vulnerabilities (OSV)

Beyond the static detectors, phi queries every resolved (name, version) against the OSV database, which aggregates GHSA, OpenSSF malicious-packages, and CVE feeds. Hits append to the same risk score using their advisory severity:

OSV severityphi points
CRITICAL+35
HIGH+20
MODERATE+10
LOW / UNKNOWN+5

Disable with --no-advisories for offline use. Network failures are non-fatal — phi prints a warning and proceeds without advisory data.

Adding a new detector

In internal/analyzer/detectors.go, append to the detectors slice:

{
    name:        "Your Detector",
    description: "What it catches",
    severity:    SeverityHigh,
    patterns: mustCompile(
        `your\s+regex\s+here`,
    ),
}

For context-aware detection (needs the package name or a parsed AST), set a matcher function instead of patterns. The scoring and UI layers pick up new detectors automatically. Tests live in internal/analyzer/analyzer_test.go.