import type { Detection, Observations, Report, InfraResumo, SegurancaResumo, Rastreamento, } from '../core/types.js'; import { detectar } from '../detection/engine.js'; import { heuristicas } from '../detection/heuristics.js'; import { carregarDataset } from '../detection/dataset.js'; import { TECNOLOGIAS, significadoGenerico } from '../knowledge/tecnologias.js'; import { inferirArquitetura } from '../knowledge/arquitetura.js'; export function montarRelatorio(obs: Observations): Report { const doMotor = detectar(obs); const doHeuristico = heuristicas(obs, doMotor); const deteccoes = enriquecer([...doMotor, ...doHeuristico]); return { alvo: { urlSolicitada: obs.urlSolicitada, urlFinal: obs.urlFinal || obs.urlSolicitada, modo: obs.modo, }, redirecionamentos: obs.redirecionamentos, deteccoes, infraestrutura: montarInfra(obs, deteccoes), anatomia: obs.anatomia, arquiteturaProvavel: inferirArquitetura(deteccoes, obs), seguranca: montarSeguranca(obs), rastreamento: montarRastreamento(deteccoes, obs), alertasLegado: montarAlertasLegado(obs, deteccoes), lighthouse: obs.lighthouse, naoDeterminado: montarNaoDeterminado(obs, deteccoes), avisos: obs.avisos, }; } function enriquecer(deteccoes: Detection[]): Detection[] { const ds = carregarDataset(); return deteccoes.map((d) => ({ ...d, significado: TECNOLOGIAS[d.nome]?.significado ?? ds.tecnologias[d.nome]?.description ?? significadoGenerico(d.categorias), })); } const CABECALHOS_SEGURANCA: [string, string][] = [ ['HSTS', 'strict-transport-security'], ['CSP', 'content-security-policy'], ['X-Frame-Options', 'x-frame-options'], ['X-Content-Type-Options', 'x-content-type-options'], ['Referrer-Policy', 'referrer-policy'], ['Permissions-Policy', 'permissions-policy'], ]; const NOTAS = ['F', 'F', 'E', 'D', 'C', 'B', 'A']; function montarSeguranca(obs: Observations): SegurancaResumo | undefined { if (!Object.keys(obs.headers).length) return undefined; const presentes: string[] = []; const ausentes: string[] = []; for (const [rotulo, header] of CABECALHOS_SEGURANCA) { if (obs.headers[header] != null) presentes.push(rotulo); else ausentes.push(rotulo); } const pontos = presentes.length; return { nota: NOTAS[pontos] ?? 'F', pontos, total: CABECALHOS_SEGURANCA.length, presentes, ausentes }; } const CATEGORIAS_RASTREIO = new Set([ 'Analytics', 'Tag managers', 'Advertising', 'Marketing automation', 'Customer data platform', 'Retargeting', ]); function montarRastreamento(deteccoes: Detection[], obs: Observations): Rastreamento { const rastreadores = deteccoes.filter((d) => d.categorias.some((c) => CATEGORIAS_RASTREIO.has(c)), ).length; return { rastreadores, dominiosTerceiros: dominiosExternos(obs).length }; } function montarAlertasLegado(obs: Observations, deteccoes: Detection[]): string[] { const alertas: string[] = []; const feno = obs.html + ' ' + obs.scriptSrc.join(' ') + ' ' + obs.linksAssets.join(' '); if (/[?&/=]UA-\d{4,}/.test(feno)) { alertas.push('Universal Analytics (UA-…): descontinuado pelo Google em 2023.'); } if (/jquery[.-]migrate/i.test(feno)) { alertas.push('jQuery Migrate: costuma indicar base de código legada.'); } if (/\.swf(\?|$|")/i.test(feno)) { alertas.push('Adobe Flash (.swf): tecnologia morta desde 2020.'); } if (deteccoes.some((d) => d.nome === 'AngularJS')) { alertas.push('AngularJS (Angular 1.x): sem suporte oficial desde 2022.'); } return alertas; } function montarInfra(obs: Observations, deteccoes: Detection[]): InfraResumo { const protocolo = obs.protocolo ? [ obs.protocolo.http2 ? 'HTTP/2' : 'HTTP/1.1', obs.protocolo.http3Anunciado ? 'HTTP/3 anunciado (Alt-Svc)' : undefined, ] .filter(Boolean) .join(' · ') : undefined; const cdn = deteccoes.find((d) => d.categorias.includes('CDN'))?.nome ?? obs.cdnAparente ?? undefined; return { cdn, servidor: obs.servidorAparente, protocolo, certificado: obs.certificado, dns: obs.dns, dominiosExternos: dominiosExternos(obs), emailNoDns: !!(obs.dns?.mx && obs.dns.mx.length > 0), }; } function dominiosExternos(obs: Observations): string[] { let hostAlvo = ''; try { hostAlvo = new URL(obs.urlFinal || obs.urlSolicitada).hostname.replace(/^www\./, ''); } catch { /* ignore */ } const hosts = new Set(); const fontes = [...obs.scriptSrc, ...obs.linksAssets, ...(obs.browser?.dominiosTerceiros ?? [])]; for (const ref of fontes) { try { const h = new URL(ref, obs.urlFinal || obs.urlSolicitada).hostname; const raiz = h.replace(/^www\./, ''); if (raiz && raiz !== hostAlvo && !raiz.endsWith('.' + hostAlvo)) hosts.add(raiz); } catch { /* relativo, mesmo host */ } } return [...hosts].sort().slice(0, 30); } function montarNaoDeterminado(obs: Observations, deteccoes: Detection[]): string[] { const itens: string[] = []; const temCategoria = (c: string) => deteccoes.some((d) => d.categorias.includes(c)); const tem = (n: string) => deteccoes.some((d) => d.nome === n); if (!temCategoria('Databases') && !tem('MySQL')) itens.push('banco de dados utilizado'); if (!temCategoria('Programming languages')) itens.push('linguagem do servidor'); if (obs.cdnAparente || temCategoria('CDN')) itens.push('servidor de origem (escondido atrás da CDN)'); const php = deteccoes.find((d) => d.nome === 'PHP'); if (php && !php.versao) itens.push('versão exata do PHP'); if (obs.protocolo && !obs.protocolo.http3Anunciado) { itens.push('uso real de HTTP/3 (não anunciado nos cabeçalhos)'); } if (obs.modo === 'rapido') { itens.push('conteúdo carregado por JavaScript (rode com --completo para observar)'); } return itens; }