108 lines
7.0 KiB
JavaScript
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; });
|
|
}
|