--- name: managing-dependencies description: Evaluates packages, manages dependencies, and addresses supply chain security. Use when adding npm/pip/cargo/bundler/go dependencies, auditing packages, reviewing lockfile changes, checking for vulnerabilities, comparing package alternatives, or assessing package trustworthiness. license: CC0-1.0 compatibility: Claude Code, Codex CLI, or any agent with bash and network access metadata: author: andrew version: "1.0" --- # Package Management **Before suggesting any package, verify it exists on the registry.** Check that the name, maintainer, and purpose match expectations. Do not hallucinate package names. **Decline to:** - Suggest a package that cannot be verified to exist - Add a dependency when stdlib provides equivalent functionality - Run install scripts without explicit user approval - Auto-merge updates that violate the safety criteria below **If verification fails** (registry unreachable, metadata incomplete, provenance missing), default to refusal and explain why. ## Before Adding a Dependency Check in order: 1. **Standard library** - Does the language already provide this? (e.g., date parsing, HTTP, JSON) 2. **Transitive cost** - How many dependencies does it bring? Check with `npm ls`, `pip show`, `bundle info`, `cargo tree` 3. **Smaller alternative** - Is there a focused package that does just what's needed? 4. **Inline it** - Can you write 20-50 lines instead of adding a dependency? When in doubt, don't add. PRs that only remove dependencies are usually good PRs. Always clarify if a dependency is for development only (`--save-dev`, `group :development`, `[tool.poetry.group.dev.dependencies]`) to minimize the production attack surface. ## Evaluating a Package For quick checks, visit the package's registry page and repo. When you need depth: ```bash # Any ecosystem via ecosyste.ms curl -s "https://packages.ecosyste.ms/api/v1/registries//packages/" | jq '{ dependent_repos: .dependent_repos_count, dependent_packages: .dependent_packages_count, latest: .latest_release_number, latest_release: .latest_release_published_at, created: .first_release_published_at, maintainers: (.maintainers | length), repo: .repository_url, license: .normalized_licenses, advisories: (.advisories | length), archived: .repo_metadata.archived }' # See API Reference below for full registry list ``` **Good signals:** - High dependent count (other packages trust it) - Few direct dependencies - Responsive issue tracker - Multiple maintainers - OSI-approved license - Provenance attestation (see below) **Less reliable signals:** - GitHub stars (gameable) - Download counts (gameable) - Commit frequency (stable packages don't need commits) - Contributor count **Red flags:** - Package less than 90 days old - Maintainer account created recently - Name similar to popular package (typosquatting) - No license or non-OSI license - Vendors copies of common dependencies - Very few downloads for claimed purpose - Runs code on install (postinstall scripts, setup.py) **OpenSSF Scorecard** - check project security practices: ```bash scorecard --repo=github.com/owner/repo # Or visit: https://securityscorecards.dev ``` Score below 5 warrants a closer look, though small or mature projects often score lower without being risky. If `Maintained` or `Dangerous-Workflow` scores are below 3, flag as higher risk. ## Typosquatting Patterns Watch for these when verifying package names: - **Character substitution**: `djang0` vs `django`, `requets` vs `requests` - **Character omission**: `loadsh` vs `lodash`, `electon` vs `electron` - **Homoglyphs**: `pyp1` (one) vs `pypi` (letter i), Cyrillic `а` vs Latin `a` - **Delimiter variation**: `cross-env` vs `crossenv` vs `cross_env` - **Scope confusion**: `@angular-devkit/core` vs `@angulardevkit/core` - **Combosquatting**: `lodash-js`, `axios-api`, `express-utils` - **Namespace confusion** (Maven): `org.fasterxml` vs `com.fasterxml` Always copy package names from official docs rather than typing from memory. ## AI-Suggested Packages (Slopsquatting) AI coding assistants hallucinate package names regularly. Attackers register these names with malicious code. Before installing any AI-suggested package: 1. **Verify it exists**: `npm view ` or `pip index versions ` 2. **Check age and downloads**: Very new + few downloads = suspect 3. **Cross-reference docs**: Does the framework's official docs mention it? 4. **Check for typosquatting**: See patterns above Hallucinated names are often repeatable across sessions, making them predictable targets for attackers. ## Dependency Confusion When using both public and private registries, attackers can publish public packages matching your internal package names with high version numbers. **Defenses:** Use scoped/namespaced packages for internal code: ```bash # npm - attackers can't register under your scope @yourcompany/internal-utils # Configure scope to route to private registry # .npmrc @yourcompany:registry=https://your-internal-registry.com ``` For pip, use `--index-url` for private registry, `--extra-index-url` for PyPI: ```bash # Correct - private registry checked first pip install --index-url https://private.example.com/simple \ --extra-index-url https://pypi.org/simple \ mypackage ``` Defensively register your internal package names on public registries with placeholder packages. ## Provenance and Attestation Check if a package has verified build provenance: ```bash # npm - check for attestation npm audit signatures # PyPI - check for Sigstore attestation curl -s "https://pypi.org/pypi//json" | jq '.urls[0].digests' # Look for attestation bundle in release assets # GitHub Actions - verify with gh gh attestation verify --owner ``` **Trusted publishing** means the package was published directly from CI (GitHub Actions, GitLab CI) without maintainer credentials. The registry verifies the source via OIDC. npm, PyPI, and RubyGems support this. If a package has provenance, you can verify the published artifact matches the source repo and commit. Support varies by ecosystem; absence of attestation doesn't mean insecure. ## Version Constraints **Applications:** Use ranges in manifest, pin via lockfile. **Libraries:** Use wide ranges. Don't force consumers to upgrade. | Ecosystem | Exact | Flexible | Patch only | |-----------|-------|----------|------------| | npm/Cargo | `1.0.0` | `^1.0.0` | `~1.0.0` | | Bundler | `= 1.0.0` | `~> 1.0` | `~> 1.0.0` | | pip | `==1.0.0` | `~=1.0` | | | Go | `v1.2.3` | MVS | MVS | Bundler's `~>` is commonly misread: `~> 1.0` allows `1.x` (minor updates), `~> 1.0.0` allows `1.0.x` (patch only). Go uses minimal version selection - it picks the minimum version satisfying all requirements. Don't vendor specific versions or use replace directives to simulate ranges. ## Lockfiles Always commit lockfiles. **Detect ecosystem:** - `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` / `bun.lock` → npm - `Gemfile.lock` → Bundler - `Cargo.lock` → Cargo - `poetry.lock` / `uv.lock` / `requirements.txt` with hashes → Python - `go.sum` → Go **In CI, install from lockfile** (don't regenerate): ```bash npm ci # not npm install pip install --require-hashes -r requirements.txt bundle install --frozen cargo build --locked uv sync --frozen go mod verify ``` **Review lockfile changes in PRs:** - Watch for changes to `resolved` URLs (lockfile injection attack) - Large diffs can hide malicious additions - Unexpected registry URL changes are red flags **Regenerate on conflict** rather than manually merging: ```bash # npm rm package-lock.json && npm install # Bundler rm Gemfile.lock && bundle install # Cargo rm Cargo.lock && cargo generate-lockfile # Poetry rm poetry.lock && poetry lock # uv rm uv.lock && uv lock ``` ## Security Audits Run in CI and before adding dependencies: ```bash # npm npm audit # Bundler (install bundler-audit gem first) bundle audit check --update # pip (install pip-audit first) pip-audit # Cargo (install cargo-audit first) cargo audit # Go govulncheck ./... ``` ## When Vulnerability Has No Patch First check reachability: is the vulnerable code path actually called by your usage? Many CVEs affect features you don't use. Options in order of preference: 1. **Override transitive** - Force newer version of vulnerable transitive dependency 2. **Fork and patch** - Apply security fix to fork, reference fork in manifest 3. **Remove dependency** - Find alternative or inline the functionality 4. **Accept risk** - Document why it's not exploitable in your context ## Vendoring Copy dependencies into your repo for airgapped environments, auditing, or when you need to patch upstream: ```bash # Go - built-in go mod vendor go build -mod=vendor # Ruby bundle package --all bundle install --local # Python (pip download, then install offline) pip download -r requirements.txt -d ./vendor pip install --no-index --find-links=./vendor -r requirements.txt # npm (less common, use npm-pack-all or similar) ``` Vendoring trades registry availability risk for increased repo size and manual update burden. Justified for airgapped builds, audited security-critical code, or long-term forks you maintain. ## Check Licenses ```bash # npm npx license-checker --summary # pip pip-licenses # Bundler bundle licenses # Cargo cargo license ``` **Safe:** MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Unlicense **Review needed:** LGPL, MPL-2.0 **Copyleft (affects distribution):** GPL, AGPL **No license = not open source** - do not use. ## Install Scripts Package managers execute code during install - a common attack vector: - npm: `preinstall`, `postinstall` in package.json - pip: `setup.py` runs during install - Ruby: `extconf.rb` for native extensions **Audit before allowing scripts:** ```bash # npm - install without running scripts, then review npm install --ignore-scripts # After reviewing, run scripts explicitly npm rebuild ``` For high-security environments, consider disabling install scripts globally and allowlisting specific packages. ## GitHub Actions Actions are dependencies too. Pin to commit SHA, not tags: ```yaml # Bad - tag can be moved - uses: actions/checkout@v4 # Good - immutable reference - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 ``` Use Dependabot or Renovate to keep pinned SHAs updated. ## Automated Updates | Tool | Platform | Best for | |------|----------|----------| | Dependabot | GitHub only | Simple needs, free | | Renovate | Multi-platform | Complex configs, monorepos | | Snyk | Multi-platform | Security-first, vulnerability focus | **Safe auto-merge criteria** (all must apply): - Patch updates only (not minor/major) - Dev dependencies only - Established, trusted packages - All CI checks pass - Package available 3+ days (catches quick reverts of bad releases) **Never auto-merge:** - Major version updates - Production dependencies - New packages not yet in your codebase - Security-sensitive packages (crypto, auth) Example Renovate config for conservative auto-merge: ```json { "packageRules": [ { "matchUpdateTypes": ["patch"], "matchDepTypes": ["devDependencies"], "automerge": true, "minimumReleaseAge": "3 days" } ] } ``` --- # API Reference ## ecosyste.ms ```bash curl -s "https://packages.ecosyste.ms/api/v1/registries//packages/" | jq ``` **Registries:** `npmjs.org`, `pypi.org`, `rubygems.org`, `crates.io`, `proxy.golang.org`, `nuget.org`, `repo1.maven.org`, `cocoapods.org`, `hub.docker.com` ### Risk Thresholds | Field | Threshold | Risk | |-------|-----------|------| | `latest_release_published_at` | >2 years ago | Possibly abandoned | | `dependent_repos_count` | <500 | Low adoption | | `first_release_published_at` | <90 days ago | Too new | | `maintainers` (length) | <2 | Bus factor risk | | `versions_count` | =1 | Immature | | `repo_metadata.archived` | true | Abandoned | | `advisories` | non-empty | Known vulnerabilities | ### Development Distribution Score Found at `.repo_metadata.metadata.development_distribution_score`. Measures commit distribution across contributors (0-1). - **<0.15** - Single contributor dominance (high bus factor) - **0.15-0.5** - Moderate distribution - **>0.5** - Well distributed ## OpenSSF Scorecard API ```bash curl -s "https://api.scorecard.dev/projects/github.com//" | jq '{ score: .score, maintained: (.checks[] | select(.name == "Maintained") | .score), dangerous_workflow: (.checks[] | select(.name == "Dangerous-Workflow") | .score), code_review: (.checks[] | select(.name == "Code-Review") | .score) }' ``` Human-readable: `https://ossf.github.io/scorecard-visualizer/#/projects/github.com//` ## deps.dev API Alternative for dependency graph analysis: ```bash curl -s "https://api.deps.dev/v3/systems//packages/" | jq ``` **Ecosystems:** `npm`, `pypi`, `cargo`, `go`, `maven`, `nuget` --- # About This Skill - **Repository:** https://github.com/andrew/managing-dependencies - **Author:** Andrew Nesbitt (https://nesbitt.io) - **License:** CC0-1.0