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..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[]); @@ -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,17 +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 versions = await promisify(exec)(`npm show '${name}' versions --json`).then( - ({ stdout }) => JSON.parse(stdout) as string[] - ); + 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[]) + .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 }; } 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 {