From 4ab64919fdc89d23d1daa5fdaf238df526189994 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Tue, 23 Jun 2026 15:59:50 +0200 Subject: [PATCH 1/3] Escape package name before using in spawn Preventing shell expansion using single quotes did not work on Windows as the cmd prompt version of `npm show` would use the quote marks in the api call. The closest specification of npm package names comes from the docs where it says that it must be usable as part of a URL. We don't use encodeURIComponent() as that would escape @ and / as well, so encodeURI is enough. https://docs.npmjs.com/cli/v10/configuring-npm/package-json#name --- packages/pluggable-widgets-tools/CHANGELOG.md | 1 + packages/pluggable-widgets-tools/src/commands/audit.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/pluggable-widgets-tools/CHANGELOG.md b/packages/pluggable-widgets-tools/CHANGELOG.md index 14e5efb0..545ec18f 100644 --- a/packages/pluggable-widgets-tools/CHANGELOG.md +++ b/packages/pluggable-widgets-tools/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed - We fixed an issue on Windows where the generated `.mpk` was missing the widget's `.xml` files and icon/tile PNGs. +- We fixed an error thrown by the `audit` command on windows. It would fail when looking up available versions for vulnerable packages. ### Changed diff --git a/packages/pluggable-widgets-tools/src/commands/audit.ts b/packages/pluggable-widgets-tools/src/commands/audit.ts index 5bd7e114..89f66bd9 100644 --- a/packages/pluggable-widgets-tools/src/commands/audit.ts +++ b/packages/pluggable-widgets-tools/src/commands/audit.ts @@ -96,7 +96,8 @@ interface UpdateablePackage { * Using the ^ version range avoids this, as the version is specific enough for npm. */ async function findSafeVersion({ name, range }: NpmAudit.Dependency): Promise { - const versions = await promisify(exec)(`npm show '${name}' versions --json`).then( + const escapedName = encodeURI(name); // npm package names must be usable as part of a URL + const versions = await promisify(exec)(`npm show ${escapedName} versions --json`).then( ({ stdout }) => JSON.parse(stdout) as string[] ); From e0e448a87e86617bbd0b240028bec8eb0ba1c087 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 24 Jun 2026 09:41:49 +0200 Subject: [PATCH 2/3] Handle errors while fetching available package versions --- .../src/commands/audit.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/pluggable-widgets-tools/src/commands/audit.ts b/packages/pluggable-widgets-tools/src/commands/audit.ts index 89f66bd9..41a9d3db 100644 --- a/packages/pluggable-widgets-tools/src/commands/audit.ts +++ b/packages/pluggable-widgets-tools/src/commands/audit.ts @@ -49,7 +49,8 @@ export async function auditPluggableWidgetsTools(fix: boolean = false) { const update = p.safeRange ? green(`${symbols.pointerSmall} ${p.safeRange}`) : red(`${symbols.cross} No update available`); - console.log(` ${whiteBright(bold(p.name))} ${p.vulnerableRange} ${update}`); + const status = p.error ? red(p.error.message) : update; + console.log(` ${whiteBright(bold(p.name))} ${p.vulnerableRange} ${status}`); }); // Add overrides for updateable dependencies @@ -86,6 +87,7 @@ interface UpdateablePackage { name: NpmAudit.PackageName; vulnerableRange: string; safeRange?: string; + error?: Error; } /** @@ -96,18 +98,23 @@ interface UpdateablePackage { * Using the ^ version range avoids this, as the version is specific enough for npm. */ async function findSafeVersion({ name, range }: NpmAudit.Dependency): Promise { + const updateablePackage = { name, vulnerableRange: range }; const escapedName = encodeURI(name); // npm package names must be usable as part of a URL - const versions = await promisify(exec)(`npm show ${escapedName} versions --json`).then( - ({ stdout }) => JSON.parse(stdout) as string[] - ); + const versions = await promisify(exec)(`npm show ${escapedName} versions --json`) + .then(({ stdout }) => JSON.parse(stdout) as string[]) + .catch(_ => new Error("Unable to fetch available versions")); + + if (versions instanceof Error) { + return { ...updateablePackage, error: versions }; + } const maxVulnerable = maxSatisfying(versions, range); const gtMaxVulnerable = ">" + maxVulnerable; const minNonVulnerable = minSatisfying(versions, gtMaxVulnerable); if (!minNonVulnerable) { - return { name, vulnerableRange: range }; + return updateablePackage; } - return { name, vulnerableRange: range, safeRange: "^" + minNonVulnerable }; + return { ...updateablePackage, safeRange: "^" + minNonVulnerable }; } From fcd4dd050b385158bafcad026147abf89f3ef8a6 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 24 Jun 2026 14:11:42 +0200 Subject: [PATCH 3/3] Handle cycles in audit report --- .../src/commands/audit.ts | 2 +- .../pluggable-widgets-tools/src/common.ts | 13 ++++++-- .../src/utils/npmAudit.ts | 30 ++++++++++++++----- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/pluggable-widgets-tools/src/commands/audit.ts b/packages/pluggable-widgets-tools/src/commands/audit.ts index 41a9d3db..ffcf9c10 100644 --- a/packages/pluggable-widgets-tools/src/commands/audit.ts +++ b/packages/pluggable-widgets-tools/src/commands/audit.ts @@ -20,7 +20,7 @@ export async function auditPluggableWidgetsTools(fix: boolean = false) { } // Collect updateable, vulnerable packages installed by pwt - const vulnerabilities = NpmAudit.collectVulnerabilities(report, report.vulnerabilities[pluggableWidgetsTools]); + const vulnerabilities = NpmAudit.collectVulnerabilities(report, pluggableWidgetsTools); const vulnerableDependencies = vulnerabilities .map(v => v.name) .reduce((unique, p) => (unique.includes(p) ? unique : [...unique, p]), [] as NpmAudit.PackageName[]); diff --git a/packages/pluggable-widgets-tools/src/common.ts b/packages/pluggable-widgets-tools/src/common.ts index 0696c87e..83bae0ef 100644 --- a/packages/pluggable-widgets-tools/src/common.ts +++ b/packages/pluggable-widgets-tools/src/common.ts @@ -1,6 +1,13 @@ -export function ensure(arg?: T): T { - if (arg == null) { - throw new Error("Did not expect an argument to be undefined"); +export function ensure(arg?: T, label: string = "argument"): T { + if (arg === null || arg === undefined) { + throw new Error(`Did not expect ${label} to be ${arg}`); } return arg; } + +export function partition>( + input: Array, + predicate: (x: T) => x is A +): [A[], B[]] { + return [input.filter(predicate), input.filter((x): x is B => !predicate(x))] as const; +} diff --git a/packages/pluggable-widgets-tools/src/utils/npmAudit.ts b/packages/pluggable-widgets-tools/src/utils/npmAudit.ts index 9eabe7d3..6832f3e2 100644 --- a/packages/pluggable-widgets-tools/src/utils/npmAudit.ts +++ b/packages/pluggable-widgets-tools/src/utils/npmAudit.ts @@ -1,8 +1,9 @@ import assert from "node:assert"; -import { exec } from "node:child_process"; import { existsSync } from "node:fs"; import { join } from "node:path"; import { widgetRoot } from "../widget/paths"; +import { ensure, partition } from "../common"; +import { exec } from "node:child_process"; export type Report = { auditReportVersion: 2; @@ -59,15 +60,28 @@ export type Vulnerability = { range: string; }; -export function collectVulnerabilities(report: Report, dependency: Dependency): Vulnerability[] { - const vulnerabilities = dependency.via.filter(v => typeof v !== "string"); - if (vulnerabilities.length > 0) { - return vulnerabilities; +export function collectVulnerabilities(report: Report, rootDependency: PackageName): Vulnerability[] { + const dependencies: PackageName[] = [rootDependency]; + const dependenciesSeen: PackageName[] = []; + const allVulnerabilities: Vulnerability[] = []; + + while (dependencies.length > 0) { + const dependencyName = ensure(dependencies.shift()); + dependenciesSeen.push(dependencyName); + + const [transients, vulnerabilities] = partition( + report.vulnerabilities[dependencyName].via, + v => typeof v === "string" + ); + + if (vulnerabilities.length > 0) { + allVulnerabilities.push(...vulnerabilities); + continue; + } + dependencies.push(...transients.filter(d => !dependenciesSeen.includes(d))); } - return dependency.via - .filter(v => typeof v === "string") - .flatMap(v => collectVulnerabilities(report, report.vulnerabilities[v])); + return allVulnerabilities; } export async function run(): Promise {