forked from wangziqi/gongxue-base
317 lines
14 KiB
JavaScript
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;
|
|
}
|
|
}
|