| name | supply-chain-security |
| description | Typosquatting detection, install script analysis, dependency confusion prevention, and phantom dependency detection for npm/pip. |
Supply Chain Security
Patterns for auditing third-party dependencies before they own your production environment.
Typosquatting Detection
Common substitution patterns attackers use against popular packages:
def levenshtein(a: str, b: str) -> int:
if len(a) < len(b):
return levenshtein(b, a)
if not b:
return len(a)
prev = list(range(len(b) + 1))
for i, ca in enumerate(a):
curr = [i + 1]
for j, cb in enumerate(b):
curr.append(min(prev[j + 1] + 1, curr[j] + 1, prev[j] + (ca != cb)))
prev = curr
return prev[-1]
POPULAR_PACKAGES = ['express', 'lodash', 'react', 'axios', 'moment', 'chalk']
def is_typosquat(pkg: str, threshold: int = 2) -> list[str]:
return [p for p in POPULAR_PACKAGES if 0 < levenshtein(pkg, p) <= threshold]
typosquats = {
'lodahs': 'lodash',
'expres': 'express',
'reakt': 'react',
'axois': 'axios',
'momnet': 'moment',
}
Common substitution patterns to check manually:
- Character transposition:
lodash → lodahs
- Missing character:
express → expres
- Extra character:
chalk → cchalk
- Hyphen/underscore swap:
my-lib → my_lib
- Homoglyph:
rn instead of m in package name
Install Script Audit
npm query ":root > *" | \
node -e "const d=require('/dev/stdin');
Object.entries(d.dependencies||{}).forEach(([k,v])=>{
const pkg = require(\`./node_modules/\${k}/package.json\`);
if(pkg.scripts?.preinstall||pkg.scripts?.postinstall||pkg.scripts?.install)
console.log(k, Object.keys(pkg.scripts))
})"
npm pack lodash --dry-run
npm show <package> scripts
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
function auditInstallScripts(nodeModulesDir: string): void {
const packages = fs.readdirSync(nodeModulesDir)
for (const pkg of packages) {
const pkgJsonPath = path.join(nodeModulesDir, pkg, 'package.json')
if (!fs.existsSync(pkgJsonPath)) continue
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'))
const scripts = pkgJson.scripts ?? {}
const suspicious = ['preinstall', 'postinstall', 'install']
.filter(s => s in scripts)
if (suspicious.length > 0) {
console.warn(`[WARN] ${pkg} has install scripts: ${suspicious.join()}`)
.()
}
}
}
()
Dependency Confusion Risk Assessment
INTERNAL_PACKAGES=("@mycompany/auth" "@mycompany/utils" "internal-logger")
for pkg in "${INTERNAL_PACKAGES[@]}"; do
result=$(npm show "$pkg" version 2>/dev/null)
if [ -n "$result" ]; then
echo "RISK: $pkg exists on public registry at version $result"
else
echo "OK: $pkg not on public registry"
fi
done
Prevention in .npmrc:
@mycompany:registry=https://npm.mycompany.com
@internal:registry=https://npm.mycompany.com
//npm.mycompany.com/:always-auth=true
Phantom Dependency Detection
npx depcheck --ignores="@types/*" .
pip install pipreqs
pipreqs . --print | sort > required.txt
pip freeze | sort > installed.txt
diff required.txt installed.txt
import { execSync } from 'child_process'
import fs from 'fs'
function detectPhantomDeps(srcDir: string): void {
const pkgJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
const declared = new Set([
...Object.keys(pkgJson.dependencies ?? {}),
...Object.keys(pkgJson.devDependencies ?? {}),
])
const imports = execSync(
`rg --no-heading -oh "from ['\"]([^./][^'\"]+)['\"]" ${srcDir} | sed "s/from ['\"]//;s/['\"]//;s/\\/.*//"`
).toString().split('\n').filter(Boolean)
const unique = new Set(imports)
for (const imp of unique) {
const root = imp.startsWith('@') ? imp.split('/').(, ).() : imp.()[]
(!declared.(root)) {
.()
}
}
}
Lock File Integrity Verification
npm ci --dry-run
npm install --package-lock-only
git diff --exit-code package-lock.json
pip install --require-hashes -r requirements.txt
pip-compile --generate-hashes requirements.in -o requirements.txt
npm audit / pip-audit Integration
npm audit --audit-level=high
pip install pip-audit
pip-audit --requirement requirements.txt --fail-on CRITICAL,HIGH
- name: Security audit
run: |
npm audit --audit-level=moderate
npx better-npm-audit audit --level moderate
SBOM Generation
npm install -g @cyclonedx/cyclonedx-npm
cyclonedx-npm --output-format JSON --output-file sbom.json
pip install cyclonedx-bom
cyclonedx-bom -r -o sbom.xml
syft . -o spdx-json > sbom.spdx.json
Known Malicious Package Indicators
Red flags when reviewing a new dependency:
- Install scripts that curl | bash external URLs
- Package published < 48 hours ago with thousands of weekly downloads
- Zero issues/stars on GitHub but high download count
- package.json "main" points to an obfuscated file
- Dependencies on packages with random hex names
- Author email is a free provider with a new account
- Version bump from 1.x to 99.x (dependency confusion attack)
Dependency Pinning Strategy
{
"dependencies": {
"express": "4.18.2",
"lodash": "4.17.21"
},
"devDependencies": {
"typescript": "^5.3.0"
}
}
npm shrinkwrap
{
"extends": ["config:base"],
"automerge": false,
"reviewers": ["security-team"]
}
Dependency Risk Scoring
Score each dependency on a 0-100 risk scale before adoption:
| Signal | Low Risk (0-2) | Medium Risk (3-5) | High Risk (6-10) |
|---|
| Age | > 3 years | 1-3 years | < 1 year |
| Maintainers | 3+ active | 1-2 active | 1 inactive |
| Downloads | > 1M/week | 100K-1M/week | < 100K/week |
| GitHub stars | > 5K | 500-5K | < 500 |
| Last commit | < 3 months | 3-12 months | > 12 months |
| Open issues | < 50 | 50-200 | > 200 unresolved |
| Install scripts | None | postinstall (build) | preinstall + network |
| Dependencies | < 5 | 5-20 | > 20 transitive |
| CVE history | 0 unfixed | Fixed promptly | Unfixed > 30 days |
| License | MIT/Apache/BSD | LGPL/MPL | GPL/AGPL/Unknown |
Risk Assessment Template
Package: <name>@<version>
Purpose: <why we need it>
Alternatives: <what else we considered>
Signals:
Age: X years (score: N)
Maintainers: X active (score: N)
Downloads: X/week (score: N)
Last commit: X days ago (score: N)
Dependencies: X transitive (score: N)
CVEs: X unfixed (score: N)
Total Risk Score: XX/100
Decision: [ADOPT | REVIEW | REJECT]
Reviewed by: <name>
Date: <date>
Automated Risk Check
npm show <package> time modified maintainers repository
curl -s "https://api.npmjs.org/downloads/point/last-week/<package>" | jq .downloads
npm audit --json | jq '.vulnerabilities["<package>"]'
pip show <package>
pip-audit -r requirements.txt --format json
Rule: Never npm install <unknown-package> in production without running npm audit, checking install scripts, and verifying the package name against known typosquats.
Dependency risk scoring adapted from Trail of Bits supply-chain-risk-auditor plugin.