Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
110 lines
4.5 KiB
JavaScript
110 lines
4.5 KiB
JavaScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
// Audits what the desktop artifact ships, not what npm labels production.
|
|
//
|
|
// `npm audit --omit=dev` covers the Node production closure, but the renderer
|
|
// roots live in `devDependencies` (so electron-builder keeps their unread
|
|
// sources out of `app.asar`) while vite still bundles them into
|
|
// `dist-renderer`. A vulnerability in react would therefore ship without the
|
|
// production audit ever seeing it. This audits the full npm report and fails
|
|
// on anything that lands in the shipped desktop closure — Node production
|
|
// plus everything reachable from the declared renderer roots.
|
|
//
|
|
// An advisory names a package and npm resolves it to the installed copies it
|
|
// actually reaches (`nodes`). Only a copy whose exact version is in the
|
|
// shipped closure fails here: the same name installed at a vulnerable version
|
|
// on a tooling-only path (electron → @electron/get → undici) is not shipped
|
|
// and must not turn this red. When npm elides the paths, the name alone
|
|
// fails, since the miss would otherwise be silent.
|
|
import { execFileSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join, resolve } from 'node:path';
|
|
import { npmSpawnOptions } from './npm-spawn.mjs';
|
|
import { collectWorkspaceClosure } from './third-party-closure.mjs';
|
|
|
|
const repoRoot = resolve(import.meta.dirname, '..');
|
|
const SEVERITY_RANK = { info: 0, low: 1, moderate: 2, high: 3, critical: 4 };
|
|
// Matches the `--audit-level=moderate` the production audit step uses.
|
|
const FAIL_RANK = SEVERITY_RANK.moderate;
|
|
|
|
function npmAuditReport() {
|
|
const options = npmSpawnOptions({
|
|
cwd: repoRoot,
|
|
encoding: 'utf8',
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
});
|
|
try {
|
|
return JSON.parse(execFileSync('npm', ['audit', '--json'], options));
|
|
} catch (error) {
|
|
// npm audit exits nonzero when it finds anything; the JSON is complete.
|
|
if (typeof error.stdout === 'string' && error.stdout.trim().startsWith('{')) {
|
|
return JSON.parse(error.stdout);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const closure = collectWorkspaceClosure({
|
|
workspaceName: '@maka/desktop',
|
|
manifestPath: join(repoRoot, 'apps', 'desktop', 'package.json'),
|
|
});
|
|
const shippedVersions = new Map();
|
|
for (const { name, version } of closure) {
|
|
if (!shippedVersions.has(name)) shippedVersions.set(name, new Set());
|
|
shippedVersions.get(name).add(version);
|
|
}
|
|
|
|
const lockPackages =
|
|
JSON.parse(readFileSync(join(repoRoot, 'package-lock.json'), 'utf8')).packages ?? {};
|
|
|
|
function shippedCopies(vulnerability) {
|
|
const shipped = shippedVersions.get(vulnerability.name);
|
|
if (!shipped) return [];
|
|
const nodes = vulnerability.nodes ?? [];
|
|
if (nodes.length === 0) return [...shipped].map((version) => `${version} (paths elided)`);
|
|
return nodes
|
|
.map((node) => lockPackages[node]?.version)
|
|
.filter((version) => version !== undefined && shipped.has(version));
|
|
}
|
|
|
|
const report = npmAuditReport();
|
|
const flagged = Object.values(report.vulnerabilities ?? {})
|
|
.filter(
|
|
(vulnerability) =>
|
|
(SEVERITY_RANK[vulnerability.severity] ?? SEVERITY_RANK.critical) >= FAIL_RANK,
|
|
)
|
|
.map((vulnerability) => ({ vulnerability, copies: shippedCopies(vulnerability) }))
|
|
.filter(({ copies }) => copies.length > 0)
|
|
.sort((left, right) => left.vulnerability.name.localeCompare(right.vulnerability.name));
|
|
|
|
console.log(
|
|
`[audit-shipped] desktop shipped closure: ${shippedVersions.size} packages; ` +
|
|
`advisories reaching it at moderate or above: ${flagged.length}`,
|
|
);
|
|
for (const { vulnerability, copies } of flagged) {
|
|
const causes = (vulnerability.via ?? [])
|
|
.map((via) => (typeof via === 'string' ? `via ${via}` : `${via.title} (${via.url})`))
|
|
.join('; ');
|
|
console.error(
|
|
`[audit-shipped] ${vulnerability.name}@${copies.join(', ')}: ${vulnerability.severity} — ${causes}`,
|
|
);
|
|
}
|
|
if (flagged.length > 0) process.exit(1);
|