// Parcurgerea dosarului si orchestrarea detectorilor. Nimic nu se sare in tacere: fiecare // fisier vazut ajunge intr-o singura categorie numarata, si suma se verifica la sfarsit. import { readdirSync, statSync, readFileSync, lstatSync } from 'node:fs'; import { join, relative, sep, basename, resolve, extname } from 'node:path'; import { facContext, aplicaSuprimarea, REZOLVARI_PE_FISIER } from './context.mjs'; import { detecteazaJs } from './detect-js.mjs'; import { detecteazaPy } from './detect-py.mjs'; import { detecteazaJava } from './detect-java.mjs'; import { detecteazaGo } from './detect-go.mjs'; import { detecteazaPem } from './pem.mjs'; import { citesteManifest, NUME_MANIFEST } from './manifeste.mjs'; export const VERSIUNE = '0.2.0'; export const DOSARE_EXCLUSE_IMPLICIT = ['.git', '.hg', '.svn', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.gradle', '.idea', '.mypy_cache', '.pytest_cache']; const LIMBAJ = { '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript', '.ts': 'javascript', '.tsx': 'javascript', '.mts': 'javascript', '.cts': 'javascript', '.py': 'python', '.pyw': 'python', '.java': 'java', '.go': 'go', }; const DETECTOR = { javascript: detecteazaJs, python: detecteazaPy, java: detecteazaJava, go: detecteazaGo }; const EXEMPLE = 5; export function limbajul(cale) { return LIMBAJ[extname(cale).toLowerCase()] || null; } // binar = contine un octet NUL in primii 8192 octeti export function eBinar(buf) { const n = Math.min(buf.length, 8192); for (let i = 0; i < n; i++) if (buf[i] === 0) return true; return false; } export function scaneaza(radacina, optiuni = {}) { const o = { maxFileBytes: optiuni.maxFileBytes ?? 1048576, maxFiles: optiuni.maxFiles ?? 50000, excludeDirs: new Set([...(optiuni.noDefaultExcludes ? [] : DOSARE_EXCLUSE_IMPLICIT), ...(optiuni.excludeDirs || [])]), }; const root = resolve(radacina); const st = statSync(root); if (!st.isDirectory()) throw new Error(`not a directory: ${radacina}`); const inceput = new Date(); const stats = { filesSeen: 0, codeFiles: {}, textFilesPemOnly: 0, manifests: 0, manifestErrors: [], notRead: { binary: 0, tooLarge: 0, fileLimit: 0, unreadable: 0 }, notReadExamples: { binary: [], tooLarge: [], fileLimit: [], unreadable: [] }, symlinksNotFollowed: 0, resolutionLimitFiles: 0, resolutionLimitExamples: [], dirsExcluded: {}, dirsUnreadable: 0, }; const gasiri = []; const biblioteci = []; let cititeSauIncercate = 0; const noteaza = (motiv, rel, extra) => { stats.notRead[motiv]++; if (stats.notReadExamples[motiv].length < EXEMPLE) stats.notReadExamples[motiv].push(extra ? `${rel} (${extra})` : rel); }; const stiva = [root]; while (stiva.length) { const dir = stiva.pop(); let intrari; try { intrari = readdirSync(dir, { withFileTypes: true }); } catch { stats.dirsUnreadable++; continue; } intrari.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); const subdosare = []; for (const e of intrari) { const abs = join(dir, e.name); if (e.isSymbolicLink()) { stats.symlinksNotFollowed++; continue; } if (e.isDirectory()) { if (o.excludeDirs.has(e.name)) { stats.dirsExcluded[e.name] = (stats.dirsExcluded[e.name] || 0) + 1; continue; } subdosare.push(abs); continue; } if (!e.isFile()) continue; stats.filesSeen++; const rel = relative(root, abs).split(sep).join('/'); if (cititeSauIncercate >= o.maxFiles) { noteaza('fileLimit', rel); continue; } cititeSauIncercate++; let marime; try { marime = lstatSync(abs).size; } catch (err) { noteaza('unreadable', rel, err.code); continue; } if (marime > o.maxFileBytes) { noteaza('tooLarge', rel, `${marime} bytes`); continue; } let buf; try { buf = readFileSync(abs); } catch (err) { noteaza('unreadable', rel, err.code); continue; } if (eBinar(buf)) { noteaza('binary', rel); continue; } const text = buf.toString('utf8'); const limbaj = limbajul(abs); const ctx = facContext(text, limbaj || 'text', rel); if (limbaj) { stats.codeFiles[limbaj] = (stats.codeFiles[limbaj] || 0) + 1; DETECTOR[limbaj](ctx); if (ctx.rezolvariPestePlafon) { stats.resolutionLimitFiles++; if (stats.resolutionLimitExamples.length < EXEMPLE) stats.resolutionLimitExamples.push(rel); } } else { stats.textFilesPemOnly++; } detecteazaPem(text, ctx); gasiri.push(...aplicaSuprimarea(ctx.gasiri)); if (NUME_MANIFEST.test(basename(abs))) { stats.manifests++; const r = citesteManifest(text, basename(abs), rel); if (r.eroare) stats.manifestErrors.push(`${rel}: ${r.eroare}`); biblioteci.push(...r.biblioteci); } } for (let i = subdosare.length - 1; i >= 0; i--) stiva.push(subdosare[i]); } // garda: fiecare fisier vazut e intr-o singura categorie const cod = Object.values(stats.codeFiles).reduce((a, b) => a + b, 0); const nr = stats.notRead; const suma = cod + stats.textFilesPemOnly + nr.binary + nr.tooLarge + nr.fileLimit + nr.unreadable; if (suma !== stats.filesSeen) throw new Error(`internal accounting error: ${stats.filesSeen} files seen, ${suma} accounted for`); stats.codeFilesTotal = cod; gasiri.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || a.column - b.column)); biblioteci.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line)); return { tool: { name: 'aere-crypto-inventory', version: VERSIUNE }, root, rootName: basename(root), options: { maxFileBytes: o.maxFileBytes, maxFiles: o.maxFiles, excludeDirs: [...o.excludeDirs].sort(), maxResolvedVariablesPerFile: REZOLVARI_PE_FISIER }, startedAt: inceput.toISOString(), finishedAt: new Date().toISOString(), findings: gasiri, libraries: biblioteci, stats, }; }