// Controlul negativ al suitei: strica, intr-o COPIE a modulului, cate o regula sau un detector si // cere ca proba tinta sa iasa ROSIE pe numele ei, ca suita sa fi rulat INTREAGA (aceleasi nume ca // linia de baza) si ca martorul neatins sa ramana VERDE. Altfel verdictul e STRICAT, nu "prins". // Un ciot care nu se poate pune (ancora lipsa sau dubla) e tot STRICAT: un control care nu a // plantat nimic nu a masurat nimic. // Folosire: node test/control-negativ.mjs (cod 0 doar daca toate mutatiile sunt PRINSE) import { mkdtempSync, cpSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; const AICI = dirname(fileURLToPath(import.meta.url)); const RADACINA = join(AICI, '..'); const SUITA = join(AICI, 'proba.mjs'); const B = 'negativ: fisierul binar e sarit si numarat'; const MUTATII = [ { nume: 'secp256k1 clasificat quantum-safe', fisier: 'lib/catalog.mjs', ancora: "return f({ ...baza, ...rez({ c: CLS.V, qv: true, motiv, rec, q: 0, cl, oid: '1.2.840.10045.2.1' }) });", cu: "return f({ ...baza, ...rez({ c: e256k1 ? CLS.S : CLS.V, qv: true, motiv, rec, q: 0, cl, oid: '1.2.840.10045.2.1' }) });", tinta: 'clasificare: secp256k1 este quantum-vulnerable, cu recomandare de cont post-cuantic', martor: B, }, { nume: 'comentariile // nu mai sunt ignorate (JS/Java/Go)', fisier: 'lib/lexer.mjs', ancora: "if (c === '/' && d === '/') {", cu: "if (false && c === '/' && d === '/') {", tinta: 'negativ: comentariile JS nu produc gasiri', martor: B, }, { nume: 'linia raportata cu unu mai mult', fisier: 'lib/lexer.mjs', ancora: 'return { line: lo + 1, column: pos - inceputuri[lo] + 1 };', cu: 'return { line: lo + 2, column: pos - inceputuri[lo] + 1 };', tinta: 'pozitiv: js/node-crypto.mjs', martor: 'negativ: fisier fara criptografie da zero gasiri, si a fost citit', }, { nume: 'variabila reasignata e "rezolvata" la prima valoare (ghicit)', fisier: 'lib/context.mjs', ancora: 'if (atribuiri.length !== 1) return null;', cu: 'if (atribuiri.length < 1) return null;', tinta: 'negativ: algoritm din variabila reasignata sau din parametru iese unknown, nu ghicit', martor: B, }, { nume: 'fisierele binare nu mai sunt sarite', fisier: 'lib/scan.mjs', ancora: "if (eBinar(buf)) { noteaza('binary', rel); continue; }", cu: "if (false && eBinar(buf)) { noteaza('binary', rel); continue; }", tinta: B, martor: 'negativ: comentariile JS nu produc gasiri', }, { nume: 'comentariile # nu mai sunt ignorate (Python)', fisier: 'lib/lexer.mjs', ancora: " if (c === '#') {", cu: " if (false && c === '#') {", tinta: 'negativ: comentariile si docstring-urile Python nu produc gasiri', martor: 'pozitiv: js/node-crypto.mjs', }, // 2026-09-29, revizuirea adversariala: formele liniare trebuie sa dea exact ce dadeau cele vechi { nume: 'cautarea PEM nu mai reia de la caracterul urmator dupa un antet fara sfarsit', fisier: 'lib/pem.mjs', ancora: '{ reBegin.lastIndex = m.index + 1; continue; }', cu: "{ if (process.env.AERE_PLANTA_NICIODATA === 'da') reBegin.lastIndex = m.index + 1; continue; }", tinta: 'limite: un bloc PEM care incepe in liniutele unui antet fara sfarsit e gasit', martor: B, }, { nume: 'esecul unui literal regex JS tinut minte fara starea "in clasa"', fisier: 'lib/lexer.mjs', ancora: 'if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1)) break;', cu: "if (esecRegex && (esecRegex[j] & (inClasa ? 2 : 1) || (process.env.AERE_PLANTA_NICIODATA !== 'da' && esecRegex[j]))) break;", tinta: 'limite: literalul regex JS se cauta din starea lui, nu din ce a esuat alt literal pe acelasi rand', martor: B, }, { nume: 'sirul neterminat Python nu mai muta inceputul randului', fisier: 'lib/lexer.mjs', ancora: 'if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 ? -1 : ultimaLinieNoua + 1;', cu: "if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 && process.env.AERE_PLANTA_NICIODATA === 'da' ? -1 : ultimaLinieNoua + 1;", tinta: 'limite: docstring Python pe randul lui (si dupa un sir neterminat), dar nu dupa cod pe acelasi rand', martor: B, }, { nume: 'plafonul de variabile pe fisier scos', fisier: 'lib/context.mjs', ancora: 'if (ctx.rezolvari.size >= REZOLVARI_PE_FISIER) {', cu: "if (ctx.rezolvari.size >= REZOLVARI_PE_FISIER && process.env.AERE_PLANTA_NICIODATA === 'da') {", tinta: 'limite: peste plafonul de variabile pe fisier numele ies unknown si fisierul se numara', martor: B, }, { nume: 'caracterele de control din rezumat nu mai sunt inlocuite', fisier: 'lib/rezumat.mjs', ancora: "return L.map((l) => l.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, '?')).join('\\n');", cu: "return L.map((l) => (process.env.AERE_PLANTA_NICIODATA === 'da' ? l.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, '?') : l)).join('\\n');", tinta: 'rezumat: caracterele de control din numele de fisiere nu ajung in terminal', martor: B, }, { nume: 'MD5 clasificat quantum-safe', fisier: 'lib/catalog.mjs', ancora: "MD5: { n: 'MD5', c: CLS.W, oid: '1.2.840.113549.2.5' },", cu: "MD5: { n: 'MD5', c: CLS.S, oid: '1.2.840.113549.2.5' },", tinta: 'clasificare: MD5 si SHA-1 sunt weak-now, SHA-256 quantum-safe', martor: 'negativ: fisier prea mare sarit si numarat', }, { nume: 'bom-ref fara amprenta (coliziuni)', fisier: 'lib/cbom.mjs', ancora: ":${createHash('sha256').update(k).digest('hex').slice(0, 12)}`;\n const cp", cu: '`;\n const cp', tinta: 'cbom: bom-ref unice si stabile intre doua rulari', martor: B, }, { nume: '--fail-on vulnerable ignorat', fisier: 'lib/rezumat.mjs', ancora: "if ((cer.has('vulnerable') || cer.has('any')) && vuln.length)", cu: "if (false && (cer.has('vulnerable') || cer.has('any')) && vuln.length)", tinta: 'cli: --fail-on vulnerable iese 1 pe cod vulnerabil si 0 pe cod fara criptografie', martor: 'cbom: structura CycloneDX 1.6 (campuri obligatorii, tipuri, enumerari)', }, { nume: 'apelurile din siruri numarate ca folosire', fisier: 'lib/context.mjs', ancora: 'for (const m of code.matchAll(g)) if (!inSir(siruri, m.index)) r.push(m);', cu: 'for (const m of code.matchAll(g)) r.push(m);', tinta: 'negativ: apelul scris intr-un sir JS nu e folosire', martor: B, }, { nume: 'fisierul prea mare sarit fara sa fie numarat', fisier: 'lib/scan.mjs', ancora: "if (marime > o.maxFileBytes) { noteaza('tooLarge', rel, `${marime} bytes`); continue; }", cu: 'if (marime > o.maxFileBytes) { continue; }', tinta: 'negativ: fisier prea mare sarit si numarat', martor: 'pozitiv: js/node-crypto.mjs', }, { nume: 'suprimarea importurilor acoperite de apeluri scoasa (numarare dubla)', fisier: 'lib/context.mjs', ancora: " if (g.evidenceKind !== 'import' || !g.suppressedBy) return true;", cu: ' return true;', tinta: 'pozitiv: go/crypto.go', martor: B, }, { nume: 'blocul PEM intreg (cu cheia privata) pus in contextul gasirii', fisier: 'lib/pem.mjs', ancora: '{ bucata: `-----BEGIN ${tip}-----` }', cu: '{ bucata: m[0] }', tinta: 'pozitiv: cheie privata generata la rulare e raportata, iar valoarea ei nu apare in iesire', martor: B, }, ]; function copiaza() { const d = mkdtempSync(join(tmpdir(), 'ci-control-')); cpSync(join(RADACINA, 'inventar.mjs'), join(d, 'inventar.mjs')); cpSync(join(RADACINA, 'lib'), join(d, 'lib'), { recursive: true }); return d; } function fisiereModul(d) { return ['inventar.mjs', ...readdirSync(join(d, 'lib')).map((f) => `lib/${f}`)].filter((f) => statSync(join(d, f)).isFile()).sort(); } function ruleaza(d) { const r = spawnSync(process.execPath, [SUITA, '--json'], { encoding: 'utf8', timeout: 300000, env: { ...process.env, CRYPTO_INVENTORY_MODULE: join(d, 'inventar.mjs') }, }); const ultima = (r.stdout || '').trim().split('\n').pop() || ''; try { const j = JSON.parse(ultima); return { ok: true, j, status: r.status }; } catch { return { ok: false, motiv: `suita nu a raportat JSON (status ${r.status}): ${(r.stderr || '').trim().split('\n').slice(0, 3).join(' | ')}` }; } } // linia de baza: o copie neatinsa, toata verde, si suita chiar a masurat COPIA const baza = copiaza(); let linieBaza; try { const r = ruleaza(baza); if (!r.ok) { console.log(`STRICAT linia de baza: ${r.motiv}`); process.exitCode = 3; } else if (r.j.modul !== join(baza, 'inventar.mjs')) { console.log(`STRICAT linia de baza: suita a masurat ${r.j.modul}, nu copia`); process.exitCode = 3; } else if (r.j.verzi !== r.j.rulate) { console.log(`STRICAT linia de baza: ${r.j.rosii} probe rosii pe copia neatinsa: ${Object.keys(r.j.erori).join('; ')}`); process.exitCode = 3; } else linieBaza = r.j; } finally { rmSync(baza, { recursive: true, force: true }); } if (linieBaza) { const numeBaza = Object.keys(linieBaza.rezultate).sort(); console.log(`linia de baza: ${linieBaza.rulate} probe, toate verzi, pe o copie neatinsa a modulului\n`); const verdicte = []; for (const mu of MUTATII) { const d = copiaza(); let verdict; let detaliu = ''; try { const cale = join(d, mu.fisier); const text = readFileSync(cale, 'utf8'); const n = text.split(mu.ancora).length - 1; if (!numeBaza.includes(mu.tinta) || !numeBaza.includes(mu.martor)) { verdict = 'STRICAT'; detaliu = 'tinta sau martorul nu exista in suita'; } else if (n !== 1) { verdict = 'STRICAT'; detaliu = `ciotul nu s-a putut pune: ancora apare de ${n} ori`; } else { writeFileSync(cale, text.replace(mu.ancora, mu.cu)); // cmp: exact un fisier difera de original, si acela e cel mutat const diferite = fisiereModul(d).filter((f) => readFileSync(join(d, f), 'utf8') !== readFileSync(join(RADACINA, f), 'utf8')); if (diferite.length !== 1 || diferite[0] !== mu.fisier) { verdict = 'STRICAT'; detaliu = `copia difera in: ${diferite.join(', ') || 'nimic'}`; } else { const r = ruleaza(d); if (!r.ok) { verdict = 'STRICAT'; detaliu = r.motiv; } else if (r.j.modul !== join(d, 'inventar.mjs')) { verdict = 'STRICAT'; detaliu = `suita a masurat ${r.j.modul}`; } else if (JSON.stringify(Object.keys(r.j.rezultate).sort()) !== JSON.stringify(numeBaza)) { verdict = 'STRICAT'; detaliu = `suita nu a rulat intreaga: ${r.j.rulate} din ${linieBaza.rulate}`; } else if (r.j.rezultate[mu.tinta] !== 'ROSU') { verdict = 'NEPRINS'; detaliu = 'proba tinta a ramas verde'; } else if (r.j.rezultate[mu.martor] !== 'VERDE') { verdict = 'STRICAT'; detaliu = `martorul a iesit rosu: ${r.j.erori[mu.martor]}`; } else { verdict = 'PRINS'; detaliu = `tinta rosie (${r.j.erori[mu.tinta].slice(0, 110)}); ${r.j.rosii} din ${r.j.rulate} rosii in total; martor verde`; } } } } finally { rmSync(d, { recursive: true, force: true }); } verdicte.push(verdict); console.log(`${verdict.padEnd(8)} ${mu.nume}\n tinta: ${mu.tinta}\n ${detaliu}`); } const prinse = verdicte.filter((v) => v === 'PRINS').length; console.log(`\n${prinse} din ${MUTATII.length} mutatii PRINSE; ${verdicte.filter((v) => v === 'NEPRINS').length} neprinse; ${verdicte.filter((v) => v === 'STRICAT').length} stricate`); process.exitCode = prinse === MUTATII.length ? 0 : 1; }