fix: secure inventory workflow for lifecycle candidate

This commit is contained in:
2026-09-07 18:47:04 +08:00
parent 8ea2ec726e
commit 55f5cebc86
11 changed files with 418 additions and 36 deletions

View File

@@ -0,0 +1,90 @@
// Published baselines take precedence. Invalid published data never falls
// back to vendor candidates. This verifier performs no network/lifecycle work.
const fs = require('node:fs');
const path = require('node:path');
const { createHash } = require('node:crypto');
const hash = data => createHash('sha256').update(data).digest('hex');
const packageName = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
const exactVersion = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
const sha256 = /^[a-f0-9]{64}$/;
const checkedFile = relative => /(?:\.(?:c|m)?js|\.d\.ts)$/.test(relative) || path.posix.basename(relative) === 'package.json';
function fail(message) { throw new Error(message); }
function recordPath(relative) {
if (typeof relative !== 'string' || !relative || relative.includes('\\') || relative.startsWith('/') || relative.includes(':') || /[\x00-\x1f]/.test(relative) ||
relative.split('/').some(part => !part || part === '.' || part === '..') || !checkedFile(relative)) fail('Unsafe or unsupported baseline file path');
return relative;
}
function containedFile(root, relative) {
const actual = fs.realpathSync(path.join(root, recordPath(relative)));
if (!actual.startsWith(root + path.sep) || !fs.statSync(actual).isFile()) fail('Installed file escapes package root');
return actual;
}
function installedFiles(directory, prefix = '') {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
if (entry.name === 'node_modules') return [];
const relative = prefix + entry.name;
// pnpm may link the package root; links within the artifact are forbidden.
if (entry.isSymbolicLink()) fail('Unexpected link inside installed package');
return entry.isDirectory() ? installedFiles(path.join(directory, entry.name), relative + '/') : checkedFile(relative) ? [relative] : [];
});
}
function metadataUrl(value) {
if (typeof value !== 'string') fail('Published registry metadata missing');
let url;
try { url = new URL(value); } catch { fail('Published registry metadata invalid'); }
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) fail('Published registry metadata must be a credential-free HTTP URL');
}
function validateRecord(record, published) {
if (!record || typeof record !== 'object' || !packageName.test(record.name || '') || !exactVersion.test(record.version || '') ||
!record.files || typeof record.files !== 'object' || Array.isArray(record.files) || !Object.keys(record.files).length) fail('Invalid package baseline');
for (const [relative, checksum] of Object.entries(record.files)) {
recordPath(relative);
if (!sha256.test(checksum)) fail('Invalid baseline file digest');
}
if (published) {
metadataUrl(record.registry); metadataUrl(record.tarball);
const integrity=typeof record.integrity === 'string' && /^sha(256|384|512)-([A-Za-z0-9+/]+={0,2})$/.exec(record.integrity);
if (!integrity || Buffer.from(integrity[2], 'base64').length !== Number(integrity[1])/8 || Buffer.from(integrity[2], 'base64').toString('base64') !== integrity[2]) fail('Published integrity missing or invalid');
if (!Object.hasOwn(record.files, 'package.json')) fail('Published baseline must include package.json');
} else if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.tgz$/.test(record.archive || '') || !sha256.test(record.sha256 || '')) fail('Invalid vendor archive baseline');
}
function verifyPackages(projectRoot = path.resolve(__dirname, '..')) {
const root = fs.realpathSync(projectRoot);
const publishedPath = path.join(root, 'scripts/published-packages.json');
let published = false;
try { fs.lstatSync(publishedPath); published = true; } catch (error) { if (error.code !== 'ENOENT') throw error; }
const baseline = JSON.parse(fs.readFileSync(published ? publishedPath : path.join(root, 'vendor/local-packages.json'), 'utf8'));
if (baseline.formatVersion !== 1 || !Array.isArray(baseline.packages) || !baseline.packages.length) fail('Unsupported or empty package baseline');
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
const names = new Set(); let count = 0;
for (const record of baseline.packages) {
validateRecord(record, published);
if (names.has(record.name)) fail('Duplicate package baseline');
names.add(record.name);
const expectedSpecifier = published ? record.version : `file:vendor/${record.archive}`;
const declarations = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']
.filter(section => Object.hasOwn(manifest[section] || {}, record.name)).map(section => manifest[section][record.name]);
if (!declarations.length || declarations.some(value => value !== expectedSpecifier)) fail(`Unpinned dependency ${record.name}`);
if (!published && hash(fs.readFileSync(path.join(root, 'vendor', record.archive))) !== record.sha256) fail(`Archive drift ${record.name}`);
// Resolve pnpm virtual stores and package.json exports through Node.
const metadataPath = require.resolve(`${record.name}/package.json`, { paths: [root] });
const installed = fs.realpathSync(path.dirname(metadataPath));
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
if (metadata.name !== record.name || metadata.version !== record.version) fail(`Installed identity drift ${record.name}`);
const extra = installedFiles(installed).filter(relative => !Object.hasOwn(record.files, relative));
if (extra.length) fail(`Unexpected executable or metadata files ${record.name}: ${extra.join(', ')}`);
for (const [relative, checksum] of Object.entries(record.files)) {
// Historical vendor packages may normalize metadata. Published mode
// instead checks package.json against the final published artifact.
if (!published && relative === 'package.json') continue;
if (hash(fs.readFileSync(containedFile(installed, relative))) !== checksum) fail(`Installed byte drift ${record.name}/${relative}`);
count++;
}
}
return { mode: published ? 'published' : 'vendor', packages: baseline.packages.length, files: count };
}
module.exports = { verifyPackages };
if (require.main === module) {
const result = verifyPackages();
console.log(`Verified ${result.packages} ${result.mode} packages and ${result.files} installed files.`);
}