Skip to content

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

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

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

Conversation

@seuninfoo-svg

Copy link
Copy Markdown

PR: Add pnpm Lockfile Source Policy Checker

Summary

Implements a static pnpm-lock.yaml source-policy checker that rejects dependencies from untrusted sources (git, file:, link:, plain HTTP, unknown registries) before installation. This mitigates supply-chain risks where a lockfile could introduce dependencies from Git URLs, local paths, or unexpected tarball hosts even when package manifests look normal.

Motivation

A lockfile can introduce dependencies from:

  • Git URLs (git+https://, git+ssh://)
  • Local file paths (file:../local-package)
  • Link protocols (link:/absolute/path, link:./local-dep)
  • Plain HTTP tarballs (http://registry.example.com/pkg.tgz)
  • Unexpected HTTPS registries (not npm/yarn/npmmirror)

Even when package.json manifests specify only registry versions, transitive dependencies or lockfile manipulation can introduce these sources. This checker makes such sources reviewable via an explicit allowlist.

Changes

New Files

File Purpose
scripts/check-lockfile-sources.mjs Main checker script
scripts/check-lockfile-sources.test.mjs Test suite (19 tests)
config/lockfile-allowlist.json Allowlist configuration (empty by default)

Modified Files

File Change
package.json Added lockfile:check, lockfile:test, preinstall, install:check scripts; added yaml dev dependency
scripts/check-logging.mjs Added exception for new checker script (uses console for CLI output)

Acceptance Criteria

All criteria from the issue are satisfied:

# Criterion Implementation
1 Reject git, git+ssh, file:, link:, plain http: ✅ checkPackageResolution() + checkImporters()
2 Allow normal npm registry over HTTPS ✅ Default allowlist: registry.npmjs.org, registry.yarnpkg.com, registry.npmmirror.com
3 Documented, initially empty allowlist for exceptional hosts ✅ config/lockfile-allowlist.json with JSON schema comment
4 Positive and negative fixtures ✅ 19 tests covering allowed/rejected sources
5 Run before dependency installation ✅ preinstall script + manual install:check
6 Print package/source identifiers without credentials/full URLs ✅ sanitizeUrl() strips user:pass@ from output
7 Must not fetch any dependency ✅ Only reads local pnpm-lock.yaml and lockfile-allowlist.json

Usage

Manual Check

pnpm lockfile:check
# ✅ All lockfile sources are allowed.

# Or with custom paths
node scripts/check-lockfile-sources.mjs /path/to/pnpm-lock.yaml /path/to/allowlist.json

Pre-Install (CI / Local)

# Enable pnpm pre/post scripts once
pnpm config set enable-pre-post-scripts true

# Then preinstall runs automatically before pnpm install
pnpm install

# Or run explicitly in CI
pnpm install:check

Allowlisting Exceptional Hosts

Edit config/lockfile-allowlist.json:

{
  "allowlist": ["github.com", "npm.pkg.github.com", "registry.internal.company.com"]
}

Subdomains are automatically allowed (e.g., github.com permits raw.githubusercontent.com).

Test Coverage

pnpm lockfile:test
# 19 tests passing:
#  - allows standard registry packages with integrity only
#  - allows HTTPS tarball from known registry (npm, yarn, npmmirror)
#  - rejects git source without allowlist
#  - allows git source when host is in allowlist
#  - allows git subdomain when parent domain is in allowlist
#  - rejects file: source
#  - rejects non-HTTPS tarball
#  - rejects tarball from unknown registry host
#  - allows tarball from unknown registry when host is in allowlist
#  - rejects link: source in packages section
#  - allows workspace link: dependencies in importers (link:../)
#  - rejects external link: dependencies in importers (link:/absolute, link:./)
#  - formats errors correctly
#  - formats success correctly
#  - handles missing allowlist file gracefully
#  - handles malformed allowlist gracefully
#  - actual repository lockfile passes

Example Output

Violations Found

❌ Lockfile source policy violations found:

  • bad-git-pkg@1.0.0: git source not allowed (host: github.com, repo: https://github.com/user/repo.git)
  • bad-file-pkg@1.0.0: file: source not allowed
  • bad-http-pkg@1.0.0: non-HTTPS tarball not allowed (url: http://registry.example.com/pkg.tgz)
  • bad-registry-pkg@1.0.0: tarball from unrecognized registry host (host: private.registry.com)
  • . > external-link: external link: dependency not allowed (link:/absolute/path)

To allow an exceptional host, add it to config/lockfile-allowlist.json
Example: { "allowlist": ["github.com", "registry.example.com"] }

Clean Lockfile

✅ All lockfile sources are allowed.

Design Decisions

  1. No network calls - Only reads local files; safe for CI/air-gapped environments
  2. Subdomain matching - Allowlist entry github.com permits api.github.com, raw.githubusercontent.com, etc.
  3. Credentials stripped - Output shows https://github.com/... not https://token@github.com/...
  4. Workspace links allowed - link:../../packages/... passes; link:./local and link:/absolute fail
  5. Graceful degradation - Missing allowlist file → empty allowlist; malformed JSON → clear error
  6. Uses yaml package - Node.js lacks built-in YAML parser; yaml is a well-maintained, zero-dependency library

CI Integration

Add to CI pipeline (e.g., .github/workflows/ci.yml):

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

# Or before install
- name: Pre-install lockfile check
  run: pnpm install:check

Related

close #493

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

1 participant