243 lines
12 KiB
JavaScript
243 lines
12 KiB
JavaScript
// 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;
|
|
}
|