Files
Genarrative/scripts/check-pingora-cutover-evidence-verify.mjs
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

636 lines
20 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
const VERIFY_SCRIPT = 'scripts/ops/pingora-cutover-evidence-verify.mjs';
const failures = [];
const tmpRoot = mkdtempSync(
path.join(tmpdir(), 'genarrative-pingora-cutover-evidence-verify-'),
);
try {
main();
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
if (failures.length > 0) {
console.error('[check:pingora-cutover-evidence-verify] FAILED');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('[check:pingora-cutover-evidence-verify] OK');
function main() {
assertScriptShape();
assertValidEvidenceBundleManifest();
assertValidCommandEvidenceManifest();
assertRequireSummaryOkPassesForOkManifest();
assertRequireSummaryOkFailsForCriticalManifest();
assertRequireSummaryOkFailsWhenSummaryMissing();
assertMissingSchemaVersionFails();
assertUnsupportedSchemaVersionFails();
assertMissingFileFails();
assertSizeMismatchFails();
assertSha256MismatchFails();
assertUnsafePathFails();
assertStringMetadataFailsExceptManifest();
assertExtraFileFailsByDefault();
assertExtraDirectoryFailsByDefault();
assertExtraSymlinkFailsByDefault();
assertAllowExtraFilesOverrideSucceeds();
assertRejectsRootPath();
assertRejectsEntryPathControlCharacters();
assertRejectsSymlinkBundleDir();
assertRejectsNonDirectoryBundleDir();
assertRejectsInvalidJson();
assertRejectsBothEntryModes();
assertRejectsRelativeEntryPath();
assertRejectsManifestFilePathControlCharacters();
}
function assertScriptShape() {
const content = readFileSync(VERIFY_SCRIPT, 'utf8');
assertIncludes(content, 'createHash', '证据校验脚本必须计算 sha256。');
assertIncludes(content, 'sizeBytes', '证据校验脚本必须校验 sizeBytes。');
assertIncludes(
content,
'证据目录不能是符号链接',
'证据校验脚本必须拒绝符号链接证据目录。',
);
assertIncludes(
content,
'不会修改证据目录',
'证据校验脚本 usage 必须说明只读边界。',
);
assertIncludes(
content,
'未登记',
'证据校验脚本必须说明默认拒绝未登记证据条目。',
);
assertIncludes(
content,
'--allow-extra-files',
'证据校验脚本必须把未登记条目的例外开关显式化。',
);
assertIncludes(
content,
'--require-summary-ok',
'证据校验脚本必须支持正式 runbook 要求 summary OK。',
);
assertIncludes(
content,
'manifest.summary.status is not OK',
'证据校验脚本必须在 summary 非 OK 时给出结构化失败原因。',
);
assertIncludes(
content,
'manifest.schemaVersion 必须是 1',
'证据校验脚本必须拒绝未知 manifest schemaVersion。',
);
assertIncludes(
content,
'manifest.files.${key}.path',
'证据校验脚本必须校验 manifest.files 路径安全。',
);
if (
content.includes('writeFile') ||
content.includes('chmod(') ||
content.includes('spawn(') ||
content.includes('execFile')
) {
failures.push('证据校验脚本必须保持只读,不能写文件或执行外部命令。');
}
}
function assertValidEvidenceBundleManifest() {
const fixture = prepareEvidenceBundleFixture('valid-bundle');
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 0, '合法证据包 manifest 应校验通过。');
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, '合法证据包校验 stdout');
assertEqual(output.ok, true, '合法证据包 stdout 必须 ok=true。');
assertEqual(output.checkedCount, 4, '合法证据包必须校验四个元数据文件。');
assertEqual(output.failedCount, 0, '合法证据包不能有失败项。');
assertEqual(
output.files?.every((item) => item.status === 'OK'),
true,
'合法证据包每个文件都应 OK。',
);
}
function assertValidCommandEvidenceManifest() {
const fixture = prepareCommandEvidenceFixture('valid-command');
const result = runVerify([
'--manifest',
path.join(fixture.dir, 'manifest.json'),
]);
assertStatus(result, 0, '合法命令证据 manifest 应校验通过。');
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, '合法命令证据校验 stdout');
assertEqual(output.ok, true, '合法命令证据 stdout 必须 ok=true。');
assertEqual(output.checkedCount, 3, '合法命令证据必须校验三个元数据文件。');
}
function assertRequireSummaryOkPassesForOkManifest() {
const fixture = prepareEvidenceBundleFixture('summary-ok');
const result = runVerify([
'--bundle-dir',
fixture.dir,
'--require-summary-ok',
]);
assertStatus(
result,
0,
'要求 summary OK 且 manifest.summary.status=OK 时应通过。',
);
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, 'summary OK 校验 stdout');
assertEqual(output.ok, true, 'summary OK 校验 stdout 必须 ok=true。');
assertEqual(
output.requireSummaryOk,
true,
'summary OK 校验 stdout 必须记录 requireSummaryOk=true。',
);
assertEqual(
output.summary?.status,
'OK',
'summary OK 校验 stdout 必须输出 summary.status=OK。',
);
}
function assertRequireSummaryOkFailsForCriticalManifest() {
const fixture = prepareEvidenceBundleFixture('summary-critical');
const manifestPath = path.join(fixture.dir, 'manifest.json');
const manifest = readJson(manifestPath);
manifest.summary.status = 'CRITICAL';
writeJson(manifestPath, manifest);
const result = runVerify([
'--bundle-dir',
fixture.dir,
'--require-summary-ok',
]);
assertStatus(
result,
1,
'要求 summary OK 但 manifest.summary.status=CRITICAL 时必须失败。',
);
assertIncludes(
result.stdout,
'manifest.summary.status is not OK',
'summary 非 OK 失败必须写入 JSON reason。',
);
}
function assertRequireSummaryOkFailsWhenSummaryMissing() {
const fixture = prepareCommandEvidenceFixture('summary-missing');
const manifestPath = path.join(fixture.dir, 'manifest.json');
const manifest = readJson(manifestPath);
delete manifest.summary;
writeJson(manifestPath, manifest);
const result = runVerify([
'--bundle-dir',
fixture.dir,
'--require-summary-ok',
]);
assertStatus(
result,
1,
'要求 summary OK 但 manifest 缺少 summary 时必须失败。',
);
assertIncludes(
result.stdout,
'manifest.summary.status is not OK',
'缺少 summary 失败必须写入 JSON reason。',
);
}
function assertMissingSchemaVersionFails() {
const fixture = prepareEvidenceBundleFixture('schema-version-missing');
const manifestPath = path.join(fixture.dir, 'manifest.json');
const manifest = readJson(manifestPath);
delete manifest.schemaVersion;
writeJson(manifestPath, manifest);
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, 'manifest 缺少 schemaVersion 时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'manifest.schemaVersion 必须是 1',
'缺少 manifest schemaVersion 必须给出明确错误。',
);
}
function assertUnsupportedSchemaVersionFails() {
const fixture = prepareCommandEvidenceFixture('schema-version-unsupported');
const manifestPath = path.join(fixture.dir, 'manifest.json');
const manifest = readJson(manifestPath);
manifest.schemaVersion = 2;
writeJson(manifestPath, manifest);
const result = runVerify(['--manifest', manifestPath]);
assertStatus(result, 1, 'manifest schemaVersion 非 1 时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'manifest.schemaVersion 必须是 1',
'未知 manifest schemaVersion 必须给出明确错误。',
);
}
function assertMissingFileFails() {
const fixture = prepareEvidenceBundleFixture('missing-file');
rmSync(path.join(fixture.dir, 'snapshot.stderr.txt'), { force: true });
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, 'manifest 指向缺失文件时必须失败。');
assertIncludes(
result.stdout,
'missing file',
'缺失文件失败必须写入 JSON reason。',
);
}
function assertSizeMismatchFails() {
const fixture = prepareEvidenceBundleFixture('size-mismatch');
const manifestPath = path.join(fixture.dir, 'manifest.json');
const manifest = readJson(manifestPath);
manifest.files.snapshot.sizeBytes += 1;
writeJson(manifestPath, manifest);
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, 'sizeBytes 不一致时必须失败。');
assertIncludes(
result.stdout,
'size mismatch',
'sizeBytes 不一致必须写入 JSON reason。',
);
}
function assertSha256MismatchFails() {
const fixture = prepareEvidenceBundleFixture('sha256-mismatch');
const manifestPath = path.join(fixture.dir, 'manifest.json');
const manifest = readJson(manifestPath);
manifest.files.snapshot.sha256 = '0'.repeat(64);
writeJson(manifestPath, manifest);
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, 'sha256 不一致时必须失败。');
assertIncludes(
result.stdout,
'sha256 mismatch',
'sha256 不一致必须写入 JSON reason。',
);
}
function assertUnsafePathFails() {
const fixture = prepareEvidenceBundleFixture('unsafe-path');
const manifestPath = path.join(fixture.dir, 'manifest.json');
const manifest = readJson(manifestPath);
manifest.files.snapshot.path = '../snapshot.json';
writeJson(manifestPath, manifest);
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, 'manifest 文件 path 逃逸证据目录时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'只能是证据目录内的安全文件名',
'unsafe path 失败必须给出明确错误。',
);
}
function assertStringMetadataFailsExceptManifest() {
const fixture = prepareEvidenceBundleFixture('string-metadata');
const manifestPath = path.join(fixture.dir, 'manifest.json');
const manifest = readJson(manifestPath);
manifest.files.snapshot = 'snapshot.json';
writeJson(manifestPath, manifest);
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, '非 manifest 文件仍用字符串登记时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'必须使用 { path, sizeBytes, sha256 } 元数据对象',
'字符串 metadata 失败必须给出明确错误。',
);
}
function assertExtraFileFailsByDefault() {
const fixture = prepareEvidenceBundleFixture('extra-file');
writeFileSync(
path.join(fixture.dir, 'operator-note.txt'),
'manual note\n',
'utf8',
);
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, '证据目录混入未登记普通文件时默认必须失败。');
assertIncludes(
result.stdout,
'extra file not registered in manifest.files',
'未登记普通文件失败必须写入 JSON reason。',
);
assertIncludes(
result.stdout,
'operator-note.txt',
'未登记普通文件失败必须输出文件名。',
);
}
function assertExtraDirectoryFailsByDefault() {
const fixture = prepareEvidenceBundleFixture('extra-directory');
mkdirSync(path.join(fixture.dir, 'notes'), { recursive: true });
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, '证据目录混入未登记目录时默认必须失败。');
assertIncludes(
result.stdout,
'extra directory not registered in manifest.files',
'未登记目录失败必须写入 JSON reason。',
);
}
function assertExtraSymlinkFailsByDefault() {
const fixture = prepareEvidenceBundleFixture('extra-symlink');
writeFileSync(path.join(fixture.dir, 'target.txt'), 'target\n', 'utf8');
const manifest = readJson(path.join(fixture.dir, 'manifest.json'));
manifest.files.target = metadataFor(path.join(fixture.dir, 'target.txt'));
writeJson(path.join(fixture.dir, 'manifest.json'), manifest);
symlinkSync(
path.join(fixture.dir, 'target.txt'),
path.join(fixture.dir, 'target-link.txt'),
);
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, '证据目录混入未登记符号链接时默认必须失败。');
assertIncludes(
result.stdout,
'extra symlink not registered in manifest.files',
'未登记符号链接失败必须写入 JSON reason。',
);
}
function assertAllowExtraFilesOverrideSucceeds() {
const fixture = prepareEvidenceBundleFixture('allow-extra-file');
writeFileSync(
path.join(fixture.dir, 'operator-note.txt'),
'manual note\n',
'utf8',
);
const result = runVerify([
'--bundle-dir',
fixture.dir,
'--allow-extra-files',
]);
assertStatus(result, 0, '显式允许未登记文件时 verifier 应通过。');
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, '允许未登记文件校验 stdout');
assertEqual(output.ok, true, '允许未登记文件时 stdout 必须 ok=true。');
assertEqual(
output.allowExtraFiles,
true,
'允许未登记文件时 stdout 必须记录 allowExtraFiles=true。',
);
assertEqual(
output.extraFiles?.length,
0,
'允许未登记文件时 stdout 不应输出失败 extraFiles。',
);
}
function assertRejectsRootPath() {
const result = runVerify(['--bundle-dir', path.parse(process.cwd()).root]);
assertStatus(result, 1, '证据目录为文件系统根目录时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'不能是文件系统根目录',
'根目录失败必须给出明确错误。',
);
}
function assertRejectsEntryPathControlCharacters() {
const fixture = prepareEvidenceBundleFixture('entry-control-character');
const result = runVerify([
'--bundle-dir',
`${fixture.dir}\n--allow-extra-files`,
]);
assertStatus(result, 1, '入口路径带换行控制字符时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'--bundle-dir 不能包含换行或 NUL 字符',
'入口路径控制字符失败必须给出明确错误。',
);
}
function assertRejectsSymlinkBundleDir() {
const fixture = prepareEvidenceBundleFixture('symlink-target');
const linkPath = path.join(tmpRoot, 'symlink-bundle');
symlinkSync(fixture.dir, linkPath);
const result = runVerify(['--bundle-dir', linkPath]);
assertStatus(result, 1, '证据目录是符号链接时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'证据目录不能是符号链接',
'符号链接证据目录失败必须给出明确错误。',
);
}
function assertRejectsNonDirectoryBundleDir() {
const filePath = path.join(tmpRoot, 'not-a-dir');
writeFileSync(filePath, 'not a dir\n', 'utf8');
const result = runVerify(['--bundle-dir', filePath]);
assertStatus(result, 1, '证据目录不是目录时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'证据目录必须是目录',
'非目录证据路径失败必须给出明确错误。',
);
}
function assertRejectsInvalidJson() {
const dir = path.join(tmpRoot, 'invalid-json');
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, 'manifest.json'), '{ not json\n', 'utf8');
const result = runVerify(['--bundle-dir', dir]);
assertStatus(result, 1, 'manifest JSON 非法时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'manifest 不是合法 JSON',
'非法 JSON 失败必须给出明确错误。',
);
}
function assertRejectsBothEntryModes() {
const fixture = prepareEvidenceBundleFixture('both-entry-modes');
const result = runVerify([
'--bundle-dir',
fixture.dir,
'--manifest',
path.join(fixture.dir, 'manifest.json'),
]);
assertStatus(result, 1, '同时提供 manifest 和 bundle-dir 时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'只能二选一',
'入口参数互斥失败必须给出明确错误。',
);
}
function assertRejectsRelativeEntryPath() {
const result = runVerify(['--bundle-dir', 'relative-evidence']);
assertStatus(result, 1, '入口路径是相对路径时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'必须是绝对路径',
'相对路径失败必须给出明确错误。',
);
}
function assertRejectsManifestFilePathControlCharacters() {
const fixture = prepareEvidenceBundleFixture('metadata-control-character');
const manifestPath = path.join(fixture.dir, 'manifest.json');
const manifest = readJson(manifestPath);
manifest.files.snapshot.path = 'snapshot.json\noperator-note.txt';
writeJson(manifestPath, manifest);
const result = runVerify(['--bundle-dir', fixture.dir]);
assertStatus(result, 1, 'manifest 文件 path 带控制字符时必须失败。');
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'只能是证据目录内的安全文件名',
'manifest 文件 path 控制字符失败必须给出明确错误。',
);
}
function prepareEvidenceBundleFixture(name) {
const dir = path.join(tmpRoot, name);
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, 'snapshot.json'), '{"ok":true}\n', 'utf8');
writeFileSync(path.join(dir, 'snapshot.stdout.txt'), '{"ok":true}\n', 'utf8');
writeFileSync(path.join(dir, 'snapshot.stderr.txt'), '', 'utf8');
writeFileSync(
path.join(dir, 'snapshot-command.json'),
'{"executable":"node"}\n',
'utf8',
);
writeJson(path.join(dir, 'manifest.json'), {
schemaVersion: 1,
summary: {
status: 'OK',
},
files: {
manifest: 'manifest.json',
snapshot: metadataFor(path.join(dir, 'snapshot.json')),
snapshotParseError: null,
snapshotStdout: metadataFor(path.join(dir, 'snapshot.stdout.txt')),
snapshotStderr: metadataFor(path.join(dir, 'snapshot.stderr.txt')),
snapshotCommand: metadataFor(path.join(dir, 'snapshot-command.json')),
directLive: null,
directLiveParseError: null,
directLiveStdout: null,
directLiveStderr: null,
directLiveCommand: null,
},
});
return { dir };
}
function prepareCommandEvidenceFixture(name) {
const dir = path.join(tmpRoot, name);
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, 'command.stdout.txt'), 'ok\n', 'utf8');
writeFileSync(path.join(dir, 'command.stderr.txt'), '', 'utf8');
writeFileSync(
path.join(dir, 'command-record.json'),
'{"exitCode":0}\n',
'utf8',
);
writeJson(path.join(dir, 'manifest.json'), {
schemaVersion: 1,
summary: {
status: 'OK',
exitCode: 0,
},
files: {
manifest: 'manifest.json',
stdout: metadataFor(path.join(dir, 'command.stdout.txt')),
stderr: metadataFor(path.join(dir, 'command.stderr.txt')),
commandRecord: metadataFor(path.join(dir, 'command-record.json')),
},
});
return { dir };
}
function metadataFor(filePath) {
const content = readFileSync(filePath);
return {
path: path.basename(filePath),
sizeBytes: content.length,
sha256: createHash('sha256').update(content).digest('hex'),
};
}
function runVerify(args) {
return spawnSync('node', [VERIFY_SCRIPT, ...args], {
cwd: process.cwd(),
encoding: 'utf8',
});
}
function readJson(filePath) {
try {
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch (error) {
failures.push(`${filePath} 不是合法 JSON: ${error.message}`);
return {};
}
}
function writeJson(filePath, value) {
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function parseJson(raw, label) {
try {
return JSON.parse(raw);
} catch (error) {
failures.push(`${label} 不是合法 JSON: ${error.message}\n${raw}`);
return {};
}
}
function assertStatus(result, expected, reason) {
const actual = result.status ?? 0;
if (actual !== expected) {
failures.push(
`${reason} 预期退出码 ${expected},实际 ${actual}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
}
}
function assertIncludes(content, needle, reason) {
if (!String(content).includes(needle)) {
failures.push(`${reason} 缺少: ${needle}`);
}
}
function assertEqual(actual, expected, reason) {
if (actual !== expected) {
failures.push(
`${reason} 预期 ${JSON.stringify(expected)},实际 ${JSON.stringify(actual)}`,
);
}
}