Stop supply chain attacks before they reach your pipeline.
Sentinel audits npm package updates using three independent security validators and a Byzantine fault-tolerant consensus engine — the same class of algorithm that keeps distributed systems honest even when individual nodes lie.
The event-stream attack (2018) compromised 2 million projects. A trusted
package was transferred to a malicious maintainer, who published a patch update
containing an encrypted cryptocurrency wallet stealer. It passed every existing
security check because it looked like a routine update.
The attack vector is almost always the same:
- Attacker gains control of a trusted package
- Publishes a patch or minor update — low scrutiny
- The new version introduces network calls, shell execution, or dynamic
eval - The payload runs silently during
npm installor at runtime
You can't trust any single signal. A package can have a clean reputation, pass a malware scanner, and still phone home your environment variables.
Sentinel runs three independent validators on every update. No single validator is trusted absolutely. The final verdict requires a supermajority — at least 2 out of 3 validators must agree.
This is Byzantine fault tolerance applied to dependency security: even if one validator is fooled or bypassed, two honest validators can still reach the correct verdict.
Package Update (old → new)
│
├──────────────────────────────────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Validator 1 │ │ Validator 2 │ │ Validator 3 │
│ ENTROPY │ │ BEHAVIORAL │ │ METADATA │
│ │ │ │ │ │
│ Shannon bits │ │ Dangerous API │ │ Install scripts │
│ per byte of │ │ fingerprinting │ │ Maintainer count │
│ source code │ │ │ │ Deprecation │
│ │ │ exec, eval, │ │ Native binaries │
│ Obfuscation │ │ net, fs, │ │ │
│ = entropy │ │ process.env │ │ │
│ spike │ │ │ │ │
└───────┬───────┘ └────────┬────────┘ └────────┬─────────┘
│ │ │
└────────────────────────┼──────────────────────────┘
│
▼
┌─────────────────────────┐
│ BYZANTINE CONSENSUS │
│ │
│ 2+ REJECT → REJECT │
│ 1 REJECT → WARN │
│ 2+ WARN → WARN │
│ 3 APPROVE → APPROVE │
└─────────────────────────┘
Computes Shannon entropy of source code before and after the update. A patch bump that suddenly introduces high-entropy content (obfuscated strings, encoded payloads, compressed blobs) is flagged. Thresholds scale by version bump type: a patch is held to stricter entropy stability than a major rewrite.
Scans for new dangerous API categories introduced by the update:
| Category | Severity | Pattern |
|---|---|---|
exec_access |
critical | child_process, exec, spawn |
eval_usage |
critical | eval(), Function(), vm.runIn |
net_access |
high | https, fetch, XMLHttpRequest |
install_scripts |
high | preinstall, postinstall |
fs_access |
medium | fs.readFileSync, fs.promises |
env_access |
medium | process.env, process.argv |
crypto_access |
low | crypto.* |
The key insight: Sentinel flags APIs that are new to this update, not just
present. A package that has always used fs is not suspicious. A package that
suddenly gains child_process in a patch update is.
Inspects the npm registry metadata for the new version: lifecycle scripts that
run on install, deprecation flags, native compilation requirements (node-gyp),
and single-maintainer packages with elevated risk.
Requires Python 3.10+. No external dependencies — uses only the standard library.
# Clone
git clone https://github.com/amadoalvarez/sentinel.git
cd sentinel
# Run directly
python3 sentinel.py lodash 4.17.20 4.17.21
# Or install as a CLI tool
pip install -e .
sentinel lodash 4.17.20 4.17.21
# Verbose mode shows entropy numbers and escalated capabilities
sentinel express 4.18.0 4.19.0 -v$ sentinel lodash 4.17.20 4.17.21
============================================================
SENTINEL — Analyzing lodash
4.17.20 → 4.17.21
============================================================
[*] Fetching package metadata...
[*] Downloading 4.17.20...
[*] Downloading 4.17.21...
[*] Extracting code files...
Old: 12 files, New: 12 files
[*] Running validators...
[+] Entropy: APPROVE — Entropy delta 0.0104 within patch threshold
[+] Behavioral: APPROVE — No new dangerous API categories introduced
[+] Metadata: APPROVE — Metadata checks passed
============================================================
[+] CONSENSUS: APPROVED (supermajority consensus)
============================================================
Exit code: 0 — safe to proceed.
The following simulates the event-stream pattern: an innocent formatting
package where the new version adds a hidden file that collects environment
variables, runs shell commands, encrypts the output, and exfiltrates it over
HTTPS.
=== FULL ATTACK SIMULATION ===
Entropy: REJECT — Entropy delta 0.3277 exceeds patch threshold 0.3
Behavioral: REJECT — CRITICAL: New exec_access capability detected
NEW: crypto_access (low)
NEW: exec_access (critical)
NEW: env_access (medium)
NEW: net_access (high)
Metadata: APPROVE
CONSENSUS: REJECT
=== ATTACK CORRECTLY DETECTED ===
Exit code: 1 — block the update.
Validators → Consensus
─────────────────────────────────
3x APPROVE → APPROVE
2x APPROVE, 1x WARN → APPROVE
2x WARN, 1x APPROVE → WARN
1x REJECT, 2x OK → WARN
2x REJECT, 1x OK → REJECT
3x REJECT → REJECT
A single compromised or noisy validator cannot produce a false REJECT or slip a malicious package through as APPROVE. Two validators must agree.
python3 test_sentinel.pyFull output from the test suite:
Sentinel Test Suite
========================================
--- Entropy Validator ---
[OK] Clean patch: APPROVE — delta=0.0104
[OK] Suspicious patch: WARN — delta=0.1461
entropy: 4.1325 → 3.9864
--- Behavioral Validator ---
[OK] Clean behavioral: APPROVE
[OK] New network detected: WARN — HIGH: New net_access capability detected
Escalated: ['net_access']
[OK] Exec injection detected: REJECT — CRITICAL: New exec_access capability detected
[OK] Eval injection detected: REJECT — CRITICAL: New eval_usage capability detected
--- Byzantine Consensus ---
[OK] Consensus: 3 approve → APPROVE
[OK] Consensus: 1 reject + 2 approve → WARN
[OK] Consensus: 2 reject + 1 approve → REJECT
[OK] Consensus: 2 warn + 1 approve → WARN
--- Full Attack Simulation ---
=== FULL ATTACK SIMULATION ===
Entropy: REJECT — Entropy delta 0.3277 exceeds patch threshold 0.3
Behavioral: REJECT — CRITICAL: New exec_access capability detected
NEW: crypto_access (low)
NEW: exec_access (critical)
NEW: env_access (medium)
NEW: net_access (high)
Metadata: APPROVE
CONSENSUS: REJECT
=== ATTACK CORRECTLY DETECTED ===
========================================
ALL TESTS PASSED
Add this step to your workflow to audit npm dependency updates before they land:
name: Dependency Security Audit
on:
pull_request:
paths:
- 'package.json'
- 'package-lock.json'
jobs:
sentinel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Clone Sentinel
run: git clone https://github.com/amadoalvarez/sentinel.git /tmp/sentinel
- name: Detect changed packages
id: changed
run: |
# Extract changed package name and versions from package-lock.json diff
# Adapt this to your lockfile format (npm/yarn/pnpm)
git diff HEAD~1 HEAD -- package-lock.json | grep '"version"' | head -20
- name: Run Sentinel audit
run: |
# Example: audit a specific package update detected in the PR
python3 /tmp/sentinel/sentinel.py ${{ env.PACKAGE_NAME }} \
${{ env.OLD_VERSION }} \
${{ env.NEW_VERSION }}
# Exit code 1 = REJECT or WARN → blocks merge#!/bin/bash
# .git/hooks/pre-commit
# Audit any npm packages bumped in this commit
SENTINEL_PATH="/path/to/sentinel/sentinel.py"
# Check if package.json changed
if git diff --cached --name-only | grep -q "package.json"; then
echo "[Sentinel] package.json changed — run sentinel audit manually:"
echo " python3 $SENTINEL_PATH <package> <old> <new>"
fi.PHONY: audit-dep
audit-dep:
@python3 path/to/sentinel.py $(PKG) $(OLD) $(NEW)
# Usage: make audit-dep PKG=lodash OLD=4.17.20 NEW=4.17.21Byzantine fault tolerance is a property of distributed systems: the ability to reach correct consensus even when some participants are faulty, slow, or actively malicious. The name comes from the Byzantine Generals Problem (Lamport, Shostak, Pease — 1982).
Supply chain security has the same structure. Each validator is an independent observer with different blind spots:
- The entropy validator can be fooled by well-formatted malicious code
- The behavioral scanner can be fooled by obfuscated API names
- The metadata validator only sees registry data, not code
No single validator is reliable in isolation. Requiring supermajority agreement raises the cost of a successful bypass: an attacker must simultaneously defeat multiple independent detection mechanisms.
Further reading:
- The Byzantine Generals Problem (1982)
- event-stream incident analysis
- Supply chain attacks in the npm ecosystem
- SLSA: Supply-chain Levels for Software Artifacts
- Automatic lockfile diffing (detect which packages changed)
- PyPI and Go module support
- Maintainer change detection (compares across versions)
- SHA256 integrity verification against npm registry dist hash
- JSON output mode for programmatic consumption
- Parallel validator execution
Issues and pull requests are welcome. Run the test suite before submitting:
python3 test_sentinel.pyAll tests must pass. New validators should include at least two tests: one clean case and one that demonstrates the attack pattern it catches.
MIT — see LICENSE.
Created by Amado Alvarez Sueiras.