Files
maka/scripts/audit-shipped-dependencies.mjs
agent-radar 7be535059e
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

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);