Skip to content
Open
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ typecheck: deps ; for dir in $(TS_WORKSPACES); do $(call exec_or_bunx,tsc,--noEm
audit: deps
pnpm -r install
pnpm -r --if-present run audit
pnpm audit
pnpm run audit

lockfile:
pnpm install --lockfile-only
Expand Down
2 changes: 1 addition & 1 deletion frontend-pwa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"preview": "vite preview",
"gen:api": "bunx orval --config orval.config.cjs",
"test": "vitest run",
"audit": "pnpm audit",
"audit": "node ./scripts/run-audit.mjs",
"audit:snyk": "bun x snyk test"
},
"engines": {
Expand Down
79 changes: 79 additions & 0 deletions frontend-pwa/scripts/run-audit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/** @file Ensures `pnpm audit` only fails for known, patched validator vulnerability.
*
* The validator package currently has no upstream patch release. We vendor the
* required fix locally and treat the advisory as mitigated when the patched
* code is present. Any additional vulnerabilities remain fatal.
*/

import { spawnSync } from 'node:child_process';
import { isValidatorPatched } from '../../security/validator-patch.js';
const TARGET_ADVISORY = 'GHSA-9965-vmph-33xx';

function runAuditJson() {
const result = spawnSync('pnpm', ['audit', '--json'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'inherit'],
});

if (result.error) {
throw result.error;
}

if (!result.stdout) {
throw new Error('pnpm audit produced no output to parse');
}

let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch (error) {
error.message = `Failed to parse pnpm audit JSON: ${error.message}`;
throw error;
}

return { parsed, status: result.status ?? 0 };
}

function main() {
const { parsed, status } = runAuditJson();
const advisories = Object.values(parsed.advisories ?? {});

const unexpected = advisories.filter(
(advisory) => advisory.github_advisory_id !== TARGET_ADVISORY,
);

if (unexpected.length > 0) {
console.error('Unexpected vulnerabilities detected by pnpm audit:');
for (const advisory of unexpected) {
console.error(`- ${advisory.github_advisory_id}: ${advisory.title}`);
}
process.exit(1);
}

const targetFinding = advisories.find(
(advisory) => advisory.github_advisory_id === TARGET_ADVISORY,
);

if (!targetFinding) {
process.exit(status);
}

if (!isValidatorPatched()) {
console.error(
'Validator vulnerability GHSA-9965-vmph-33xx found but local patch missing.',
);
process.exit(1);
}

console.info(
'Validator vulnerability GHSA-9965-vmph-33xx mitigated by local patch; audit passes.',
);
process.exit(0);
}

try {
main();
} catch (error) {
console.error(error);
process.exit(1);
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"devDependencies": {
"@biomejs/biome": "^2.2.4",
"@mermaid-js/mermaid-cli": "^11.12.0",
"puppeteer": "^23.11.1",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"markdownlint-cli": "^0.45.0",
"prettier-plugin-sort-json": "^3.0.0",
"puppeteer": "^23.11.1",
"vite": "^7.1.9"
},
"name": "myapp",
Expand Down Expand Up @@ -44,6 +44,9 @@
"tar-fs": "3.1.1",
"ws": "8.18.3",
"pino": "9.13.1"
},
"patchedDependencies": {
"validator@13.15.15": "patches/validator@13.15.15.patch"
}
}
}
44 changes: 44 additions & 0 deletions patches/validator@13.15.15.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
diff --git a/es/lib/isURL.js b/es/lib/isURL.js
index 368e97d9f41baffe6a8db0164e2da3fa459a0be1..c79fe301aff2fa40b769a8443405f99e4df0f407 100644
--- a/es/lib/isURL.js
+++ b/es/lib/isURL.js
@@ -91,6 +91,17 @@ export default function isURL(url, options) {
return false;
}
split[0] = url.slice(2);
+ } else {
+ var firstColon = url.indexOf(':');
+ if (firstColon >= 0) {
+ var potentialProtocol = url.slice(0, firstColon).toLowerCase();
+ if (options.protocols.indexOf(potentialProtocol) !== -1) {
+ return false;
+ }
+ if (options.require_valid_protocol) {
+ return false;
+ }
+ }
}
url = split.join('://');
if (url === '') {
diff --git a/lib/isURL.js b/lib/isURL.js
index fdd5ea64fb108c2d81bb9b3b1c88abd9c0272bea..00a3d0e3faf2467c6cfbfb5146dd4fd4bbae3e6c 100644
--- a/lib/isURL.js
+++ b/lib/isURL.js
@@ -97,6 +97,17 @@ function isURL(url, options) {
return false;
}
split[0] = url.slice(2);
+ } else {
+ var firstColon = url.indexOf(':');
+ if (firstColon >= 0) {
+ var potentialProtocol = url.slice(0, firstColon).toLowerCase();
+ if (options.protocols.indexOf(potentialProtocol) !== -1) {
+ return false;
+ }
+ if (options.require_valid_protocol) {
+ return false;
+ }
+ }
}
url = split.join('://');
if (url === '') {
9 changes: 7 additions & 2 deletions pnpm-lock.yaml

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

16 changes: 8 additions & 8 deletions security/audit-exceptions.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[
{
"addedAt": "2024-01-01",
"advisory": "GHSA-xxxx-xxxx-xxxx",
"expiresAt": "2030-01-01",
"id": "EXAMPLE-0001",
"introducedBy": "dependency@1.2.3",
"package": "example-package",
"reason": "Example exception for development",
"reviewer": "security@example.com"
"id": "VAL-2025-0001",
"package": "validator",
"advisory": "GHSA-9965-vmph-33xx",
"introducedBy": "validator@13.15.15",
"reason": "Local patch hardens isURL protocol parsing until upstream ships a fix.",
"reviewer": "security@wildside.dev",
"addedAt": "2025-02-14",
"expiresAt": "2026-02-14"
}
]
65 changes: 64 additions & 1 deletion security/validate-audit.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @file Validate audit exception entries against schema and expiry. */
import { spawnSync } from 'node:child_process';
import Ajv from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
import { isValidatorPatched } from './validator-patch.js';

/**
* Load a JSON file using the import attribute supported by the current Node
Expand Down Expand Up @@ -84,7 +86,68 @@ function assertNoExpired(entries) {
}
}

function runAuditJson() {
const result = spawnSync('pnpm', ['audit', '--json'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'inherit'],
});

if (result.error) {
throw result.error;
}

Comment on lines +89 to +98
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Error handling for missing stdout differs from run-audit.mjs.

The current approach returns an empty advisories object when stdout is missing, whereas run-audit.mjs throws an error. Please update this implementation to match run-audit.mjs for consistent error handling and to prevent silent failures.

if (!result.stdout) {
return { advisories: {} };
}

try {
Comment on lines +89 to +103
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate pnpm audit failures instead of ignoring stdout

In runAuditJson, the script treats a missing stdout as an empty audit result and continues to print “Audit exceptions valid…”. If pnpm audit --json fails before producing output (network outage, registry error, authentication problems), stderr is printed but the function returns {advisories: {}} and the process exits with 0. This bypasses the audit entirely and hides real failures. The helper should propagate the command’s exit status or throw when no JSON is produced so audit failures stop the build.

Useful? React with 👍 / 👎.

return JSON.parse(result.stdout);
} catch (error) {
error.message = `Failed to parse pnpm audit JSON: ${error.message}`;
throw error;
}
}
Comment on lines +89 to +109
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add timeout and tighten result handling for pnpm audit

Prevent indefinite hangs; fail with a clear message on timeout; accept non-zero status when JSON is provided.

Apply:

 function runAuditJson() {
-  const result = spawnSync('pnpm', ['audit', '--json'], {
-    encoding: 'utf8',
-    stdio: ['ignore', 'pipe', 'inherit'],
-  });
+  const result = spawnSync('pnpm', ['audit', '--json'], {
+    encoding: 'utf8',
+    stdio: ['ignore', 'pipe', 'inherit'],
+    timeout: 60_000,
+  });
 
   if (result.error) {
     throw result.error;
   }
 
-  if (!result.stdout) {
+  if (result.timedOut) {
+    throw new Error('pnpm audit timed out after 60s');
+  }
+
+  if (!result.stdout || !result.stdout.trim()) {
     return { advisories: {} };
   }
 
   try {
     return JSON.parse(result.stdout);
   } catch (error) {
     error.message = `Failed to parse pnpm audit JSON: ${error.message}`;
     throw error;
   }
 }
🤖 Prompt for AI Agents
In security/validate-audit.js around lines 89 to 109, add a 60_000ms timeout to
the spawnSync call to prevent hangs, check result.timedOut and throw a clear
Error('pnpm audit timed out after 60s') if true, and tighten stdout handling by
treating empty or whitespace-only stdout as no advisories (return { advisories:
{} }) — leave behavior that accepts non-zero exit codes intact so JSON output is
still parsed when present.


function assertMitigated(entries, advisories) {
if (advisories.length === 0) {
return;
}

const exceptionsById = new Map(entries.map((entry) => [entry.advisory, entry]));
const unexpected = [];

for (const advisory of advisories) {
const exception = exceptionsById.get(advisory.github_advisory_id);
if (!exception) {
unexpected.push(advisory);
continue;
}

if (
advisory.github_advisory_id === 'GHSA-9965-vmph-33xx' &&
!isValidatorPatched()
) {
console.error(
'Validator vulnerability GHSA-9965-vmph-33xx reported but local patch is missing.',
);
process.exit(1);
}
}

if (unexpected.length > 0) {
console.error('pnpm audit reported vulnerabilities without exceptions:');
for (const advisory of unexpected) {
console.error(`- ${advisory.github_advisory_id}: ${advisory.title}`);
}
process.exit(1);
}
}

assertValidSchema(data);
assertNoExpired(data);

console.log('Audit exceptions valid');
const auditJson = runAuditJson();
const advisories = Object.values(auditJson.advisories ?? {});
assertMitigated(data, advisories);

console.log('Audit exceptions valid and vulnerabilities accounted for');
22 changes: 22 additions & 0 deletions security/validator-patch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/** @file Helpers for verifying the locally patched validator dependency. */

import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';

function resolveRulesetRequire() {
const workspaceRequire = createRequire(new URL('../frontend-pwa/package.json', import.meta.url));
const orvalRequire = createRequire(workspaceRequire.resolve('orval/package.json'));
const coreRequire = createRequire(orvalRequire.resolve('@orval/core/package.json'));
return createRequire(coreRequire.resolve('@ibm-cloud/openapi-ruleset/package.json'));
}

export function resolveValidatorPath() {
const rulesetRequire = resolveRulesetRequire();
return rulesetRequire.resolve('validator/lib/isURL');
}

export function isValidatorPatched() {
const validatorPath = resolveValidatorPath();
const contents = readFileSync(validatorPath, 'utf8');
return contents.includes("var firstColon = url.indexOf(':');");
}
Loading