Dependency risk: penthera deps
Starter. For “is this package safe to add?”
Dependency risk: what can your packages do?
A lockfile change is code you did not write, about to run with your app’s access to files, network, environment and CI secrets.penthera deps reads
package-lock.json, statically scans every installed package, and tells you
what each one can do, and which packages are new or gained capabilities in an
upgrade.
Every capability carries
file:line evidence. It is a regex-and-tokenizer pass,
not a full parser: deterministic, never executes package code, and fast enough
for every PR (0.4 s for Penthera’s own 61 packages, 5 to 16 s for real app
trees of 500 to 1,700 packages). A capability is what code
can do, not proof that it is malicious; the findings carry confidence
labels (potential for obfuscation and typosquats).
Only installed code is read. A package that is in the lockfile but not in
node_modules (or .venv) or npm’s cache is listed as not read, never as
clean. On a fresh clone that is every package:
node_modules or from npm’s local cache (~/.npm/_cacache), looked up by the
lockfile’s integrity hash and verified byte for byte. --online is the only
network use: it asks the npm registry for publish dates, publisher changes and
deprecations, and fetches old tarballs the cache does not have (verified the
same way). To comment the capability diff on every PR that changes the
lockfile, use the GitHub Action below.
npm and Python
The same command reads Python projects:uv.lock, poetry.lock, pdm.lock,
Pipfile.lock or requirements*.txt (pinned and --hash lines), and the
installed .venv / venv (or --venv <path>) through
site-packages/*.dist-info RECORD files, without running pip or Python. A
repo with both a package-lock.json and Python inputs gets one combined report.
subprocess/os.system,
socket/requests/httpx/aiohttp, os.environ bulk access,
eval/exec/compile, computed __import__, native .so/.pyd, AI-agent
indicators), plus what is specific to Python: setup.py custom commands
(install-time code; risky when it downloads and runs: a shell next to network
code, a download or send command, a recon command like whoami, pip install
of more packages, or decoded code, in setup.py or in the helper modules it
imports, three levels deep, including packages under src/. Running git, a
compiler or a build tool, or exec of a local version file, is not risky),
code that runs on import (module-level code, or a local function it calls,
that makes a network call or runs a download or send command: medium; tests,
scripts, examples and files outside a package do not count), a chat bot token
or a webhook URL with its key in a file with network code (high), a file that
downloads a program and starts it or makes it executable within 15 lines, or
runs a response with exec (high, “runs code it downloads”), statements
hidden after 150 or more spaces, and exec of an encoded literal through a
renamed decoder, .pth files with import lines and a shipped
sitecustomize.py or usercustomize.py (they run on every interpreter start,
so installing the package is enough to run them: medium, high when the code
they load is risky), including ones a wheel places at the site-packages root
from <name>-<version>.data/purelib/ or platlib/, as the malicious litellm
1.107.0 did, unpickling, exec of a
base64/zlib-decoded payload, installed files that no longer match their
RECORD hash, and .pth files no distribution owns. Old versions for the diff
come from pip’s HTTP cache (matched by the lockfile’s sha256) or uv’s cache
(by recorded hash), otherwise they are marked “not compared”. Typosquats are
checked against the top 8,000 PyPI projects, PEP 503 normalized. Known-malicious
PyPI versions, dropped files and endpoints come from the ioc-pypi-2026 rule
pack and match whether or not the package is installed.
--online registry checks are npm-only for now.
License policy
penthera deps applies the licenses section of
penthera.policy.json when there is one:
Licenses come from the lockfile (npm records them in v2 and v3), else from
the installed
package.json. Expressions are parsed as SPDX:
MIT OR GPL-3.0-only passes permissive because one branch is enough, while
MIT AND GPL-3.0-only does not. Deprecated ids (GPL-3.0, GPL-2.0+) are
matched to their current form, so a deny of AGPL-3.0-only also catches a
package that says AGPL-3.0. A breach and UNLICENSED (no rights granted)
are high. A missing, custom (SEE LICENSE IN ...) or non-SPDX license is
medium, or high with "unknown": "block". allowPackages exempts packages your
legal review has cleared. Findings use category license and appear in every
output format, and --sbom records each component’s license.
Back to the README · All commands