Skip to content

Commit 8145936

Browse files
authored
Merge pull request #545 from cipherstash/chore/dependabot-alert-cleanup
fix(deps): resolve all 26 open Dependabot alerts via active pnpm overrides
2 parents 410c266 + 0489974 commit 8145936

9 files changed

Lines changed: 449 additions & 590 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@cipherstash/wizard": patch
3+
---
4+
5+
Add `@anthropic-ai/sdk` `^0.106.0` as a direct dependency so the
6+
auto-installed peer of `@anthropic-ai/claude-agent-sdk` resolves to a release
7+
patched against GHSA-p7fg-763f-g4gf, instead of the vulnerable 0.81.0 the
8+
peer range alone would select. The wizard never imports the SDK directly —
9+
this is a peer-resolution pin only; no behaviour change.

e2e/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"@cipherstash/wizard": "workspace:*"
1616
},
1717
"devDependencies": {
18+
"@types/semver": "7.5.8",
19+
"semver": "^7.8.0",
1820
"vitest": "catalog:repo",
1921
"yaml": "^2.9.0"
2022
}

e2e/tests/supply-chain.e2e.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readFileSync } from 'node:fs'
22
import { dirname, join, resolve } from 'node:path'
33
import { fileURLToPath } from 'node:url'
4+
import semver from 'semver'
45
import { describe, expect, it } from 'vitest'
56
import { parse as parseYaml } from 'yaml'
67

@@ -16,6 +17,27 @@ const read = (p: string) => readFileSync(join(REPO_ROOT, p), 'utf8')
1617
const readJson = (p: string) => JSON.parse(read(p))
1718
const readYaml = (p: string) => parseYaml(read(p))
1819

20+
// Map every package in the lockfile's `packages:` section to its resolved
21+
// versions. Keys there are bare `name@version` (peer suffixes live under
22+
// `snapshots:`), and scoped names keep their leading `@`, so split on the
23+
// LAST `@`; strip any stray peer suffix defensively.
24+
const resolvedVersionsByName = (): Map<string, string[]> => {
25+
const lock = readYaml('pnpm-lock.yaml') as {
26+
packages?: Record<string, unknown>
27+
}
28+
const byName = new Map<string, string[]>()
29+
for (const key of Object.keys(lock.packages ?? {})) {
30+
const at = key.lastIndexOf('@')
31+
if (at <= 0) continue // no scope-only or malformed keys
32+
const name = key.slice(0, at)
33+
const version = key.slice(at + 1).split('(')[0]
34+
const list = byName.get(name)
35+
if (list) list.push(version)
36+
else byName.set(name, [version])
37+
}
38+
return byName
39+
}
40+
1941
describe('supply chain — pnpm configuration', () => {
2042
it('packageManager is pnpm ≥ 10.26 (needed for blockExoticSubdeps)', () => {
2143
const pm = readJson('package.json').packageManager as string
@@ -46,6 +68,44 @@ describe('supply chain — pnpm configuration', () => {
4668
expect(Array.isArray(allow)).toBe(true)
4769
expect(allow.length).toBeLessThanOrEqual(3)
4870
})
71+
72+
it('minimumReleaseAgeExclude contains only first-party packages', () => {
73+
// The cooldown exclusion list exists for first-party packages that ship
74+
// on their own release cadence. Third-party security fixes must use the
75+
// one-off bypass (`pnpm install --config.minimum-release-age=0` with an
76+
// exact pin) instead — a name-scoped exclusion exempts every future
77+
// release of the package. See SKILL.md "Bypass the install cooldown".
78+
const ws = readYaml('pnpm-workspace.yaml') as {
79+
minimumReleaseAgeExclude?: string[]
80+
}
81+
const FIRST_PARTY = [/^@prisma-next\//, /^@cipherstash\//]
82+
for (const entry of ws.minimumReleaseAgeExclude ?? []) {
83+
expect(
84+
FIRST_PARTY.some((re) => re.test(entry)),
85+
`"${entry}" is not a first-party cooldown exclusion`,
86+
).toBe(true)
87+
}
88+
})
89+
90+
it('security overrides stay range-scoped and remain a small allowlist (≤12 entries)', () => {
91+
// Every override must be scoped to the advisory's vulnerable range
92+
// (`pkg@<range>`), never a blanket `pkg` pin — a blanket pin silently
93+
// rewrites versions outside the vulnerable range forever. The count cap
94+
// mirrors onlyBuiltDependencies: growth forces a conscious review.
95+
const ws = readYaml('pnpm-workspace.yaml') as {
96+
overrides?: Record<string, string>
97+
}
98+
const selectors = Object.keys(ws.overrides ?? {})
99+
expect(selectors.length).toBeLessThanOrEqual(12)
100+
for (const selector of selectors) {
101+
// A version-scoped selector has an `@` after the package name
102+
// (position > 0 handles `@scope/pkg@range`).
103+
expect(
104+
selector.lastIndexOf('@') > 0,
105+
`override "${selector}" is not scoped to a version range`,
106+
).toBe(true)
107+
}
108+
})
49109
})
50110

51111
describe('supply chain — registry pinning (.npmrc)', () => {
@@ -90,6 +150,69 @@ describe('supply chain — pnpm-lock.yaml integrity', () => {
90150
}
91151
expect(offenders).toEqual([])
92152
})
153+
154+
it('every security override actually took effect (nothing left in a vulnerable range)', () => {
155+
// The shape test ("security overrides stay range-scoped") proves the
156+
// selectors are well-formed; this proves they *worked*. For each override
157+
// `selector -> target`, no package may resolve to a version that still
158+
// matches the vulnerable `selector` yet fails the `target` — that pair is
159+
// exactly a silent regression (e.g. a re-resolve demoting fast-uri below
160+
// its pin, un-fixing the advisory). Note `target` can sit inside its own
161+
// selector range (js-yaml@>=4.0.0 <5 -> 4.2.0 normalises all 4.x to the
162+
// patched release), so the check is "matched but not raised", not the
163+
// stricter "no version matches the selector".
164+
const ws = readYaml('pnpm-workspace.yaml') as {
165+
overrides?: Record<string, string>
166+
}
167+
const overrides = Object.entries(ws.overrides ?? {})
168+
// Guard the vacuous case: an empty/removed block would pass every loop
169+
// below with zero iterations.
170+
expect(overrides.length).toBeGreaterThan(0)
171+
172+
const byName = resolvedVersionsByName()
173+
const offenders: string[] = []
174+
for (const [selector, target] of overrides) {
175+
const at = selector.lastIndexOf('@')
176+
const name = selector.slice(0, at)
177+
const vulnerableRange = selector.slice(at + 1)
178+
for (const version of byName.get(name) ?? []) {
179+
if (
180+
semver.satisfies(version, vulnerableRange) &&
181+
!semver.satisfies(version, target)
182+
) {
183+
offenders.push(
184+
`${name}@${version} still matches vulnerable "${vulnerableRange}" (override target "${target}" not applied)`,
185+
)
186+
}
187+
}
188+
}
189+
expect(offenders).toEqual([])
190+
})
191+
192+
it('package.json has no top-level `overrides` (pnpm only reads pnpm-workspace.yaml)', () => {
193+
// pnpm silently ignores a top-level npm-format `overrides` block; the
194+
// security pins must live in pnpm-workspace.yaml `overrides`. Guards
195+
// against the block being moved back here, where it would look applied
196+
// but do nothing.
197+
const pkg = readJson('package.json') as { overrides?: unknown }
198+
expect(pkg.overrides).toBeUndefined()
199+
})
200+
201+
it('@anthropic-ai/sdk resolves to the peer-pinned patched version (≥ 0.106.0)', () => {
202+
// Not an override but a peer-resolution pin: packages/wizard depends on
203+
// @anthropic-ai/sdk@^0.106.0 to force the auto-installed peer of
204+
// @anthropic-ai/claude-agent-sdk past the advisory-vulnerable 0.81.0
205+
// (GHSA-p7fg-763f-g4gf). The override-effect test cannot cover a peer
206+
// pin, so assert the resolved version directly.
207+
const versions = resolvedVersionsByName().get('@anthropic-ai/sdk') ?? []
208+
expect(versions.length).toBeGreaterThan(0)
209+
for (const version of versions) {
210+
expect(
211+
semver.gte(version, '0.106.0'),
212+
`@anthropic-ai/sdk@${version} is below the patched 0.106.0`,
213+
).toBe(true)
214+
}
215+
})
93216
})
94217

95218
describe('supply chain — CI hardening (.github/workflows/tests.yml)', () => {

package.json

Lines changed: 1 addition & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,6 @@
1919
"url": "git+https://github.com/cipherstash/stack.git"
2020
},
2121
"license": "MIT",
22-
"workspaces": {
23-
"packages": [
24-
"packages/*",
25-
"examples/*"
26-
],
27-
"catalogs": {
28-
"repo": {
29-
"@cipherstash/auth": "0.40.0",
30-
"tsup": "8.4.0",
31-
"tsx": "4.19.3",
32-
"typescript": "5.6.3",
33-
"vitest": "3.1.3"
34-
},
35-
"security": {
36-
"@clerk/nextjs": "6.39.2",
37-
"next": "15.5.10",
38-
"vite": "6.4.1"
39-
}
40-
}
41-
},
4222
"scripts": {
4323
"build": "turbo build --filter './packages/*'",
4424
"build:js": "turbo build --filter './packages/protect' --filter './packages/nextjs'",
@@ -59,7 +39,7 @@
5939
"@biomejs/biome": "^2.4.15",
6040
"@changesets/cli": "^2.31.0",
6141
"@types/node": "^22.19.19",
62-
"js-yaml": "^4.1.1",
42+
"js-yaml": "^4.2.0",
6343
"rimraf": "^6.1.3",
6444
"turbo": "2.9.14",
6545
"vitest": "catalog:repo"
@@ -83,58 +63,5 @@
8363
"onlyBuiltDependencies": [
8464
"node-pty"
8565
]
86-
},
87-
"overrides": {
88-
"@babel/runtime": "7.26.10",
89-
"brace-expansion@^5": ">=5.0.5",
90-
"body-parser": "2.2.1",
91-
"vite": "catalog:security",
92-
"pg": "^8.16.3",
93-
"postgres": "^3.4.7",
94-
"js-yaml": "4.1.1",
95-
"test-exclude": "^7.0.1",
96-
"glob": ">=11.1.0",
97-
"qs": ">=6.14.1",
98-
"lodash": ">=4.18.0",
99-
"minimatch": ">=10.2.3",
100-
"@isaacs/brace-expansion": ">=5.0.1",
101-
"fast-xml-parser": ">=5.3.4",
102-
"next": ">=15.5.15",
103-
"ajv": ">=8.18.0",
104-
"esbuild@<=0.24.2": ">=0.25.0",
105-
"picomatch@^4": ">=4.0.4",
106-
"picomatch@^2": ">=2.3.2",
107-
"rollup@>=4.0.0 <4.59.0": ">=4.59.0",
108-
"drizzle-orm": ">=0.45.2",
109-
"postcss": ">=8.5.10",
110-
"hono": ">=4.12.14",
111-
"@hono/node-server": ">=1.19.13",
112-
"@prisma-next/adapter-postgres": "0.6.0-dev.8",
113-
"@prisma-next/cli": "0.6.0-dev.8",
114-
"@prisma-next/config": "0.6.0-dev.8",
115-
"@prisma-next/contract": "0.6.0-dev.8",
116-
"@prisma-next/contract-authoring": "0.6.0-dev.8",
117-
"@prisma-next/driver-postgres": "0.6.0-dev.8",
118-
"@prisma-next/emitter": "0.6.0-dev.8",
119-
"@prisma-next/errors": "0.6.0-dev.8",
120-
"@prisma-next/family-sql": "0.6.0-dev.8",
121-
"@prisma-next/framework-components": "0.6.0-dev.8",
122-
"@prisma-next/ids": "0.6.0-dev.8",
123-
"@prisma-next/migration-tools": "0.6.0-dev.8",
124-
"@prisma-next/operations": "0.6.0-dev.8",
125-
"@prisma-next/psl-parser": "0.6.0-dev.8",
126-
"@prisma-next/psl-printer": "0.6.0-dev.8",
127-
"@prisma-next/sql-contract": "0.6.0-dev.8",
128-
"@prisma-next/sql-contract-emitter": "0.6.0-dev.8",
129-
"@prisma-next/sql-contract-psl": "0.6.0-dev.8",
130-
"@prisma-next/sql-contract-ts": "0.6.0-dev.8",
131-
"@prisma-next/sql-errors": "0.6.0-dev.8",
132-
"@prisma-next/sql-operations": "0.6.0-dev.8",
133-
"@prisma-next/sql-relational-core": "0.6.0-dev.8",
134-
"@prisma-next/sql-runtime": "0.6.0-dev.8",
135-
"@prisma-next/sql-schema-ir": "0.6.0-dev.8",
136-
"@prisma-next/target-postgres": "0.6.0-dev.8",
137-
"@prisma-next/ts-render": "0.6.0-dev.8",
138-
"@prisma-next/utils": "0.6.0-dev.8"
13966
}
14067
}

packages/nextjs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"devDependencies": {
4040
"@clerk/nextjs": "catalog:security",
4141
"dotenv": "^17.4.2",
42+
"next": "catalog:security",
4243
"tsup": "catalog:repo",
4344
"typescript": "catalog:repo",
4445
"vitest": "catalog:repo"

packages/wizard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
},
3131
"dependencies": {
3232
"@anthropic-ai/claude-agent-sdk": "^0.3.143",
33+
"@anthropic-ai/sdk": "^0.106.0",
3334
"@cipherstash/auth": "catalog:repo",
3435
"@clack/prompts": "1.4.0",
3536
"dotenv": "17.4.2",

0 commit comments

Comments
 (0)