Files
vendoo/tools/verify-release-manifest-staging-3.0.mjs
Masterluke77andGitHub 637275effc Updater 3.4 – Lifecycle Hardening (#28)
Production-ready updater lifecycle hardening with final-head checks, Windows PowerShell 5.1 compatibility, reproducible package generation, provenance validation, payload rollback protection and documented acceptance.
2026-07-10 19:54:36 +02:00

115 lines
6.4 KiB
JavaScript

import {
copyFileSync,
existsSync,
mkdtempSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { applyReleaseManifest, buildReleaseManifest, MANIFEST_FILE, readAndValidateManifest } from './release-manifest-lib.mjs';
import { createPackageManifest, PACKAGE_MANIFEST_SCHEMA } from './updater-lifecycle-3.4.mjs';
const temp = mkdtempSync(join(tmpdir(), 'vendoo-release-manifest-'));
const source = join(temp, 'source');
const destination = join(temp, 'repo');
const requiredBodies = new Map([
['package.json', '{"name":"vendoo","version":"1.43.0"}\n'],
['package-lock.json', '{"name":"vendoo","lockfileVersion":3}\n'],
['bootstrap.mjs', 'export const bootstrap = true;\n'],
['server.mjs', 'export const server = true;\n'],
['lib/db.mjs', 'export const db = true;\n'],
['db/schema.sql', 'CREATE TABLE test(id INTEGER PRIMARY KEY);\n'],
['update-manifest.json', '{"version":"1.43.0","updaterVersion":"3.4.0"}\n'],
['UPDATE_VENDOO.bat', '@echo off\r\n'],
['scripts/updater/Vendoo.Updater.Preflight.ps1', 'Write-Output preflight\n'],
['scripts/updater/Vendoo.Updater.UI.ps1', 'Write-Output ui\n'],
['scripts/updater/Vendoo.Updater.Host.ps1', 'Write-Output host\n'],
['scripts/updater/Vendoo.Updater.Worker34.ps1', 'Write-Output worker34\n'],
['tools/apply-release-manifest.mjs', 'export {};\n'],
['tools/updater-lifecycle-3.4.mjs', 'export const lifecycle = true;\n'],
['tools/check-release-payload-3.4.mjs', 'export const payloadGuard = true;\n'],
]);
function write(root, rel, content) {
const target = join(root, rel);
mkdirSync(join(target, '..'), { recursive: true });
writeFileSync(target, content, 'utf8');
}
function writePackageManifest(root, removedFiles = []) {
const sourceManifest = buildReleaseManifest(root, { version: '1.43.0', updaterVersion: '3.4.0', removedFiles });
const packageManifest = createPackageManifest(sourceManifest, {
sourceCommit: '1'.repeat(40),
baseCommit: '2'.repeat(40),
createdAt: '2026-07-10T12:00:00Z',
});
writeFileSync(join(root, MANIFEST_FILE), `${JSON.stringify(packageManifest, null, 2)}\n`, 'utf8');
return packageManifest;
}
try {
mkdirSync(join(destination, '.git'), { recursive: true });
for (const [rel, body] of requiredBodies) write(source, rel, body);
write(source, 'public/new.js', 'export const current = 3;\n');
write(source, 'logs/secret.log', 'must-not-enter-manifest');
write(destination, 'db/schema.sql', 'OLD SCHEMA');
write(destination, 'baseline-only.txt', 'must survive overlay');
write(destination, 'obsolete.txt', 'must be removed explicitly');
const manifest = writePackageManifest(source, ['obsolete.txt']);
if (manifest.schema !== PACKAGE_MANIFEST_SCHEMA) throw new Error('Paketmanifest verwendet nicht Schema v2.');
if (manifest.files.some(entry => entry.path === 'logs/secret.log')) throw new Error('Runtime-Log wurde in das Release-Manifest aufgenommen.');
const result = applyReleaseManifest(source, destination);
if (result.copied !== manifest.files.length || result.controlCopied !== 1 || result.removed !== 1) throw new Error('Release-Zähler sind falsch.');
if (!existsSync(join(destination, 'baseline-only.txt'))) throw new Error('Overlay hat eine nicht deklarierte Baseline-Datei gelöscht.');
if (existsSync(join(destination, 'obsolete.txt'))) throw new Error('Explizite Löschung wurde nicht angewendet.');
if (readFileSync(join(destination, 'db/schema.sql'), 'utf8') !== requiredBodies.get('db/schema.sql')) throw new Error('db/schema.sql wurde nicht korrekt überlagert.');
if (!existsSync(join(destination, MANIFEST_FILE))) throw new Error('release-manifest.json wurde nicht als Steuerdatei in die Arbeitskopie übernommen.');
readAndValidateManifest(destination, { allowExtraFiles: true });
let controlDeletionBlocked = false;
try { buildReleaseManifest(source, { version: '1.43.0', updaterVersion: '3.4.0', removedFiles: [MANIFEST_FILE] }); }
catch (error) { controlDeletionBlocked = /Steuerdatei/.test(error.message); }
if (!controlDeletionBlocked) throw new Error('Löschung von release-manifest.json wurde nicht blockiert.');
const tamperedManifest = JSON.parse(readFileSync(join(source, MANIFEST_FILE), 'utf8'));
tamperedManifest.baseCommit = '3'.repeat(40);
writeFileSync(join(source, MANIFEST_FILE), `${JSON.stringify(tamperedManifest, null, 2)}\n`, 'utf8');
let provenanceBlocked = false;
try { readAndValidateManifest(source); } catch (error) { provenanceBlocked = /Payload-Hash/.test(error.message); }
if (!provenanceBlocked) throw new Error('Manipulierte Paket-Provenienz wurde nicht blockiert.');
writeFileSync(join(source, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
const corrupt = join(temp, 'corrupt');
mkdirSync(corrupt, { recursive: true });
for (const entry of manifest.files) {
const target = join(corrupt, entry.path);
mkdirSync(join(target, '..'), { recursive: true });
copyFileSync(join(source, entry.path), target);
}
copyFileSync(join(source, MANIFEST_FILE), join(corrupt, MANIFEST_FILE));
writeFileSync(join(corrupt, 'db/schema.sql'), 'CORRUPT', 'utf8');
let corruptionBlocked = false;
try { applyReleaseManifest(corrupt, destination); } catch (error) { corruptionBlocked = /SHA-256|Dateigröße/.test(error.message); }
if (!corruptionBlocked) throw new Error('Manipulierte Release-Datei wurde nicht blockiert.');
const missing = join(temp, 'missing');
mkdirSync(missing, { recursive: true });
for (const entry of manifest.files) {
if (entry.path === 'db/schema.sql') continue;
const target = join(missing, entry.path);
mkdirSync(join(target, '..'), { recursive: true });
copyFileSync(join(source, entry.path), target);
}
copyFileSync(join(source, MANIFEST_FILE), join(missing, MANIFEST_FILE));
let missingBlocked = false;
try { applyReleaseManifest(missing, destination); } catch (error) { missingBlocked = /fehlende Paketdatei|Pflichtdatei|verweist auf fehlende/.test(error.message); }
if (!missingBlocked) throw new Error('Fehlende Pflichtdatei db/schema.sql wurde nicht blockiert.');
console.log(`Release-Manifest-Gate 3.4 erfolgreich: ${manifest.files.length} Dateien hashgeprüft, Paket-Provenienz validiert, Overlay bewahrt Baseline, Löschungen sind explizit, Manipulation und fehlendes db/schema.sql werden blockiert.`);
} finally {
rmSync(temp, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}