// threat intelligence · v0.6.0 · npm + Go
§ 03 Detectors 13 npm · 6 Go · OSV across both

Three layers. One score. Every dependency.

phi runs three layers on every dependency: pattern detectors (regex), source-level detectors (AST-validated JS or Go), and a known-vulnerability check against OSV. Hits from any layer add to the same risk score — whether the package came from npm or Go's module proxy.

Scoring

Severity Points
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

Score Verdict Behavior
0 – 19 SAFE Install silently.
20 – 59 REVIEW Prompt the developer (or fail in --json / --frozen mode).
60 – 100 BLOCKED Refuse 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 npm detectors (13)

The original detector suite, focused on Node's threat surface: arbitrary code execution at install / import time, obfuscation, credential theft, network exfiltration, typosquats, and the rest. Each fires AST-validated where possible to keep false positives low. Go-specific detectors are documented below in § 03.G.

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.

Real fire (REVIEW): discord.js bundles an internal _eval(script) method documented as "calls eval() on a script with the client as this". Phi's AST walker correctly identifies this as a real call expression and scores it CRITICAL — but the verdict lands at REVIEW (score 35), not BLOCKED. A library you trust can ship through with --force; the phi-report.json audit trail records the override.

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.

Real fire: the event-stream / flatmap-stream incident (Nov 2018). A new maintainer added flatmap-stream as a sub-dependency, which contained an obfuscated payload targeting users with the Copay Bitcoin wallet in their dependency tree. Phi's hex-escape detector with diversity check matches the exact obfuscation shape used in that payload. ~2M weekly downloads at the time of compromise.

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.

Real fire: the eslint-scope compromise (Jul 2018). A maintainer's npm token was stolen and used to publish a malicious version that read ~/.npmrc and exfiltrated the publish token to attacker infrastructure — laying the groundwork for further package takeovers. Phi's file-based credential reference detector fires unconditionally on .npmrc reads.

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.

Real fire: event-stream → flatmap-stream chain (Nov 2018) and ua-parser-js (Oct 2021). Both used postinstall hooks to fetch and execute remote payloads — the canonical npm supply-chain attack pattern. With phi, lifecycle scripts don't run at install time at all. --allow-scripts esbuild,sharp opts in by package name; everything else stays inert.

06 · Crypto Mining CRITICAL

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

Real fire: the ua-parser-js compromise (Oct 2021). Versions 0.7.29, 0.8.0, and 1.0.0 were published with malicious code that pulled a Linux/macOS XMR miner and a Windows password stealer via postinstall. ~7M weekly downloads at the time. Similar pattern: coa and rc compromises (Nov 2021) — same author cluster, same delivery shape.

07 · Wallet Drain CRITICAL

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

Real fire: the event-stream / flatmap-stream payload (Nov 2018) was a wallet drain — it specifically checked for Copay Bitcoin wallet objects in the consuming app's dependency graph and exfiltrated their private keys when found. Modern variants target Solana, Ethereum, and Cosmos wallets via the ethers.Wallet / web3.eth APIs phi flags here.

08 · Reverse Shell CRITICAL

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

Most npm-distributed reverse shells get their hands dirty in two places — the literal shell-trigger string AND a lifecycle script that fires it. Phi flags the string regardless of whether it's wired up; the install-script detector (§ 05 above) catches the trigger. Two layers of the same attack land in BLOCKED easily.

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.

Real fire: ctx and phpass typosquats (May 2022). Both packages exfiltrated environment variables — including AWS keys and DB credentials — to attacker-controlled URLs. The exfil endpoints used pastebin-shaped services that phi's allowlist covers. The pattern recurs every few months; the names change, the destinations don't.

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.

Real fire: crossenv (Aug 2017) — typosquat of cross-env that exfiltrated environment variables on first require(). Single-character difference from a package with millions of weekly downloads. Phi's distance-1 check catches this exact shape: insertion, deletion, or substitution of one character against a curated list of high-traffic names.

11 · File System Access HIGH

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

Pattern set is intentionally short — these are paths a package has no business reading, full stop. Keeping the set narrow keeps false positives at zero. Credential-harvester npm packages discovered in 2022-2024 typosquat campaigns commonly hit the AWS credentials path before exfiltrating; that's what this detector exists to catch.

12 · Credential Exfil Flow CRITICAL v0.2.2

Combined-flow detector — fires when the same source file BOTH reads a third-party credential AND makes an outbound HTTP call (fetch, axios, got, http.request, XMLHttpRequest, navigator.sendBeacon, .post(...), etc.).

Smart matcher with a token-to-canonical-host allowlist. Reading GITHUB_TOKEN and posting to api.github.com is silent (octokit and similar clients); the same token going to evil.example.com fires. Subdomain-aware — api.github.com matches the canonical github.com.

File-based reads (.npmrc, id_rsa, .aws/credentials) bypass the host allowlist entirely — there's no legitimate destination for someone's SSH key. Combined with the existing Credential Theft detector this pushes real exfiltration into BLOCKED territory.

Why combined-flow: the Credential Theft detector and the Network Exfiltration detector each catch one half of the pattern. Real attacks need both — read the credential AND ship it somewhere. The 2024-2026 wave of typosquat campaigns against the Solana, Ethereum, and AI/ML ecosystems all share this exact shape: harvest process.env.*, POST to an attacker-controlled domain. The combined-flow detector matches the shape itself, with the canonical-host allowlist suppressing legit API clients (octokit, the AWS SDK, Stripe, Twilio, Sendgrid).

13 · Linux System Tampering CRITICAL v0.2.2

Symbols and primitives that don't belong in npm packages. Direct response to recent QLNX-style RAT tradecraft (PAM backdoors, eBPF rootkits, fileless execution).

Conservative word-bounded regex. False positive rate is essentially zero in practice — these symbols simply don't appear in legitimate Node packages.

Real fire: the QLNX RAT analysis (May 2026 reporting). QLNX-class threats targeting developer machines use PAM module injection, eBPF program loading, and LD_PRELOAD writes to maintain covert access — 58 documented remote commands across the toolchain. Phi flags the symbols at install time, before any of them have a chance to execute. The class of threat exists because devs have npm tokens, AWS keys, and CI/CD credentials on their boxes; phi cuts the npm vector.

Go-specific detectors (v0.6.0+)

When phi runs in Go mode, six per-module detectors plus one project-level detector fire against each fetched module's source tree. Same scoring ladder (CRITICAL/HIGH/MEDIUM/LOW), same verdict thresholds, same phi.lock audit trail — but tuned for Go's threat surface: init() at program start, cgo, build-tag-gated payloads, and //go:generate directives.

14 · go-cgo-shellexec CRITICAL

A file uses cgo (import "C") AND calls os/exec.Command or syscall.Exec in the same package. The combination means "I can run unaudited C code AND I can spawn arbitrary processes" — the highest severity Go signal phi flags.

15 · go-init-network HIGH

init() function body contains calls into net. or net/http.. Go runs every imported package's init() automatically at program start — outbound HTTP/DNS in init means merely importing the module is enough to phone home.

16 · go-init-fs-write HIGH

init() writes to the filesystem outside os.TempDir()os.Create, os.OpenFile, os.WriteFile, os.MkdirAll, etc. Side effects on import are the Go-shaped equivalent of npm's postinstall script.

17 · go-go-generate-curl HIGH

A //go:generate directive shells out to curl, wget, or contains an http:// / https:// URL. go generate isn't part of go build, but CI pipelines commonly run it — giving the module a foothold to fetch and run arbitrary remote code.

18 · go-build-tag-gated MEDIUM

A file has a restrictive //go:build directive (three+ tokens or a negation like !debug / !test) AND contains net. / net/http. / os/exec. / syscall. calls. The pattern of "only compile this code under these specific platforms / build flavors AND it does dangerous things" is a hint of a platform-targeted payload that won't fire in CI's default build.

19 · go-unsafe-import LOW

File imports the unsafe package. Not malicious on its own — performance-sensitive code legitimately uses unsafe.Pointer for raw pointer arithmetic. Phi surfaces it as a signal because unsafe is over-represented in supply-chain incidents: it lets an attacker bypass type safety and memory protections.

go-replace-with-fork MEDIUM project-level

The user's own go.mod has a replace directive pointing to a different VCS organization than the original. e.g. replace github.com/foo/bar => github.com/quux/bar — same-name module, different owner. Most replaces are legitimate (local forks for development, organization-internal mirrors), but cross-org replaces deserve a deliberate "yes, I trust this fork" from a reviewer.

This detector fires at the project level, not per-module — phi reads your go.mod's replace block during install and audit, not the upstream module's own go.mod.

Implementation note: Go zips are scanned without extraction via archive/zip + the Go parser (go/parser with SkipObjectResolution for speed). Per-file 5 MiB cap, per-module 10-second wallclock timeout, panic-recovery on every file so a crafted AST can't crash phi. The analyzer uses golang.org/x/mod/zip's CheckZip equivalent to reject zips with path-traversal entries or wrong-prefix file names before any file is read.

Informational notices

Beyond the verdict-bearing detectors, phi surfaces small info-only notes for packages worth a second look. They don't affect the score and they don't block install — they're a nudge.

Currently shipped: deprecation guidance for packages with safer successors:

Package Replacement
vm2 isolated-vm (12 critical sandbox-escape CVEs through CVE-2026-44009; the proxy-sandbox architecture cannot be fully secured)
request / request-promise undici, axios, or got (request was discontinued 2020)
node-uuid uuid (renamed)
tslint eslint + @typescript-eslint
bower npm / yarn / pnpm
node-sass sass (Dart Sass)
babel-eslint @babel/eslint-parser (renamed)
left-pad, is-array stdlib (String.prototype.padStart, Array.isArray)

Notices appear under the report card as note deprecated — <message> and ship in phi-report.json under each package's notices[] array.

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 severity phi 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.