// Proba remedierii B1 (remediere.mjs), pe servere TLS REALE pornite local si scanate de scanerul AERE (scaneazaAdresa din // readiness-service.mjs, aceeasi masuratoare ca /v1/pq/readiness, pe 127.0.0.1 si alt port). Fara retea externa. // 1. acoperirea: fiecare id pe care scanerul il poate emite (extras din SURSA lui) si pe care planificatorul il face actiune // manual/auto are reteta si judecator; controlul: un id nou, plantat, e prins ca neacoperit // 2. fragmentele generate poarta fiecare setare, pe fiecare profil (grupurile, versiunea, HSTS) // 3. grupurile scrise in fragmentele nginx/apache/haproxy/node sunt acceptate de OpenSSL (3.5); controlul: un nume stricat e refuzat // 4. cap la cap, profilul node: server CLASIC -> scanare -> plan -> reteta -> server cu OPTIUNILE RETETEI (luate din reteta, nu // rescrise) -> rescanare -> REZOLVAT pe hndl-exposed, tls12-accepted, hsts-missing; chain-untrusted (autosemnat) NEREZOLVAT // 5. controale negative: remediere falsa (numai HSTS) -> NEREZOLVAT pe schimbul de cheie si pe TLS 1.2; "PQ nepreferat" disparut // fiindca PQ a disparut de tot -> NEREZOLVAT, nu REZOLVAT; alta gazda, scanare de dinainte de aplicare, scanare cazuta -> NEMASURAT // 6. lantul judecatilor se re-verifica; o judecata schimbata e prinsa // node proba-remediere.mjs iesire 0 = toate cum trebuia import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import tls from 'node:tls'; import https from 'node:https'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { planeaza, CLASIFICARE_SCAN } from './plan-migrare.mjs'; import { reteta, verifica, verificaRemedierea, REMEDIERI, PROFILURI, defectDinRef } from './remediere.mjs'; import { pathToFileURL } from 'node:url'; import { caleaScanerului } from './control-plane.mjs'; // scanerul se incarca de unde e (readiness/ intr-o copie publica, langa planul de control in depozitul de dezvoltare) const { scaneazaAdresa } = await import(pathToFileURL(caleaScanerului()).href); const AICI = path.dirname(fileURLToPath(import.meta.url)); let bune = 0, rele = 0; const cer = (nume, ok) => { if (ok) { bune++; console.log(` OK ${nume}`); } else { rele++; console.log(` RAU ${nume}`); } }; // 1. acoperirea, derivata din sursa scanerului const SRC = fs.readFileSync(caleaScanerului(), 'utf8'); const idsScaner = [...new Set([...SRC.matchAll(/add\('([a-z0-9-]+)',\s*'([a-z]+)'/g)].map((m) => m[1]))]; function neacoperite(ids) { return ids.filter((id) => { const a = Object.hasOwn(CLASIFICARE_SCAN, id) ? CLASIFICARE_SCAN[id]({ id }, 'x.exemplu.com') : { method: 'manual' }; if (!a || a.method === 'blocked') return false; // informativ sau blocat onest: nu are ce remedia pe server return !Object.hasOwn(REMEDIERI, id); }); } cer(`controlul metodei: din sursa scanerului ies ${idsScaner.length} id-uri (cerut >= 8)`, idsScaner.length >= 8); cer(`acoperirea: fiecare defect remediabil al scanerului are reteta (neacoperite: ${neacoperite(idsScaner).join(',') || 'niciunul'})`, neacoperite(idsScaner).length === 0); cer('CONTROL: un id nou al scanerului, fara reteta, e prins', neacoperite([...idsScaner, 'tls-ceva-nou']).join() === 'tls-ceva-nou'); // 2. fragmentele poarta fiecare setare const planToate = planeaza({ scan: { domain: 'toate.exemplu.com', findings: ['hndl-exposed', 'tls12-accepted', 'hsts-missing', 'tls13-missing', 'rsa-short', 'chain-untrusted', 'cert-expiring'].map((id) => ({ id, severity: 'medium' })) } }); for (const p of PROFILURI) { const r = reteta(planToate, p); const d = r.domains[0]; const t = d.config.fragment.join('\n'); cer(`${p}: grupul hibrid primul, versiunea 1.3, HSTS in fragment; ${d.operational.length} pasi operationali (certificatul)`, /X25519MLKEM768[:,] ?(tls\.)?X25519/.test(t) && /1\.3|TLSv1\.3|TLS13/.test(t) && /Strict-Transport-Security/i.test(t) && d.operational.length === 3 && typeof r.measured === 'string'); } cer('actiunile de cod (inventar) raman fara reteta de server, cu motivul scris', reteta({ actions: [{ ref: 'algorithm:rsa', method: 'manual' }] }, 'nginx').none.length === 1); let aruncat = false; try { reteta(planToate, 'constructor'); } catch { aruncat = true; } cer('un profil necunoscut (si unul mostenit din prototip) e refuzat', aruncat); // 3. grupurile din fragmente, acceptate de OpenSSL const grupuriDin = (p) => { const t = reteta(planToate, p).domains[0].config.fragment.join('\n'); const m = t.match(/(X25519MLKEM768:[A-Za-z0-9:]+)/); return m && m[1]; }; for (const p of ['nginx', 'apache', 'haproxy', 'node']) { const g = grupuriDin(p); let ok = false; try { tls.createSecureContext({ ecdhCurve: g }); ok = true; } catch {} cer(`${p}: lista "${g}" acceptata de OpenSSL ${process.versions.openssl}`, !!g && ok); } let refuzat = false; try { tls.createSecureContext({ ecdhCurve: 'X25519MLKEM768:X25519BOGUS' }); } catch { refuzat = true; } cer('CONTROL: o lista cu un grup stricat e refuzata de OpenSSL (metoda poate iesi rosie)', refuzat); // 4-6. cap la cap pe servere reale const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-b1-remediere-')); const DOM = 'remediere.proba.invalid'; execFileSync('openssl', ['req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', '-nodes', '-days', '365', '-subj', `/CN=${DOM}`, '-addext', `subjectAltName=DNS:${DOM}`, '-keyout', path.join(T, 'k.pem'), '-out', path.join(T, 'c.pem')], { stdio: 'ignore' }); const cert = fs.readFileSync(path.join(T, 'c.pem')), key = fs.readFileSync(path.join(T, 'k.pem')); function server(opt, antete = {}) { return new Promise((res) => { const s = https.createServer({ cert, key, ...opt }, (q, r) => { for (const [k, v] of Object.entries(antete)) r.setHeader(k, v); r.end('ok'); }); s.listen(0, '127.0.0.1', () => res(s)); }); } const scaneaza = (s) => scaneazaAdresa(DOM, { address: '127.0.0.1', family: 4 }, { port: s.address().port, jurnal: false }); const inchide = (s) => new Promise((r) => s.close(() => r())); try { const clasic = await server({ ecdhCurve: 'X25519:P-256', minVersion: 'TLSv1.2' }); const inainte = await scaneaza(clasic); const ids = (inainte.findings || []).map((f) => f.id); cer(`serverul clasic, scanat: ${ids.join(',')}`, ids.includes('hndl-exposed') && ids.includes('tls12-accepted') && ids.includes('hsts-missing')); const plan = planeaza({ scan: { domain: DOM, findings: inainte.findings } }); const r = reteta(plan, 'node'); const d = r.domains.find((x) => x.domain === DOM); cer(`reteta node: optiunile ${JSON.stringify(d.config.options)}, antete ${Object.keys(d.config.headers).join(',')}`, d.config.options.ecdhCurve && d.config.options.minVersion === 'TLSv1.3' && d.config.headers['strict-transport-security']); await inchide(clasic); const aplicatLa = new Date().toISOString(); await new Promise((z) => setTimeout(z, 20)); const reparat = await server({ ecdhCurve: 'X25519:P-256', minVersion: 'TLSv1.2', ...d.config.options }, d.config.headers); const dupa = await scaneaza(reparat); await inchide(reparat); const v = verifica(plan, dupa, { appliedAt: aplicatLa }); const st = Object.fromEntries(v.results.map((x) => [x.defect, x.state])); cer(`dupa reteta: schimb de cheie ${st['hndl-exposed']}, TLS 1.2 ${st['tls12-accepted']}, HSTS ${st['hsts-missing']}`, st['hndl-exposed'] === 'RESOLVED' && st['tls12-accepted'] === 'RESOLVED' && st['hsts-missing'] === 'RESOLVED'); cer(`lantul autosemnat ramane ${st['chain-untrusted']} (operational, nu o setare): nu se pretinde rezolvat`, st['chain-untrusted'] === 'UNRESOLVED'); cer(`dupa reteta, scanerul vede PQ: ${dupa.summary.pqKeyExchange}`, dupa.summary.pqKeyExchange === 'X25519MLKEM768'); // 5. controale negative const fals = await server({ ecdhCurve: 'X25519:P-256', minVersion: 'TLSv1.2' }, d.config.headers); const dupaFals = await scaneaza(fals); await inchide(fals); const vf = Object.fromEntries(verifica(plan, dupaFals, { appliedAt: aplicatLa }).results.map((x) => [x.defect, x.state])); cer(`CONTROL: remediere falsa (numai HSTS): schimb de cheie ${vf['hndl-exposed']}, TLS 1.2 ${vf['tls12-accepted']}, HSTS ${vf['hsts-missing']}`, vf['hndl-exposed'] === 'UNRESOLVED' && vf['tls12-accepted'] === 'UNRESOLVED' && vf['hsts-missing'] === 'RESOLVED'); const planPref = { actions: [{ ref: CLASIFICARE_SCAN['pq-not-preferred']({}, DOM).ref, method: 'manual' }] }; const vp = verifica(planPref, dupaFals, { appliedAt: aplicatLa }).results[0]; cer(`CONTROL: "PQ nepreferat" absent fiindca PQ lipseste de tot -> ${vp.state} (${vp.reason})`, vp.state === 'UNRESOLVED'); cer('CONTROL: scanarea altei gazde -> NEMASURAT', verifica(plan, { ...dupa, domain: 'alta.exemplu.com' }, { appliedAt: aplicatLa }).results.every((x) => x.state === 'UNMEASURED')); cer('CONTROL: scanarea de DINAINTE de aplicare -> NEMASURAT', verifica(plan, inainte, { appliedAt: aplicatLa }).results.every((x) => x.state === 'UNMEASURED')); cer('CONTROL: scanarea cazuta -> NEMASURAT', verifica(plan, { domain: DOM, error: 'no_tls' }).results.every((x) => x.state === 'UNMEASURED')); cer('ref-urile planului se citesc inapoi in defect si domeniu (derivat din clasificator)', v.results.every((x) => defectDinRef(x.ref)?.domain === DOM)); // R2 (2026-09-29): fara margine de timp (nici --aplicat-la, nici generatedAt) nicio judecata nu iese REZOLVAT; cu generatedAt ca margine se judeca cer('R2: fara momentul aplicarii si fara generatedAt -> NEMASURAT, nu verde pe o scanare de oricand', verifica(plan, dupa).results.every((x) => x.state === 'UNMEASURED')); cer('R2: planul generat DUPA scanare -> NEMASURAT', verifica({ ...plan, generatedAt: new Date(Date.parse(dupa.measuredAt) + 60000).toISOString() }, dupa).results.every((x) => x.state === 'UNMEASURED')); const vg = Object.fromEntries(verifica({ ...plan, generatedAt: aplicatLa }, dupa).results.map((x) => [x.defect, x.state])); cer(`R2: cu generatedAt ca margine se judeca (schimb de cheie ${vg['hndl-exposed']})`, vg['hndl-exposed'] === 'RESOLVED'); // R1 (2026-09-29): un domeniu care nu e nume de gazda nu intra in nicio comanda generata const rau = 'x.exemplu.com; touch /tmp/aere-pwn'; const planRau = { actions: ['chain-untrusted', 'cert-expiring', 'hsts-missing'].map((id) => ({ ref: CLASIFICARE_SCAN[id]({ id }, rau).ref, method: 'manual' })) }; const rr = reteta(planRau, 'nginx'); const tot = JSON.stringify(rr.domains); cer(`R1: domeniul "${rau}" nu produce nicio configuratie sau comanda (${rr.none.length} actiuni refuzate)`, !tot.includes('touch') && rr.domains.length === 0 && rr.none.length === 3); const rb = reteta({ actions: [{ ref: CLASIFICARE_SCAN['chain-untrusted']({ id: 'chain-untrusted' }, DOM).ref, method: 'manual' }] }, 'nginx'); cer('R1 CONTROL: un nume de gazda valid produce comanda cu el', JSON.stringify(rb.domains).includes(`-servername ${DOM}`)); // 6. lantul cer('lantul judecatilor se re-verifica', verificaRemedierea(v.records).ok); const alterat = JSON.parse(JSON.stringify(v.records)); const i = alterat.findIndex((e) => e.record.state === 'UNRESOLVED'); alterat[i].record.state = 'RESOLVED'; cer('CONTROL: o judecata NEREZOLVAT rescrisa REZOLVAT e prinsa', verificaRemedierea(alterat).ok === false); } catch (e) { rele++; console.log(` RAU cap la cap a cazut: ${e.message}`); } finally { fs.rmSync(T, { recursive: true, force: true }); } console.log(`\n${bune} treceri, ${rele} esecuri`); process.exitCode = rele ? 1 : 0;