'use strict'; // Proba planificatorului de migrare (B1), cu CONTROALE NEGATIVE: un asset vulnerabil TREBUIE sa apara in plan cu tinta si metoda // corecte; un asset deja post-cuantic NU trebuie sa apara; HNDL (schimb de cheie) trebuie sa fie mai urgent decat o semnatura. // node proba-plan-migrare.mjs -> 0 toate cum trebuia, 1 altfel import { planeaza } from './plan-migrare.mjs'; let rele = 0; let total = 0; function cer(nume, cond) { total++; console.log(` [${cond ? 'OK ' : 'RAU '}] ${nume}`); if (!cond) rele++; } // fixtura de inventar: doua vulnerabile (schimb de cheie + semnatura de certificat) + unul deja PQ (nu trebuie in plan) const inventory = [ { ref: 'algorithm:ecdh', assetType: 'algorithm', name: 'ECDH', primitive: 'key-agree', quantumVulnerable: true }, { ref: 'algorithm:rsa', assetType: 'algorithm', name: 'RSA-2048', primitive: 'signature', quantumVulnerable: true }, { ref: 'cert:interna', assetType: 'certificate', primitive: 'signature', quantumVulnerable: true, certificate: { subjectName: 'CN=api.intern', issuerName: 'CN=Intern CA', notValidAfter: '2030-01-01T00:00:00Z' } }, { ref: 'cert:webpki', assetType: 'certificate', primitive: 'signature', quantumVulnerable: true, certificate: { subjectName: 'CN=www.exemplu.com', issuerName: "C=US, O=Let's Encrypt, CN=R3", notValidAfter: '2026-12-01T00:00:00Z' } }, { ref: 'algorithm:mlkem', assetType: 'algorithm', name: 'ML-KEM-768', primitive: 'kem', quantumVulnerable: false }, { ref: 'algorithm:mldsa', assetType: 'algorithm', name: 'ML-DSA-65', primitive: 'signature', quantumVulnerable: false }, { ref: 'algorithm:aes', assetType: 'algorithm', name: 'AES-256-GCM', primitive: 'ae', quantumVulnerable: false }, ]; // 2026-09-27: constatarile de scanare se DERIVA din sursa scanerului (readiness-service.mjs, apelurile lui add(id, severitate, // titlu)), nu se scriu de mana. Fixtura veche folosea id-uri inventate ('tls-kex-classical', 'tls12-enabled'), deci proba era verde // pe un clasificator care, pe id-urile reale, arunca `tls13-missing` si facea din `pq-not-preferred`/`auth-classical` expuneri CRITICE. import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const { caleaScanerului } = await import('./control-plane.mjs'); const SRC_SCANER = fs.readFileSync(caleaScanerului(), 'utf8'); function constatariDinScaner(src) { const re = /add\('([a-z0-9-]+)',\s*'([a-z]+)',\s*[`']([^`']+)[`']/g; const f = []; let m; while ((m = re.exec(src))) f.push({ id: m[1], severity: m[2], title: m[3] }); return f; } const REALE = constatariDinScaner(SRC_SCANER); const dupaId = (id) => REALE.find((x) => x.id === id); const scan = { domain: 'client.exemplu.com', findings: [dupaId('hndl-exposed'), dupaId('tls12-accepted')].filter(Boolean) }; const plan = planeaza({ inventory, scan }); const byRef = Object.fromEntries(plan.actions.map((a) => [a.ref, a])); console.log('plan:', plan.summary.total, 'actiuni;', JSON.stringify(plan.summary.byUrgency), 'auto=' + plan.summary.autoAere, 'manual=' + plan.summary.manual, 'blocked=' + plan.summary.blocked); // pozitive cer('ECDH (schimb de cheie) e in plan, auto prin gateway', byRef['algorithm:ecdh'] && byRef['algorithm:ecdh'].product === 'gateway'); cer('RSA (semnatura) e in plan, tinta ML-DSA prin PKI', byRef['algorithm:rsa'] && /ML-DSA/.test(byRef['algorithm:rsa'].target) && byRef['algorithm:rsa'].product === 'pki'); cer('certificatul intern e in plan, auto prin PKI', byRef['cert:interna'] && byRef['cert:interna'].product === 'pki'); cer('certificatul WebPKI public e BLOCAT (niciun CA public PQ), nu se pretinde auto', byRef['cert:webpki'] && byRef['cert:webpki'].method === 'blocked' && byRef['cert:webpki'].blocker); cer('scanarea: schimbul de cheie clasic e in plan, auto prin gateway', byRef['tls-kex:client.exemplu.com'] && byRef['tls-kex:client.exemplu.com'].product === 'gateway'); cer('scanarea: TLS 1.2 e in plan, manual (config)', byRef['tls-conf:client.exemplu.com:tls12-accepted'] && byRef['tls-conf:client.exemplu.com:tls12-accepted'].method === 'manual'); // controale negative: ce NU trebuie sa fie in plan cer('CONTROL: ML-KEM (deja PQ) NU e in plan', !byRef['algorithm:mlkem']); cer('CONTROL: ML-DSA (deja PQ) NU e in plan', !byRef['algorithm:mldsa']); cer('CONTROL: AES-256 (nu e cu risc cuantic de spart) NU e in plan', !byRef['algorithm:aes']); // HNDL: schimbul de cheie e mai urgent decat semnatura (memorie: HNDL nu se aplica semnaturilor) const uKex = byRef['tls-kex:client.exemplu.com'].urgency, uSig = byRef['algorithm:rsa'].urgency; const rang = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }; cer(`HNDL: schimbul de cheie (${uKex}) e mai urgent decat semnatura (${uSig})`, rang[uKex] < rang[uSig]); // planul e sortat pe urgenta (prima actiune e cea mai urgenta) cer('planul e sortat: prima actiune e CRITICA', plan.actions[0] && plan.actions[0].urgency === 'CRITICAL'); // CONTROL NEGATIV al metodei: un plan pe un inventar CURAT (totul PQ) e GOL const planCurat = planeaza({ inventory: [{ ref: 'a', assetType: 'algorithm', name: 'ML-KEM-768', primitive: 'kem', quantumVulnerable: false }] }); cer('CONTROL: inventar tot-PQ -> plan gol', planCurat.actions.length === 0); // CLASIFICAREA PE CONTRACTUL REAL AL SCANERULUI: fiecare id din sursa scanerului e in tabel, si semantica lui e cea corecta const CLASIFICARE_SCAN = (await import('./plan-migrare.mjs')).CLASIFICARE_SCAN || {}; // modul fara tabel -> nimic acoperit (rosu masurat, nu cadere) const distincte = [...new Set(REALE.map((x) => x.id))]; cer(`scanerul emite ${distincte.length} id-uri distincte, extrase din sursa (control pozitiv al extragerii: >= 10)`, distincte.length >= 10); const neacoperite = distincte.filter((i) => !Object.hasOwn(CLASIFICARE_SCAN, i)); cer(`fiecare id al scanerului are o clasificare scrisa (neacoperite: ${neacoperite.join(',') || 'niciunul'})`, neacoperite.length === 0); // CONTROL NEGATIV al acoperirii: un id nou, adaugat in scaner fara clasificare, TREBUIE sa iasa neacoperit const cuNou = [...distincte, 'pq-ceva-nou']; cer('CONTROL: un id nou al scanerului, neclasificat, e prins de verificarea acoperirii', cuNou.some((i) => !Object.hasOwn(CLASIFICARE_SCAN, i))); const unul = (id) => { const f = dupaId(id); const pl = planeaza({ scan: { domain: 'd.test', findings: [f] } }); return pl.actions[0] || null; }; cer('numai expunerea HNDL e CRITICA si automata (gateway)', unul('hndl-exposed') && unul('hndl-exposed').urgency === 'CRITICAL' && unul('hndl-exposed').product === 'gateway'); cer('PQ deja suportat dar nepreferat NU e CRITIC (ordinea grupurilor, MEDIE, manual)', unul('pq-not-preferred') && unul('pq-not-preferred').urgency === 'MEDIUM' && unul('pq-not-preferred').method === 'manual'); cer('TLS 1.3 lipsa produce o actiune INALTA (inainte era aruncata)', unul('tls13-missing') && unul('tls13-missing').urgency === 'HIGH'); cer('informativul pq-preference-unmeasured nu devine actiune', unul('pq-preference-unmeasured') === null); cer('autentificarea clasica e BLOCATA onest (CA public fara PQ), nu CRITICA', unul('auth-classical') && unul('auth-classical').method === 'blocked' && unul('auth-classical').urgency !== 'CRITICAL'); cer('expirarea certificatului e o actiune de EXPIRARE, nu de semnatura clasica', unul('cert-expiring') && /tls-cert-expiry/.test(unul('cert-expiring').ref)); cer('lantul neincrezut produce o actiune (inainte era aruncat)', unul('chain-untrusted') !== null); // in toate formele reale, o singura actiune e CRITICA: cea HNDL const critice = distincte.map((i) => unul(i)).filter((a) => a && a.urgency === 'CRITICAL'); cer(`pe toate cele ${distincte.length} forme reale, exact o actiune CRITICA (HNDL): ${critice.map((a) => a.ref).join(',')}`, critice.length === 1 && /^tls-kex:/.test(critice[0].ref)); // CERTIFICATELE PE FORMA REALA (2026-09-27): certificate si chei generate cu openssl, inventarul REAL si control-plane.mjs --json pe ele. // Fixtura de sus scrie `primitive: 'signature'` pe certificate; inventarul real scrie primitiva CHEII ('unknown' la RSA, 'other' la EC), // si pe forma aceea regula veche a planificatorului nu se aplica niciodata. Aici se masoara exact forma aceea. { const os = await import('node:os'); const { spawnSync } = await import('node:child_process'); const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-plan-cert-')); const D = path.join(T, 'deploy', 'tls'); fs.mkdirSync(D, { recursive: true }); const openssl = (args) => { let r = spawnSync('openssl', args, { encoding: 'utf8' }); if (r.error && r.error.code === 'ENOENT') r = spawnSync('C:\\Program Files\\Git\\mingw64\\bin\\openssl.exe', args, { encoding: 'utf8' }); return !r.error && r.status === 0; }; const facut = openssl(['req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-keyout', path.join(D, 'api.key'), '-out', path.join(D, 'api.crt'), '-subj', '/O=Client/CN=api.internal', '-days', '800']) && openssl(['req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes', '-keyout', path.join(D, 'edge.key'), '-out', path.join(D, 'edge.crt'), '-subj', '/CN=edge.internal', '-days', '30']) && openssl(['req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes', '-keyout', path.join(D, 'wild.key'), '-out', path.join(D, 'wild.crt'), '-subj', '/CN=*.svc.internal', '-days', '400']); cer('certificate reale: openssl a generat cele trei certificate si chei (fara el nimic de mai jos nu masoara)', facut); const cp = spawnSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), 'control-plane.mjs'), '--code', T, '--json'], { encoding: 'utf8' }); let pr = null; try { pr = JSON.parse(cp.stdout); } catch { pr = null; } cer(`certificate reale: control-plane.mjs --json a rulat (cod ${cp.status})`, cp.status === 0 && pr && Array.isArray(pr.actions)); const { caleaInventarului } = await import('./control-plane.mjs'); const inv = await import((await import('node:url')).pathToFileURL(caleaInventarului()).href); const certInv = (inv.scan(T).findings || []).filter((g) => g.assetType === 'certificate'); // controlul fixturii: inventarul real chiar da primitive NEDECISE pe certificate; daca intr-o zi da 'signature', cazul de fata nu mai // masoara regula veche, si trebuie stiut cer(`certificate reale: controlul fixturii, inventarul da ${certInv.length} certificate cu primitivele ${[...new Set(certInv.map((g) => g.primitive))].join(',')}`, certInv.length === 3 && certInv.every((g) => g.primitive === 'unknown' || g.primitive === 'other')); const act = (fis) => (pr && pr.actions ? pr.actions.find((a) => new RegExp(`:deploy/tls/${fis.replace('.', '\\.')}:`).test(a.ref)) : null); const api = act('api.crt'), edge = act('edge.crt'), wild = act('wild.crt'), cheie = act('api.key'); cer(`certificatul RSA intern (800 de zile) -> auto prin PKI, ML-DSA, INALTA, pentru acelasi CN (${api ? [api.method, api.product, api.urgency, api.cn].join(' ') : 'lipsa'})`, api && api.method === 'auto-aere' && api.product === 'pki' && /ML-DSA/.test(api.target) && api.urgency === 'HIGH' && api.cn === 'api.internal'); cer(`certificatul EC intern (30 de zile) -> auto prin PKI, MEDIE (${edge ? [edge.method, edge.product, edge.urgency, edge.cn].join(' ') : 'lipsa'})`, edge && edge.method === 'auto-aere' && edge.product === 'pki' && edge.urgency === 'MEDIUM' && edge.cn === 'edge.internal'); cer(`certificatul wildcard -> manual, cu motivul scris, fara CN inventat (${wild ? [wild.method, (wild.blocker || '').slice(0, 40)].join(' | ') : 'lipsa'})`, wild && wild.method === 'manual' && !wild.product && /wildcard/.test(wild.blocker || '') && !wild.cn); cer(`cheia privata -> manual, numita ca cheie privata, cu motivul (${cheie ? cheie.asset + ' | ' + (cheie.how || '').slice(0, 50) : 'lipsa'})`, cheie && cheie.method === 'manual' && /private-key/.test(cheie.asset) && /private key/.test(cheie.how || '')); try { fs.rmSync(T, { recursive: true, force: true }); } catch {} } console.log(`\nB1 planificator: ${total - rele}/${total} cum trebuia`); process.exitCode = rele ? 1 : 0;