aere-quantum/control-plane/proba-executa-migrare.mjs

212 lines
21 KiB
JavaScript

// Proba executorului B1 (executa-migrare.mjs), pe produse REALE pornite local: un KMS (pq-kms/server.mjs cu cheie-radacina si jeton
// generate aici, scrise in fisiere 0600, niciodata tiparite), un CA PQ (pq-pki/cli.mjs init + intermediate, parola din mediu), un gateway
// PQ pornit de executor in fata unui upstream HTTP local, si un certificat clasic de test pentru gateway (openssl). Fara retea externa.
// Forma 2 a inregistrarilor (2026-09-29, in engleza): records/record, steps/step, before/after, verdict OK|FAILED|DRY-RUN|SKIPPED-no-consent.
// Cazuri (fiecare cu perechea lui):
// 1. fara consimtamant + execute=true -> nimic executat: aceleasi chei in KMS, niciun certificat scris, niciun gateway pornit
// 2. consimtamant 'all' + dry run -> la fel, nimic atins; inregistrarile spun DRY-RUN
// 3. consimtamant 'all' + execute -> gateway OK (hybrid-only: un client numai-PQ trece, unul numai-CLASIC e refuzat, masurat),
// KMS OK (cheie hibrida, latest_version 1), PKI OK (ML-DSA-65 pana la radacina); execution.json ok
// 4. a doua executie -> KMS ROTESTE (latest_version 2), min_decryption_version pastreaza versiunea veche
// 5. plantare: cheia gateway-ului NU e a certificatului -> actiunea gateway FAILED (gateway-ul refuza sa porneasca), CELELALTE merg
// 6. plantare: o inregistrare din execution.json schimbata -> verificaExecutie o prinde; nemodificata -> trece (controlul metodei)
// 7. actiunile 'manual'/'blocked' nu se executa niciodata (notExecutable in sumar)
// Atacurile revizuirii adversariale (2026-09-27), fiecare reprodus pe codul vechi inainte de reparatie:
// 8. produs 'constructor'/'toString' (mostenit din prototip) -> FAILED, nu OK fara actiune
// 9. gateway pe hybrid-preferred -> OK, iar inregistrarea spune ca un client clasic e inca acceptat
// 10. un camp `key` din plan care numeste o cheie straina existenta -> cheia straina NU e rotita
// 11. modul uscat scrie tinta efectiva; 12. un cn wildcard -> FAILED, niciun fisier scris
// Ref-urile REALE ale planificatorului (control-plane.mjs --code pe un dosar de cod generat aici), 2026-09-27:
// 13. doua actiuni KMS din acelasi fisier lung (liniile 4 si 7) -> DOUA chei distincte; consimtamantul pentru una nu o roteste pe cealalta
// 14. o actiune PKI fara cn, cu un ref real lung -> CN-ul implicit e un nume de gazda valid si certificatul se emite
// Numele fisierelor se citesc din INREGISTRARI (ce a raportat executorul), nu se ghicesc.
// node proba-executa-migrare.mjs iesire 0 = toate cum trebuia
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import http from 'node:http';
import crypto from 'node:crypto';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { executa, verificaExecutie } from './executa-migrare.mjs';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const KMS_SERVER = path.join(AICI, '..', 'pq-kms', 'server.mjs');
const PKI_CLI = path.join(AICI, '..', 'pq-pki', 'cli.mjs');
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-b1-exec-'));
const copii = [];
let up = null;
const dormi = (ms) => new Promise((r) => setTimeout(r, ms));
const rezultate = []; let ok = 0, rau = 0;
const fisiere = (d) => (fs.existsSync(d) ? fs.readdirSync(d).filter((f) => f !== 'execution.json') : []);
const pas = (r, cheie) => (r && r.steps ? (r.steps.find((s) => s[cheie]) || {})[cheie] : undefined);
const inreg = (x) => x.records.map((i) => i.record);
function cere(cond, ce) { rezultate.push((cond ? 'OK ' : 'RAU ') + ce); if (cond) ok++; else rau++; }
function alnum(n) { const a = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'; return Array.from(crypto.randomBytes(n)).map((b) => a[b % a.length]).join(''); }
function ruleaza(cmd, args, env, ms = 60000) {
return new Promise((resolve) => {
const p = spawn(cmd, args, { env: { ...process.env, ...env }, stdio: ['ignore', 'pipe', 'pipe'] });
let o = '', e = ''; p.stdout.on('data', (b) => { o += b; }); p.stderr.on('data', (b) => { e += b; });
const t = setTimeout(() => p.kill(), ms);
p.on('close', (cod) => { clearTimeout(t); resolve({ cod, o, e }); });
p.on('error', (err) => { clearTimeout(t); resolve({ cod: -1, o, e: String(err.message) }); });
});
}
async function openssl(args) {
let r = await ruleaza('openssl', args, {});
if (r.cod === -1 && /ENOENT/.test(r.e)) r = await ruleaza('C:\\Program Files\\Git\\mingw64\\bin\\openssl.exe', args, {});
if (r.cod !== 0) throw new Error('openssl a cazut: ' + r.e.slice(0, 200));
}
async function kmsGet(url, token, cale) { const r = await fetch(url + cale, { headers: { authorization: 'Bearer ' + token } }); let j = null; try { j = await r.json(); } catch {} return { status: r.status, j }; }
try {
// --- KMS local
const kmsPort = 18420 + crypto.randomInt(1000); const kmsUrl = `http://127.0.0.1:${kmsPort}`;
const kmsToken = alnum(40); const tokenFile = path.join(T, 'kms.token'); fs.writeFileSync(tokenFile, kmsToken + '\n', { mode: 0o600 });
fs.mkdirSync(path.join(T, 'kms'), { recursive: true, mode: 0o700 });
const kms = spawn(process.execPath, [KMS_SERVER], { env: { ...process.env, AERE_KMS_ROOT_KEY: crypto.randomBytes(32).toString('hex'), AERE_KMS_TOKEN: kmsToken, AERE_KMS_PORT: String(kmsPort), AERE_KMS_HOST: '127.0.0.1', AERE_KMS_DATA_DIR: path.join(T, 'kms') }, stdio: ['ignore', 'pipe', 'pipe'] });
copii.push(kms); let kmsErr = ''; kms.stderr.on('data', (b) => { kmsErr += b; });
let sus = false; for (let i = 0; i < 100 && !sus; i++) { try { const r = await fetch(kmsUrl + '/v1/health'); sus = r.status < 500; } catch { await dormi(100); } }
if (!sus) throw new Error('KMS-ul local nu a pornit: ' + kmsErr.slice(0, 200));
// --- CA PQ local (parola numai in mediul copiilor si in optiuni, niciodata tiparita)
const parola = alnum(26); const ca = path.join(T, 'ca');
let r = await ruleaza(process.execPath, [PKI_CLI, 'init', '--dir', ca, '--name', 'root', '--org', 'Proba'], { AERE_PKI_PASSPHRASE: parola });
if (r.cod !== 0) throw new Error('pki init: ' + (r.e || r.o).slice(0, 200));
r = await ruleaza(process.execPath, [PKI_CLI, 'intermediate', '--dir', ca, '--ca', 'root', '--name', 'issuing'], { AERE_PKI_PASSPHRASE: parola });
if (r.cod !== 0) throw new Error('pki intermediate: ' + (r.e || r.o).slice(0, 200));
// --- certificat clasic de test pentru gateway (ce are clientul azi) + o a doua pereche pentru plantarea "cheia nu e a certificatului"
await openssl(['req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes', '-keyout', path.join(T, 'gw.key'), '-out', path.join(T, 'gw.crt'), '-subj', '/CN=localhost', '-days', '2', '-addext', 'subjectAltName=DNS:localhost']);
await openssl(['req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes', '-keyout', path.join(T, 'alt.key'), '-out', path.join(T, 'alt.crt'), '-subj', '/CN=localhost', '-days', '2']);
// --- upstream HTTP local
up = http.createServer((q, s) => s.end('ok')); await new Promise((r2) => up.listen(0, '127.0.0.1', r2)); const upPort = up.address().port;
// --- planul (forma 2 a lui plan-migrare)
const plan = { actions: [
{ ref: 'tls-kex:localhost', asset: 'TLS on localhost', target: 'X25519MLKEM768 (hybrid)', method: 'auto-aere', product: 'gateway', urgency: 'CRITICAL', how: 'PQ Gateway in front' },
{ ref: 'kem:app-secret', asset: 'the application envelope', target: 'ML-KEM-768 (hybrid with X25519)', method: 'auto-aere', product: 'kms', urgency: 'CRITICAL', how: 'hybrid envelope through the KMS' },
{ ref: 'sig:svc.internal', asset: 'the service identity', target: 'ML-DSA-65', method: 'auto-aere', product: 'pki', urgency: 'HIGH', cn: 'svc.internal', how: 'PQ certificate' },
{ ref: 'tls-cert:public.example', asset: 'public certificate', target: 'ML-DSA-65', method: 'blocked', product: null, urgency: 'MEDIUM', blocker: 'public CA without post-quantum' },
] };
const gw = (extra = {}) => ({ cert: path.join(T, 'gw.crt'), key: path.join(T, 'gw.key'), upstream: `http://127.0.0.1:${upPort}`, mode: 'hybrid-only', selfSigned: true, servername: 'localhost', keep: false, ...extra });
const baza = (out, extra = {}) => ({ out: path.join(T, out), gateway: gw(), kms: { url: kmsUrl, token: kmsToken }, pki: { dir: ca, ca: 'issuing', roots: path.join(ca, 'root.crt'), passphrase: parola }, ...extra });
const chei = async () => { const l = await kmsGet(kmsUrl, kmsToken, '/v1/keys'); return (l.j && l.j.keys ? l.j.keys.length : -1); };
const chei0 = await chei();
// 1. fara consimtamant
let x = await executa(plan, baza('e1', { consent: [], execute: true }));
cere(x.summary.skipped === 3 && x.summary.ok === 0 && x.summary.notExecutable === 1, `1. fara consimtamant: 3 sarite, 0 executate, 1 neexecutabila (${JSON.stringify(x.summary)})`);
cere((await chei()) === chei0 && fisiere(path.join(T, 'e1')).length === 0, `1. fara consimtamant: KMS neatins, niciun fisier scris (${fisiere(path.join(T, 'e1')).join(',') || 'niciunul'})`);
// 2. uscat
x = await executa(plan, baza('e2', { consent: 'all', execute: false }));
cere(x.summary.dryRun === 3 && x.summary.ok === 0, `2. uscat: 3 DRY-RUN, 0 executate (${JSON.stringify(x.summary)})`);
cere((await chei()) === chei0 && fisiere(path.join(T, 'e2')).length === 0, `2. uscat: KMS neatins, nimic scris (${fisiere(path.join(T, 'e2')).join(',') || 'niciun fisier'})`);
// 3. executat
x = await executa(plan, baza('e3', { consent: 'all', execute: true }));
const rec = (ref) => inreg(x).find((i) => i.ref === ref);
const g = rec('tls-kex:localhost'), k = rec('kem:app-secret'), p = rec('sig:svc.internal');
cere(x.summary.ok === 3 && x.summary.failed === 0, `3. executat: 3 OK (${JSON.stringify(x.summary)})`);
cere(g && g.verdict === 'OK' && g.steps.some((s) => /TLS 1\.3 test/.test(s.step) && s.ok) && pas(g, 'file') && fs.existsSync(pas(g, 'file')), `3. gateway (hybrid-only): un client numai-PQ termina strangerea, configuratie emisa`);
cere(g && g.after && g.after.classicalAccepted === false, `3. gateway hybrid-only: un client numai-CLASIC e REFUZAT, masurat (${g && g.after ? g.after.guarantee : '?'})`);
cere(k && k.verdict === 'OK' && k.after && k.after.latest_version === 1 && (await chei()) === chei0 + 1, `3. kms: cheie hibrida creata, latest_version=${k && k.after ? k.after.latest_version : '?'}`);
cere(p && p.verdict === 'OK' && p.steps.some((s) => /chain verified/.test(s.step) && s.ok) && pas(p, 'cert') && fs.existsSync(pas(p, 'cert')) && fs.existsSync(pas(p, 'key')), `3. pki: certificat ${p && p.after ? p.after.alg : '?'} emis si verificat pana la radacina (${p && p.after ? p.after.chain.join(' <- ') : '?'})`);
const v3 = verificaExecutie(JSON.parse(fs.readFileSync(x.file, 'utf8')));
cere(v3.ok && v3.seq === x.records.length, `3. execution.json: lantul de hash-uri se verifica (${v3.seq} inregistrari)`);
// 4. a doua executie: KMS roteste
x = await executa(plan, baza('e4', { consent: ['kem:app-secret'], execute: true }));
const k2 = inreg(x).find((i) => i.ref === 'kem:app-secret');
cere(k2 && k2.verdict === 'OK' && k2.before && k2.before.exists && k2.after.latest_version === 2 && (await chei()) === chei0 + 1, `4. a doua executie: cheia ROTITA (latest_version=${k2 && k2.after ? k2.after.latest_version : '?'}), nu creata a doua oara`);
cere(k2 && k2.after && k2.after.min_decryption_version !== null && k2.after.min_decryption_version <= 1, `4. intoarcere: versiunea veche ramane decriptabila (min_decryption_version=${k2 && k2.after ? k2.after.min_decryption_version : '?'})`);
// 5. plantare: cheia nu e a certificatului -> gateway FAILED, restul OK
x = await executa(plan, baza('e5', { consent: 'all', execute: true, gateway: gw({ key: path.join(T, 'alt.key') }) }));
const g5 = inreg(x).find((i) => i.ref === 'tls-kex:localhost');
cere(g5 && g5.verdict === 'FAILED' && x.summary.failed === 1 && x.summary.ok === 2, `5. plantare (cheia nu e a certificatului): gateway FAILED (${g5 ? g5.error : '?'}), celelalte 2 OK`);
cere(!fisiere(path.join(T, 'e5')).some((f) => f.endsWith('.gateway.env')) && fisiere(path.join(T, 'e5')).some((f) => f.endsWith('.crt')), '5. plantare: nicio configuratie de gateway emisa pentru actiunea cazuta (certificatul actiunii PKI, da)');
// 6. tamper pe execution.json
const dosar = JSON.parse(fs.readFileSync(path.join(T, 'e3', 'execution.json'), 'utf8'));
cere(verificaExecutie(dosar).ok, '6. controlul metodei: dosarul nemodificat se verifica');
const t2 = JSON.parse(JSON.stringify(dosar)); const iK = t2.records.findIndex((i) => i.record.ref === 'kem:app-secret'); t2.records[iK].record.verdict = 'OK-fals';
const vt = verificaExecutie(t2);
cere(!vt.ok && vt.seq === iK, `6. plantare: o inregistrare schimbata e prinsa la seq ${vt.seq} (${vt.reason})`);
const t3 = JSON.parse(JSON.stringify(dosar)); t3.records.splice(iK, 1); t3.records.forEach((e, i) => { e.seq = i; });
cere(!verificaExecutie(t3).ok, '6. plantare: o inregistrare STEARSA e prinsa (lantul nu mai leaga)');
// 7. neexecutabile
cere(x.summary.notExecutable === 1 && !x.records.some((i) => i.record.ref === 'tls-cert:public.example'), '7. actiunea blocata (CA public fara PQ) nu e nici macar incercata');
// --- atacurile revizuirii adversariale (2026-09-27), fiecare reprodus inainte de reparatie ---
// 8. produs mostenit din Object.prototype: inainte, `constructor`/`toString` ieseau OK fara nicio actiune
for (const numeProt of ['constructor', 'toString']) {
const xp = await executa({ actions: [{ ref: 'prot:' + numeProt, method: 'auto-aere', product: numeProt }] }, baza('e8-' + numeProt, { consent: 'all', execute: true }));
const rp = inreg(xp).find((i) => i.ref === 'prot:' + numeProt);
cere(xp.summary.ok === 0 && xp.summary.failed === 1 && rp.verdict === 'FAILED', `8. ATAC: produs '${numeProt}' (mostenit din prototip) -> FAILED, nu OK (${rp.error})`);
}
// 9. gateway pe modul implicit hybrid-preferred: OK, dar inregistrarea spune ONEST ca un client clasic e inca acceptat
x = await executa({ actions: [plan.actions[0]] }, baza('e9', { consent: 'all', execute: true, gateway: gw({ mode: 'hybrid-preferred' }) }));
const g9 = inreg(x).find((i) => i.ref === 'tls-kex:localhost');
cere(g9 && g9.verdict === 'OK' && g9.after.classicalAccepted === true && /hybrid-only/.test(g9.after.guarantee), `9. hybrid-preferred: OK, iar inregistrarea spune ca un client clasic e inca acceptat (${g9 && g9.after && g9.after.guarantee ? g9.after.guarantee.slice(0, 70) : '?'})`);
// 10. un camp `key` din plan care numeste o cheie STRAINA existenta nu o mai roteste (numele vine numai din ref)
const strain = 'cheie-straina-prod';
let rs = await fetch(`${kmsUrl}/v1/keys/${strain}`, { method: 'POST', headers: { authorization: 'Bearer ' + kmsToken, 'content-type': 'application/json' }, body: JSON.stringify({ type: 'encrypt' }) });
if (rs.status === 404 || rs.status === 405) rs = await fetch(`${kmsUrl}/v1/keys`, { method: 'POST', headers: { authorization: 'Bearer ' + kmsToken, 'content-type': 'application/json' }, body: JSON.stringify({ name: strain, type: 'encrypt' }) });
const vStrain0 = (await kmsGet(kmsUrl, kmsToken, `/v1/keys/${strain}`)).j?.latest_version;
x = await executa({ actions: [{ ...plan.actions[1], ref: 'kem:alta', key: strain }] }, baza('e10', { consent: 'all', execute: true }));
const vStrain1 = (await kmsGet(kmsUrl, kmsToken, `/v1/keys/${strain}`)).j?.latest_version;
const k10 = inreg(x).find((i) => i.ref === 'kem:alta');
cere(vStrain0 === 1 && vStrain1 === 1 && k10 && k10.after && k10.after.key !== strain && /^mig-kem-alta/.test(k10.after.key), `10. ATAC: cheie straina numita in plan NU e rotita (versiunea ei ${vStrain0} -> ${vStrain1}); s-a lucrat pe ${k10 && k10.after ? k10.after.key : '?'}`);
// 11. modul uscat arata TINTA efectiva, ca omul sa consimta la ce se atinge de fapt
x = await executa({ actions: [{ ...plan.actions[1], ref: 'kem:vizibil', key: strain }] }, baza('e11', { consent: 'all', execute: false }));
const u11 = inreg(x).find((i) => i.ref === 'kem:vizibil');
cere(u11 && u11.verdict === 'DRY-RUN' && /mig-kem-vizibil/.test(u11.effectiveTarget) && u11.steps.some((s) => /mig-kem-vizibil/.test(s.step)), `11. uscat: tinta efectiva e scrisa (${u11 ? u11.effectiveTarget : '?'})`);
// 12. PKI: un cn wildcard e refuzat INAINTE de emitere, si nu ramane niciun fisier scris
x = await executa({ actions: [{ ...plan.actions[2], ref: 'sig:wild', cn: '*.bank.example' }] }, baza('e12', { consent: 'all', execute: true }));
const p12 = inreg(x).find((i) => i.ref === 'sig:wild');
cere(p12 && p12.verdict === 'FAILED' && fisiere(path.join(T, 'e12')).length === 0, `12. ATAC: cn wildcard refuzat, niciun fisier scris (${p12 ? p12.error : '?'})`);
// --- ref-urile REALE ale planificatorului: control-plane.mjs pe un dosar de cod generat aici ---
const cod = path.join(T, 'cod');
fs.mkdirSync(path.join(cod, 'services', 'payments', 'settlement'), { recursive: true });
fs.mkdirSync(path.join(cod, 'services', 'auth', 'tokens'), { recursive: true });
fs.writeFileSync(path.join(cod, 'services', 'payments', 'settlement', 'envelope.js'), [
"const crypto = require('node:crypto');", "const { publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });",
'function wrap(k) {', ' return crypto.publicEncrypt(publicKey, k);', '}', 'function wrapForAudit(k, auditKey) {',
' return crypto.publicEncrypt(auditKey, k);', '}', 'module.exports = { wrap, wrapForAudit };', ''].join('\n'));
fs.writeFileSync(path.join(cod, 'services', 'auth', 'tokens', 'session.js'), [
"const crypto = require('node:crypto');", "const { privateKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });",
'function signSession(data) {', " return crypto.sign('sha256', data, privateKey);", '}', "const h = crypto.createSign('RSA-SHA256');",
'module.exports = { signSession, h };', ''].join('\n'));
const cp = await ruleaza(process.execPath, [path.join(AICI, 'control-plane.mjs'), '--code', cod, '--json'], {});
if (cp.cod !== 0) throw new Error('control-plane.mjs a cazut: ' + (cp.e || cp.o).slice(0, 200));
const planReal = JSON.parse(cp.o);
const kmsReal = planReal.actions.filter((a) => a.method === 'auto-aere' && a.product === 'kms' && /envelope\.js:/.test(a.ref));
const pkiReal = planReal.actions.filter((a) => a.method === 'auto-aere' && a.product === 'pki' && /session\.js:/.test(a.ref) && !a.cn);
// controlul fixturii: planificatorul REAL chiar a dat cazurile pe care le masuram, altfel 13/14 nu inseamna nimic
cere(kmsReal.length === 2 && pkiReal.length >= 1, `13/14. controlul fixturii: planificatorul real da ${kmsReal.length} actiuni KMS in envelope.js si ${pkiReal.length} PKI fara cn in session.js (${kmsReal.map((a) => a.ref.split(':').slice(-1)).join(',')})`);
// 13. doua chei distincte, iar consimtamantul pentru una nu o roteste pe cealalta
const c13 = await chei();
x = await executa({ actions: kmsReal }, baza('e13', { consent: 'all', execute: true }));
const r13 = kmsReal.map((a) => inreg(x).find((i) => i.ref === a.ref));
const n13 = r13.map((r) => (r && r.after ? r.after.key : null));
const noi13 = (await chei()) - c13;
cere(x.summary.ok === 2 && n13[0] && n13[1] && n13[0] !== n13[1] && noi13 === 2, `13. doua ref-uri reale din acelasi fisier -> doua chei DISTINCTE create (${n13.join(' / ')}; chei noi in KMS: ${noi13})`);
x = await executa({ actions: kmsReal }, baza('e13b', { consent: [kmsReal[0].ref], execute: true }));
const v13 = [];
for (const nume of n13) v13.push(nume ? (await kmsGet(kmsUrl, kmsToken, `/v1/keys/${encodeURIComponent(nume)}`)).j?.latest_version : null);
cere(v13[0] === 2 && v13[1] === 1, `13. consimtamant numai pentru ${kmsReal[0].ref.split(':').slice(-2).join(':')} -> cheia lui rotita (v${v13[0]}), a celuilalt NEATINSA (v${v13[1]})`);
cere(n13.every((nm) => nm && /^[a-z0-9][a-z0-9_-]{0,63}$/.test(nm)), `13. numele sunt nume KMS valide (${n13.map((nm) => (nm ? nm.length : 0)).join(', ')} caractere)`);
// 14. PKI fara cn: CN-ul implicit e un nume de gazda valid, certificatul se emite, si fiecare actiune isi scrie fisierele ei
x = await executa({ actions: pkiReal }, baza('e14', { consent: 'all', execute: true }));
const r14 = pkiReal.map((a) => inreg(x).find((i) => i.ref === a.ref));
cere(r14.every((r) => r && r.verdict === 'OK' && r.after && /\.internal$/.test(r.after.cn)), `14. PKI pe ref-uri reale fara cn: ${r14.map((r) => (r ? r.verdict + ' ' + (r.after ? r.after.cn : r.error) : '?')).join(' | ')}`);
const certs14 = r14.map((r) => pas(r, 'cert'));
cere(certs14.every(Boolean) && new Set(certs14).size === certs14.length && certs14.every((f) => fs.existsSync(f)), `14. fiecare actiune PKI isi are fisierele ei (${certs14.length} certificate, ${new Set(certs14).size} distincte)`);
} catch (e) {
rezultate.push('RAU proba nu a putut rula: ' + String(e.message).replace(/[A-Za-z0-9+/=]{48,}/g, '…').slice(0, 300)); rau++;
} finally {
for (const c of copii) { try { c.kill(); } catch {} }
// upstream-ul local tinea bucla vie: proba tiparea verdictul si nu mai iesea (rularile din fundal nu se terminau niciodata)
if (up) { try { up.closeAllConnections(); up.close(); } catch {} }
}
for (const l of rezultate) console.log(l);
if (!rau) { try { fs.rmSync(T, { recursive: true, force: true }); } catch {} }
console.log(`B1 executor: ${ok}/${ok + rau} cum trebuia`);
if (rau) console.log('dosarul de proba a ramas pentru cercetare: ' + T);
process.exitCode = rau ? 1 : 0;