// Contextul unui fisier scanat si uneltele comune ale detectorilor. import { curata, inSir, inceputuriDeRand, randul } from './lexer.mjs'; import { evalueaza } from './catalog.mjs'; export function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } export function facContext(text, limbaj, rel) { const { code, siruri } = curata(text, limbaj); const inceputuri = inceputuriDeRand(text); const ctx = { text, code, siruri, limbaj, rel, gasiri: [], inSir: (pos) => inSir(siruri, pos), poz: (pos) => randul(inceputuri, pos), // potriviri in cod: numele API-ului (inceputul potrivirii) nu are voie sa fie intr-un sir potriviri(re) { const r = []; const g = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g'); for (const m of code.matchAll(g)) if (!inSir(siruri, m.index)) r.push(m); return r; }, adauga(pos, api, fel, activ, extra = {}) { const ev = evalueaza(activ); const { line, column } = randul(inceputuri, pos); const bucata = extra.bucata !== undefined ? extra.bucata : ''; ctx.gasiri.push({ file: rel, line, column, language: limbaj, api, evidenceKind: fel, assetType: activ.assetType || (ev.protocolType ? 'protocol' : 'algorithm'), group: ev.grup, name: ev.nume || activ.nume || api, primitive: ev.primitiv || 'unknown', parameterSetIdentifier: ev.param !== undefined && ev.param !== null && ev.param !== '' ? String(ev.param) : undefined, curve: ev.curba || undefined, mode: ev.mod || undefined, padding: ev.padding || activ.padding || undefined, cryptoFunctions: activ.functii || undefined, classification: ev.classification, quantumVulnerable: ev.quantumVulnerable, reason: activ.nota && !ev.reason.includes(activ.nota) ? `${ev.reason} ${activ.nota}` : ev.reason, recommendation: ev.recommendation, nistQuantumSecurityLevel: ev.nistQuantumSecurityLevel, classicalSecurityLevel: ev.classicalSecurityLevel, oid: ev.oid, protocolType: ev.protocolType, protocolVersion: ev.protocolVersion, suppressedBy: extra.suprimaDe || undefined, resolvedFrom: extra.rezolvatDin || undefined, context: normalizeaza(bucata), material: activ.material || undefined, certificate: activ.certificat || undefined, }); }, }; return ctx; } function normalizeaza(s) { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > 120 ? t.slice(0, 117) + '...' : t; } // Argumentele unui apel: de la pozitia de dupa '(' pana la ')' echilibrata. // Intoarce textul argumentelor (din vederea de cod) si limitele lui. export function argumente(ctx, dupaParanteza) { const c = ctx.code; let adancime = 1; let j = dupaParanteza; while (j < c.length) { if (ctx.inSir(j)) { j++; continue; } const ch = c[j]; if (ch === '(' || ch === '[' || ch === '{') adancime++; else if (ch === ')' || ch === ']' || ch === '}') { adancime--; if (adancime === 0) break; } j++; if (j - dupaParanteza > 4000) break; } return { text: c.slice(dupaParanteza, j), start: dupaParanteza, end: j }; } // imparte argumentele pe virgulele de nivel zero export function imparteArgumente(ctx, start, end) { const c = ctx.code; const r = []; let adancime = 0; let s = start; for (let j = start; j < end; j++) { if (ctx.inSir(j)) continue; const ch = c[j]; if (ch === '(' || ch === '[' || ch === '{') adancime++; else if (ch === ')' || ch === ']' || ch === '}') adancime--; else if (ch === ',' && adancime === 0) { r.push({ text: c.slice(s, j), start: s }); s = j + 1; } } if (end > s) r.push({ text: c.slice(s, end), start: s }); return r.map((a) => { const lead = a.text.length - a.text.trimStart().length; return { text: a.text.trim(), start: a.start + lead }; }).filter((a) => a.text.length); } // un literal de sir simplu, fara interpolare; intoarce valoarea sau null export function literal(s) { const t = String(s).trim(); const m = /^(?:[rRbBuU]{0,2})(['"`])([^'"`\n$\\]*)\1$/.exec(t); return m ? m[2] : null; } // identificator simplu (nu proprietate, nu apel) export function identificator(s) { const t = String(s).trim(); return /^[A-Za-z_$][\w$]*$/.test(t) ? t : null; } // Rezolvarea conservatoare a unei variabile la un literal: EXACT o atribuire in fisier, // sub forma unei declaratii cu literal, si numele nu apare ca parametru de functie. // Orice alta forma: null (gasirea iese "unknown", nu se ghiceste). // Rezultatul depinde numai de fisier si de nume, deci se tine minte; si cel mult REZOLVARI_PE_FISIER nume distincte se rezolva // intr-un fisier (2026-09-29, revizuirea adversariala): fiecare rezolvare citeste tot fisierul, deci un fisier cu mii de apeluri // pe variabile costa patratic. Un nume peste plafon iese "unknown", si fisierul se numara in statistici (nu se sare in tacere). export const REZOLVARI_PE_FISIER = 64; export function rezolva(ctx, nume) { if (!identificator(nume)) return null; if (!ctx.rezolvari) ctx.rezolvari = new Map(); if (ctx.rezolvari.has(nume)) return ctx.rezolvari.get(nume); if (ctx.rezolvari.size >= REZOLVARI_PE_FISIER) { ctx.rezolvariPestePlafon = (ctx.rezolvariPestePlafon || 0) + 1; return null; } const r = rezolvaOData(ctx, nume); ctx.rezolvari.set(nume, r); return r; } function rezolvaOData(ctx, nume) { const c = ctx.code; const e = escapeRe(nume); const atribuiri = ctx.potriviri(new RegExp(`(?])`, 'g')); if (atribuiri.length !== 1) return null; const k = atribuiri[0].index; const ls0 = c.lastIndexOf('\n', k) + 1; const le0 = c.indexOf('\n', k) < 0 ? c.length : c.indexOf('\n', k); const linie = c.slice(ls0, le0); let parametru = false; if (ctx.limbaj === 'javascript') { // intervalele fara capat ([^(]*, [^:]*, ...) sunt marginite: pornite de la fiecare cuvant cheie, ar citi pana la capatul // fisierului pe un text fara paranteza (sau doua puncte) inchisa, deci un fisier facut anume ar costa patratic parametru = new RegExp(`(?:function\\b[^(]{0,200}\\(|\\()([^()]{0,2000}(?|\\{)`).test(c) || new RegExp(`\\bfor\\s*\\(\\s*(?:const|let|var)?\\s*${e}\\s+(?:of|in)\\b`).test(c) || new RegExp(`(?`).test(c); const m = new RegExp(`\\b(?:const|let|var)\\s+${e}\\s*=\\s*(['"\`])([^'"\`\\n$\\\\]*)\\1\\s*[;,]?\\s*$`).exec(linie); return !parametru && m ? { valoare: m[2], line: ctx.poz(k).line } : null; } if (ctx.limbaj === 'python') { parametru = new RegExp(`\\bdef\\s+\\w+\\s*\\([^)]{0,2000}(? 60 ? arg.text.slice(0, 57) + '...' : arg.text }; } export function necunoscut(expr, api) { return { grup: 'UNKNOWN', motiv: `Algorithm for ${api} is given by the expression "${expr}", which this tool does not resolve statically (it resolves only a variable with exactly one literal assignment in this file, for at most ${REZOLVARI_PE_FISIER} distinct variables per file).` }; } // Obiectul literal care contine pozitia (acolade echilibrate), pentru a citi parametri vecini. export function obiectulDin(ctx, pos) { const c = ctx.code; let adancime = 0; let s = pos; while (s > 0 && pos - s < 2000) { s--; if (ctx.inSir(s)) continue; if (c[s] === '}') adancime++; else if (c[s] === '{') { if (adancime === 0) break; adancime--; } } if (c[s] !== '{') return null; const a = argumente(ctx, s + 1); return { text: c.slice(s, a.end + 1), start: s, end: a.end + 1 }; } // Suprimarea: o gasire din import cade daca acelasi fisier are o gasire din apel care o acopera. // Pentru hash-uri si MAC se compara numele intreg, pentru cifruri si KDF familia, altfel grupul. function tokeni(g) { if (g.group === 'HASH') return [`HASH:${g.name}`]; if (g.group === 'MAC') return g.name.startsWith('HMAC-') ? [`MAC:${g.name}`, `HASH:${g.name.slice(5)}`] : [`MAC:${g.name}`]; if (g.group === 'CIPHER' || g.group === 'KDF') return [`${g.group}:${g.name.split('-')[0]}`]; return [g.group]; } export function aplicaSuprimarea(gasiri) { const acoperite = new Set(); for (const g of gasiri) if (g.evidenceKind !== 'import') { acoperite.add(g.group); for (const t of tokeni(g)) acoperite.add(t); } const r = gasiri.filter((g) => { if (g.evidenceKind !== 'import' || !g.suppressedBy) return true; const lista = g.suppressedBy === 'AUTO' ? tokeni(g) : g.suppressedBy; return !lista.some((x) => acoperite.has(x)); }); // dubluri: acelasi rand, acelasi nume, acelasi API const vazut = new Set(); return r.filter((g) => { const k = `${g.line}|${g.name}|${g.api}|${g.classification}`; if (vazut.has(k)) return false; vazut.add(k); return true; }).map((g) => { const o = { ...g }; delete o.suppressedBy; return o; }); } // numele unui cifru in stil OpenSSL (node:crypto): aes-256-gcm, des-ede3-cbc, bf-cbc, rc4, chacha20-poly1305 export function cifruOpenssl(s) { const t = String(s).toLowerCase().trim(); let m; if ((m = /^(?:id-)?aes-?(128|192|256)(?:-(\w+(?:-\w+)?))?$/.exec(t))) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: m[2] || 'cbc' }; if ((m = /^(camellia|aria|sm4)-?(128|192|256)?(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: m[1].toUpperCase(), param: m[2], mod: m[3] }; if (/^des-ede3(-\w+)?$|^des3$|^des-ede(-\w+)?$/.test(t)) return { grup: 'CIPHER', nume: '3DES', mod: (/-(cbc|ecb|cfb|ofb)$/.exec(t) || [])[1] || (t === 'des-ede3' || t === 'des-ede' ? 'ecb' : 'cbc') }; if ((m = /^des(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: 'DES', mod: m[1] || 'cbc' }; if (/^rc4(-\d+)?$|^rc4-hmac-md5$/.test(t)) return { grup: 'CIPHER', nume: 'RC4' }; if (/^rc2(-\w+)?$/.test(t)) return { grup: 'CIPHER', nume: 'RC2' }; if ((m = /^(?:bf|blowfish)(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: 'Blowfish', mod: m[1] }; if ((m = /^(cast5|cast|idea|seed)(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: m[1].toUpperCase(), mod: m[2] }; if (t === 'chacha20-poly1305') return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305' }; if (t === 'chacha20') return { grup: 'CIPHER', nume: 'ChaCha20' }; return null; }