Files
gongxue-base/scripts/taro-supply-chain-audit.js
2026-07-12 19:26:57 +08:00

317 lines
14 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { createRequire } from 'node:module';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { enforceTaroH5RuntimePatches } from './taro-components-h5-runtime-patch.js';
const repoRoot = process.cwd();
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
export const securedBundleDependencies = Object.freeze({
'lodash-es': Object.freeze({
version: '4.18.1',
integrity: 'sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==',
}),
swiper: Object.freeze({
version: '12.1.2',
integrity: 'sha512-4gILrI3vXZqoZh71I1PALqukCFgk+gpOwe1tOvz5uE9kHtl2gTDzmYflYCwWvR4LOvCrJi6UEEU+gnuW5BtkgQ==',
}),
});
export const allowedInvalidEdges = Object.freeze([
Object.freeze({ parent: '@tarojs/components', dependency: 'swiper', declared: '11.1.15' }),
Object.freeze({ parent: '@tarojs/components-react', dependency: 'swiper', declared: '11.1.15' }),
Object.freeze({ parent: '@tarojs/plugin-platform-h5', dependency: 'lodash-es', declared: '4.17.21' }),
Object.freeze({ parent: '@tarojs/taro-h5', dependency: 'lodash-es', declared: '4.17.21' }),
]);
export const reviewedHighCriticalToolchainPackages = new Set([
'@tarojs/cli',
'@tarojs/plugin-doctor',
'@tarojs/webpack5-runner',
'cacheable-request',
'decompress',
'download',
'download-git-repo',
'git-clone',
'glob',
'got',
'html-minifier',
'http-cache-semantics',
'serialize-javascript',
]);
export const reviewedHighCriticalAdvisories = new Set([
1093404,
1102456,
1105440,
1109842,
1113686,
1122670,
]);
export const npmLsArgs = Object.freeze([
'ls',
...Object.keys(securedBundleDependencies),
'--workspace',
'@tiku-saas/taro',
'--all',
'--json',
]);
export function npmAuditArgs(registry = process.env.NPM_AUDIT_REGISTRY || 'https://registry.npmjs.org/') {
return [
'audit',
`--registry=${registry}`,
'--workspace',
'@tiku-saas/taro',
'--audit-level=high',
'--json',
];
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
function dependencyProblem(problem) {
const match = /^invalid: ((?:@[^/\s]+\/)?[^@\s]+)@([^\s]+)\s+(.+)$/.exec(String(problem || ''));
return match ? { name: match[1], version: match[2], location: match[3] } : null;
}
function expectedProblemKey(name) {
return `${name}@${securedBundleDependencies[name].version}`;
}
function collectTreeProblems(node, dependencyName = '', state = { problems: [], invalidEdges: [] }) {
if (node?.error?.code === 'ELSPROBLEMS' && !state.errorCode) state.errorCode = node.error.code;
for (const problem of node?.problems || []) state.problems.push(String(problem));
if (node?.invalid) {
assert(securedBundleDependencies[dependencyName], `npm ls reported an unexpected invalid dependency: ${dependencyName}`);
const edgePattern = /"([^"]+)" from node_modules\/((?:@[^/,]+\/)?[^,\s]+)/g;
let match;
let matched = false;
while ((match = edgePattern.exec(String(node.invalid)))) {
matched = true;
state.invalidEdges.push({ parent: match[2], dependency: dependencyName, declared: match[1] });
}
assert(matched, `npm ls returned an unparseable invalid edge for ${dependencyName}: ${node.invalid}`);
}
assert(!node?.extraneous, `npm ls reported an extraneous dependency: ${dependencyName}`);
assert(!node?.missing, `npm ls reported a missing dependency: ${dependencyName}`);
for (const [name, dependency] of Object.entries(node?.dependencies || {})) {
collectTreeProblems(dependency, name, state);
}
return state;
}
export function validateNpmLsPayload(payload) {
assert(payload && typeof payload === 'object', 'npm ls did not return a JSON object');
assert(payload.error?.code === 'ELSPROBLEMS', 'npm ls must fail only with the reviewed Taro exact-dependency ELSPROBLEMS result');
const state = collectTreeProblems(payload);
const problemCounts = new Map();
const actualProblemKeys = new Set();
for (const problem of state.problems) {
const parsed = dependencyProblem(problem);
assert(parsed, `npm ls returned an unexpected problem: ${problem}`);
assert(securedBundleDependencies[parsed.name], `npm ls returned an unapproved problem: ${problem}`);
assert(parsed.version === securedBundleDependencies[parsed.name].version, `npm ls problem uses an unexpected ${parsed.name} version: ${problem}`);
assert(parsed.location.replace(/\\/g, '/').endsWith(`/node_modules/${parsed.name}`), `npm ls problem has an unexpected location: ${problem}`);
const key = `${parsed.name}@${parsed.version}`;
actualProblemKeys.add(key);
problemCounts.set(key, (problemCounts.get(key) || 0) + 1);
}
const expectedProblemKeys = new Set(Object.keys(securedBundleDependencies).map(expectedProblemKey));
assert(actualProblemKeys.size === expectedProblemKeys.size, 'npm ls must report exactly the two reviewed invalid packages');
for (const key of expectedProblemKeys) {
assert(actualProblemKeys.has(key), `npm ls did not report the reviewed invalid package ${key}`);
const dependency = key.slice(0, key.lastIndexOf('@'));
const expectedCount = 1 + allowedInvalidEdges.filter(edge => edge.dependency === dependency).length;
assert(problemCounts.get(key) === expectedCount, `npm ls must report ${key} once at the root and once per reviewed invalid edge`);
}
const actualEdges = new Set(state.invalidEdges.map(edge => `${edge.parent}>${edge.dependency}@${edge.declared}`));
const expectedEdges = new Set(allowedInvalidEdges.map(edge => `${edge.parent}>${edge.dependency}@${edge.declared}`));
assert(actualEdges.size === expectedEdges.size, `npm ls invalid-edge set changed: expected ${expectedEdges.size}, received ${actualEdges.size}`);
for (const edge of expectedEdges) assert(actualEdges.has(edge), `npm ls did not report the reviewed invalid edge ${edge}`);
return {
packages: [...expectedProblemKeys].sort(),
edges: allowedInvalidEdges.map(edge => ({ ...edge })),
};
}
export function validateAuditPayload(payload) {
assert(payload && typeof payload === 'object', 'npm audit did not return a JSON object');
assert(payload.metadata?.vulnerabilities, 'npm audit payload is missing vulnerability metadata');
const vulnerabilities = payload.vulnerabilities || {};
for (const dependency of Object.keys(securedBundleDependencies)) {
assert(!vulnerabilities[dependency], `npm audit still reports ${dependency}; the bundle mitigation is not effective`);
}
const reviewed = [];
const moderate = [];
const observedAdvisories = new Set();
for (const [name, vulnerability] of Object.entries(vulnerabilities)) {
const severity = String(vulnerability.severity || '').toLowerCase();
if (severity === 'high' || severity === 'critical') {
assert(reviewedHighCriticalToolchainPackages.has(name), `new unreviewed ${severity} Taro vulnerability: ${name}`);
for (const via of vulnerability.via || []) {
if (!via || typeof via !== 'object') continue;
const viaSeverity = String(via.severity || '').toLowerCase();
if (viaSeverity !== 'high' && viaSeverity !== 'critical') continue;
assert(reviewedHighCriticalAdvisories.has(via.source), `new unreviewed ${viaSeverity} Taro advisory ${via.source} in ${name}`);
observedAdvisories.add(via.source);
}
reviewed.push({ name, severity, direct: Boolean(vulnerability.isDirect) });
} else {
moderate.push({ name, severity, direct: Boolean(vulnerability.isDirect) });
}
}
return {
counts: { ...payload.metadata.vulnerabilities },
reviewedAdvisories: [...observedAdvisories].sort((left, right) => left - right),
reviewedHighCritical: reviewed.sort((left, right) => left.name.localeCompare(right.name)),
other: moderate.sort((left, right) => left.name.localeCompare(right.name)),
};
}
function findPackageJsonFromEntry(entryPath, expectedName) {
let current = path.dirname(entryPath);
while (current !== path.dirname(current)) {
const candidate = path.join(current, 'package.json');
if (fs.existsSync(candidate)) {
const manifest = readJson(candidate);
if (manifest.name === expectedName) return candidate;
}
current = path.dirname(current);
}
throw new Error(`cannot locate package.json for ${expectedName}`);
}
function resolveDependencyPackage(parentPackagePath, dependency) {
const requireFromParent = createRequire(parentPackagePath);
try {
return requireFromParent.resolve(`${dependency}/package.json`);
} catch {
return findPackageJsonFromEntry(requireFromParent.resolve(dependency), dependency);
}
}
export function validateManifestLockAndInstall(root = repoRoot) {
const rootManifest = readJson(path.join(root, 'package.json'));
const lock = readJson(path.join(root, 'package-lock.json'));
const installed = {};
for (const [dependency, expected] of Object.entries(securedBundleDependencies)) {
assert(rootManifest.overrides?.[dependency] === expected.version, `package.json must override ${dependency} to ${expected.version}`);
const lockEntry = lock.packages?.[`node_modules/${dependency}`];
assert(lockEntry?.version === expected.version, `package-lock.json must resolve ${dependency} to ${expected.version}`);
assert(lockEntry?.integrity === expected.integrity, `package-lock.json has an unexpected integrity for ${dependency}`);
const installedManifest = readJson(path.join(root, 'node_modules', dependency, 'package.json'));
assert(installedManifest.version === expected.version, `node_modules has ${dependency}@${installedManifest.version}; run npm ci to install the secured lock`);
installed[dependency] = installedManifest.version;
}
for (const edge of allowedInvalidEdges) {
const parentPackagePath = path.join(root, 'node_modules', ...edge.parent.split('/'), 'package.json');
assert(fs.existsSync(parentPackagePath), `reviewed Taro parent package is not installed: ${edge.parent}`);
const parentManifest = readJson(parentPackagePath);
assert(parentManifest.dependencies?.[edge.dependency] === edge.declared, `${edge.parent} no longer declares the reviewed ${edge.dependency}@${edge.declared} edge; remove or update the exception`);
const resolvedPackagePath = resolveDependencyPackage(parentPackagePath, edge.dependency);
const resolvedManifest = readJson(resolvedPackagePath);
assert(resolvedManifest.version === securedBundleDependencies[edge.dependency].version, `${edge.parent} resolves ${edge.dependency}@${resolvedManifest.version} instead of the secured override`);
}
return installed;
}
function runNpm(args) {
return spawnSync(npmCommand, args, {
cwd: repoRoot,
encoding: 'utf8',
maxBuffer: 20 * 1024 * 1024,
env: process.env,
});
}
function parseCommandJson(result, label) {
try {
return JSON.parse(result.stdout || '{}');
} catch (error) {
throw new Error(`${label} did not return valid JSON: ${error.message}`);
}
}
export function runSupplyChainAudit() {
const versions = validateManifestLockAndInstall();
const h5RuntimePatches = enforceTaroH5RuntimePatches({ root: repoRoot, mode: 'check' });
const lsResult = runNpm(npmLsArgs);
assert(lsResult.status === 1, `npm ls returned ${lsResult.status}; expected the reviewed ELSPROBLEMS status`);
const invalid = validateNpmLsPayload(parseCommandJson(lsResult, 'npm ls'));
const auditResult = runNpm(npmAuditArgs());
assert(auditResult.status === 0 || auditResult.status === 1, `npm audit failed operationally with status ${auditResult.status}: ${auditResult.stderr}`);
const audit = validateAuditPayload(parseCommandJson(auditResult, 'npm audit'));
return {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
status: 'pass-with-reviewed-toolchain-risk',
securedBundleDependencies: versions,
h5RuntimePatches,
npmLs: invalid,
audit,
riskBoundary: {
appliesTo: 'Taro 4.2.0 CLI and build toolchain; swiper and lodash-es bundle advisories are mitigated',
controls: [
'build only on isolated trusted runners',
'do not expose Taro development servers to public networks',
'do not process untrusted templates, archives, repositories, or CLI arguments',
'publish only reviewed apps/taro/dist static artifacts',
],
},
};
}
function printHuman(result) {
console.log('[PASS] Taro supply-chain override and install state');
console.log(`[PASS] bundle dependencies: ${Object.entries(result.securedBundleDependencies).map(([name, version]) => `${name}@${version}`).join(', ')}`);
const patchSummary = Object.entries(result.h5RuntimePatches.patches)
.map(([id, patch]) => `${id}=${patch.installedSha256}`)
.join(', ');
console.log(`[PASS] Taro H5 runtime patches: ${result.h5RuntimePatches.version} ${patchSummary}`);
console.log(`[PASS] npm ls exceptions restricted to ${result.npmLs.edges.length} reviewed Taro exact-dependency edges`);
console.log(`[WARN] full Taro workspace audit remains ${result.audit.counts.total} findings (${result.audit.counts.critical} critical, ${result.audit.counts.high} high, ${result.audit.counts.moderate} moderate)`);
console.log('[WARN] remaining high/critical findings are confined to the reviewed Taro CLI/build-toolchain package allowlist');
}
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
const json = process.argv.includes('--json');
try {
const result = runSupplyChainAudit();
if (json) console.log(JSON.stringify(result, null, 2));
else printHuman(result);
} catch (error) {
if (json) console.log(JSON.stringify({ schemaVersion: 1, status: 'fail', error: error.message }, null, 2));
else console.error(`[FAIL] ${error.message}`);
process.exitCode = 1;
}
}