Skip to content
← All tools

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.

Check a package now

Type a name or paste a package.json and get a red, amber or green light, then come back here to see why.

Open Package Stoplight
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-001

Pre-built executable or shared library shipped in a JavaScript or Python package

highSUSPICIOUS-indicatornpm + PyPINative and binary payloadsPartly checked

Files 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.

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 only

A .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.

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-003

Embedded archives, blobs or data files decoded and run at runtime

highSUSPICIOUS-indicatornpm + PyPINative and binary payloadsGuidance only

Large 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.

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 first
  • file 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-004

File type does not match its extension

highSUSPICIOUS-indicatornpm + PyPINative and binary payloadsGuidance only

A file named .png, .jpg, .txt, .css or .json whose magic bytes identify an executable, archive or script (MZ, \x7fELF, PK, #!/bin/sh).

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-001

Package writes CI workflow or pipeline files

criticalMALICIOUS-indicatornpm + PyPICI/CD specificPartly checked

Code 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.

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" package
  • git 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-002

Reads runner OIDC or Actions tokens and secrets context

criticalMALICIOUS-indicatornpm + PyPICI/CD specificGuidance only

References 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.

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" package
  • Run 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-003

Package repository runs untrusted pull-request code with secrets (pull_request_target)

highSUSPICIOUS-indicatornpm + PyPICI/CD specificGuidance only

The 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.

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/workflows
  • Look 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-004

Release workflow uses a long-lived publish token or unpinned actions

mediumINFORMATIONALnpm + PyPICI/CD specificGuidance only

The 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.

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/workflows
  • npm view <pkg>@<version> dist.attestations # trusted publishing produces attestations
  • git 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-005

Unexpected repositories, branches or workflow runs created under the maintainer's account

highMALICIOUS-indicatornpm + PyPICI/CD specificGuidance only

New 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.

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,visibility
  • gh run list --repo <owner>/<repo> --limit 20
  • Review 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-001

Wholesale read of the environment

highSUSPICIOUS-indicatornpm + PyPICredential and secret harvestingChecked today

Object.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.

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" package
  • grep -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-002

Environment read followed by an outbound request

criticalMALICIOUS-indicatornpm + PyPICredential and secret harvestingChecked today

Environment 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.

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 request
  • Run 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-003

Reads 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.

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" package
  • npm 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-004

Reads cloud provider credential files

criticalMALICIOUS-indicatornpm + PyPICredential and secret harvestingGuidance only

Access 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.

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" package
  • Check 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-005

Reads SSH private keys or known_hosts

criticalMALICIOUS-indicatornpm + PyPICredential and secret harvestingGuidance only

Reads of ~/.ssh/id_rsa, id_ed25519, id_ecdsa, known_hosts or config, or a directory walk of ~/.ssh by package code.

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" package
  • In 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-006

Reads browser profiles or crypto wallet stores

criticalMALICIOUS-indicatornpm + PyPICredential and secret harvestingGuidance only

Paths 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.

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-007

Reads source-control and CI tokens

highMALICIOUS-indicatornpm + PyPICredential and secret harvestingGuidance only

GITHUB_TOKEN, GH_TOKEN, GITLAB_TOKEN, ~/.config/gh/hosts.yml, ~/.git-credentials, or invocation of gh auth token / git credential fill from package code.

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" package
  • Review 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-001

New dependency on an obscure package added in a patch or minor release

highSUSPICIOUS-indicatornpm + PyPIDependency graphGuidance only

A 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.

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-002

Dependency pinned to a git URL, tarball URL or file path

mediumSUSPICIOUS-indicatornpm + PyPIDependency graphGuidance only

A 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).

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-003

Unpinned dependency range (*, latest, open ">=")

mediumINFORMATIONALnpm + PyPIDependency graphChecked today

dependencies or devDependencies declared as "*", "latest" or an open-ended range, so the next install resolves to whatever was published most recently.

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-004

Lockfile integrity hash missing, changed or off-registry

highSUSPICIOUS-indicatornpm + PyPIDependency graphGuidance only

A 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.

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 disagree
  • npm audit signatures
  • pip 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-005

Package appears in a malicious-package advisory

criticalMALICIOUS-indicatornpm + PyPIDependency graphPartly checked

The 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).

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 entries
  • npm audit
  • Search 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-006

Known vulnerability (CVE or GHSA) in the package

mediumINFORMATIONALnpm + PyPIDependency graphChecked today

The package or version has one or more published vulnerability advisories in OSV, GitHub Advisories or the registry audit.

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 --json
  • osv-scanner --lockfile package-lock.json
  • pip-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-001

Install hook downloads remote content

criticalMALICIOUS-indicatornpmInstall-time executionChecked today

A 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.

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 --json
  • npm pack <pkg>@<version> --ignore-scripts && tar -xzOf <pkg>-<version>.tgz package/package.json
  • tar -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-002

Install hook pipes a download into a shell

criticalMALICIOUS-indicatornpmInstall-time executionPartly checked

A hook line of the form "download | sh", "download | bash", "sh -c \"$(download)\"" or "node -e \"$(download)\"": fetched text executed without being saved or hashed.

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.install
  • npm 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-003

Install hook runs encoded PowerShell or cmd

criticalMALICIOUS-indicatornpmInstall-time executionGuidance only

powershell/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".

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>.tgz
  • grep -rniE "powershell|pwsh|-enc|EncodedCommand|ExecutionPolicy|WindowStyle" package/package.json package/*.js package/scripts 2>/dev/null
  • echo "<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-004

Install hook runs node -e with eval or Function

highSUSPICIOUS-indicatornpmInstall-time executionGuidance only

A hook such as node -e "..." or node -p "..." whose inline program contains eval, new Function, require("child_process"), Buffer.from(..., "base64") or a URL.

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-005

Install hook spawns a shell or child process

highSUSPICIOUS-indicatornpmInstall-time executionPartly checked

preinstall/install/postinstall runs sh, bash, cmd, exec, spawn, or a script that calls child_process.exec/spawn with shell: true and non-constant arguments.

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 --json
  • npm 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-006

Install-time write to shell profile, SSH, npmrc or cron

criticalMALICIOUS-indicatornpm + PyPIInstall-time executionPartly checked

Code 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.

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" package
  • After 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-007

Package declares an install-time hook at all

lowINFORMATIONALnpmInstall-time executionChecked today

package.json contains scripts.preinstall, scripts.install or scripts.postinstall (or a binding.gyp, which npm turns into an implicit node-gyp install script).

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_modules
  • npm view <pkg>@<version> scripts
  • npm 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-008

Install hook newly added in a patch or minor release

highSUSPICIOUS-indicatornpmInstall-time executionGuidance only

Version 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.

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 --json
  • for v in <old> <new>; do echo $v; npm view <pkg>@$v scripts --json; done
  • npm 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-001

No repository declared

lowINFORMATIONALnpm + PyPIMetadataChecked today

The repository field in package.json is missing or empty; on PyPI there is no Source or Homepage URL.

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 bugs
  • Search 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-002

Empty or boilerplate description and README

lowINFORMATIONALnpm + PyPIMetadataChecked today

No description, a README under a couple of lines, copy-pasted text from another package, or text that contradicts what the code does.

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 -40
  • Search 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-003

Repository URL is a placeholder, dead, or points at an unrelated project

mediumSUSPICIOUS-indicatornpm + PyPIMetadataGuidance only

The 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.

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.url
  • git 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-004

Brand-new package with abnormal download growth

mediumSUSPICIOUS-indicatornpm + PyPIMetadataGuidance only

A package created within days whose weekly downloads are in the thousands, with no dependents, stars or discussion, or downloads that spike on one day.

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.created
  • curl -s "https://api.npmjs.org/downloads/range/last-month/<pkg>" # look for a single-day spike
  • Check 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-005

Many near-identical packages from one author

highSUSPICIOUS-indicatornpm + PyPIMetadataGuidance only

One account owns dozens of recently created packages with sequential or templated names, identical package.json structure, the same install hook and tiny code bodies.

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-006

Very low adoption

lowINFORMATIONALnpmMetadataChecked today

Fewer than 100 weekly downloads (npm).

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-007

No release for more than ten years

lowINFORMATIONALnpm + PyPIMetadataChecked today

The latest version was published over ten years ago.

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.latest
  • npm 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-001

Typosquat: name within one or two edits of a popular package

highSUSPICIOUS-indicatornpm + PyPIIdentity and namingPartly checked

Name 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).

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.url
  • npm 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 neighbours
  • pip 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-002

Combosquat: popular name plus a plausible suffix or prefix

highSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance only

A 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.

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.created
  • Open 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-003

Scope confusion: lookalike scope, or unscoped copy of a scoped package

highSUSPICIOUS-indicatornpmIdentity and namingGuidance only

A 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.

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.url
  • npm org ls <scope> # only for scopes you administer
  • Compare 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-004

Dependency confusion: internal-looking name on the public registry with a very high version

criticalSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance only

A 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.

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-url
  • Compare 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-005

Brandjacking: vendor or product name used by an unrelated publisher

mediumSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance only

A 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.

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 homepage
  • Check 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-006

Star-jacking: repository link borrows a popular project's reputation

highSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance only

The 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.

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 gitHead
  • In 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-007

Plausible but unreferenced name registered just after being suggested ("slopsquatting")

mediumSUSPICIOUS-indicatornpm + PyPIIdentity and namingGuidance only

A 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.

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 history
  • Search 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-001

Hardcoded IP address as a network destination

highSUSPICIOUS-indicatornpm + PyPINetwork exfiltrationChecked today

A 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.

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-002

Chat-app webhook or bot API used as a data sink

highMALICIOUS-indicatornpm + PyPINetwork exfiltrationGuidance only

Calls 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.

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" package
  • Example 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-003

Paste, file-drop, request-capture or tunnel hosts in code

highSUSPICIOUS-indicatornpm + PyPINetwork exfiltrationGuidance only

Literal 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.

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-004

DNS used as a data channel

highMALICIOUS-indicatornpm + PyPINetwork exfiltrationPartly checked

dns.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.

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" package
  • Look 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-005

Mining pool or stratum endpoint

criticalMALICIOUS-indicatornpm + PyPINetwork exfiltrationChecked today

stratum+tcp:// style endpoints, well-known mining pool hostnames, or references to miners (coinhive, cryptonight, xmrig) in package code or in a downloaded binary name.

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" package
  • On 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-001

eval or Function on decoded data

criticalMALICIOUS-indicatornpm + PyPIObfuscationChecked today

eval(), 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.

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\(" package
  • Decode 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-002

Base64 decode piped into exec or eval

criticalMALICIOUS-indicatornpm + PyPIObfuscationChecked today

Buffer.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.

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" package
  • Decode 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-003

Obfuscator signatures: hex identifiers and string-array rotators

highSUSPICIOUS-indicatornpmObfuscationGuidance only

Source 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.

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-004

Minified or packed code in a package that claims to be source

mediumSUSPICIOUS-indicatornpmObfuscationPartly checked

Main 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.

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-005

Dynamic code loading with computed names

mediumSUSPICIOUS-indicatornpm + PyPIObfuscationChecked today

require() or import() with a computed argument, require(["fs","child"+"_process"].join("")), globalThis["ev"+"al"], Python __import__ or getattr(builtins, ...) with assembled strings.

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-006

Invisible or bidirectional Unicode in source

highSUSPICIOUS-indicatornpm + PyPIObfuscationGuidance only

Bidirectional 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.

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}]' package
  • git 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-001

Ownership transfer or new maintainer on an established package

highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance only

A 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.

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 about
  • for v in <last-old> <first-new>; do npm view <pkg>@$v _npmUser; done # who actually published each
  • npm 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-002

Release by a publisher who has never published this package, or a long-dormant package suddenly republished

highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsPartly checked

The _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.

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 -20
  • for 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-003

Published version has no matching git tag or commit

highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance only

The 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.

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.url
  • git 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-004

Sudden version jump or out-of-pattern version number

mediumSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance only

A 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.

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 --json
  • npm 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-005

Tarball contents differ from the repository at the release commit

criticalMALICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance only

Files, 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).

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 pkg
  • git clone <repo> src && cd src && git checkout <tag> && npm ci --ignore-scripts && npm run build && npm pack --ignore-scripts
  • diff -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-006

Build provenance dropped from a package that used to have it

highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsPartly checked

Earlier versions carry a signed provenance attestation (npm provenance, PyPI attestations via a trusted publisher) and the latest version does not.

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; done
  • npm audit signatures # in a project that depends on the package
  • PyPI: 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-007

Valid provenance, but the source repository or workflow is not the expected one

mediumINFORMATIONALnpm + PyPIMaintainer and publish signalsPartly checked

The 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).

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, workflow
  • Compare 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-008

Publisher email domain expired or re-registered

highSUSPICIOUS-indicatornpm + PyPIMaintainer and publish signalsGuidance only

A 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.

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 domains
  • whois <domain> # creation date after the package's first release is the red flag
  • dig +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-009

One account publishing new versions across many packages at once

criticalMALICIOUS-indicatornpmMaintainer and publish signalsGuidance only

A 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.

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 clustering
  • Check 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-010

High-download package with a single maintainer

lowINFORMATIONALnpmMaintainer and publish signalsChecked today

One registry maintainer and a large weekly download count (this library uses over 500,000).

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 --json
  • curl -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-001

setup.py overrides install commands (cmdclass) to run code

highSUSPICIOUS-indicatorPyPIPyPI specificsGuidance only

A 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.

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 it
  • tar -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-002

Side effects at import time in __init__.py or top-level modules

highSUSPICIOUS-indicatorPyPIPyPI specificsGuidance only

Module-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.

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 -120
  • python -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 only

A .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.

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-004

Wheel and sdist for the same release do not match

highMALICIOUS-indicatorPyPIPyPI specificsGuidance only

Different 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.

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.txt
  • Compare 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-005

sdist-only release or unusual build backend for a pure-Python package

mediumINFORMATIONALPyPIPyPI specificsGuidance only

A 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.

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-006

Install resolves a private name from the public index (extra-index-url)

criticalSUSPICIOUS-indicatorPyPIPyPI specificsGuidance only

A 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.

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.toml
  • pip 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-001

Filesystem walk of the home directory for secrets

highMALICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance only

Recursive 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.

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-002

Process listing or security-tool enumeration

mediumSUSPICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance only

Calls 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.

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" package
  • Look 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-003

Clipboard monitoring or crypto-address swapping

criticalMALICIOUS-indicatornpmBehavioural and runtimePartly checked

Clipboard 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.

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 *=" package
  • Search 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-004

TLS certificate verification disabled

mediumSUSPICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance only

NODE_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.

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-005

Sandbox, VM or analysis-environment evasion checks

highMALICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance only

Checks 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.

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" package
  • Run 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-006

Conditional payload: date, locale, timezone or IP-geolocation gate

highSUSPICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance only

Code 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.

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" package
  • Change 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-007

Protestware: destructive or disruptive action conditioned on location or politics

criticalMALICIOUS-indicatornpm + PyPIBehavioural and runtimeGuidance only

Code 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.

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 condition
  • Read 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-008

Keystroke or input capture

highMALICIOUS-indicatornpm + PyPIBehavioural and runtimePartly checked

Raw-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.

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-009

Package modifies other installed packages or its own files

highMALICIOUS-indicatornpm + PyPIBehavioural and runtimePartly checked

Writes 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.

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" package
  • Hash 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

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.

The tools people use most on PlayCISO.

All tools
🖤 Black Friday: new customers get 3 months of Pro for $5, then $5/month. Ends 30 Nov. Claim 3 months for $5 →