aere-quantum/crypto-inventory/lib/context.mjs

254 lines
12 KiB
JavaScript

// 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(`(?<![\\w$.])${e}\\s*(?:[-+*/%|&^]|\\?\\?|\\|\\||&&)?(?::=|=)(?![=>])`, '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}(?<![\\w$.])${e}(?![\\w$])[^()]{0,2000})\\)\\s*(?:=>|\\{)`).test(c)
|| new RegExp(`\\bfor\\s*\\(\\s*(?:const|let|var)?\\s*${e}\\s+(?:of|in)\\b`).test(c)
|| new RegExp(`(?<![\\w$.])${e}\\s*=>`).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}(?<![\\w.])${e}\\b`).test(c) || new RegExp(`\\blambda\\b[^:]{0,500}\\b${e}\\b`).test(c)
|| new RegExp(`\\bfor\\s+[^:\\n]{0,500}(?<![\\w.])${e}\\b[^:\\n]{0,500}\\bin\\b`).test(c) || new RegExp(`\\bas\\s+${e}\\b`).test(c);
const m = new RegExp(`^[ \\t]*${e}\\s*(?::\\s*str\\s*)?=\\s*(['"])([^'"\\n\\\\]*)\\1\\s*$`, 'm').exec(c.slice(c.lastIndexOf('\n', k) + 1));
return !parametru && m && m.index === 0 ? { valoare: m[2], line: ctx.poz(k).line } : null;
}
if (ctx.limbaj === 'java') {
parametru = new RegExp(`\\(([^()]{0,2000}\\bString\\s+${e}\\b[^()]{0,2000})\\)`).test(c);
const ls = c.lastIndexOf('\n', k) + 1;
const le = c.indexOf('\n', k) < 0 ? c.length : c.indexOf('\n', k);
const m = new RegExp(`\\bString\\s+${e}\\s*=\\s*"([^"\\n\\\\]*)"\\s*;`).exec(c.slice(ls, le));
return !parametru && m ? { valoare: m[1], line: ctx.poz(k).line } : null;
}
if (ctx.limbaj === 'go') {
parametru = new RegExp(`\\bfunc\\b[^{]{0,500}\\([^)]{0,2000}\\b${e}\\s+(?:\\w+\\s*,\\s*)*string\\b`).test(c) || new RegExp(`\\bfunc\\b[^{]{0,500}\\([^)]{0,2000}\\b${e}\\s+string\\b`).test(c);
const ls = c.lastIndexOf('\n', k) + 1;
const le = c.indexOf('\n', k) < 0 ? c.length : c.indexOf('\n', k);
const m = new RegExp(`^\\s*(?:(?:const|var)\\s+)?${e}\\s*(?:string\\s*)?(?::=|=)\\s*"([^"\\n\\\\]*)"\\s*$`).exec(c.slice(ls, le));
return !parametru && m ? { valoare: m[1], line: ctx.poz(k).line } : null;
}
return null;
}
// valoarea unui argument: literal, variabila rezolvata sau necunoscuta
export function valoareArgument(ctx, arg) {
if (!arg) return { fel: 'lipsa' };
const l = literal(arg.text);
if (l !== null) return { fel: 'literal', valoare: l };
const id = identificator(arg.text);
if (id) {
const r = rezolva(ctx, id);
if (r) return { fel: 'rezolvat', valoare: r.valoare, din: `${id} (line ${r.line})` };
}
return { fel: 'necunoscut', expresie: arg.text.length > 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;
}