Vibecosystem supply-chain-security

Typosquatting detection, install script analysis, dependency confusion prevention, and phantom dependency detection for npm/pip.

install
source · Clone the upstream repo
git clone https://github.com/vibeeval/vibecosystem
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/vibeeval/vibecosystem "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/supply-chain-security" ~/.claude/skills/vibeeval-vibecosystem-supply-chain-security && rm -rf "$T"
manifest: skills/supply-chain-security/SKILL.md
source content

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:

# Levenshtein distance check against known-good package list
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]

# Examples of known typosquats
typosquats = {
    'lodahs': 'lodash',       # character swap
    'expres': 'express',      # missing char
    'reakt': 'react',         # phonetic substitution
    'axois': 'axios',         # transposition
    'momnet': 'moment',       # transposition
}

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

# List all packages with install scripts (npm)
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))
    })"

# Safer: use npm pack --dry-run to see what a package would install
npm pack lodash --dry-run

# Check a specific package's install scripts before installing
npm show <package> scripts
// Automated install script audit
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(', ')}`)
      console.warn(`       ${scripts[suspicious[0]]}`)
    }
  }
}

auditInstallScripts('./node_modules')

Dependency Confusion Risk Assessment

# Check if your internal package names exist on the public registry
# If they do AND the public version is newer, npm may pull the public one

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
:

# Force scoped packages to your private registry
@mycompany:registry=https://npm.mycompany.com
@internal:registry=https://npm.mycompany.com

# Block public fallback for scoped packages
//npm.mycompany.com/:always-auth=true

Phantom Dependency Detection

# Find imports/requires that aren't in package.json
# Node.js (using depcheck)
npx depcheck --ignores="@types/*" .

# Python (using pipreqs)
pip install pipreqs
pipreqs . --print | sort > required.txt
pip freeze | sort > installed.txt
diff required.txt installed.txt
// Custom phantom dep detector for TypeScript
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 ?? {}),
  ])

  // Extract all import/require statements
  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('/').slice(0, 2).join('/') : imp.split('/')[0]
    if (!declared.has(root)) {
      console.warn(`[PHANTOM] "${root}" is imported but not in package.json`)
    }
  }
}

Lock File Integrity Verification

# Verify package-lock.json integrity (npm)
npm ci --dry-run

# Detect if lock file is out of sync with package.json
npm install --package-lock-only
git diff --exit-code package-lock.json

# pip: verify requirements are pinned with hashes
pip install --require-hashes -r requirements.txt

# Generate hash-pinned requirements
pip-compile --generate-hashes requirements.in -o requirements.txt

npm audit / pip-audit Integration

# npm - break build on high severity
npm audit --audit-level=high

# pip-audit
pip install pip-audit
pip-audit --requirement requirements.txt --fail-on CRITICAL,HIGH

# GitHub Actions integration
- name: Security audit
  run: |
    npm audit --audit-level=moderate
    npx better-npm-audit audit --level moderate

SBOM Generation

# CycloneDX for Node.js
npm install -g @cyclonedx/cyclonedx-npm
cyclonedx-npm --output-format JSON --output-file sbom.json

# CycloneDX for Python
pip install cyclonedx-bom
cyclonedx-bom -r -o sbom.xml

# SPDX via syft
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

// package.json: exact pinning for production deps
{
  "dependencies": {
    "express": "4.18.2",      // exact - no caret/tilde
    "lodash": "4.17.21"
  },
  "devDependencies": {
    "typescript": "^5.3.0"   // range OK for dev tools
  }
}
# Convert all ranges to exact versions
npm shrinkwrap
# or use: npm-shrinkwrap.json over package-lock.json for published packages

# Renovate bot for automated safe updates with PR-per-dep
# renovate.json
{
  "extends": ["config:base"],
  "automerge": false,
  "reviewers": ["security-team"]
}

Dependency Risk Scoring

Score each dependency on a 0-100 risk scale before adoption:

SignalLow Risk (0-2)Medium Risk (3-5)High Risk (6-10)
Age> 3 years1-3 years< 1 year
Maintainers3+ active1-2 active1 inactive
Downloads> 1M/week100K-1M/week< 100K/week
GitHub stars> 5K500-5K< 500
Last commit< 3 months3-12 months> 12 months
Open issues< 5050-200> 200 unresolved
Install scriptsNonepostinstall (build)preinstall + network
Dependencies< 55-20> 20 transitive
CVE history0 unfixedFixed promptlyUnfixed > 30 days
LicenseMIT/Apache/BSDLGPL/MPLGPL/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: check package metadata
npm show <package> time modified maintainers repository

# Check download stats
curl -s "https://api.npmjs.org/downloads/point/last-week/<package>" | jq .downloads

# Check for known vulnerabilities
npm audit --json | jq '.vulnerabilities["<package>"]'

# Python: check PyPI metadata
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.