Package Scanning Rules Library
Why should a package be treated as malicious, or only suspicious? 80 rules for npm and PyPI, each with the observable signal, the attacker behaviour behind it, commands to check it by hand, the usual false positives, and an honest note on whether our scanners check it today. Free to browse.
Type a name or paste a package.json and get a red, amber or green light, then come back here to see why.
- rules
- 80
- malicious / suspicious / info
- 29 / 40 / 11
- categories
- 12
- checked today + partly
- 16 + 15
How we decide malicious vs suspicious
Every rule belongs to one of three classes. Rules are evidence, and evidence combines. This is the reviewer rubric the library is written for; it is not a score our scanners compute (their real formulas are described below).
MALICIOUS-indicator
Behaviour with no legitimate reason in a library: sending the environment to a remote host, writing to ~/.ssh at install, decode-then-eval. One critical malicious indicator is enough to block. A high one means quarantine, and block if any second indicator appears.
SUSPICIOUS-indicator
A capability or pattern with common innocent uses: an install hook, a minified bundle, a new maintainer. Score it: critical 5, high 3, medium 2, low 1 points. 5 points or more: quarantine. 8 or more across at least two categories: block until a person has verified. Independent categories count more than many hits in one.
INFORMATIONAL
Context that never decides by itself: low usage, a stale release, no repository, a known (non-malicious) vulnerability, provenance present. Zero points; it explains why you are looking, and what to check next.
Context moves the call: a high-download package with a brand-new maintainer, or an install hook that did not exist in the previous version, deserves a step up. A rule that names a false positive (native builds, official SDKs) is closed by the evidence it lists, not by intuition.
How to read an amber result
In the Package Stoplight and the Package Scanner the score starts at 60. Good signals (maintained, popular, source linked, no known advisories) add 8 each; weak signals (stale, low usage, no repository) subtract 8; strong ones (possible typosquat, known advisories) subtract 22. Green is 75 or more, amber is 50 to 74, red is under 50, and a package that cannot be found is red.
So amber is "thin or unusual", not "malicious". Match each reason it shows to a rule: Low usage, No source repo, Stale. Then run that rule's manual checks. Red reasons map to Possible typosquat and Known vulns (which includes malicious-package advisories, so read the advisory ID).
Provenance is shown but never scored, on purpose: see PSR-PUB-007. And a green light only means the checks that run came back clean. Most rules in this library are not automated yet, which the "Our scanner" label on every rule states: 16 checked today, 15 partly, 49 guidance only.
The rules
Showing 80 of 80 rules
PSR-BIN-001Pre-built executable or shared library shipped in a JavaScript or Python package
highSUSPICIOUS-indicatornpm + PyPINative and binary payloadsPartly checkedFiles with ELF, PE or Mach-O headers (.exe, .dll, .so, .dylib, .bin, or no extension) in a package that describes itself as pure JavaScript or pure Python, particularly outside a documented prebuilds/ or bin/ folder.
Pre-built executable or shared library shipped in a JavaScript or Python package
Why it matters
A binary cannot be code-reviewed from the tarball. It is the easiest place to hide a stealer or loader, and scanners that only read text never open it.
How to verify manually
npm pack <pkg>@<version> --ignore-scripts && tar -tzf <pkg>-<version>.tgz | grep -viE "\.(js|cjs|mjs|ts|json|md|txt|map)$"tar -xzf <pkg>-<version>.tgz && find package -type f -exec file {} + | grep -E "ELF|PE32|Mach-O"sha256sum the file and look the hash up in a malware-intelligence service before executing anything.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Native-addon packages ship compiled code on purpose (esbuild, swc, sharp, better-sqlite3, python wheels with compiled extensions). Rule out: the binary is documented, comes from the vendor's own project, matches its release assets by hash, and the package is on the expected platform-specific optionalDependencies layout.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Partly checked
sp-bundled-binary matches .exe/.dll/.dylib/.so/.bin/.elf in the package.json "files" array. That field is usually a list of globs rather than the real tarball listing, and .node files are not in the list, so real coverage is low.
References
PSR-BIN-002.node native addon in a package with no native-build purpose
highSUSPICIOUS-indicatornpmNative and binary payloadsGuidance onlyA .node file (or a binding.gyp that compiles one) loaded with require() in a package whose stated function is pure logic: string handling, formatting, a wrapper around an HTTP API.
.node native addon in a package with no native-build purpose
Why it matters
A native addon runs with full process privileges and can do anything the user can. For a utility library it is unneeded risk and a convenient way to hide behaviour from JavaScript-level review.
How to verify manually
tar -tzf <pkg>-<version>.tgz | grep -E "\.node$|binding\.gyp"file package/**/*.node; strings -n 8 <file>.node | grep -iE "http|/\.ssh|/etc/|token"
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Performance-critical packages (crypto, image, database drivers) legitimately use addons. Rule out: the README explains why, the source is in the repository, and prebuilt files are downloaded from the vendor's release assets.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-BIN-003Embedded archives, blobs or data files decoded and run at runtime
highSUSPICIOUS-indicatornpm + PyPINative and binary payloadsGuidance onlyLarge opaque files (.zip, .tar, .gz, .dat, .bin, .wasm, images or fonts with surprising size) read by code that then extracts, decodes or loads them, or a test fixture folder that is read by non-test code.
Embedded archives, blobs or data files decoded and run at runtime
Why it matters
Payloads hidden in assets avoid text-based scanning. Unpacking and running them at install or first use is a loader technique.
How to verify manually
tar -tvzf <pkg>-<version>.tgz | sort -k3 -n | tail -15 # biggest files firstfile package/<suspicious>; binwalk package/<suspicious> # embedded data?grep -rnE "readFileSync\(.*(\.dat|\.bin|\.zip|\.png|\.woff)|unzip|extractAll|zlib\.(inflate|gunzip)" package
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
WASM modules, bundled fonts, databases of timezones or locale data and compressed lookup tables are normal. Rule out: the asset type matches its extension (file agrees), it is read by the feature that needs it, and nothing executes its content.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked. The engine reads only .js/.cjs/.mjs/.ts files.
References
PSR-BIN-004File type does not match its extension
highSUSPICIOUS-indicatornpm + PyPINative and binary payloadsGuidance onlyA file named .png, .jpg, .txt, .css or .json whose magic bytes identify an executable, archive or script (MZ, \x7fELF, PK, #!/bin/sh).
File type does not match its extension
Why it matters
Disguising an executable as an image or text file defeats reviewers who skim the file list and tools that classify by extension.
How to verify manually
tar -xzf <pkg>-<version>.tgz && find package -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.txt" -o -name "*.css" \) -exec file {} + | grep -vE "image|text"
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Some images and CSS files embed data URIs or have unusual headers, and ZIP-based formats (.docx, .jar) carry PK headers by design. Rule out: the extension and content type agree for that format.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-CI-001Package writes CI workflow or pipeline files
criticalMALICIOUS-indicatornpm + PyPICI/CD specificPartly checkedCode that creates or edits .github/workflows/*.yml, .gitlab-ci.yml, .circleci/config.yml, Jenkinsfile or azure-pipelines.yml in the project or in repositories reachable with a token.
Package writes CI workflow or pipeline files
Why it matters
A new workflow runs with the repository's secrets. Adding one (often named innocuously) is a way to exfiltrate CI secrets and persist after the package is removed.
How to verify manually
grep -rnE "\.github/workflows|\.gitlab-ci|\.circleci|Jenkinsfile|azure-pipelines" packagegit log --all --diff-filter=A --name-only -- .github/workflows # new workflow files you did not add
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Scaffolding tools (create-*, generators, init commands) write workflow files, on explicit command and into the new project. Rule out: it runs only from a user-invoked generator, not an install hook or import.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Partly checked
sp-github-actions-poison matches writeFile calls whose path contains .github/ or .circleci/. GitLab, Jenkins and Azure files and non-writeFile APIs are not matched.
References
PSR-CI-002Reads runner OIDC or Actions tokens and secrets context
criticalMALICIOUS-indicatornpm + PyPICI/CD specificGuidance onlyReferences to ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_RUNTIME_TOKEN, runner memory or work directories, or iteration of environment variables when CI=true / GITHUB_ACTIONS is set.
Reads runner OIDC or Actions tokens and secrets context
Why it matters
Inside a CI run the process can mint cloud credentials, read repository secrets and publish packages. A dependency that checks it is in CI and reaches for these is targeting the pipeline, not the developer.
How to verify manually
grep -rnE "ACTIONS_ID_TOKEN|ACTIONS_RUNTIME|GITHUB_ACTIONS|process\.env\.CI\b" packageRun builds with step-security style egress monitoring or an allow-listed network, and review any unexpected destinations.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Test frameworks and reporters read CI to adjust output (colors, reporters, retries). Rule out: only the CI boolean is read and no token variable is touched.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-CI-003Package repository runs untrusted pull-request code with secrets (pull_request_target)
highSUSPICIOUS-indicatornpm + PyPICI/CD specificGuidance onlyThe package's own repository has a workflow triggered by pull_request_target (or workflow_run) that checks out the pull-request head and runs its code or scripts with write permissions or access to publishing secrets.
Package repository runs untrusted pull-request code with secrets (pull_request_target)
Why it matters
A fork's pull request can then execute with the project's secrets. This is a common route by which a package's release pipeline is hijacked and a malicious version is published legitimately.
How to verify manually
In the repository: grep -rn "pull_request_target" .github/workflowsLook in the same workflow for actions/checkout with ref: ${{ github.event.pull_request.head.sha }} followed by npm install, npm test or any script step.Check the workflow's permissions: block and whether secrets are passed to that job.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
pull_request_target is safe for labelling or commenting workflows that never check out or run PR code. Rule out: the job does not execute code from the PR, and permissions are read-only.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
Not checked. The scanners do not read the package's repository.
References
PSR-CI-004Release workflow uses a long-lived publish token or unpinned actions
mediumINFORMATIONALnpm + PyPICI/CD specificGuidance onlyThe publishing workflow reads a stored NPM_TOKEN / PYPI_API_TOKEN secret instead of trusted publishing (OIDC), references third-party actions by tag or branch rather than a full commit SHA, or has recently been edited before a surprise release.
Release workflow uses a long-lived publish token or unpinned actions
Why it matters
A static token can be stolen and used from anywhere. Mutable action references let someone else change what your release job runs. Both weaken the guarantee that a release came from reviewed code.
How to verify manually
In the repository: grep -rnE "NPM_TOKEN|PYPI_API_TOKEN|NODE_AUTH_TOKEN|id-token: *write|uses: .*@(main|master|v[0-9])" .github/workflowsnpm view <pkg>@<version> dist.attestations # trusted publishing produces attestationsgit log -p -- .github/workflows | head -200 # recent edits before the release
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Plenty of maintainers still use tokens, and tag-pinned actions are common. This is a hygiene note about how much to trust the release, not a malicious indicator.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
Not checked. Only the presence of a provenance attestation on the latest version is reported.
References
PSR-CI-005Unexpected repositories, branches or workflow runs created under the maintainer's account
highMALICIOUS-indicatornpm + PyPICI/CD specificGuidance onlyNew public repositories with generic or data-dump names, new branches carrying workflow files, or unexplained workflow runs on repositories owned by a maintainer whose credentials may have been exposed.
Unexpected repositories, branches or workflow runs created under the maintainer's account
Why it matters
Attackers who obtain a developer token often use the code host itself as a drop box and to run their workflows. Seeing that artefact on the maintainer's account is evidence the token was used by someone else.
How to verify manually
gh repo list <owner> --limit 100 --json name,createdAt,visibilitygh run list --repo <owner>/<repo> --limit 20Review the account security log and token list; revoke and rotate every token that could have been exposed.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Maintainers do create test repositories and scheduled workflows. Rule out: you recognise each item, and creation times line up with known activity.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked. This is an account-side check for maintainers and their dependents.
References
PSR-CRED-001Wholesale read of the environment
highSUSPICIOUS-indicatornpm + PyPICredential and secret harvestingChecked todayObject.keys/entries/values(process.env), JSON.stringify(process.env), { ...process.env } passed to a function, or in Python dict(os.environ), os.environ.copy() serialised and sent or written.
Wholesale read of the environment
Why it matters
Libraries read the specific variables they document (NODE_ENV, DEBUG, HTTPS_PROXY). Enumerating the whole environment sweeps up cloud keys, CI tokens and API secrets without having to know their names.
How to verify manually
grep -rnE "Object\.(keys|entries|values)\(process\.env\)|JSON\.stringify\(process\.env\)|\.\.\.process\.env" packagegrep -rnE "os\.environ(\.copy\(\)|\)|\.items)" <extracted-python-package>
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Config loaders (dotenv, cross-env, env-cmd), test runners and build tools pass the environment to child processes. Rule out: the data never goes to a network call or a file outside the project, and the package is a known tooling dependency.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Checked today
sp-process-env-mass-read matches Object.keys/entries(process.env) in package JS source. It does not match JSON.stringify(process.env) or spread, and it does not read Python.
References
PSR-CRED-002Environment read followed by an outbound request
criticalMALICIOUS-indicatornpm + PyPICredential and secret harvestingChecked todayEnvironment or credential data flowing into fetch, http(s).request, axios, got, requests.post, urllib, a socket or a DNS lookup in the same module or call chain.
Environment read followed by an outbound request
Why it matters
Reading secrets and then talking to the network is the definition of an infostealer. The sink is the proof: it separates "uses an API key" from "ships every key somewhere".
How to verify manually
grep -rnE "process\.env" package | head -50 # then follow each hit to see whether it reaches a requestRun the package inside a network-isolated VM or container with a canary secret in the environment (CANARY_TOKEN=pcx-test) and watch egress with tcpdump or a deny-all firewall log.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
SDKs read an API key from the environment and send it as an Authorization header to their own documented API. Rule out: the destination host belongs to the vendor that issued that key, and only that one variable is read.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Seen in the wild
crossenv (2017): a typosquat of cross-env that sent environment variables to a remote server.
Does our scanner check this today? Checked today
sp-env-exfil: process.env followed anywhere later in the scanned source by fetch, http or axios. It is a text proximity match across concatenated files (noisy on large SDKs) and does not read Python.
References
PSR-CRED-003Reads npm or registry credentials
criticalMALICIOUS-indicatornpm + PyPICredential and secret harvestingGuidance only.npmrc, .yarnrc(.yml), .pypirc, pip.conf, NPM_TOKEN, NODE_AUTH_TOKEN or _authToken strings read by package code, especially together with a network call.
Reads npm or registry credentials
Why it matters
Publish tokens let an attacker push trojanised releases of every package the victim maintains, which is how one compromise becomes dozens. Registry credentials are the pivot of self-propagating supply chain worms.
How to verify manually
grep -rniE "\.npmrc|\.yarnrc|\.pypirc|_authToken|NPM_TOKEN|NODE_AUTH_TOKEN|TWINE_PASSWORD" packagenpm token list # on your own account: revoke anything you do not recognise
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Publishing and release tools (semantic-release, np, changesets, twine wrappers) read these on purpose. Rule out: the package is a documented release tool, you installed it for that purpose, and the read is not followed by a request to a host other than the registry.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Seen in the wild
eslint-scope 3.7.2 (July 2018) was published with stolen maintainer credentials and its install script sent the user's .npmrc to a remote host.
Does our scanner check this today? Guidance only
No policy looks for registry credential files or token variable names.
References
PSR-CRED-004Reads cloud provider credential files
criticalMALICIOUS-indicatornpm + PyPICredential and secret harvestingGuidance onlyAccess to ~/.aws/credentials or config, ~/.config/gcloud, ~/.azure, ~/.kube/config, ~/.docker/config.json or the matching environment variables (AWS_SECRET_ACCESS_KEY, GOOGLE_APPLICATION_CREDENTIALS) by a package that is not a cloud SDK.
Reads cloud provider credential files
Why it matters
Cloud keys turn a developer machine into an account takeover and a billing event. They are high-value, stored in well-known paths and rarely rotated, so stealers go straight to them.
How to verify manually
grep -rnE "\.aws|\.azure|gcloud|\.kube|\.docker/config|AWS_SECRET|GOOGLE_APPLICATION_CREDENTIALS" packageCheck the package category: an SDK for that cloud has a reason, a string utility does not.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Official cloud SDKs, Terraform/CDK helpers and deployment CLIs read these files by design. Rule out by confirming the package is the vendor's own (scope, publisher, repository) and the paths are read through the SDK's documented credential chain.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-CRED-005Reads SSH private keys or known_hosts
criticalMALICIOUS-indicatornpm + PyPICredential and secret harvestingGuidance onlyReads of ~/.ssh/id_rsa, id_ed25519, id_ecdsa, known_hosts or config, or a directory walk of ~/.ssh by package code.
Reads SSH private keys or known_hosts
Why it matters
Private keys give lateral movement into servers and source hosting. known_hosts and config map where those keys work, so the attacker knows what to try.
How to verify manually
grep -rnE "\.ssh|id_rsa|id_ed25519|id_ecdsa|known_hosts" packageIn a sandbox with a decoy key (ssh-keygen -f ~/.ssh/decoy), watch file access: sudo strace -f -e trace=openat -p <pid> or auditd on ~/.ssh.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
SSH client libraries (ssh2, node-ssh, paramiko) and git-over-ssh tooling read keys, but only a path the caller passes in. Rule out: the path is a function argument, not a hardcoded home-directory location.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-CRED-006Reads browser profiles or crypto wallet stores
criticalMALICIOUS-indicatornpm + PyPICredential and secret harvestingGuidance onlyPaths for Chrome/Edge/Firefox profile data (Login Data, Cookies, Local State, logins.json), wallet extension storage, or wallet files (wallet.dat, keystore directories, Exodus/Electrum folders) referenced by a non-browser, non-wallet package.
Reads browser profiles or crypto wallet stores
Why it matters
Browser stores hold session cookies and saved passwords; wallet files hold funds. Neither is reachable by a legitimate library, so the path strings are close to proof of intent.
How to verify manually
grep -rniE "Login Data|Local State|Cookies|logins\.json|wallet\.dat|keystore|Exodus|Electrum|Local Extension Settings" package
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Browser automation and test tooling (puppeteer, playwright, selenium wrappers) reference profile directories for their own throwaway profiles. Rule out: the path is a temp profile the tool creates, not the user's real profile directory.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-CRED-007Reads source-control and CI tokens
highMALICIOUS-indicatornpm + PyPICredential and secret harvestingGuidance onlyGITHUB_TOKEN, GH_TOKEN, GITLAB_TOKEN, ~/.config/gh/hosts.yml, ~/.git-credentials, or invocation of gh auth token / git credential fill from package code.
Reads source-control and CI tokens
Why it matters
A source-control token lets an attacker read private repositories, alter workflows and publish from CI. It is the usual next step after a developer-machine compromise.
How to verify manually
grep -rniE "GITHUB_TOKEN|GH_TOKEN|GITLAB_TOKEN|git-credentials|gh auth token|credential fill|hosts\.yml" packageReview the account audit log for token use from unfamiliar IPs after an install of the package.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Release and Git tooling (octokit wrappers, semantic-release, changesets) legitimately use these. Rule out: documented purpose, a token passed in by the caller, and requests only to the code host.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-DEP-001New dependency on an obscure package added in a patch or minor release
highSUSPICIOUS-indicatornpm + PyPIDependency graphGuidance onlyA release that adds a dependency not present before, where the dependency is new, has few downloads, a single maintainer, an unrelated purpose, or a code size that does not match its name.
New dependency on an obscure package added in a patch or minor release
Why it matters
The compromise does not need to touch the target package's own code. Adding one small dependency moves the payload one hop down, where reviewers rarely look.
How to verify manually
diff <(npm view <pkg>@<old> dependencies --json) <(npm view <pkg>@<new> dependencies --json)npm view <new-dep> time.created maintainers dist.unpackedSize
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Routine refactoring adds helper dependencies all the time. Rule out: the dependency is established, widely used, and the commit adding it is visible, reviewed and explained.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Seen in the wild
event-stream (2018): flatmap-stream was added as a new dependency in a minor release.
Does our scanner check this today? Guidance only
Not checked. No version-to-version comparison.
References
PSR-DEP-002Dependency pinned to a git URL, tarball URL or file path
mediumSUSPICIOUS-indicatornpm + PyPIDependency graphGuidance onlyA dependency version given as github:user/repo#branch, git+ssh://..., an https tarball link, an IP address, or in Python a direct URL requirement (name @ hxxps://example[.]invalid/file.whl).
Dependency pinned to a git URL, tarball URL or file path
Why it matters
Anything outside the registry skips its integrity metadata and takedown process, and a branch reference can change after review. Git dependencies can also run a prepare script at install.
How to verify manually
npm view <pkg>@<version> dependencies --json | grep -E "git|http|file:|github:"jq ".dependencies, .devDependencies, .optionalDependencies" package.json | grep -E "git|http|file:|github:"
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Teams pin forks or unreleased fixes via git on purpose. Rule out: the reference is a full commit SHA on a repository you control or trust, not a branch or tag.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
Not checked. sp-wildcard-dep only looks for * and latest ranges.
References
PSR-DEP-003Unpinned dependency range (*, latest, open ">=")
mediumINFORMATIONALnpm + PyPIDependency graphChecked todaydependencies or devDependencies declared as "*", "latest" or an open-ended range, so the next install resolves to whatever was published most recently.
Unpinned dependency range (*, latest, open ">=")
Why it matters
Not malicious, but it turns any future compromise of that dependency into an automatic install in your next build.
How to verify manually
jq ".dependencies, .devDependencies" package.json | grep -E "\"(\*|latest|>=)"npm ci # installs exactly what the lockfile pins
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Libraries (not applications) legitimately use ranges; lockfiles in the consuming application pin them. Rule out: your application commits a lockfile and installs with npm ci.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Checked today
sp-wildcard-dep lists dependencies set to "*" or "latest". Open ">=" ranges are not flagged.
References
PSR-DEP-004Lockfile integrity hash missing, changed or off-registry
highSUSPICIOUS-indicatornpm + PyPIDependency graphGuidance onlyA lockfile entry without an integrity hash, an integrity value that changed while the version did not, a "resolved" URL that is not the registry, or in Python a requirements file without --hash pins where hash-checking is expected.
Lockfile integrity hash missing, changed or off-registry
Why it matters
The lockfile is your pin. If an artifact changes under the same version, or resolves to a non-registry host, what you install is no longer what you reviewed.
How to verify manually
git diff -- package-lock.json | grep -E "integrity|resolved"npm ci # fails if package.json and lockfile disagreenpm audit signaturespip install --require-hashes -r requirements.txt
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Switching registries, mirrors or a lockfile regenerated on a different npm version legitimately changes resolved URLs and sometimes adds hashes. Rule out: the change is confined to resolved-host rewrites and integrity values for unchanged versions are identical.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
The engine defines sp-no-integrity, but the daily scan skips it (it fires on every package without lockfile context) and no lockfile is read.
References
PSR-DEP-005Package appears in a malicious-package advisory
criticalMALICIOUS-indicatornpm + PyPIDependency graphPartly checkedThe exact package and version matches an advisory for malicious code (OSV records these with a MAL- identifier from the OpenSSF malicious-packages feed, and registries publish takedown notices).
Package appears in a malicious-package advisory
Why it matters
This is not a heuristic. Someone has already analysed and confirmed the package. Anything installed before the takedown may have run.
How to verify manually
osv-scanner --lockfile package-lock.json # lists advisories including malicious-package entriesnpm auditSearch your CI logs and developer machines for installs of the affected versions during the exposure window.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Advisories apply to specific versions. Rule out: your resolved version is outside the affected range, confirmed in the lockfile. Do not rule out a malicious-package advisory because the package "looks fine".
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Partly checked
The Stoplight queries OSV by package name and counts every advisory as "Known vulns", so malicious-package entries are included but not separated from ordinary CVEs. The engine also hardcodes three names in sp-known-malicious-name (event-stream, flatmap-stream, eslint-scope@3.7.2).
References
PSR-DEP-006Known vulnerability (CVE or GHSA) in the package
mediumINFORMATIONALnpm + PyPIDependency graphChecked todayThe package or version has one or more published vulnerability advisories in OSV, GitHub Advisories or the registry audit.
Known vulnerability (CVE or GHSA) in the package
Why it matters
A vulnerability is a bug someone can exploit; it is not malware. Keeping the two apart matters: a vulnerable package gets patched, a malicious one gets removed and incident-handled.
How to verify manually
npm audit --jsonosv-scanner --lockfile package-lock.jsonpip-audit -r requirements.txt
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Advisories may not apply to your usage path or may be fixed in a newer version you already resolve. Rule out with the advisory's affected range and a reachability check.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Checked today
Package Stoplight and Package Scanner query OSV (api.osv.dev) by name and score any advisory as a bad signal (-22). The engine has sp-known-vuln-dep, but the fetcher does not populate its vulnerability input, so only the Stoplight path actually runs.
References
PSR-INST-001Install hook downloads remote content
criticalMALICIOUS-indicatornpmInstall-time executionChecked todayA preinstall, install or postinstall script (or the file it runs) that fetches from the network: curl, wget, fetch, https.get, Invoke-WebRequest, or a URL literal on the hook line.
Install hook downloads remote content
Why it matters
Install hooks run with the installer's privileges before any review. Downloading a second stage at that moment keeps the payload out of the published tarball, so registry scanning sees a clean package while the real code arrives later and can change per victim.
How to verify manually
npm view <pkg>@<version> scripts --jsonnpm pack <pkg>@<version> --ignore-scripts && tar -xzOf <pkg>-<version>.tgz package/package.jsontar -xzOf <pkg>-<version>.tgz package/<script-file-named-in-hook> | grep -nE "curl|wget|fetch|https?:|Invoke-WebRequest"
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Native-addon packages legitimately download a prebuilt binary at install (node-pre-gyp, prebuild-install, puppeteer, playwright). Rule out: the host is the vendor's own release host named in the README, the file is checksum-verified, and the same hook existed in the previous 3 versions. A first-ever download hook added in a patch release is not a false positive.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Seen in the wild
ua-parser-js (October 2021): hijacked releases ran an install script that fetched and executed binaries.
Does our scanner check this today? Checked today
NPM Scanner policies sp-postinstall-network (curl/wget/fetch/http on the postinstall string) and sp-http-in-install (URL in any of the three hooks). String-level regex only: it does not open the script file the hook calls. Not run by the Stoplight.
References
PSR-INST-002Install hook pipes a download into a shell
criticalMALICIOUS-indicatornpmInstall-time executionPartly checkedA hook line of the form "download | sh", "download | bash", "sh -c \"$(download)\"" or "node -e \"$(download)\"": fetched text executed without being saved or hashed.
Install hook pipes a download into a shell
Why it matters
This is the shortest path from "registry package" to "arbitrary remote code on a developer laptop or CI runner". Legitimate installers that do this are rare in libraries and are never a dependency of an application.
How to verify manually
npm view <pkg>@<version> scripts.postinstall scripts.preinstall scripts.installnpm pack <pkg>@<version> --ignore-scripts && tar -xzOf <pkg>-<version>.tgz package/package.json | grep -nE "\| *(ba)?sh|sh -c|\$\("
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Vendor CLIs that bootstrap themselves (a language toolchain installer wrapped as an npm package) do this on purpose. Rule out by confirming the package is the vendor's official one (publisher, repository, provenance) and that the URL is the vendor's documented install host. Everything else: no known benign reason.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Partly checked
sp-postinstall-network matches the "curl" or "wget" word on the postinstall line, so a pipe-to-shell is caught as a download, but the pipe itself is not detected and preinstall/install are only checked for a URL.
References
PSR-INST-003Install hook runs encoded PowerShell or cmd
criticalMALICIOUS-indicatornpmInstall-time executionGuidance onlypowershell/pwsh with -enc, -EncodedCommand, -nop -w hidden, -ExecutionPolicy Bypass, or cmd /c chains in a lifecycle hook, often guarded by a platform check such as process.platform === "win32".
Install hook runs encoded PowerShell or cmd
Why it matters
Encoded or hidden PowerShell has no purpose in a build hook. It hides the command from a casual reader and from log review, and a platform guard means Linux analysis sandboxes see nothing.
How to verify manually
npm pack <pkg>@<version> --ignore-scripts && tar -xzf <pkg>-<version>.tgzgrep -rniE "powershell|pwsh|-enc|EncodedCommand|ExecutionPolicy|WindowStyle" package/package.json package/*.js package/scripts 2>/dev/nullecho "<base64-argument>" | base64 -d | iconv -f UTF-16LE -t UTF-8 # read it, never run it
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Windows-focused build tooling can call PowerShell with plain, readable commands and no encoding or hidden window. Rule out by reading the full command: a literal, short, documented command is fine; a base64 blob never is.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
No policy looks for PowerShell, encoded commands or platform guards.
References
PSR-INST-004Install hook runs node -e with eval or Function
highSUSPICIOUS-indicatornpmInstall-time executionGuidance onlyA hook such as node -e "..." or node -p "..." whose inline program contains eval, new Function, require("child_process"), Buffer.from(..., "base64") or a URL.
Install hook runs node -e with eval or Function
Why it matters
Inline programs avoid shipping a visible .js file for scanners and reviewers to open. Combined with eval or a decode step it is a loader, not a build step.
How to verify manually
npm view <pkg>@<version> scripts --json | grep -nE "node -[ep]"Copy the inline program to a file and read it in an editor; do not paste it into a shell.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Short node -e one-liners that print a version, check an engine or create a directory are common and harmless. Rule out: the program is under about 100 characters and contains none of eval, Function, child_process, base64 or a network call.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
sp-obfuscated-eval scans package source files for eval(atob/Buffer.from), not the inline text of a script hook.
References
PSR-INST-005Install hook spawns a shell or child process
highSUSPICIOUS-indicatornpmInstall-time executionPartly checkedpreinstall/install/postinstall runs sh, bash, cmd, exec, spawn, or a script that calls child_process.exec/spawn with shell: true and non-constant arguments.
Install hook spawns a shell or child process
Why it matters
A lifecycle hook that spawns a shell is the mechanism almost every install-time payload uses. By itself it is only a capability; it turns malicious when the command is built from fetched or decoded data.
How to verify manually
npm view <pkg>@<version> scripts --jsonnpm pack <pkg>@<version> --ignore-scripts && tar -xzf <pkg>-<version>.tgz && grep -rnE "child_process|execSync|spawnSync|shell: *true" package --include=*.js --include=*.cjs --include=*.mjs
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
node-gyp builds, husky/lefthook git-hook setup, patch-package and tsc/rollup builds all spawn processes in install or prepare. Rule out: the command is a constant string naming a well-known tool that is a declared dependency.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Partly checked
sp-preinstall-exec (sh/bash/exec/spawn on the preinstall line only) and sp-spawn-shell (any exec( in source, very broad and noisy). install/postinstall script text is not checked for shell use.
References
PSR-INST-006Install-time write to shell profile, SSH, npmrc or cron
criticalMALICIOUS-indicatornpm + PyPIInstall-time executionPartly checkedCode reachable from an install hook that writes or appends to ~/.bashrc, ~/.zshrc, ~/.profile, ~/.ssh/authorized_keys, ~/.npmrc, a crontab, a systemd user unit, a LaunchAgent plist or a Windows Run key.
Install-time write to shell profile, SSH, npmrc or cron
Why it matters
A package has no reason to alter files outside its own directory. These locations give the attacker persistence that survives uninstalling the package, or a path back in (SSH key, poisoned registry config).
How to verify manually
grep -rnE "\.bashrc|\.zshrc|\.profile|authorized_keys|\.npmrc|crontab|LaunchAgents|systemd|CurrentVersion.Run" packageAfter a suspicious install in a throwaway VM: diff ~/.bashrc ~/.zshrc ~/.ssh ~/.npmrc against a snapshot, and run crontab -l
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Shell-completion installers and CLI "setup" commands may append to a profile, but they do so on an explicit user command, not in an automatic install hook, and they print what they changed.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Partly checked
sp-fs-write-outside-package flags writeFile calls to /etc, /root, /home or ../ paths. It does not know the specific persistence locations above and does not check the hook linkage.
References
PSR-INST-007Package declares an install-time hook at all
lowINFORMATIONALnpmInstall-time executionChecked todaypackage.json contains scripts.preinstall, scripts.install or scripts.postinstall (or a binding.gyp, which npm turns into an implicit node-gyp install script).
Package declares an install-time hook at all
Why it matters
Not evidence of anything on its own. It marks the packages where install-time risk exists, so review effort goes where code can run before you read it.
How to verify manually
npm query ":attr(scripts, [postinstall])" # in a project with node_modulesnpm view <pkg>@<version> scriptsnpm config get ignore-scripts
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Most hooks are benign (native builds, husky, telemetry opt-outs). Treat as a triage flag only. Rule out by reading the hook; if it is a single call to a known build tool, allow.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Checked today
Lead policy lp-install-script in the NPM Scanner engine adds weight to packages with an install hook; it prioritises scanning rather than producing a verdict.
References
PSR-INST-008Install hook newly added in a patch or minor release
highSUSPICIOUS-indicatornpmInstall-time executionGuidance onlyVersion N has no preinstall/install/postinstall script, version N+1 (typically a patch bump) adds one, especially on a package that never had a hook.
Install hook newly added in a patch or minor release
Why it matters
Account-takeover releases most often add exactly one thing: a lifecycle hook that runs the payload. A previously hook-free library gaining one is a much stronger signal than the hook alone.
How to verify manually
npm view <pkg> versions --jsonfor v in <old> <new>; do echo $v; npm view <pkg>@$v scripts --json; donenpm pack <pkg>@<old> <pkg>@<new> --ignore-scripts; mkdir a b; tar -xzf <pkg>-<old>.tgz -C a; tar -xzf <pkg>-<new>.tgz -C b; diff -ru a b | head -200
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
A maintainer may add a native build step or a postinstall message deliberately, and the change will be in the changelog and the git history. Rule out: the same change is a commit in the public repository, tagged, reviewed, with the hook doing something the release notes explain.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Seen in the wild
ua-parser-js (October 2021) and eslint-scope 3.7.2 (July 2018): both account-takeover releases added an install-time script that the previous version did not have.
Does our scanner check this today? Guidance only
Scanners look at one version at a time. No version-to-version diff is computed.
References
PSR-META-001No repository declared
lowINFORMATIONALnpm + PyPIMetadataChecked todayThe repository field in package.json is missing or empty; on PyPI there is no Source or Homepage URL.
No repository declared
Why it matters
Without a source link a reviewer has nothing to compare the artifact with, and typosquats usually omit it. Many honest private or tiny packages omit it too.
How to verify manually
npm view <pkg> repository homepage bugsSearch the registry description and README for a source link; check the maintainer's other packages.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Hobby and legacy packages often have no repository. Rule out: an otherwise consistent history, real adoption and a matching public repository found by name.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Checked today
Shown as "No source repo" (a warn signal worth -8 on the 0-100 score) in the Stoplight and Package Scanner, and as sp-no-source-repo / lp-no-repository in the engine.
References
PSR-META-002Empty or boilerplate description and README
lowINFORMATIONALnpm + PyPIMetadataChecked todayNo description, a README under a couple of lines, copy-pasted text from another package, or text that contradicts what the code does.
Empty or boilerplate description and README
Why it matters
Throwaway malicious packages are not worth documenting. A README copied from the squatted target is part of the disguise.
How to verify manually
npm view <pkg> description readme | head -40Search a distinctive sentence from the README to find the package it was copied from.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Internal helpers and early-stage libraries have sparse docs. Rule out: documentation and code agree, and other signals (history, maintainers, adoption) are healthy.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Checked today
sp-missing-readme flags a missing README or one under 50 characters. Copied or contradictory text is not detected.
References
PSR-META-003Repository URL is a placeholder, dead, or points at an unrelated project
mediumSUSPICIOUS-indicatornpm + PyPIMetadataGuidance onlyThe repository URL returns not found, is an obvious placeholder (user/repo), points at a repository whose name and contents have nothing to do with the package, or a bugs/homepage URL on a recently registered domain.
Repository URL is a placeholder, dead, or points at an unrelated project
Why it matters
It signals the metadata was filled to pass a glance. The reader sees a link; nothing behind it can be audited.
How to verify manually
npm view <pkg> repository.url homepage bugs.urlgit ls-remote <repo-url> # does it resolve?whois <homepage-domain> # created recently?
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Repositories get renamed, archived or deleted by honest maintainers, leaving stale links. Rule out: the repository moved (redirect, same owner) and an old release was built from it.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
The scanners check that a repository field exists, not whether the URL resolves or is related.
References
PSR-META-004Brand-new package with abnormal download growth
mediumSUSPICIOUS-indicatornpm + PyPIMetadataGuidance onlyA package created within days whose weekly downloads are in the thousands, with no dependents, stars or discussion, or downloads that spike on one day.
Brand-new package with abnormal download growth
Why it matters
Inflated counts make a package look trusted, and genuine spikes show a campaign or an automated install fan-out.
How to verify manually
npm view <pkg> time.createdcurl -s "https://api.npmjs.org/downloads/range/last-month/<pkg>" # look for a single-day spikeCheck the Dependents tab on the npmjs.com package page: a package with thousands of downloads and no dependents is odd.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Mirrors, CI caches and security scanners download new packages in volume, and a newly launched tool can really go viral. Rule out: coverage in the project's own channels and a public repository with commit history older than the package.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
Not checked. The Stoplight reads current weekly downloads and last-publish date only; it does not read first-publish date or download history.
References
PSR-META-005Many near-identical packages from one author
highSUSPICIOUS-indicatornpm + PyPIMetadataGuidance onlyOne account owns dozens of recently created packages with sequential or templated names, identical package.json structure, the same install hook and tiny code bodies.
Many near-identical packages from one author
Why it matters
Mass registration is how typosquat and dependency-confusion campaigns scale. The packages are cheap, disposable and often published in automated batches.
How to verify manually
npm search --searchlimit 100 "maintainer:<user>"npm view <pkg-a> scripts; npm view <pkg-b> scripts # identical?Diff two tarballs: they should differ only in the name.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Template-based monorepo families, scoped component libraries and generated SDKs share structure by design. Rule out: they share one repository and documented release process.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked. Packages are assessed one at a time.
References
PSR-META-006Very low adoption
lowINFORMATIONALnpmMetadataChecked todayFewer than 100 weekly downloads (npm).
Very low adoption
Why it matters
Popularity is not safety, but widely used packages get looked at. A package nobody else uses has had no community review and is where a one-off malicious package sits.
How to verify manually
curl -s "https://api.npmjs.org/downloads/point/last-week/<pkg>"npm view <pkg> time.created # combine with age: a new, unused package is the weak combination
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Plenty of valid niche or internal-turned-public packages have low numbers. This is an amber nudge, not a finding. Rule out by reading the code and checking the author.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Checked today
Package Stoplight and Package Scanner flag "Low usage" under 100 downloads a week (npm only, warn, -8 points). PyPI has no download count in the data used.
References
PSR-META-007No release for more than ten years
lowINFORMATIONALnpm + PyPIMetadataChecked todayThe latest version was published over ten years ago.
No release for more than ten years
Why it matters
Unmaintained code gets no security fixes and its registry account may be abandoned, which is a takeover target. The age alone says nothing about malice.
How to verify manually
npm view <pkg> time.modified dist-tags.latestnpm view <pkg> deprecated
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Small stable utilities need no updates for years. Rule out: the code is tiny, well understood and has no install hook or network use.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Checked today
Package Stoplight and Package Scanner show "Stale" when the last publish is over 3,650 days ago. The NPM Scanner engine has sp-abandoned-high-deps (over 1,095 days with 500+ dependents) but dependents are not populated, so it does not fire.
References
PSR-NAME-001Typosquat: name within one or two edits of a popular package
highSUSPICIOUS-indicatornpm + PyPIIdentity and namingPartly checkedName differs from a heavily downloaded package by a dropped, doubled, swapped or substituted character, a visually confusable pair (l/1, O/0, rn/m) or a changed delimiter (react-dom vs reactdom, lodash vs lodahs).
Typosquat: name within one or two edits of a popular package
Why it matters
Attackers rely on a mistyped npm install or pip install, an AI-suggested name or a copied typo. Installs happen automatically in CI, so one typo in a manifest is enough.
How to verify manually
npm view <pkg> name version time.created dist-tags repository.urlnpm view <popular-name> maintainers; compare to npm view <pkg> maintainers (should be unrelated for a typosquat)npm search <popular-name> --searchlimit 20 # see near-name neighbourspip index versions <pkg> # PyPI; compare normalised names, - _ and . are equivalent
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Many legitimate packages sit one edit from another (short names such as vue, zod, jest, next, ora; forks; ecosystem plugins). Rule out: independent repository with real history, a distinct purpose, a description that does not impersonate the popular package, and downloads that grew over time.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Seen in the wild
crossenv (2017), a typosquat of cross-env.
Does our scanner check this today? Partly checked
Package Stoplight and Package Scanner flag a name 1 to 2 Levenshtein edits from a built-in list of 20 popular npm and 20 popular PyPI names ("Possible typosquat", scored as a bad signal). The list is small, so most typosquats of other popular packages are missed, and short popular names produce false positives. The NPM Scanner engine policy sp-typosquat relies on an input the fetcher does not populate, so it does not fire.
References
PSR-NAME-002Combosquat: popular name plus a plausible suffix or prefix
highSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance onlyA popular package name combined with words such as -js, -node, -utils, -helper, -sdk, -official, -pro, -v2, -secure, -cli, or a brand prefix, published by an unrelated author with no link to the real project.
Combosquat: popular name plus a plausible suffix or prefix
Why it matters
Edit distance misses these because the name is far from the original in characters but close in meaning. A developer or an LLM looking for "the X helper" picks the squatted name.
How to verify manually
npm view <combo-name> repository.url maintainers time.createdOpen the real project's README and package.json: does it name this package as an official companion? If not, treat it as unrelated.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Official companion packages follow the same pattern (@types/*, <name>-cli, <name>-plugin-*). Rule out: the real project's documentation links to it, the maintainers overlap, and the scope or repository belongs to the same organisation.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked. Edit distance does not catch affixed names.
References
PSR-NAME-003Scope confusion: lookalike scope, or unscoped copy of a scoped package
highSUSPICIOUS-indicatornpmIdentity and namingGuidance onlyA scope one character from a trusted one (@angullar/core, @typs/node), an unscoped package named like a scoped one (core instead of @org/core), or a scoped package whose scope is a personal account while claiming to be a company product.
Scope confusion: lookalike scope, or unscoped copy of a scoped package
Why it matters
Scopes are the namespace a team trusts. A lookalike scope borrows that trust, and a missing scope is exactly what dependency confusion exploits.
How to verify manually
npm view @scope/pkg maintainers repository.urlnpm org ls <scope> # only for scopes you administerCompare the scope spelling character by character with the vendor's documentation.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Organisations sometimes register an unscoped alias for convenience, and personal scopes publish forks. Rule out: the vendor documents the alias and lists the maintainers.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-NAME-004Dependency confusion: internal-looking name on the public registry with a very high version
criticalSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance onlyA public package whose name matches your private or internal package (company-prefixed, -internal, -corp, a name seen in your own lockfile or repositories), published recently, with a version number far above your internal one (for example 99.x or a date-like version) and often an install hook.
Dependency confusion: internal-looking name on the public registry with a very high version
Why it matters
If the installer consults the public registry for a name that also exists privately, the highest version wins. The attacker needs only to guess or leak an internal name to get code execution on build servers.
How to verify manually
npm view <internal-name> time.created versions # does a public package exist at all?npm config get registry; npm config ls -l | grep -i scope # is your internal scope pinned to your private registry?pip index versions <internal-name>; pip config list # check extra-index-url vs index-urlCompare against your private registry's highest version for the same name.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
A real public package may share a generic name with an internal one by coincidence. Rule out: the public package is old, has real adoption and the maintainers have no connection to a name that appeared in your private manifests only.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Seen in the wild
Alex Birsan's dependency confusion research (February 2021) executed code inside many large companies this way. torchtriton on PyPI (December 2022) did the same to PyTorch nightly installs through a pip extra index.
Does our scanner check this today? Guidance only
The scanners do not know your private package names and do not read your registry configuration.
References
PSR-NAME-005Brandjacking: vendor or product name used by an unrelated publisher
mediumSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance onlyA package name containing a cloud, AI, security or payments brand (aws-, openai-, stripe-, claude-, wallet names) whose publisher, repository and homepage have no tie to that company.
Brandjacking: vendor or product name used by an unrelated publisher
Why it matters
Developers search the registry for "the SDK for X". Brand words make a squat look official, and AI coding assistants sometimes recommend names that were only invented by pattern.
How to verify manually
npm view <pkg> maintainers repository.url homepageCheck the vendor's own documentation for the official package name and scope.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Community wrappers and unofficial clients for well-known services are common and often legitimate. Rule out: the README says it is unofficial, the repository has history, and the code does only what it claims.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-NAME-006Star-jacking: repository link borrows a popular project's reputation
highSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance onlyThe repository field points at a well-starred repository whose own package.json (or pyproject.toml) has a different package name and whose releases do not publish this package, and no provenance ties the release to that repository.
Star-jacking: repository link borrows a popular project's reputation
Why it matters
Registry pages show repository stars and a link. Pointing the field at a famous repository makes a throwaway package look established. npm does not verify that the publisher controls the repo unless provenance is used.
How to verify manually
npm view <pkg> repository.url gitHeadIn the repository, search for the package name in package.json and in the release workflow. If it is absent, the link is borrowed.npm audit signatures # a valid provenance attestation names the real source repository and workflow
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Monorepos publish many packages from one repository, so the root package.json will not match; legitimate forks also point at upstream. Rule out: the package lives in a subdirectory declared in repository.directory, or the release workflow publishes it.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
The scanners only check whether a repository field exists, not whether it is genuine.
References
PSR-NAME-007Plausible but unreferenced name registered just after being suggested ("slopsquatting")
mediumSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance onlyA newly created package whose name looks like a natural helper for a popular library or task, has no dependents, no repository history and no external mentions, and appears in your code or a prompt answer after an AI assistant suggested it.
Plausible but unreferenced name registered just after being suggested ("slopsquatting")
Why it matters
Language models sometimes invent package names that sound right. Attackers register those names and wait for a developer to run the suggested install command.
How to verify manually
npm view <pkg> time.created dist-tags repository.url # created days ago, tiny historySearch the library's own documentation for the name. If the docs do not mention it, do not install it.Open the package page on npmjs.com (Dependents tab) or deps.dev: a plausible helper nobody depends on is the pattern.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Brand-new but real packages exist and their authors announce them. Rule out: the author or project announced it, source is public with commit history older than the package, and the install count is not just the author's own.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
Not checked. Package age (first publish date) is not read; the scanners use last-publish date only.
References
PSR-NET-001Hardcoded IP address as a network destination
highSUSPICIOUS-indicatornpm + PyPINetwork exfiltrationChecked todayA literal IPv4/IPv6 address used as the host of an HTTP request, socket or net.connect in package code (for example 203[.]0[.]113[.]7 in an example), including raw sockets to a numeric port.
Hardcoded IP address as a network destination
Why it matters
Real services are reached by name. A numeric destination avoids DNS-based blocking and reputation lookups and is typical of throwaway command-and-control or exfiltration servers.
How to verify manually
grep -rnE "([0-9]{1,3}\.){3}[0-9]{1,3}" package --include=*.js --include=*.cjs --include=*.mjs | grep -vE "127\.0\.0\.1|0\.0\.0\.0|localhost|version"whois <ip>; check the address in a threat-intel feed before connecting to it from anything.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Loopback and private ranges (127.0.0.1, 10.x, 192.168.x), documentation ranges in comments or tests, version strings that look like IPs, and public DNS resolver addresses (for example in a DNS library). Rule out by excluding those and confirming the remaining address is not used by request code.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Checked today
sp-hardcoded-ip: any dotted quad in the scanned source plus any of fetch/http/connect/socket anywhere in it. It does not exclude loopback or private ranges, so it over-fires.
References
PSR-NET-002Chat-app webhook or bot API used as a data sink
highMALICIOUS-indicatornpm + PyPINetwork exfiltrationGuidance onlyCalls to Discord webhook paths (/api/webhooks/), Telegram bot API (/bot<token>/sendMessage, sendDocument), Slack incoming webhooks or similar from a package whose purpose has nothing to do with messaging.
Chat-app webhook or bot API used as a data sink
Why it matters
Chat webhooks are free, TLS-protected, need no infrastructure and are rarely blocked on egress. Stealers use them as a drop box. A webhook URL embedded in a library is the attacker's mailbox.
How to verify manually
grep -rniE "discord(app)?\.com/api/webhooks|api\.telegram\.org/bot|hooks\.slack\.com/services" packageExample of what a defanged finding looks like: hxxps://discord[.]com/api/webhooks/<id>/<token>
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Notification libraries and chatbot SDKs call these APIs, but the webhook URL is supplied by the user at runtime. Rule out: the URL is a parameter or config value, not a literal hardcoded in the package.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-NET-003Paste, file-drop, request-capture or tunnel hosts in code
highSUSPICIOUS-indicatornpm + PyPINetwork exfiltrationGuidance onlyLiteral references to paste services, anonymous file hosts, request-inspection services or tunnelling domains (pastebin-style, transfer-style, webhook-capture, ngrok-style) used as a fetch source or upload target.
Paste, file-drop, request-capture or tunnel hosts in code
Why it matters
Paste sites serve as a movable payload host (stage two is a text file the attacker can edit); capture and tunnel hosts serve as exfiltration endpoints that need no domain registration.
How to verify manually
grep -rniE "paste|transfer\.sh|file\.io|webhook\.site|requestbin|ngrok|trycloudflare|localtunnel" package | grep -vE "README|\.md:"Check whether the host is contacted at install time: npm install --ignore-scripts first, then run the hook by hand in a sandbox with network logging.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Developer tooling for sharing logs or exposing a local server mentions these services, and test fixtures may include them in strings. Rule out: it is a documented feature that runs on explicit user command.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-NET-004DNS used as a data channel
highMALICIOUS-indicatornpm + PyPINetwork exfiltrationPartly checkeddns.lookup/resolve*, nslookup, dig, host or Python socket.gethostbyname on a hostname built by concatenating encoded local data (hostname, username, hex or base32 chunks) onto an attacker-controlled domain, usually in an install hook.
DNS used as a data channel
Why it matters
DNS passes through most egress controls. Encoding a username, hostname or token as subdomain labels leaks it to whoever runs the authoritative name server, and also confirms the install executed (a beacon).
How to verify manually
grep -rnE "dns\.(lookup|resolve|promises)|nslookup|dig |gethostbyname" packageLook for string building on the argument: `${os.hostname()}.${...}`. In a sandbox, log queries with a local resolver and look for long or high-entropy labels.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
DNS and networking libraries resolve names supplied by their caller, and some packages do a connectivity check against a fixed well-known name. Rule out: the name is fixed or user-provided, never assembled from machine data.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Partly checked
sp-dns-lookup-install matches dns.lookup, nslookup or "dig " in the preinstall/install/postinstall script strings. It does not look inside the files those hooks run, and does not judge whether the name is built from local data.
References
PSR-NET-005Mining pool or stratum endpoint
criticalMALICIOUS-indicatornpm + PyPINetwork exfiltrationChecked todaystratum+tcp:// style endpoints, well-known mining pool hostnames, or references to miners (coinhive, cryptonight, xmrig) in package code or in a downloaded binary name.
Mining pool or stratum endpoint
Why it matters
No library has a reason to connect to a mining pool. It is resource hijacking: the victim pays the compute and power bill.
How to verify manually
grep -rniE "stratum\+(tcp|ssl)|xmrig|cryptonight|coinhive|minexmr|nanopool|moneroocean" packageOn a machine that installed the package: top/htop for sustained high CPU from node or an unfamiliar binary.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Security research tooling, blocklists and documentation can contain pool names as data. Rule out: the strings sit in a blocklist or test fixture and nothing connects to them.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Seen in the wild
ua-parser-js (October 2021): hijacked releases installed a cryptocurrency miner.
Does our scanner check this today? Checked today
sp-crypto-miner-pattern matches stratum+tcp, pool.minexmr, coinhive and cryptonight in package JS source.
References
PSR-OBF-001eval or Function on decoded data
criticalMALICIOUS-indicatornpm + PyPIObfuscationChecked todayeval(), new Function(), vm.runInNewContext, Python exec()/eval()/compile() whose argument comes from atob, Buffer.from(x, "base64"|"hex"), zlib inflate, String.fromCharCode, base64.b64decode or a reversed or XOR-ed string.
eval or Function on decoded data
Why it matters
Decode-then-execute exists only to hide what runs. Honest code does not need to conceal its own logic from the people who install it.
How to verify manually
grep -rnE "eval\(|new Function\(|vm\.run|atob\(|fromCharCode|b64decode|exec\(" packageDecode the blob offline into a text file (base64 -d blob.txt > out.bin; file out.bin; strings out.bin | less) and read it. Never feed it to node, python or sh.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Template engines, bundlers, REPLs, sandboxes and code generators use Function or vm with code they were handed. Rule out: the executed string comes from the caller's input, not from a literal embedded in the package.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Seen in the wild
event-stream (2018): the flatmap-stream dependency carried an encrypted payload decrypted and run only inside a specific victim application.
Does our scanner check this today? Checked today
sp-obfuscated-eval matches eval( immediately followed by atob, Buffer.from or decode. Other decode or Function patterns are not matched.
References
PSR-OBF-002Base64 decode piped into exec or eval
criticalMALICIOUS-indicatornpm + PyPIObfuscationChecked todayBuffer.from(<literal>, "base64") (or Python base64.b64decode) whose result reaches exec, execSync, eval or subprocess on the same statement or the next few lines, typically with a long literal.
Base64 decode piped into exec or eval
Why it matters
This is the compact "unpack and run" idiom. A long base64 literal passed to a command runner is a staged payload, not data.
How to verify manually
grep -rnE "Buffer\.from\(.{40,}, *['\"]base64" packageDecode to a file with base64 -d and read it as text. Check whether it is a script, an ELF/PE header (file <out>) or a URL.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Packages that embed small base64 assets (icons, WASM, fonts) decode them, but pass them to image or WebAssembly APIs, not to exec. Rule out by following the decoded variable to its sink.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Checked today
sp-base64-exec matches Buffer.from(..., "base64") followed by exec or eval. Python is not covered.
References
PSR-OBF-003Obfuscator signatures: hex identifiers and string-array rotators
highSUSPICIOUS-indicatornpmObfuscationGuidance onlySource dominated by identifiers like _0x4f2a1b, a large array of escaped strings accessed through a lookup function, an immediately-invoked rotation loop on that array, long runs of \x41-style escapes, or very high Shannon entropy in string literals.
Obfuscator signatures: hex identifiers and string-array rotators
Why it matters
These are the fingerprints of javascript-obfuscator style tools. Open source libraries have no reason to make their own logic unreadable; malware does, to defeat grep and human review.
How to verify manually
grep -c "_0x[0-9a-f]\{4,\}" <file>Pretty-print a copy (npx prettier --write copy.js) and look for the string array and rotator at the top; do not run it.Compare with the repository: the same file in git should be readable source.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Some commercial or anti-tamper browser SDKs ship deliberately obfuscated bundles. Rule out: the vendor documents it, the package is from the vendor's own scope, and the obfuscated file corresponds to a build artifact in their repository.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
No entropy, hex-identifier or string-array detection exists.
References
PSR-OBF-004Minified or packed code in a package that claims to be source
mediumSUSPICIOUS-indicatornpmObfuscationPartly checkedMain entry files of a few very long lines (for example one 200 KB line), no source map, no matching src/ in the tarball, and a repository whose source for that file is readable and different.
Minified or packed code in a package that claims to be source
Why it matters
Unreadable shipped code that does not match the repository is how a reviewer is stopped from diffing a release. The repo looks clean; the tarball is not.
How to verify manually
tar -xzf <pkg>-<version>.tgz && wc -l package/<main> && wc -c package/<main>git clone <repo> && git checkout <tag>; build the package from source and diff the output with the tarball (npm pack in the repo, then diff -r).
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Many legitimate libraries ship a minified browser bundle (dist/*.min.js). Rule out: it is in a dist folder, has a .map, matches a reproducible build from the tagged commit, and the unminified entry point is also present.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Partly checked
sp-minified-obfuscated: concatenated source over 1,000 characters with fewer than 5 lines and no sourceMappingURL. Minified .min.js files are excluded from the fetch, so it only judges the unminified files it receives.
References
PSR-OBF-005Dynamic code loading with computed names
mediumSUSPICIOUS-indicatornpm + PyPIObfuscationChecked todayrequire() or import() with a computed argument, require(["fs","child"+"_process"].join("")), globalThis["ev"+"al"], Python __import__ or getattr(builtins, ...) with assembled strings.
Dynamic code loading with computed names
Why it matters
Assembling a module or function name at runtime defeats static greps for child_process, fs or eval. The only reason to hide the name is to evade the grep.
How to verify manually
grep -rnE "require\( *[^'\"]|\[.{1,20}\]\.join\(|globalThis\[|__import__|getattr\(builtins" package
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Plugin systems, loaders, bundlers and test runners use dynamic require with a path the user provides. Rule out: the computed value comes from configuration or an argument, not from string fragments in the package.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Checked today
sp-dynamic-require matches require( followed by anything other than a quote. It cannot distinguish plugin loaders from evasion, so treat a hit as a lead.
References
PSR-OBF-006Invisible or bidirectional Unicode in source
highSUSPICIOUS-indicatornpm + PyPIObfuscationGuidance onlyBidirectional control characters (U+202A to U+202E, U+2066 to U+2069), zero-width characters (U+200B, U+200C, U+200D, U+2060, U+FEFF mid-file) or large invisible-character runs in source files or string literals.
Invisible or bidirectional Unicode in source
Why it matters
Bidirectional controls can make code display differently from how it executes (Trojan Source). Zero-width runs can carry an encoded payload that is invisible in a diff.
How to verify manually
grep -rnP '[\x{202A}-\x{202E}\x{2066}-\x{2069}\x{200B}-\x{200D}\x{2060}]' packagegit diff --word-diff-regex=. with a viewer that shows control characters (cat -A, or an editor with "render control characters").
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Right-to-left language strings, emoji with zero-width joiners and localisation files legitimately contain these. Rule out: the characters are inside translation or emoji data, not in code or identifiers.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-PUB-001Ownership transfer or new maintainer on an established package
highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance onlyA maintainer added to or replacing the original on an established package, followed within days or weeks by a release. In PyPI terms: new Owner/Maintainer on the project, or a PEP 541 name reassignment.
Ownership transfer or new maintainer on an established package
Why it matters
Taking over trust that already has an install base is cheaper than building it. Maintainer handover, abandoned-package takeovers and hijacked accounts all look like a new name on an old package.
How to verify manually
npm view <pkg> maintainers --json; npm view <pkg> time --json # compare dates to the maintainer changes you know aboutfor v in <last-old> <first-new>; do npm view <pkg>@$v _npmUser; done # who actually published eachnpm owner ls <pkg>
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Healthy projects add co-maintainers and hand over to successors, usually with a public issue or announcement. Rule out: an announcement in the repository, the new maintainer has existing commits and a history on the project.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Seen in the wild
event-stream (2018): the original maintainer passed publishing rights to a volunteer who later added the malicious flatmap-stream dependency.
Does our scanner check this today? Guidance only
The NPM Scanner engine defines sp-package-transferred and lp-recent-ownership-change, but npm-api.ts does not populate the ownership input, so they do not fire. The Stoplight shows maintainer count only.
References
PSR-PUB-002Release by a publisher who has never published this package, or a long-dormant package suddenly republished
highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsPartly checkedThe _npmUser of the new version differs from every earlier version, or the gap since the previous release is years long and the new version changes install behaviour or dependencies.
Release by a publisher who has never published this package, or a long-dormant package suddenly republished
Why it matters
A stolen token or account publishes as a different identity from the usual maintainer. A dormant package is a prime target because nobody is watching its releases.
How to verify manually
npm view <pkg> time --json | tail -20for v in $(npm view <pkg> versions --json | jq -r ".[-5:][]"); do echo $v $(npm view <pkg>@$v _npmUser.name); done
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Teams rotate maintainers, a revived project may restart after years, and CI bots publish as a dedicated user. Rule out: the project announced the revival, the new publisher appears on the maintainers list, or provenance links the release to the repository's workflow.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Partly checked
The Stoplight reports "Stale" when the last publish is over 10 years old. It does not compare publishers or detect a revival after a gap.
References
PSR-PUB-003Published version has no matching git tag or commit
highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance onlyThe registry has a version, but the repository has no tag, release or commit for it, or gitHead / the provenance commit does not exist in the declared repository.
Published version has no matching git tag or commit
Why it matters
Releases built from a developer machine or a stolen token skip the repository entirely. A release the public source never saw cannot have been reviewed.
How to verify manually
npm view <pkg>@<version> gitHead repository.urlgit ls-remote --tags <repo> | grep <version>git cat-file -t <gitHead> # inside a clone
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Many maintainers tag differently (v prefix, a monorepo scheme, tags created after publish) and some publish from a release branch. Rule out: the commit exists, the tarball matches that commit (see PSR-PUB-006), and tags use a consistent pattern.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-PUB-004Sudden version jump or out-of-pattern version number
mediumSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance onlyA new version far above the established series (1.4.2 followed by 9.9.9 or 99.0.0), a calendar-style version on a semver package, or many versions published in a short burst.
Sudden version jump or out-of-pattern version number
Why it matters
A very high version wins resolution in dependency confusion and in loosely pinned ranges. A burst can mean iterating on a payload until it evades checks.
How to verify manually
npm view <pkg> versions --jsonnpm view <pkg> time --json # look for many entries within minutes
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Real major bumps and calendar versioning exist, and some projects publish many pre-releases while stabilising. Rule out: a changelog or release note explains the jump, and the tag exists in the repository.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-PUB-005Tarball contents differ from the repository at the release commit
criticalMALICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance onlyFiles, install hooks or code present in the published tarball or wheel but not produced by the project's own build from the tagged source (extra files, changed dependencies, added scripts).
Tarball contents differ from the repository at the release commit
Why it matters
The repository is what reviewers read; the registry artifact is what runs. Any code that exists only in the artifact bypassed review by definition.
How to verify manually
npm pack <pkg>@<version> --ignore-scripts; mkdir pkg; tar -xzf <pkg>-<version>.tgz -C pkggit clone <repo> src && cd src && git checkout <tag> && npm ci --ignore-scripts && npm run build && npm pack --ignore-scriptsdiff -ru <built-package> ../pkg/package # package.json scripts and dependencies first, then any extra file
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Builds are not always reproducible (timestamps, minifier versions, generated files). Rule out: differences are limited to generated or compiled output and the package.json (scripts, dependencies, files) is identical.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked. No repository checkout or diff is performed.
References
PSR-PUB-006Build provenance dropped from a package that used to have it
highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsPartly checkedEarlier versions carry a signed provenance attestation (npm provenance, PyPI attestations via a trusted publisher) and the latest version does not.
Build provenance dropped from a package that used to have it
Why it matters
A maintainer who moved to trusted publishing rarely goes back. A release without attestation after a run of attested ones suggests it was published with a stolen token from outside the pipeline.
How to verify manually
for v in <v1> <v2> <latest>; do echo $v; npm view <pkg>@$v dist.attestations --json; donenpm audit signatures # in a project that depends on the packagePyPI: the project file listing shows a provenance/attestation section for files published through a trusted publisher
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
A maintainer may publish a hotfix by hand or switch CI. Rule out: the project explains it in the changelog and the next release restores attestations.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Partly checked
The Stoplight and Package Scanner read whether the latest version has an attestation and show it as information. It is never scored and never compared with earlier versions, so a drop is not detected.
References
PSR-PUB-007Valid provenance, but the source repository or workflow is not the expected one
mediumINFORMATIONALnpm + PyPIMaintainer and publish signalsPartly checkedThe attestation is cryptographically valid but names a repository, workflow file or branch different from the project's known pipeline (a fork, a new workflow, a feature branch).
Valid provenance, but the source repository or workflow is not the expected one
Why it matters
Provenance proves where a build ran, not that the code is safe. An attacker who controls a repository or a maintainer account can publish properly attested releases. The useful question is whether the origin matches the one you expect.
How to verify manually
npm view <pkg>@<version> dist.attestations.url # then read the predicate: repository, ref, workflowCompare with the previous release's attestation: same repo, same workflow path, same ref pattern?
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Projects move repositories or rename workflows. Rule out: the change is announced and consistent across all of the project's packages.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Partly checked
Attestation presence is displayed with a note that it proves origin, not intent. The workflow and repository named inside it are not read or compared.
References
PSR-PUB-008Publisher email domain expired or re-registered
highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance onlyA maintainer email address on a domain that is now expired, newly re-registered, or parked, so whoever buys it can use password reset on the registry account.
Publisher email domain expired or re-registered
Why it matters
Registry accounts are recoverable by email. A lapsed domain turns into account takeover without breaking any password or 2FA, and the package keeps its good name and install base.
How to verify manually
npm view <pkg> maintainers --json # note the email domainswhois <domain> # creation date after the package's first release is the red flagdig +short MX <domain>
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Domains are sometimes legitimately renewed after a lapse. Rule out: registration history shows continuity, or the maintainer changed address on the registry and confirms it publicly.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-PUB-009One account publishing new versions across many packages at once
criticalMALICIOUS-indicatornpmMaintainer and publish signalsGuidance onlyA single maintainer account publishes new versions of many unrelated packages within hours, often each with the same new install hook or the same added file.
One account publishing new versions across many packages at once
Why it matters
This is the worm pattern: stolen tokens are used to republish everything the victim can publish, which turns one compromised developer into a whole family of poisoned packages.
How to verify manually
npm search --searchlimit 100 "maintainer:<user>"for p in <list>; do npm view $p time.modified dist-tags.latest; done # recent modification clusteringCheck the same account's packages for an identical new file or scripts entry.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Maintainers of monorepos or package families release together (for example a framework's scoped packages). Rule out: the release is a coordinated version bump with a tag and changelog, not a new hook or file.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked. Scanning is per package, with no cross-package view of a publisher.
References
PSR-PUB-010High-download package with a single maintainer
lowINFORMATIONALnpmMaintainer and publish signalsChecked todayOne registry maintainer and a large weekly download count (this library uses over 500,000).
High-download package with a single maintainer
Why it matters
Not a sign of attack. It marks single points of failure: one stolen credential or one burnt-out maintainer affects every dependent.
How to verify manually
npm view <pkg> maintainers --jsoncurl -s "https://api.npmjs.org/downloads/point/last-week/<pkg>"
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Most small, excellent libraries have one author. This is context for a risk register, not a finding. Mitigate with pinned versions and delayed adoption instead of treating it as suspicious.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Checked today
NPM Scanner policy sp-single-maintainer-high-dl and lead policies lp-single-maintainer / lp-few-maintainers.
References
PSR-PYPI-001setup.py overrides install commands (cmdclass) to run code
highSUSPICIOUS-indicatorPyPIPyPI specificsGuidance onlyA setup.py that sets cmdclass to custom install, develop, egg_info or build_py classes whose run() calls subprocess, os.system, network functions or file writes outside the build directory.
setup.py overrides install commands (cmdclass) to run code
Why it matters
Building from an sdist executes setup.py with the installer's privileges before anything is imported. A custom install class is the Python equivalent of an npm postinstall hook.
How to verify manually
curl -s "https://pypi.org/pypi/<pkg>/<version>/json" | jq -r ".urls[] | select(.packagetype==\"sdist\") | .url" # download the sdist, do not pip-install ittar -xzf <pkg>-<version>.tar.gz && grep -nE "cmdclass|subprocess|os\.system|urllib|requests|socket" <pkg>-<version>/setup.py
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
C-extension builds, code generation and Cython projects override build commands with local compiler calls. Rule out: the override only invokes compilers or generators on files inside the project and makes no network call.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
The Stoplight and Package Scanner read PyPI JSON metadata only. setup.py is never fetched or read.
References
PSR-PYPI-002Side effects at import time in __init__.py or top-level modules
highSUSPICIOUS-indicatorPyPIPyPI specificsGuidance onlyModule-level code in __init__.py (or the first module imported) that starts threads, makes network calls, reads credentials or environment, spawns subprocesses or writes files, without any function being called.
Side effects at import time in __init__.py or top-level modules
Why it matters
import <package> is the one action every user performs. Code at module level runs for every importer, including test runs and CI, with no installer step to review.
How to verify manually
unzip -p <wheel>.whl "<pkg>/__init__.py" | head -120python -X importtime -c "import <pkg>" in a sandbox with network logging, or inspect with strace -f -e trace=network,openat
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Packages register plugins, configure logging or perform lazy-import setup at import time. Rule out: nothing at module level touches the network, subprocess, credentials or files beyond reading its own data.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked. Python source is not read by either scanner.
References
PSR-PYPI-003.pth file containing an import or exec line
criticalMALICIOUS-indicatorPyPIPyPI specificsGuidance onlyA .pth file in the wheel or sdist (installed into site-packages) containing a line that begins with "import" or contains exec, os.system, subprocess, base64 or a URL.
.pth file containing an import or exec line
Why it matters
Python processes .pth files at interpreter start-up, and lines starting with import are executed. The code therefore runs on every Python launch, not only when the package is imported, and survives importing nothing.
How to verify manually
unzip -l <wheel>.whl | grep "\.pth$"unzip -p <wheel>.whl "<name>.pth"On a machine: find "$(python -c 'import site;print(site.getsitepackages()[0])')" -name "*.pth" -exec grep -lE "^import|exec|subprocess" {} +
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Editable installs and some namespace packages use .pth files, usually with plain directory paths. Rule out: every line is a plain path, or a documented, short import used by a well-known packaging tool.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked. Wheel and sdist contents are not opened.
References
PSR-PYPI-004Wheel and sdist for the same release do not match
highMALICIOUS-indicatorPyPIPyPI specificsGuidance onlyDifferent file lists, setup.py / metadata contents, install requirements or entry points between the sdist and the wheel(s) of one version, or a file present in one artefact and absent in the repository.
Wheel and sdist for the same release do not match
Why it matters
Reviewers typically read the repository or the sdist, while installers prefer the wheel. Any code only in the wheel never went through the review path, and the mismatch is the signal.
How to verify manually
unzip -l <wheel>.whl | awk "{print \$4}" | sort > wheel.txt; tar -tzf <pkg>-<version>.tar.gz | sed "s#^[^/]*/##" | sort > sdist.txt; diff wheel.txt sdist.txtCompare METADATA Requires-Dist between the wheel and the sdist PKG-INFO.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Wheels contain compiled extensions and exclude tests, docs and build files that sdists include, and metadata is regenerated by the build backend. Rule out: differences are compiled output, license/docs/tests, and dependency lists match.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-PYPI-005sdist-only release or unusual build backend for a pure-Python package
mediumINFORMATIONALPyPIPyPI specificsGuidance onlyA pure-Python project published only as an sdist (no wheel), or a pyproject.toml whose build-system requires or build-backend points at a URL, VCS source, in-tree module or an unfamiliar package.
sdist-only release or unusual build backend for a pure-Python package
Why it matters
An sdist forces a build on install, which means project-controlled code runs at install. A non-standard build backend is code that executes during that build.
How to verify manually
curl -s "https://pypi.org/pypi/<pkg>/<version>/json" | jq ".urls[] | {packagetype, filename}"tar -xzf <sdist> && cat <dir>/pyproject.toml | sed -n "/build-system/,/^$/p"pip download <pkg>==<version> --no-deps --only-binary=:all: -d dl # fetches a wheel if one exists, runs no build
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Small projects often publish only an sdist, and legitimate in-tree backends exist for large projects. Rule out: backend is a well-known one (setuptools, hatchling, flit-core, poetry-core, pdm-backend) and no code runs in setup.py or backend hooks beyond packaging.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-PYPI-006Install resolves a private name from the public index (extra-index-url)
criticalSUSPICIOUS-indicatorPyPIPyPI specificsGuidance onlyA dependency name that exists on your private index and also on PyPI, with pip configured to use --extra-index-url; the installer chooses the highest version across both.
Install resolves a private name from the public index (extra-index-url)
Why it matters
pip treats all configured indexes as equals. An attacker who publishes your internal name publicly with a higher version gets installed instead of your own package.
How to verify manually
pip config list; grep -rn "extra-index-url" requirements*.txt pip.conf pyproject.tomlpip index versions <internal-name> # which versions does PyPI offer for your internal name?Prefer --index-url pointing to a proxy that serves a single namespace, plus --require-hashes.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
The public package may genuinely be yours (open-sourced), or an unrelated project may share a generic name. Rule out: the public project is verifiably yours, or the name is reserved by you on PyPI.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Seen in the wild
torchtriton (December 2022): a PyPI package took precedence over the PyTorch nightly dependency of the same name served from a separate index.
Does our scanner check this today? Guidance only
The scanners cannot see your index configuration or private package names.
References
PSR-RUN-001Filesystem walk of the home directory for secrets
highMALICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance onlyRecursive directory traversal starting at os.homedir(), ~, /home, C:\Users or the working directory with filename filters such as .env, id_rsa, *.pem, wallet, credentials, keystore, followed by reads of the matches.
Filesystem walk of the home directory for secrets
Why it matters
Looking for secrets by pattern is what a stealer does. Libraries operate on paths their caller gives them; they do not sweep the user's disk.
How to verify manually
grep -rnE "homedir\(\)|readdirSync|walk\(|glob\(|os\.walk|Path\.home\(\)" package | grep -iE "env|pem|key|wallet|credential|token"strace -f -e trace=openat,getdents64 node install-script.js # in a throwaway VM with decoy secrets
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Linters, bundlers, file watchers and secret scanners walk directories by design. Rule out: they start from the project directory passed by the user, and nothing read leaves the machine.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-RUN-002Process listing or security-tool enumeration
mediumSUSPICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance onlyCalls to ps, tasklist, wmic process, /proc enumeration or psutil.process_iter with checks for names of debuggers, EDR agents, antivirus, monitoring tools or analysis tools.
Process listing or security-tool enumeration
Why it matters
Malware lists running processes to decide whether it is being watched and to avoid detection or find high-value targets.
How to verify manually
grep -rniE "tasklist|wmic|ps -|/proc/|process_iter|processList" packageLook for comparison against a list of tool names (wireshark, procmon, defender, crowdstrike, tcpdump).
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Process managers, port-killers and dev servers list processes to find their own children. Rule out: lookups are for the tool's own processes or a PID the caller passes.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-RUN-003Clipboard monitoring or crypto-address swapping
criticalMALICIOUS-indicatornpmBehavioural and runtimePartly checkedClipboard read/write loops (pbpaste, xclip, GetClipboardData, navigator.clipboard) or, in browser bundles, wrapping of fetch, XMLHttpRequest or wallet provider objects (window.ethereum) with regex matches for wallet address formats and replacement with a different address.
Clipboard monitoring or crypto-address swapping
Why it matters
Replacing the destination address in a transaction the user believes they are signing steals funds without touching the user's keys. A browser-side hook in a widely used dependency reaches every site that bundles it.
How to verify manually
grep -rniE "clipboard|pbpaste|xclip|GetClipboardData|window\.ethereum|XMLHttpRequest\.prototype|window\.fetch *=" packageSearch for address-shaped regexes: 0x[a-fA-F0-9]{40}, bc1, T[A-Za-z0-9]{33}, and hardcoded replacement address lists.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Copy-to-clipboard UI helpers use clipboard writes on an explicit click, and web3 libraries use window.ethereum by design. Rule out: clipboard access only happens inside a user-triggered handler, and a wallet library never rewrites addresses it did not generate.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Seen in the wild
chalk, debug and related packages (September 2025): a phished maintainer account published releases containing browser code that swapped cryptocurrency addresses.
Does our scanner check this today? Partly checked
sp-clipboard-access matches the words clipboard, xclip, pbpaste or GetClipboardData in source, which also hits ordinary copy buttons. Provider hooking and address-pattern replacement are not detected.
References
PSR-RUN-004TLS certificate verification disabled
mediumSUSPICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance onlyNODE_TLS_REJECT_UNAUTHORIZED = "0", https.Agent({ rejectUnauthorized: false }), requests.get(verify=False), ssl._create_unverified_context, set programmatically inside a dependency rather than by the user.
TLS certificate verification disabled
Why it matters
Turning off verification lets a network position intercept anything the package sends, and malware sets it so its own exfiltration works against hosts with self-signed or mismatched certificates.
How to verify manually
grep -rnE "NODE_TLS_REJECT_UNAUTHORIZED|rejectUnauthorized *: *false|verify *= *False|_create_unverified_context" package
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Tests, local development and tools that target self-signed internal servers set it, usually behind a flag. Rule out: it is opt-in via an option or environment variable, documented, and not set at module load.
Recommended action
Allow with review. Fine to keep once a person has looked and the checks above came back clean.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-RUN-005Sandbox, VM or analysis-environment evasion checks
highMALICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance onlyChecks of hostname, username, CPU count, RAM, uptime, MAC address prefix, DMI or BIOS strings, installed hypervisor tools, or CI environment variables, used to skip the payload when the environment looks like an analysis box; typically with a beacon sending those fingerprints out.
Sandbox, VM or analysis-environment evasion checks
Why it matters
Package registries and security vendors detonate new packages in sandboxes. Malware that stays dormant there passes review and fires only on real machines.
How to verify manually
grep -rniE "vmware|virtualbox|qemu|hypervisor|sandbox|os\.cpus\(\)\.length|os\.totalmem|os\.uptime|networkInterfaces|dmidecode" packageRun the install in two environments (a plain VM and a realistic developer image) and compare network and file activity.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Telemetry libraries read OS, CPU and CI details for anonymous statistics, usually with documented opt-out. Rule out: data is limited to coarse fields, disclosed, and sent only to the vendor.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-RUN-006Conditional payload: date, locale, timezone or IP-geolocation gate
highSUSPICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance onlyCode branches on Date.now() against a hardcoded future timestamp, a long setTimeout delay, Intl.DateTimeFormat().resolvedOptions().timeZone / locale / LANG, or an IP-geolocation lookup, with the sensitive action inside the branch.
Conditional payload: date, locale, timezone or IP-geolocation gate
Why it matters
Time bombs outlast the review window; geo or locale gates hit chosen victims and stay quiet everywhere else, including in the sandbox of an analyst in another country.
How to verify manually
grep -rnE "new Date\(.{0,40}20[2-3][0-9]|setTimeout\([^,]+, *[0-9]{7,}|resolvedOptions\(\)\.(timeZone|locale)|process\.env\.(LANG|LC_ALL)|geoip|ipinfo|ip-api" packageChange the system clock/timezone/locale in a VM and rerun to see whether behaviour changes.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Licence-expiry checks, localisation, scheduling libraries and analytics use dates and locales. Rule out: the branch changes presentation or language, not file, process or network behaviour.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-RUN-007Protestware: destructive or disruptive action conditioned on location or politics
criticalMALICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance onlyCode that checks country, language or IP and then deletes or overwrites files, injects a message or loops forever; or a maintainer-added infinite loop or sabotage in a previously stable release.
Protestware: destructive or disruptive action conditioned on location or politics
Why it matters
Whatever the author's motive, a dependency that damages the systems it runs on has violated the trust the install granted. It is a supply-chain integrity event even when announced.
How to verify manually
grep -rnE "unlinkSync|rmSync|rm -rf|writeFileSync" package | head # then check the surrounding conditionRead the changelog and issues for the version; protestware is usually discussed within hours.npm view <pkg> deprecated # npm often deprecates affected versions
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Cleanup utilities delete files by design. Rule out: deletion targets a path the caller passes, never a fixed system or user path, and is not conditioned on locale or geography.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Seen in the wild
colors and faker (January 2022): maintainer-added sabotage. node-ipc (March 2022): a release that overwrote files on machines geolocated to certain countries.
Does our scanner check this today? Guidance only
Not checked.
References
PSR-RUN-008Keystroke or input capture
highMALICIOUS-indicatornpm + PyPIBehavioural and runtimePartly checkedRaw-mode stdin listeners used to record input, global keyboard hooks (iohook, uiohook, pynput.keyboard.Listener, keyboard module), or input fields hooked in browser code with sends to a remote host.
Keystroke or input capture
Why it matters
Input capture records passwords, tokens and commands typed by the user. No ordinary library needs a system-wide keyboard hook.
How to verify manually
grep -rniE "iohook|uiohook|pynput|keyboard\.(on|hook)|setRawMode|keypress|keydown" package
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
CLI prompt libraries (inquirer, readline wrappers), TUI frameworks, games and accessibility tools read keys on their own terminal or page. Rule out: input is read only on the program's own stdin or page and is not logged or transmitted.
Recommended action
Quarantine. Hold it out of builds until a person has verified it.
Does our scanner check this today? Partly checked
sp-keylogger-pattern matches readline+keypress, process.stdin with rawMode, iohook and uiohook. It also matches legitimate CLI prompt libraries, so a hit needs manual review.
References
PSR-RUN-009Package modifies other installed packages or its own files
highMALICIOUS-indicatornpm + PyPIBehavioural and runtimePartly checkedWrites into node_modules, site-packages, __dirname or another package's directory, or re-packaging and publishing with the user's token, as a way to spread or persist.
Package modifies other installed packages or its own files
Why it matters
Self-modification and cross-package writes are propagation mechanics. They turn one install into a foothold across the dependency tree or into new poisoned releases.
How to verify manually
grep -rnE "node_modules|site-packages|writeFileSync\(.*__dirname|npm publish|twine upload" packageHash node_modules before and after the install in a throwaway VM and diff the results.
Inspect archives with tar and unzip. Do not run a package's code to check it; if you must detonate, use a disposable VM with no real credentials.
Common false positives, and how to rule them out
Patch-package style tools and postinstall patchers deliberately modify other packages on explicit instruction. Rule out: documented, applied to a named target, driven by files committed to your repo.
Recommended action
Block. Do not install; remove from manifests and lockfiles and start incident triage if it already ran.
Does our scanner check this today? Partly checked
sp-self-update matches writeFileSync to __dirname or writeFile to node_modules in source. Publishing commands and Python are not checked.
References
Frequently asked questions
How can I tell if an npm package is malicious?
Look for a combination of signals, not one. The strongest are install-time code that downloads or executes remote content, reads of credential files or the whole environment followed by a network call, decoded-then-evaluated payloads, and a release whose tarball does not match the repository. Weaker signals (a typosquat-like name, no repository, a new maintainer, very low usage) raise suspicion but need a second signal. Each rule in this library says what to look at and gives the commands to check it.
What is the difference between a malicious indicator and a suspicious indicator?
A malicious indicator describes behaviour that has no legitimate reason in a library: for example sending environment variables to a remote host, or writing to ~/.ssh in an install script. One high-severity malicious indicator is enough to block. A suspicious indicator is a capability or pattern that has common innocent uses (a native build step, minified bundles, a new co-maintainer), so it needs corroboration or a human check. Informational rules give context and never decide on their own.
Does an install script (postinstall) mean a package is malware?
No. Many legitimate packages use install scripts for native builds or to download a checksum-verified prebuilt binary. The risky pattern is a hook that fetches code, pipes it to a shell, runs encoded commands, or touches shell profiles, SSH keys and registry config. A hook added for the first time in a patch release is a much stronger signal than a hook that has always been there.
Can a package with a valid provenance attestation still be malicious?
Yes. Provenance shows where a build ran, not that the code is safe. If a maintainer account or release pipeline is compromised, the real pipeline can publish attested malicious releases. That is why our scanners report provenance as information and never let it raise a score. Check whether the attestation names the repository and workflow you expect, and read the diff.
What does an amber result in the Package Stoplight mean?
Amber means "look closer", not "malicious". The Stoplight starts from 60, adds points for good signals (maintained, popular, source linked, no known advisories) and removes points for weak ones; a total from 50 to 74 is amber. Typical causes are low weekly downloads, no repository, or a very old last release. Open the rules for the reasons it shows (PSR-META-006, PSR-META-001, PSR-META-007) and run the manual checks. Amber can also appear when a check could not run; check again later.
Which of these rules does PlayCISO actually check?
Fewer than the library lists, and each rule is labelled honestly. The Package Stoplight and Package Scanner check registry metadata, typosquat distance against a small list of popular names, advisories via OSV, usage and age, and report provenance. The NPM Scanner policy engine adds pattern checks on package source and install scripts. Most rules here are guidance for your own review until we build the check.
Is the library free? What costs money?
Browsing every rule, the search, filters and the manual verification steps are free. The CSV export of the full library with every field is a Pro download.
Keep going
- Package Stoplight: red, amber or green for each dependency.
- NPM Scanner: the policy engine behind the pattern checks.
- Install Inspector: practise reading a package before it runs.
- GitHub supply-chain attack anatomy: one compromise, step by step.
- npm package security scanning: a practical guide
- Dependency confusion attacks on npm and PyPI
Defensive reference only. Examples are defanged and no working malware or live malicious address appears here. Incidents are cited only where the public record is clear. Rule IDs are stable. Last updated 2026-11-03.
More security tools
The tools people use most on PlayCISO.
Catch injected instructions hiding in content your AI agent should only read.
Score your readiness against the first international standard for AI security threats.
Critical CVEs across 21 major vendors, month by month — and what the 2026 surge means.
Build an ordered ruleset, replay labelled packets and catch shadowed and any-to-any rules.
Self-assess all six Functions, from Govern to Recover, and see your weakest one.
Search 172 threat actors across Microsoft, CrowdStrike, Mandiant and community names.