diff --git a/pgpm/workspace/.github/workflows/ci.yml b/pgpm/workspace/.github/workflows/ci.yml index 76a6202..27cb2e8 100644 --- a/pgpm/workspace/.github/workflows/ci.yml +++ b/pgpm/workspace/.github/workflows/ci.yml @@ -77,6 +77,11 @@ jobs: - name: Install run: pnpm install + # Fails when pnpm-workspace.yaml no longer matches pnpm-policy.yaml, or when a + # third-party waiver has passed its expiry date. + - name: Check supply-chain policy + run: pnpm run policy:check + - name: Cache pgpm CLI id: cache-pgpm uses: actions/cache@v4 diff --git a/pgpm/workspace/README.md b/pgpm/workspace/README.md index 10bc117..2036594 100644 --- a/pgpm/workspace/README.md +++ b/pgpm/workspace/README.md @@ -35,6 +35,23 @@ cd packages/your-module pnpm test:watch ``` +### Supply-chain policy + +A third-party release must be 14 days old before this workspace will install it, which is where most compromised releases are caught. Nothing is exempt by default. `pnpm-policy.yaml` is where you exempt what you publish yourself, approve an install script, or waive the wait for an urgent security release: + +```sh +pnpm run policy # regenerate the managed block in pnpm-workspace.yaml +pnpm run policy:check # CI: fail on drift or an expired waiver +``` + +See [pnpm-policy](https://www.npmjs.com/package/pnpm-policy) for the full configuration. + +Nothing is locked yet on the very first install, so it resolves every version from the registry and trips if any of them is newer than the wait. Get past it once, then commit the lockfile it produces — from then on installs replay the lockfile and the wait only applies to versions you are adding: + +```sh +pnpm install --config.minimumReleaseAge=0 +``` + ### Prerequisites - Node.js 20+ diff --git a/pgpm/workspace/package.json b/pgpm/workspace/package.json index 74b659c..0d479bc 100644 --- a/pgpm/workspace/package.json +++ b/pgpm/workspace/package.json @@ -20,6 +20,8 @@ "test": "pnpm -r run test", "lint": "pnpm -r run lint", "deps": "pnpm up -r -i -L", + "policy": "pnpm-policy generate", + "policy:check": "pnpm-policy check", "version": "pgpm sync-versions && git add -A" }, "devDependencies": { @@ -36,6 +38,7 @@ "makage": "^0.5.1", "pgpm": "^5.19.1", "pgsql-test": "^5.9.7", + "pnpm-policy": "^0.2.2", "prettier": "^3.7.4", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", diff --git a/pgpm/workspace/pnpm-policy.yaml b/pgpm/workspace/pnpm-policy.yaml new file mode 100644 index 0000000..f2dc676 --- /dev/null +++ b/pgpm/workspace/pnpm-policy.yaml @@ -0,0 +1,60 @@ +# Supply-chain policy for this workspace, enforced by pnpm itself. +# +# The pnpm settings it produces live in pnpm-workspace.yaml under the +# `Managed by pnpm-policy` marker. Edit this file, then run: +# +# pnpm run policy +# +# `pnpm run policy:check` fails CI when the two drift apart. +# +# https://www.npmjs.com/package/pnpm-policy + +# A third-party release must be this old before it can be installed. Most +# malicious releases (compromised maintainer account, typosquat, postinstall +# stealer) are found and yanked well inside two weeks, so the wait costs little +# and catches the attacks that move fastest. +minimumReleaseAge: 14d + +# Off by default: turning it on refuses any transitive dependency resolved from +# git or a URL rather than the registry. +blockExoticSubdeps: false + +# Nothing is exempt by default: everything this workspace installs, including the +# packages that scaffolded it, waits out the same two weeks. +# +# Exemptions are for what *you* publish, and they only start paying off once you +# do. npm reserves `@` for you, so that scope is safe to claim as +# soon as you publish into it — waiting two weeks on your own release protects +# nothing: +# +# scopes: +# - "@____username____" +# +# If you publish under an npm account whose packages are not all in one scope, +# name the account instead and let pnpm-policy ask npm what it publishes +# (`pnpm pnpm-policy inventory`): +# +# maintainers: +# - ____username____ +# inventory: ./pnpm-policy.inventory.json +scopes: [] + +# Dependencies permitted to run install scripts. An install script is arbitrary +# code at install time, so each entry is a deliberate decision and the value is +# the reason it is needed: +# +# allowBuilds: +# esbuild: native binary, downloaded at install time +# sharp: libvips bindings +allowBuilds: [] + +# Escape hatch for a third-party package that cannot wait — an urgent security +# release, typically. A reason is required, and `until` expires the waiver so +# `check` forces a re-justification instead of letting it become permanent: +# +# exceptions: +# - package: lodash +# versions: ["4.17.21"] +# reason: CVE-2021-23337 fix, published today +# until: 2026-09-01 +exceptions: [] diff --git a/pgpm/workspace/pnpm-workspace.yaml b/pgpm/workspace/pnpm-workspace.yaml index 18ec407..27bf15f 100644 --- a/pgpm/workspace/pnpm-workspace.yaml +++ b/pgpm/workspace/pnpm-workspace.yaml @@ -1,2 +1,11 @@ packages: - 'packages/*' + +# Managed by pnpm-policy — run `pnpm-policy generate` after editing pnpm-policy.yaml. + +# A third-party release must be 2w old before it can be installed. +# Most malicious releases are found and yanked well inside that window. +minimumReleaseAge: 20160 + +# Off: transitive dependencies may resolve from git or a URL. +blockExoticSubdeps: false diff --git a/pnpm/workspace/.github/workflows/ci.yml b/pnpm/workspace/.github/workflows/ci.yml index 1231187..44c5014 100644 --- a/pnpm/workspace/.github/workflows/ci.yml +++ b/pnpm/workspace/.github/workflows/ci.yml @@ -36,6 +36,11 @@ jobs: - name: Install run: pnpm install + # Fails when pnpm-workspace.yaml no longer matches pnpm-policy.yaml, or when a + # third-party waiver has passed its expiry date. + - name: Check supply-chain policy + run: pnpm run policy:check + - name: Build run: pnpm -r build diff --git a/pnpm/workspace/README.md b/pnpm/workspace/README.md index 91c541c..c3d8483 100644 --- a/pnpm/workspace/README.md +++ b/pnpm/workspace/README.md @@ -31,6 +31,23 @@ pnpm build pnpm lint ``` +### Supply-chain policy + +A third-party release must be 14 days old before this workspace will install it, which is where most compromised releases are caught. Nothing is exempt by default. `pnpm-policy.yaml` is where you exempt what you publish yourself, approve an install script, or waive the wait for an urgent security release: + +```sh +pnpm run policy # regenerate the managed block in pnpm-workspace.yaml +pnpm run policy:check # CI: fail on drift or an expired waiver +``` + +See [pnpm-policy](https://www.npmjs.com/package/pnpm-policy) for the full configuration. + +Nothing is locked yet on the very first install, so it resolves every version from the registry and trips if any of them is newer than the wait. Get past it once, then commit the lockfile it produces — from then on installs replay the lockfile and the wait only applies to versions you are adding: + +```sh +pnpm install --config.minimumReleaseAge=0 +``` + ### Prerequisites - Node.js 20+ diff --git a/pnpm/workspace/package.json b/pnpm/workspace/package.json index 61e6fda..3d32dcf 100644 --- a/pnpm/workspace/package.json +++ b/pnpm/workspace/package.json @@ -20,7 +20,9 @@ "clean": "pnpm -r run clean", "test": "pnpm -r run test", "lint": "pnpm -r run lint", - "deps": "pnpm up -r -i -L" + "deps": "pnpm up -r -i -L", + "policy": "pnpm-policy generate", + "policy:check": "pnpm-policy check" }, "devDependencies": { "@types/jest": "^30.0.0", @@ -34,6 +36,7 @@ "jest": "^30.2.0", "lerna": "^9.0.3", "makage": "^0.5.1", + "pnpm-policy": "^0.2.2", "prettier": "^3.7.4", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", diff --git a/pnpm/workspace/pnpm-policy.yaml b/pnpm/workspace/pnpm-policy.yaml new file mode 100644 index 0000000..f2dc676 --- /dev/null +++ b/pnpm/workspace/pnpm-policy.yaml @@ -0,0 +1,60 @@ +# Supply-chain policy for this workspace, enforced by pnpm itself. +# +# The pnpm settings it produces live in pnpm-workspace.yaml under the +# `Managed by pnpm-policy` marker. Edit this file, then run: +# +# pnpm run policy +# +# `pnpm run policy:check` fails CI when the two drift apart. +# +# https://www.npmjs.com/package/pnpm-policy + +# A third-party release must be this old before it can be installed. Most +# malicious releases (compromised maintainer account, typosquat, postinstall +# stealer) are found and yanked well inside two weeks, so the wait costs little +# and catches the attacks that move fastest. +minimumReleaseAge: 14d + +# Off by default: turning it on refuses any transitive dependency resolved from +# git or a URL rather than the registry. +blockExoticSubdeps: false + +# Nothing is exempt by default: everything this workspace installs, including the +# packages that scaffolded it, waits out the same two weeks. +# +# Exemptions are for what *you* publish, and they only start paying off once you +# do. npm reserves `@` for you, so that scope is safe to claim as +# soon as you publish into it — waiting two weeks on your own release protects +# nothing: +# +# scopes: +# - "@____username____" +# +# If you publish under an npm account whose packages are not all in one scope, +# name the account instead and let pnpm-policy ask npm what it publishes +# (`pnpm pnpm-policy inventory`): +# +# maintainers: +# - ____username____ +# inventory: ./pnpm-policy.inventory.json +scopes: [] + +# Dependencies permitted to run install scripts. An install script is arbitrary +# code at install time, so each entry is a deliberate decision and the value is +# the reason it is needed: +# +# allowBuilds: +# esbuild: native binary, downloaded at install time +# sharp: libvips bindings +allowBuilds: [] + +# Escape hatch for a third-party package that cannot wait — an urgent security +# release, typically. A reason is required, and `until` expires the waiver so +# `check` forces a re-justification instead of letting it become permanent: +# +# exceptions: +# - package: lodash +# versions: ["4.17.21"] +# reason: CVE-2021-23337 fix, published today +# until: 2026-09-01 +exceptions: [] diff --git a/pnpm/workspace/pnpm-workspace.yaml b/pnpm/workspace/pnpm-workspace.yaml index 18ec407..27bf15f 100644 --- a/pnpm/workspace/pnpm-workspace.yaml +++ b/pnpm/workspace/pnpm-workspace.yaml @@ -1,2 +1,11 @@ packages: - 'packages/*' + +# Managed by pnpm-policy — run `pnpm-policy generate` after editing pnpm-policy.yaml. + +# A third-party release must be 2w old before it can be installed. +# Most malicious releases are found and yanked well inside that window. +minimumReleaseAge: 20160 + +# Off: transitive dependencies may resolve from git or a URL. +blockExoticSubdeps: false