aere-quantum/control-plane/consola.mjs

108 lines
7.0 KiB
JavaScript

#!/usr/bin/env node
'use strict';
// AERE Quantum Security Control Plane - CONSOLA (B1 milestone 3): "planul de control care le uneste". Ingera ce produc uneltele - un
// buraf de la sidecar-ul B3 (jurnal de audit + dovezi) si, optional, un plan de migrare de la control-plane si executia lui - si scoate
// O SINGURA stare verificabila a unei desfasurari.
//
// PRINCIPIUL, si e chiar valoarea: consola NU se increde in ce spune despre sine burafu. Recalculeaza ea insasi lantul de hash-uri
// (reia verificaJurnal din sidecar) si integritatea FIECARUI plic (sha256(JSON.stringify(statement)) == statementHash). Un host rau
// care preda un buraf cu `chainOk:true` peste un jurnal manipulat e prins aici. Finalitatea pe lant (notarizarea capului) e in afara
// consolei offline: sidecar-ul o da cu `verify-log --attested` si verificatorul AIP-23. Iesirea e in engleza (forma 2, 2026-09-29).
//
// node consola.mjs --bundle b.json [--plan plan.json] [--execution execution.json] [--json]
// iesire 0 = verdict OK (lant intreg SI toate plicurile integre); 1 = ceva stricat (BROKEN); 2 = nu s-a putut rula.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const RAD = path.resolve(AICI, '..', '..');
const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex');
// sidecar-ul: langa planul de control intr-o copie publica (../verify-layer), sau in depozitul de dezvoltare (tools/aere-verify-layer)
export function caleaSidecarului() {
const c = [path.join(AICI, '..', 'verify-layer', 'sidecar.mjs'), path.join(RAD, 'tools', 'aere-verify-layer', 'sidecar.mjs')];
return c.find((p) => fs.existsSync(p)) || c[c.length - 1];
}
/**
* Reverifica un buraf de sidecar independent de ce declara el.
* @returns {object} starea consolei
*/
export async function evalueaza({ bundle, plan = null, execution = null }) {
const { verificaJurnal } = await import(pathToFileURL(caleaSidecarului()).href);
const intrari = Array.isArray(bundle.entries) ? bundle.entries : [];
// 1) lantul: recalculat, nu citit din bundle.chainOk
const lant = verificaJurnal(intrari);
// 2) integritatea fiecarui plic (tamper-evident, fara retea)
const plicuriRele = [];
for (const e of intrari) {
const p = e && e.proof;
const okForma = p && p.statement && typeof p.statementHash === 'string' && /^0x[0-9a-fA-F]{64}$/.test(p.statementHash);
const okInt = okForma && sha256(Buffer.from(JSON.stringify(p.statement), 'utf8')).toLowerCase() === p.statementHash.toLowerCase();
if (!okInt) plicuriRele.push({ seq: e && e.seq, kind: p && (p.kind || (p.statement && p.statement.kind)) || '?', reason: okForma ? 'statementHash != sha256(statement)' : 'invalid envelope form' });
}
// 3) minciuna auto-raportata: bundle.chainOk spune altceva decat masuratoarea noastra
const minciunaChainOk = typeof bundle.chainOk === 'boolean' && bundle.chainOk !== lant.ok;
// 4) planul de migrare (optional, informativ): forma planificatorului (actions/method/urgency). Un plan fara nicio lista
// recunoscuta e raportat ca atare, nu ca plan gol (2026-09-27: consola citea alte nume si afisa "0 actiuni" pe un plan real).
let migration = null;
if (plan) {
const items = Array.isArray(plan.actions) ? plan.actions : null;
migration = items === null
? { total: null, error: 'the plan has no recognized list (actions)' }
: {
total: items.length,
critical: items.filter((i) => String(i.urgency || '').toUpperCase() === 'CRITICAL').length,
auto: items.filter((i) => i.method === 'auto-aere').length,
manual: items.filter((i) => i.method === 'manual').length,
blocked: items.filter((i) => i.method === 'blocked').length,
};
}
// 5) executia migrarii (optional): lantul execution.json al executorului se RE-VERIFICA aici, nu se crede sumarul lui
let executie = null;
if (execution) {
const { verificaExecutie } = await import(pathToFileURL(path.join(AICI, 'executa-migrare.mjs')).href);
const v = verificaExecutie(execution);
const recs = (execution.records || []).map((e) => e && e.record).filter((r) => r && r.ref);
executie = { chainOk: v.ok, brokenAtSeq: v.ok ? null : v.seq, reason: v.ok ? null : v.reason, actions: recs.length,
ok: recs.filter((r) => r.verdict === 'OK').length, failed: recs.filter((r) => r.verdict === 'FAILED').length };
}
const okTot = lant.ok && plicuriRele.length === 0 && !minciunaChainOk && (executie === null || executie.chainOk) && !(migration && migration.total === null);
return {
v: 2, kind: 'aere-control-plane-console',
host: bundle.host || '?',
auditChain: { ok: lant.ok, count: lant.count, head: lant.head, brokenAtSeq: lant.rupt, reason: lant.motiv || null },
envelopes: { total: intrari.length, intact: intrari.length - plicuriRele.length, bad: plicuriRele },
selfReportSuspect: minciunaChainOk ? `the bundle declares chainOk=${bundle.chainOk} but the measurement gives ${lant.ok}` : null,
migration,
execution: executie,
verdict: okTot ? 'OK' : 'BROKEN',
};
}
async function main() {
const args = process.argv.slice(2);
const get = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null; };
const bf = get('--bundle'); if (!bf) { console.error('usage: node consola.mjs --bundle b.json [--plan plan.json] [--execution execution.json] [--json]'); return 2; }
const bundle = JSON.parse(fs.readFileSync(bf, 'utf8'));
const pf = get('--plan'); const plan = pf ? JSON.parse(fs.readFileSync(pf, 'utf8')) : null;
const xf = get('--execution'); const execution = xf ? JSON.parse(fs.readFileSync(xf, 'utf8')) : null;
const st = await evalueaza({ bundle, plan, execution });
if (args.includes('--json')) console.log(JSON.stringify(st, null, 1));
else {
console.log(`CONTROL PLANE CONSOLE - host ${st.host}: ${st.verdict}`);
console.log(` audit chain: ${st.auditChain.ok ? 'INTACT' : 'BROKEN at seq ' + st.auditChain.brokenAtSeq}, ${st.auditChain.count} entries, head ${st.auditChain.head || '-'}`);
console.log(` AIP-23 envelopes: ${st.envelopes.intact}/${st.envelopes.total} intact` + (st.envelopes.bad.length ? ` (bad: ${st.envelopes.bad.map((x) => x.seq).join(',')})` : ''));
if (st.selfReportSuspect) console.log(` WARNING: ${st.selfReportSuspect}`);
if (st.migration) console.log(st.migration.total === null ? ` PQ migration: ${st.migration.error}` : ` PQ migration: ${st.migration.total} actions (${st.migration.critical} critical, ${st.migration.auto} auto, ${st.migration.manual} manual, ${st.migration.blocked} blocked)`);
if (st.execution) console.log(` execution: chain ${st.execution.chainOk ? 'INTACT' : 'BROKEN at seq ' + st.execution.brokenAtSeq + ' (' + st.execution.reason + ')'}, ${st.execution.actions} actions (${st.execution.ok} OK, ${st.execution.failed} FAILED)`);
}
return st.verdict === 'OK' ? 0 : 1;
}
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
main().then((c) => { process.exitCode = c; }).catch((e) => { console.error(e.message); process.exitCode = 2; });
}