LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Dependency Management: Lock Files, Vulnerability Scanning, and Keeping Deps Fresh Without Pain

dependenciessecuritydevopspythonnodegorustautomation

Dependency management is one of those topics that everyone knows matters and almost nobody handles systematically until something breaks badly. A critical CVE in a transitive dependency, a breaking change that slipped in under a minor version bump, or an update that broke prod on a Friday afternoon — these are the moments that turn “we should really set up automated dependency updates” from a backlog item into a war room priority.

This guide covers the full picture: how lock files work and why you need them, semantic versioning promises and where they break down, scanning for vulnerabilities, automating updates safely, and building a dependency hygiene culture that doesn’t feel like a chore.


Why Dependency Management Is Hard

Modern applications depend on enormous graphs of packages. A typical Node.js app might have 5–10 direct dependencies that balloon to 500+ transitive ones. A Python service might pin Flask but transitively pull in 40 other packages. Each of those packages is maintained by humans, and humans make mistakes, stop maintaining things, and occasionally inject malicious code.

The risks are real:

  • Security vulnerabilities — Log4Shell affected millions of applications because log4j was a transitive dependency of hundreds of popular libraries.
  • Breaking changes — A dependency publishes a “patch” that accidentally breaks your code. Without a lock file, your next npm install in a fresh CI environment silently uses the new version.
  • Abandoned packages — A library that once had 50k weekly downloads is now unmaintained and full of known CVEs.
  • License creep — A new dependency introduces a GPL license into your otherwise MIT/Apache codebase.
  • Supply chain attacks — Typosquatted packages, compromised maintainer accounts, and dependency confusion attacks are increasingly common.

The goal is not to freeze your dependency graph in amber forever. The goal is to make updates deliberate, tested, and continuous rather than scary, batched, and infrequent.


Lock Files: What They Are and Why You Must Commit Them

A lock file records the exact resolved versions of every dependency — direct and transitive — along with hashes of the downloaded packages. It’s the difference between “we use React ^18.0.0” (which could resolve to 18.0.0, 18.1.0, 18.2.0, or anything in that range) and “we use React 18.2.0, sha512-abc123”.

Language Ecosystem Lock Files

Ecosystem Manifest Lock File Install Command
Node.js (npm) package.json package-lock.json npm ci
Node.js (yarn) package.json yarn.lock yarn install --frozen-lockfile
Node.js (pnpm) package.json pnpm-lock.yaml pnpm install --frozen-lockfile
Python (pip) requirements.txt requirements.lock or pip.lock pip install -r requirements.lock
Python (poetry) pyproject.toml poetry.lock poetry install
Python (uv) pyproject.toml uv.lock uv sync
Go go.mod go.sum go mod download
Rust Cargo.toml Cargo.lock cargo build
Ruby Gemfile Gemfile.lock bundle install
PHP composer.json composer.lock composer install

Always commit your lock file. This is not optional. The exception the Go docs make for libraries (not committing go.sum) is for published libraries, not applications.

Why npm install vs npm ci Matters

npm install reads package.json ranges and updates package-lock.json if a newer version in range is available. npm ci reads package-lock.json and installs exactly those versions, failing if anything is out of sync.

In CI/CD:

1
2
3
4
5
# Bad — may silently update packages on every run
- run: npm install

# Good — installs exactly what was tested
- run: npm ci

The same principle applies across ecosystems:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Python poetry
poetry install  # respects poetry.lock

# Rust
cargo build     # Cargo.lock is authoritative

# Go
go mod download # go.sum ensures integrity

# Ruby
bundle install  # reads Gemfile.lock

Semantic Versioning: The Promise and the Reality

Semantic versioning (semver) defines MAJOR.MINOR.PATCH:

  • PATCH — backwards-compatible bug fixes
  • MINOR — backwards-compatible new features
  • MAJOR — breaking changes

In theory, pinning to ^1.2.3 (compatible with >=1.2.3 <2.0.0) should be safe. In practice, libraries break this contract constantly — accidentally or intentionally. Behavior that wasn’t in the public API gets depended upon; internal refactors change observable timing; “bug fixes” turn out to break callers that relied on the buggy behavior.

Choosing Version Ranges Deliberately

For applications (deployed services, CLIs, scripts you run):

  • Pin exact versions in lock files, use whatever ranges make sense in the manifest.
  • The lock file gives you reproducibility; the range gives maintainers flexibility.

For libraries (npm packages, PyPI packages, Go modules you publish):

  • Specify the minimum compatible version, not an exact version.
  • >=1.0.0,<2.0.0 or ^1.0.0 lets your library work across a range of environments.
  • Pinning exact transitive deps in a library manifest is actively harmful — it creates conflicts.

The Caret vs Tilde Distinction (npm)

1
2
3
4
5
6
7
{
  "dependencies": {
    "express": "^4.18.0",   // >= 4.18.0 < 5.0.0 — minor updates allowed
    "lodash": "~4.17.21",   // >= 4.17.21 < 4.18.0 — patch updates only
    "axios": "1.6.7"        // exactly 1.6.7 — no range (rare, for apps)
  }
}

Vulnerability Scanning

npm audit

Built into npm, runs automatically after install, and can be enforced in CI:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Check for vulnerabilities
npm audit

# Fix automatically (safe fixes only)
npm audit fix

# Fix including semver-major updates (test carefully!)
npm audit fix --force

# Fail CI on high or critical vulnerabilities
npm audit --audit-level=high

In CI, add this to your pipeline:

1
2
- name: Security audit
  run: npm audit --audit-level=moderate

pip-audit (Python)

pip-audit scans installed packages against the Python Advisory Database (PyPA) and OSV:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
pip install pip-audit

# Audit installed packages
pip-audit

# Audit a requirements file
pip-audit -r requirements.txt

# Output as JSON for processing
pip-audit --output json

# Fix vulnerabilities automatically
pip-audit --fix

cargo audit (Rust)

1
2
3
4
5
6
7
cargo install cargo-audit

# Audit Cargo.lock
cargo audit

# Watch for new advisories
cargo audit --deny warnings

govulncheck (Go)

Google’s official Go vulnerability checker:

1
2
3
4
5
6
7
go install golang.org/x/vuln/cmd/govulncheck@latest

# Check current module
govulncheck ./...

# Check a specific binary
govulncheck -mode binary ./myapp

govulncheck is smarter than database-only scanners — it does static analysis to determine whether the vulnerable code path is actually reachable in your program.

Trivy (polyglot, great for containers)

Trivy from Aqua Security scans filesystems, container images, Git repos, and IaC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Scan filesystem
trivy fs .

# Scan a container image
trivy image nginx:latest

# Scan and fail on HIGH or CRITICAL
trivy fs --exit-code 1 --severity HIGH,CRITICAL .

# Only show unfixed (reduces noise)
trivy fs --ignore-unfixed .

# Output as SARIF for GitHub Security tab
trivy fs --format sarif --output trivy-results.sarif .

Trivy understands lock files across all major ecosystems, making it the go-to choice for polyglot repos.

Grype (alternative to Trivy)

Anchore’s Grype is another strong option:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Install
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh

# Scan directory
grype dir:.

# Scan image
grype nginx:latest

# Only fail on fixed vulnerabilities
grype dir:. --fail-on high --only-fixed

OWASP Dependency-Check

The Java-focused option, also supports .NET, Node, Python, Ruby, and more:

1
dependency-check --project myapp --scan . --format HTML --out ./report

Integrating Vulnerability Scanning in CI

The key principle: fail fast, fail clearly. Scanning in CI means you catch new vulnerabilities when they first affect your codebase, not when someone audits you or reads a security bulletin months later.

GitHub Actions Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name: Security Scan

on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: '0 6 * * 1'  # Weekly Monday scan catches new advisories

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          severity: 'HIGH,CRITICAL'
          exit-code: '1'
          ignore-unfixed: true
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

  npm-audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm audit --audit-level=high

Handling False Positives and Accepted Risks

Not every CVE requires immediate action. Some vulnerabilities are:

  • In code paths your app never calls
  • Only exploitable in environments different from yours
  • In dev-only tools that never touch production

Most scanners support suppression files:

Trivy (.trivyignore):

# CVE-2023-12345 — only affects Windows, we deploy to Linux
CVE-2023-12345

# Accepted risk: no fix available, mitigated by WAF
CVE-2023-67890

npm audit (.nsprc or via overrides in package.json):

1
2
3
4
5
{
  "overrides": {
    "vulnerable-package": "2.0.0"
  }
}

pip-audit (pyproject.toml):

1
2
[tool.pip-audit]
ignore-vuln = ["GHSA-xxxx-xxxx-xxxx"]

Document your suppression decisions — include the CVE ID, the rationale, who approved it, and a review date.


Automating Dependency Updates

Manual dependency updates are boring, error-prone, and therefore not done often enough. Automate them with bots that open PRs; your CI tests them, and you merge or decline.

Dependabot (GitHub)

Add .github/dependabot.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "06:00"
    open-pull-requests-limit: 10
    reviewers:
      - "your-team"
    labels:
      - "dependencies"
    ignore:
      # Ignore major version bumps for React — coordinate manually
      - dependency-name: "react"
        update-types: ["version-update:semver-major"]

  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"
    versioning-strategy: lockfile-only

  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Key options:

  • versioning-strategy: lockfile-only — updates the lock file without changing ranges in the manifest
  • open-pull-requests-limit — prevents inbox flooding
  • ignore — skip packages where you want manual control
  • groups — bundle related updates into a single PR (Dependabot v2)

Grouping related updates (reduces PR noise):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      eslint:
        patterns:
          - "eslint*"
          - "@eslint/*"
      aws-sdk:
        patterns:
          - "@aws-sdk/*"

Renovate (more powerful, self-hostable)

Renovate is more configurable than Dependabot and can be self-hosted. Add renovate.json to your repo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ],
  "schedule": ["every weekend"],
  "automerge": true,
  "automergeType": "pr",
  "automergeStrategy": "squash",
  "packageRules": [
    {
      "matchUpdateTypes": ["minor", "patch"],
      "matchCurrentVersion": "!/^0/",
      "automerge": true
    },
    {
      "matchUpdateTypes": ["major"],
      "automerge": false,
      "labels": ["major-update", "needs-review"]
    },
    {
      "matchPackageNames": ["node"],
      "matchManagers": ["dockerfile"],
      "automerge": false
    }
  ],
  "vulnerabilityAlerts": {
    "enabled": true,
    "schedule": ["at any time"],
    "automerge": true,
    "labels": ["security"]
  }
}

Renovate’s killer features over Dependabot:

  • Automerge with confidence — auto-merges patch/minor updates that pass CI
  • Dashboard issue — a GitHub issue listing all pending updates in one place
  • Rebase on conflict — automatically rebases stale PRs
  • More ecosystems — Helm charts, Terraform modules, Docker Compose images, Ansible Galaxy
  • Self-hosted — run on your own infra, use with GitLab, Gitea, Bitbucket

Self-Hosted Renovate with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# docker-compose.yml
services:
  renovate:
    image: renovate/renovate:latest
    environment:
      - RENOVATE_TOKEN=${GITHUB_TOKEN}
      - RENOVATE_REPOSITORIES=your-org/your-repo
      - LOG_LEVEL=info
    volumes:
      - ./renovate-config.js:/usr/src/app/config.js

Run it on a cron job (or use the hosted app for public repos):

1
2
3
docker run --rm \
  -e RENOVATE_TOKEN=$GITHUB_TOKEN \
  renovate/renovate your-org/your-repo

Language-Specific Best Practices

Python

Python dependency management has historically been fragmented. The modern answer is uv for speed and poetry for mature projects.

uv (recommended for new projects):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Create project
uv init myproject
cd myproject

# Add dependency
uv add fastapi
uv add --dev pytest ruff

# Lock and sync
uv lock
uv sync

# Run with locked env
uv run python app.py

# Update a single package
uv lock --upgrade-package fastapi

# Check outdated
uv tree --outdated

poetry (established projects):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Add dependency
poetry add httpx

# Add dev dependency
poetry add --group dev pytest

# Update all
poetry update

# Update single package
poetry update requests

# Show outdated packages
poetry show --outdated

# Export for pip-based deployments
poetry export -f requirements.txt --output requirements.txt --without-hashes

pip-tools (simple, predictable):

Keep requirements.in with loose requirements, compile to requirements.txt:

1
2
3
4
# requirements.in
flask>=2.3
sqlalchemy>=2.0
pydantic>=2.0
1
2
3
4
pip-compile requirements.in              # generates requirements.txt
pip-compile --upgrade requirements.in   # upgrade all
pip-compile --upgrade-package flask     # upgrade one
pip-sync requirements.txt               # install exactly what's pinned

Node.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Check outdated packages
npm outdated

# Update a single package
npm update lodash

# Update to latest (bypasses semver range — careful!)
npm install lodash@latest

# Use npm-check-updates for interactive update management
npx npm-check-updates
npx npm-check-updates -u          # write updates to package.json
npx npm-check-updates --interactive  # choose what to update

# Check for unused dependencies
npx depcheck

Enforcing Node.js/npm versions (prevents version drift across devs):

1
2
3
4
5
6
7
// package.json
{
  "engines": {
    "node": ">=20.0.0",
    "npm": ">=10.0.0"
  }
}

Use volta or nvm to manage local Node versions:

1
2
# .nvmrc
20.11.0
1
2
# .node-version (volta)
20.11.0

Go

Go’s module system is the most opinionated of the major ecosystems. go.mod specifies minimum versions; go.sum verifies them. The Minimum Version Selection (MVS) algorithm means Go never automatically upgrades — it always picks the minimum version that satisfies all requirements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# List all dependencies
go list -m all

# Check for updates
go list -m -u all

# Update all dependencies
go get -u ./...

# Update to latest patch only
go get -u=patch ./...

# Update a single module
go get github.com/some/package@latest

# Tidy (remove unused, add missing)
go mod tidy

# Vendor dependencies (for offline builds)
go mod vendor

# Check for vulnerabilities
govulncheck ./...

Enforcing Go version in CI:

1
2
3
4
# go.mod
go 1.22

toolchain go1.22.3

Rust

Cargo is the gold standard for dependency management. Cargo.lock for applications (commit it); libraries should not commit Cargo.lock.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Check for outdated packages
cargo outdated

# Update all packages (within semver constraints)
cargo update

# Update a single crate
cargo update -p serde

# Check for vulnerabilities
cargo audit

# Check for unused dependencies
cargo install cargo-udeps
cargo +nightly udeps

Cargo.toml features and optional deps:

1
2
3
4
5
6
7
8
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.36", features = ["full"] }
reqwest = { version = "0.11", optional = true, features = ["json"] }

[dev-dependencies]
mockall = "0.12"
proptest = "1.4"

Keep dev-dependencies separate — they’re not compiled into your release binary.


License Management

Licenses are an often-ignored dimension of dependency management. Bringing in a GPL-licensed package into a proprietary codebase is a legal problem, not just a technical one.

License Checkers

license-checker (Node.js):

1
2
3
npx license-checker --summary
npx license-checker --onlyAllow 'MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC'
npx license-checker --failOn 'GPL-2.0;GPL-3.0;AGPL-3.0'

pip-licenses (Python):

1
2
3
pip install pip-licenses
pip-licenses --format=markdown
pip-licenses --fail-on "GPL;AGPL"

cargo-license (Rust):

1
2
3
cargo install cargo-license
cargo license
cargo license --json

FOSSA / Snyk (commercial, policy-based):

For organizations with complex license policies, commercial tools like FOSSA provide policy engines, legal workflow integrations, and continuous monitoring.


Building a Dependency Hygiene Process

Individual tools matter less than having a consistent process. Here’s a practical cadence:

Weekly

  • Automated update PRs opened by Dependabot or Renovate
  • CI runs tests on each PR automatically
  • Team merges patch/minor updates that pass CI (takes 5 minutes)

Monthly

  • Review major version updates that require manual intervention
  • Check for any security advisories affecting your dependencies that weren’t auto-patched
  • npm outdated / poetry show --outdated / go list -m -u all pass-through to spot anything automation missed

Quarterly

  • Audit lock files for packages that have been abandoned (no commits in >2 years, no maintainer response to issues)
  • Review suppressed vulnerabilities — are they still suppressed for the right reasons?
  • License audit pass

When a Critical CVE Drops

Have a runbook ready:

  1. Identify: Does your application use the affected package? (trivy fs . / govulncheck ./...)
  2. Assess: Is the vulnerable code path reachable in your application?
  3. Patch: Update the dependency, run tests, deploy.
  4. Verify: Re-scan after patching to confirm resolution.
  5. Document: Note the CVE, impact, and remediation in your incident log.

Detecting Dependency Confusion Attacks

Dependency confusion is an attack where a malicious package with the same name as your private internal package is published to a public registry, tricking package managers into downloading the public (malicious) version.

Prevention

npm — use scoped packages and explicit registry config:

1
2
3
4
# .npmrc
@mycompany:registry=https://npm.mycompany.internal
//npm.mycompany.internal/:_authToken=${NPM_INTERNAL_TOKEN}
always-auth=false

pip — use –index-url or –extra-index-url carefully:

1
2
3
4
5
# Dangerous — pip may prefer the public PyPI version
pip install --extra-index-url https://pypi.internal mypackage

# Safer — internal registry first, only fall back to PyPI for public packages
pip install --index-url https://pypi.internal --extra-index-url https://pypi.org/simple mypackage

Or better, use --no-index and serve all packages (including public ones) from your internal mirror:

1
pip install --no-index --find-links /path/to/packages mypackage

Reserve your package names on public registries — even if you publish nothing there, claiming the name prevents confusion attacks.


Dependency Pinning Strategies: A Decision Framework

There’s genuine tension between security (pin exact versions, control updates) and maintenance (loose ranges reduce upgrade friction). Here’s how to think about it:

                 ┌─────────────────────────────────────────┐
                 │           WHAT ARE YOU BUILDING?         │
                 └─────────────────┬───────────────────────┘
                                   │
               ┌───────────────────┴──────────────────────┐
               │                                          │
       APPLICATION                                    LIBRARY
   (deployed service,                           (published to npm/PyPI,
    CLI tool, script)                            consumed by others)
               │                                          │
               ▼                                          ▼
   ┌─────────────────────┐                  ┌──────────────────────────┐
   │  Pin exact versions  │                  │  Specify minimum compat  │
   │  in lock file.       │                  │  range. Let consumers    │
   │  Use lock file in CI │                  │  resolve. Don't commit   │
   │  (npm ci, poetry     │                  │  lock file for libraries │
   │  install, uv sync)   │                  │  (Go: commit go.sum)     │
   └─────────────────────┘                  └──────────────────────────┘

Practical Checklist

Use this when setting up a new project or auditing an existing one:

Lock files

  • Lock file committed to version control
  • CI uses --frozen-lockfile / npm ci (not install)
  • Lock file regeneration is done locally and reviewed in PRs

Vulnerability scanning

  • Scanner runs in CI on every PR
  • Scanner runs on a schedule (weekly) to catch new advisories
  • Results integrated into PR checks (fail on HIGH/CRITICAL)
  • Suppression file exists and entries are documented + dated

Automated updates

  • Dependabot or Renovate configured
  • PR rate limits set (no inbox flooding)
  • Major updates require manual review
  • Security updates auto-merge if CI passes

Process

  • Someone is responsible for reviewing dependency PRs each week
  • A runbook exists for responding to critical CVEs
  • License policy is documented and enforced in CI

Hygiene

  • No packages with zero downloads or single maintainer in critical path (where possible)
  • Dev dependencies are separate from production dependencies
  • Unused dependencies cleaned up (depcheck, cargo udeps)

Tools Summary

Tool Ecosystem Purpose
npm audit Node.js Vulnerability scanning
pip-audit Python Vulnerability scanning
cargo audit Rust Vulnerability scanning
govulncheck Go Vulnerability scanning (call-graph-aware)
Trivy Polyglot Filesystem/image/IaC scanning
Grype Polyglot Alternative to Trivy
Dependabot All Automated update PRs (GitHub)
Renovate All Automated update PRs (self-hostable)
npm-check-updates Node.js Interactive update management
pip-compile (pip-tools) Python Lock file generation
uv Python Fast package manager with lockfile
poetry Python Dependency management + packaging
license-checker Node.js License compliance
pip-licenses Python License compliance
cargo-license Rust License compliance
depcheck Node.js Find unused dependencies
cargo udeps Rust Find unused dependencies

Closing Thoughts

Dependency management is a solved problem operationally — the tooling exists, the automation exists, and the patterns are well-understood. What’s usually missing is consistent process and someone owning it.

The single highest-leverage change most teams can make is turning on Dependabot or Renovate and committing to reviewing update PRs weekly. The compounding effect of staying current — smaller diffs, no breaking changes to absorb all at once, CVEs patched within days — is dramatic over the course of a year compared to the “update everything in a panic every six months” approach.

Start with a lock file, add a vulnerability scanner, set up automated update PRs. That’s 80% of the value with 20% of the effort.

Comments