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