Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions apps/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropzone": "^19.1.1",
"react-router-dom": "^7.18.1",
"react-router-dom": "^7.18.2",
"sonner": "^2.0.7",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
Expand Down
28 changes: 1 addition & 27 deletions scripts/dependency-audit-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,5 @@
"functionality becomes reachable in our own source. Removing the vulnerable dependency",
"also requires removing its entry: a stale acknowledgement fails the build too."
],
"acknowledged": [
{
"id": "GHSA-qwww-vcr4-c8h2",
"package": "react-router",
"severity": "high",
"acceptedVersion": "7.18.1",
"patchedVersion": "8.3.0",
"advisoryUrl": "https://github.com/advisories/GHSA-qwww-vcr4-c8h2",
"reason": "GitHub lists React Router 8.3.0 as patched (affected >=7.12.0 <8.3.0). PARTHA temporarily remains on 7.18.1 because the patched major version has not yet been validated against the current React 18 prototype: react-router@8.3.0 declares peerDependencies react>=19.2.7 and react-dom>=19.2.7, and react-router-dom has no 8.x release at all (latest 7.18.1), so adopting it means a React 19 migration plus moving all 33 react-router-dom import sites. The affected behavior is restricted to unstable RSC APIs, which PARTHA does not import or enable; PARTHA is a client-only Vite SPA mounting createBrowserRouter with no server-side React Router runtime. This acceptance expires on 2026-10-01 and does not claim that no patched version exists.",
"reachability": {
"description": "Fails if our own source starts using an unstable or server-side React Router surface, which is the only way the advisory becomes reachable.",
"roots": ["apps/frontend/src"],
"forbiddenPatterns": [
"unstable_",
"createStaticHandler",
"createStaticRouter",
"StaticRouterProvider",
"deserializeErrors",
"react-router/server",
"@react-router/node",
"@react-router/express",
"@react-router/serve"
]
},
"reviewBy": "2026-10-01"
}
]
"acknowledged": []
}
112 changes: 78 additions & 34 deletions scripts/dependency-audit.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
* The gate's value is entirely in when it *refuses* to stay quiet. These cover
* each condition under which an acknowledgement must stop being accepted.
*
* The behavioural tests deliberately use *synthetic* entries rather than
* binding to whichever exception happens to be shipped today. An earlier
* version of this file used the live react-router entry as its fixture, so
* retiring that exception — the correct action once the advisory was patched
* in 7.18.2 — broke five tests at once. A gate that fails when you fix the
* vulnerability it guards is worse than no gate: it teaches you to keep the
* exception. Structural expectations about the shipped policy live in their
* own tests below and hold for any entry, including none.
*
* Run with: node --test scripts/
*/

Expand All @@ -21,8 +30,18 @@ const policy = JSON.parse(readFileSync(join(repoRoot, 'scripts', 'dependency-aud
const FUTURE = '2099-01-01';
const TODAY = '2026-07-25';

/** The real react-router entry, so the tests bind to the shipped policy. */
const reactRouterEntry = policy.acknowledged.find((entry) => entry.id === 'GHSA-qwww-vcr4-c8h2');
/**
* A synthetic acknowledgement. `react-router` is a real lockfile entry, so the
* version-drift checks exercise the real lookup, but no assertion depends on
* which version is pinned today.
*/
const syntheticEntry = {
id: 'GHSA-test-0000-0000',
package: 'react-router',
severity: 'high',
reason: 'synthetic fixture for the policy-gate tests',
reviewBy: FUTURE,
};

function advisoryFor(entry, overrides = {}) {
return {
Expand All @@ -35,48 +54,36 @@ function advisoryFor(entry, overrides = {}) {
};
}

test('the shipped react-router acknowledgement is currently valid', () => {
assert.ok(reactRouterEntry, 'expected a GHSA-qwww-vcr4-c8h2 acknowledgement');
const problems = validateAcknowledgement(reactRouterEntry, advisoryFor(reactRouterEntry), TODAY);
test('a current acknowledgement with no drift is accepted', () => {
const problems = validateAcknowledgement(syntheticEntry, advisoryFor(syntheticEntry), TODAY);
assert.deepEqual(problems, []);
});

test('the react-router acknowledgement records the real patched version', () => {
// The earlier version of this policy claimed no patched release existed.
// GitHub lists 8.3.0 as patched, so the policy must say so and must not
// repeat npm's "No fix available" as if it meant the same thing.
assert.equal(reactRouterEntry.patchedVersion, '8.3.0');
assert.match(reactRouterEntry.reason, /8\.3\.0 as patched/);
assert.doesNotMatch(reactRouterEntry.reason, /no fixed release exists/i);
assert.match(reactRouterEntry.reason, /does not claim that no patched version exists/i);
});

test('an expired acknowledgement fails', () => {
const entry = { ...reactRouterEntry, reviewBy: '2020-01-01' };
const entry = { ...syntheticEntry, reviewBy: '2020-01-01' };
const problems = validateAcknowledgement(entry, advisoryFor(entry), TODAY);
assert.equal(problems.length, 1);
assert.match(problems[0], /expired on 2020-01-01/);
});

test('an acknowledgement fails when the advisory no longer names the accepted package', () => {
const entry = { ...reactRouterEntry, reviewBy: FUTURE };
const advisory = advisoryFor(entry, { packages: new Set(['some-other-package']) });
const problems = validateAcknowledgement(entry, advisory, TODAY);
const advisory = advisoryFor(syntheticEntry, { packages: new Set(['some-other-package']) });
const problems = validateAcknowledgement(syntheticEntry, advisory, TODAY);
assert.equal(problems.length, 1);
assert.match(problems[0], /but the advisory now affects some-other-package/);
});

test('an acknowledgement fails when the accepted package version changes', () => {
const entry = { ...reactRouterEntry, reviewBy: FUTURE, acceptedVersion: '7.0.0' };
// A version the lockfile will never pin, so this stays true across bumps.
const entry = { ...syntheticEntry, acceptedVersion: '0.0.0-not-a-real-release' };
const problems = validateAcknowledgement(entry, advisoryFor(entry), TODAY);
assert.equal(problems.length, 1);
assert.match(problems[0], /accepted for react-router@7\.0\.0 but the lockfile now pins/);
assert.match(problems[0], /accepted for react-router@0\.0\.0-not-a-real-release but the lockfile now pins/);
});

test('an acknowledgement fails when the accepted package leaves the lockfile', () => {
const entry = {
...reactRouterEntry,
reviewBy: FUTURE,
...syntheticEntry,
package: 'package-that-is-not-installed',
acceptedVersion: '1.0.0',
};
Expand All @@ -85,12 +92,10 @@ test('an acknowledgement fails when the accepted package leaves the lockfile', (
});

test('an acknowledgement fails when the affected surface becomes reachable', () => {
// The react-router acceptance rests on PARTHA never touching unstable RSC or
// server APIs. Point the guard at a root that does contain one of its own
// forbidden patterns to prove the check actually fires.
// Point the guard at a root that does contain one of its own forbidden
// patterns to prove the check actually fires.
const entry = {
...reactRouterEntry,
reviewBy: FUTURE,
...syntheticEntry,
reachability: {
roots: ['scripts'],
forbiddenPatterns: ['createStaticHandler'],
Expand All @@ -102,15 +107,54 @@ test('an acknowledgement fails when the affected surface becomes reachable', ()
assert.match(problems[0], /createStaticHandler/);
});

test('the real reachability guard finds nothing in the frontend source today', () => {
const problems = validateAcknowledgement(
{ ...reactRouterEntry, reviewBy: FUTURE },
advisoryFor(reactRouterEntry),
TODAY,
);
test('the frontend source uses no unstable or server-side React Router surface', () => {
// Kept independent of any shipped acknowledgement: PARTHA is a client-only
// Vite SPA, and the RSC/server React Router surface is where this class of
// advisory lives. This asserts that property directly, so it keeps holding
// after an exception is retired.
const entry = {
...syntheticEntry,
reachability: {
roots: ['apps/frontend/src'],
forbiddenPatterns: [
'unstable_',
'createStaticHandler',
'createStaticRouter',
'StaticRouterProvider',
'deserializeErrors',
'react-router/server',
'@react-router/node',
'@react-router/express',
'@react-router/serve',
],
},
};
const problems = validateAcknowledgement(entry, advisoryFor(entry), TODAY);
assert.deepEqual(problems, [], 'frontend source must not use unstable or server React Router APIs');
});

test('every shipped acknowledgement is well-formed and still valid today', () => {
// Holds for an empty policy, which is the healthy steady state: an
// acknowledgement should exist only while a real advisory is unpatched.
const today = new Date().toISOString().slice(0, 10);
for (const entry of policy.acknowledged || []) {
assert.ok(entry.id, 'each acknowledgement needs an advisory id');
assert.ok(entry.package, `${entry.id} needs a package`);
assert.ok(entry.reason, `${entry.id} needs a reviewable reason`);
assert.ok(entry.reviewBy, `${entry.id} needs a reviewBy date`);
assert.doesNotMatch(
entry.reason,
/no fixed release exists/i,
`${entry.id} must not repeat npm's "No fix available" as if it meant no patch exists`,
);
assert.deepEqual(
validateAcknowledgement(entry, advisoryFor(entry), today),
[],
`${entry.id} is no longer valid; re-review or remove it`,
);
}
});

test('advisories() groups npm audit output by advisory id', () => {
const report = {
vulnerabilities: {
Expand Down
Loading