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.
115 lines
6.4 KiB
JavaScript
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 });
|
|
}
|