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