Skip to content

feat: reject unreviewed dependency sources in the pnpm lockfile - #499

Closed
seuninfoo-svg wants to merge 1 commit into
Sub-Rosa-Issue:mainfrom
seuninfoo-svg:security
Closed

seuninfoo-svg wants to merge 1 commit into
Sub-Rosa-Issue:mainfrom
seuninfoo-svg:security

Conversation

@seuninfoo-svg

Copy link
Copy Markdown

Close #493

PR: Static pnpm-lock.yaml Source-Policy Checker

Summary

Add a static lockfile source-policy checker that validates all dependency sources in pnpm-lock.yaml against a strict allowlist. This prevents supply-chain risks from unreviewed dependency sources (Git URLs, local paths, plain HTTP, unexpected tarball hosts) before any installation occurs.

Problem

A lockfile can introduce dependencies from:

  • Git URLs (git:, git+ssh:, git+https:, github:, gitlab:, bitbucket:)
  • Local paths (file:, link:)
  • Plain HTTP (http:)
  • Unexpected tarball hosts

Even when package.json manifests look normal, transitive dependencies or resolution overrides can pull from these sources. A small policy check makes these sources reviewable before pnpm install.

Solution

Created scripts/check-lockfile-sources.mjs — a zero-dependency (besides existing logging package) static analyzer that:

  1. Parses pnpm-lock.yaml (YAML → custom lightweight parser, no new deps)
  2. Extracts all sources from importers (specifier + resolved version) and packages (keys + resolution objects)
  3. Validates each source against policy
  4. Prints sanitized package/source identifiers without credentials
  5. Exits non-zero on violations

Default Policy

Protocol/Source Allowed Notes
https: to npm registries ✅ registry.npmjs.org, registry.yarnpkg.com, registry.npmmirror.com, codeload.github.com
workspace: ✅ pnpm workspace protocol
link: in version field ✅ pnpm resolves workspace:* → link:
git:, git+ssh:, git+http:, git+https: ❌ Git transports
github:, gitlab:, bitbucket: ❌ pnpm shorthands
ssh: ❌ SSH transport
file: ❌ Local file paths
link: (non-workspace) ❌ External link deps
http: ❌ Plaintext HTTP

Allowlist

ALLOWLIST_HOSTS set (initially empty) for exceptional hosts — add with explicit justification.

Files Added

scripts/check-lockfile-sources.mjs      # Main checker (executable + exportable)
scripts/check-lockfile-sources.test.mjs  # 10 tests
fixtures/lockfile-sources/
  ├── valid-lockfile.yaml                # Positive fixture
  ├── invalid-lockfile.yaml              # Negative fixture (10 violations)
  └── allowlist-lockfile.yaml            # Allowlist demo

Files Modified

  • package.json — Added lockfile:sources:check and lockfile:sources:test scripts

Usage

# Check current lockfile
pnpm lockfile:sources:check

# Run test suite
pnpm lockfile:sources:test

# Check arbitrary lockfile
node scripts/check-lockfile-sources.mjs path/to/pnpm-lock.yaml

Example Output

Clean Lockfile

$ pnpm lockfile:sources:check
{"timestamp":"...","level":"info","event":"ok","message":"OK  206 dependency source(s) checked — all compliant"}

Violations

$ node scripts/check-lockfile-sources.mjs fixtures/lockfile-sources/invalid-lockfile.yaml
{"timestamp":"...","level":"error","event":"fail","message":"
FAIL  10 source policy violation(s):
  lodash-git (importer:.:dependencies:specifier): Rejected protocol: github: — github://lodash/lodash
  private-repo (importer:.:dependencies:specifier): Rejected protocol: git+ssh: — git+ssh://github.com/user/private-repo.git
  local-package (importer:.:dependencies:specifier): file: protocol not allowed — file:../local-package
  insecure-package (importer:.:dependencies:specifier): Rejected protocol: http: — http://registry.example.com/insecure-package/-/insecure-package-1.0.0.tgz
  external-link (importer:.:devDependencies:specifier): link: protocol not allowed — link:../../external-package
  ..."}
{"timestamp":"...","level":"info","event":"summary","message":"Checked 18 dependency source(s)"}

Test Coverage

Test Description
passes for valid lockfile Allowed sources only
fails for git dependency github: shorthand rejected
fails for git+ssh dependency git+ssh: rejected
fails for file: dependency file: rejected
fails for http: dependency Plain HTTP rejected
fails for external link: dependency Non-workspace link: rejected
allows workspace: specifier workspace:* allowed
allows link: in version field Workspace resolution allowed
allows HTTPS from npm registry Standard registry allowed
prints sanitized identifiers No credentials in output

Integration

Runs before dependency installation in CI:

- name: Verify lockfile sources
  run: pnpm lockfile:sources:check

No network access — purely static analysis of committed pnpm-lock.yaml.

Design Decisions

  • Custom YAML parser — Avoids adding js-yaml dependency; parser targets pnpm v9 lockfile structure only
  • Checks specifier (user intent) + version (pnpm resolution) — Catches both manifest and resolved sources
  • Handles inline resolution objects — { tarball: "...", gitHosted: true } on single line
  • Sanitizes output — Strips credentials from package keys (git+ssh://git@host/... → git+ssh://host/...)
  • Logging via existing @sub-rosa/logging — Consistent JSON diagnostics

@drips-wave

drips-wave Bot commented Oct 21, 2026

Copy link
Copy Markdown

@seuninfoo-svg Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(ci): reject unreviewed dependency sources in the pnpm lockfile

2 participants