import type { Detection, Evidence, Observations, FonteEvidencia } from '../core/types.js'; import { carregarDataset, type Dataset, type Fingerprint } from './dataset.js'; import { faixaDe, acumular } from './confidence.js'; interface Padrao { regex: RegExp; confianca: number; versaoTemplate?: string; } interface Acumulador { categorias: number[]; confianca: number; versao?: string; evidencias: Evidence[]; implica: string[]; requer: string[]; exclui: string[]; } export function detectar(obs: Observations): Detection[] { const ds = carregarDataset(); const acc = new Map(); for (const [nome, fp] of Object.entries(ds.tecnologias)) { avaliarTecnologia(nome, fp, obs, acc); } aplicarRequires(acc); aplicarExcludes(acc); aplicarImplies(acc, ds); return montarDeteccoes(acc, ds); } function avaliarTecnologia( nome: string, fp: Fingerprint, obs: Observations, acc: Map, ): void { const evidencias: Evidence[] = []; let confianca = 0; let versao: string | undefined; const registrar = (ev: Evidence, pad: Padrao, m: RegExpMatchArray | null) => { evidencias.push(ev); confianca = acumular(confianca, pad.confianca); if (!versao && pad.versaoTemplate && m) { const v = aplicarVersao(pad.versaoTemplate, m); if (v) versao = v; } }; for (const [chave, cru] of Object.entries(fp.headers ?? {})) { const valor = obs.headers[chave.toLowerCase()]; if (valor == null) continue; casar(cru, valor, (pad, m) => registrar({ fonte: 'header', descricao: `cabeçalho ${chave}`, trecho: cortar(valor) }, pad, m), ); } for (const cru of Object.keys(fp.cookies ?? {})) { for (const cookie of obs.cookies) { casar(cru || cookie.nome, cookie.nome, (pad, m) => registrar({ fonte: 'cookie', descricao: `cookie ${cookie.nome}` }, pad, m), ); } } for (const [chave, cru] of Object.entries(fp.meta ?? {})) { const valor = obs.meta[chave.toLowerCase()]; if (valor == null) continue; for (const p of aArray(cru)) { casar(p, valor, (pad, m) => registrar({ fonte: 'meta', descricao: `meta ${chave}: "${cortar(valor, 60)}"` }, pad, m), ); } } for (const campo of ['html', 'text'] as const) { for (const p of aArray(fp[campo])) { casar(p, obs.html, (pad, m) => registrar( { fonte: 'html', descricao: 'referência no HTML', trecho: m ? cortar(m[0]) : undefined }, pad, m, ), ); } } for (const p of aArray(fp.scriptSrc)) { for (const src of obs.scriptSrc) { casar(p, src, (pad, m) => registrar({ fonte: 'scriptSrc', descricao: `script: ${cortar(src, 70)}` }, pad, m), ); } } for (const p of aArray(fp.url)) { casar(p, obs.urlFinal || obs.urlSolicitada, (pad, m) => registrar( { fonte: 'url', descricao: 'caminho na URL', trecho: m ? cortar(m[0]) : undefined }, pad, m, ), ); } for (const [chave, cru] of Object.entries(fp.js ?? {})) { if (!obs.globaisJs.includes(chave)) continue; evidencias.push({ fonte: 'js', descricao: `variável global window.${chave}` }); confianca = acumular(confianca, confiancaDe(cru)); } for (const [tipo, cru] of Object.entries(fp.dns ?? {})) { const registros = registrosDns(obs, tipo); for (const reg of registros) { for (const p of aArray(cru)) { casar(p, reg, (pad, m) => registrar({ fonte: 'dns', descricao: `registro DNS ${tipo}: ${cortar(reg, 50)}` }, pad, m), ); } } } if (fp.certIssuer && obs.certificado?.emitidoPor) { casar(fp.certIssuer, obs.certificado.emitidoPor, (pad, m) => registrar({ fonte: 'certificado', descricao: 'emissor do certificado TLS' }, pad, m), ); } if (evidencias.length === 0) return; acc.set(nome, { categorias: fp.cats ?? [], confianca, versao, evidencias: dedupeEvid(evidencias), implica: aArray(fp.implies), requer: aArray(fp.requires), exclui: aArray(fp.excludes), }); } function confiancaDe(cru: string): number { const tag = cru.split(/\\;/).find((p) => p.startsWith('confidence:')); return tag ? Number(tag.slice(11)) || 100 : 100; } function dedupeEvid(evidencias: Evidence[]): Evidence[] { const vistos = new Set(); const saida: Evidence[] = []; for (const ev of evidencias) { const chave = `${ev.fonte}|${ev.descricao}`; if (!vistos.has(chave)) { vistos.add(chave); saida.push(ev); } } return saida; } function aplicarRequires(acc: Map): void { for (const [nome, a] of [...acc]) { if (a.requer.some((r) => !acc.has(r))) acc.delete(nome); } } function aplicarExcludes(acc: Map): void { for (const a of acc.values()) { for (const alvo of a.exclui) acc.delete(alvo); } } function aplicarImplies(acc: Map, ds: Dataset): void { for (let passo = 0; passo < 5; passo++) { let mudou = false; for (const [nome, a] of [...acc]) { for (const implicada of a.implica) { if (acc.has(implicada)) continue; const fp = ds.tecnologias[implicada]; if (!fp) continue; acc.set(implicada, { categorias: fp.cats ?? [], confianca: 60, evidencias: [{ fonte: 'heuristica', descricao: `implicado por ${nome}` }], implica: aArray(fp.implies), requer: [], exclui: aArray(fp.excludes), }); mudou = true; } } if (!mudou) break; } } function montarDeteccoes(acc: Map, ds: Dataset): Detection[] { const deteccoes: Detection[] = []; for (const [nome, a] of acc) { deteccoes.push({ nome, categorias: a.categorias .map((id) => ds.categorias[String(id)]?.name) .filter((n): n is string => !!n), versao: a.versao, confianca: a.confianca, faixa: faixaDe(a.confianca), evidencias: a.evidencias, implica: a.implica.length ? a.implica : undefined, }); } deteccoes.sort((x, y) => y.confianca - x.confianca || y.evidencias.length - x.evidencias.length); return deteccoes; } function casar( cru: string, valor: string, aoCasar: (pad: Padrao, m: RegExpMatchArray | null) => void, ): void { const pad = compilar(cru); if (!pad) return; const m = valor.match(pad.regex); if (m) aoCasar(pad, m); } function compilar(cru: string): Padrao | undefined { const partes = cru.split(/\\;/); const fonte = partes[0] ?? ''; let confianca = 100; let versaoTemplate: string | undefined; for (const parte of partes.slice(1)) { if (parte.startsWith('confidence:')) confianca = Number(parte.slice(11)) || 100; else if (parte.startsWith('version:')) versaoTemplate = parte.slice(8); } try { return { regex: new RegExp(fonte, 'i'), confianca, versaoTemplate }; } catch { return undefined; } } function aplicarVersao(template: string, m: RegExpMatchArray): string | undefined { const v = template.replace(/\\(\d)(?:\?([^:]*):([^)]*))?/g, (_t, g, seSim, seNao) => { const grupo = m[Number(g)]; if (seSim !== undefined) return grupo ? seSim : (seNao ?? ''); return grupo ?? ''; }); return v.trim() || undefined; } function registrosDns(obs: Observations, tipo: string): string[] { const d = obs.dns; if (!d) return []; switch (tipo.toUpperCase()) { case 'MX': return d.mx?.map((x) => x.servidor) ?? []; case 'CNAME': return d.cname ?? []; case 'TXT': return d.txt ?? []; case 'NS': return d.ns ?? []; default: return []; } } function aArray(v: T | T[] | undefined): T[] { if (v == null) return []; return Array.isArray(v) ? v : [v]; } function cortar(s: string, n = 80): string { return s.length > n ? s.slice(0, n) + '…' : s; } export const rotuloFonte: Record = { header: 'cabeçalho HTTP', html: 'HTML', meta: 'metadado', cookie: 'cookie', scriptSrc: 'script', js: 'variável JS', url: 'URL', dns: 'DNS', certificado: 'certificado', rede: 'requisição de rede', dom: 'DOM', heuristica: 'heurística', };