import chalk from 'chalk'; import boxen from 'boxen'; import type { Report, Detection, AnatomiaFrontend, InfraResumo } from '../core/types.js'; const LARGURA = 76; export function renderTerminal(rep: Report): string { const partes: string[] = []; partes.push(cabecalho(rep)); if (rep.redirecionamentos.length) partes.push(secaoRedirecionamentos(rep)); partes.push(secaoIdentidade(rep.deteccoes)); partes.push(secaoArquitetura(rep.arquiteturaProvavel)); partes.push(secaoInfraestrutura(rep.infraestrutura)); if (rep.anatomia) partes.push(secaoAnatomia(rep.anatomia)); partes.push(secaoSegurancaPrivacidade(rep)); if (rep.lighthouse) partes.push(secaoLighthouse(rep.lighthouse)); partes.push(secaoNaoDeterminado(rep.naoDeterminado)); if (rep.avisos.length) partes.push(secaoAvisos(rep.avisos)); return partes.filter(Boolean).join('\n\n'); } function cabecalho(rep: Report): string { const linhas = [ chalk.bold('Alvo: ') + rep.alvo.urlSolicitada, rep.alvo.urlFinal !== rep.alvo.urlSolicitada ? chalk.dim('Final: ') + rep.alvo.urlFinal : undefined, chalk.dim('Modo: ') + (rep.alvo.modo === 'completo' ? 'completo (navegador)' : 'rápido (HTTP)'), ].filter(Boolean); return boxen(linhas.join('\n'), { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'red', borderStyle: 'round', title: chalk.bold.red('RED · anatomia de sites'), titleAlignment: 'center', }); } function secaoRedirecionamentos(rep: Report): string { const linhas = rep.redirecionamentos.map( (r) => ` ${chalk.dim(String(r.status))} ${r.de} ${chalk.dim('→')} ${r.para}`, ); return titulo('REDIRECIONAMENTOS') + '\n' + linhas.join('\n'); } const FAMILIAS: { nome: string; cats: string[] }[] = [ { nome: 'Stack principal', cats: [ 'CMS', 'Ecommerce', 'Web frameworks', 'JavaScript frameworks', 'Static site generator', 'Page builders', 'UI frameworks', 'Programming languages', 'Databases', ], }, { nome: 'Bibliotecas', cats: ['JavaScript libraries', 'Mobile frameworks', 'JavaScript graphics'] }, { nome: 'Analytics e marketing', cats: ['Analytics', 'Tag managers', 'Marketing automation', 'CRM', 'Customer data platform', 'SEO', 'Email', 'Webmail'], }, { nome: 'Publicidade', cats: ['Advertising', 'Retargeting'] }, { nome: 'Infraestrutura', cats: ['CDN', 'Web servers', 'Reverse proxies', 'PaaS', 'IaaS', 'Caching', 'Load balancers'], }, { nome: 'Segurança', cats: ['Security', 'SSL/TLS certificate authorities', 'Cookie compliance'] }, ]; function familiaDe(d: Detection): string { for (const f of FAMILIAS) { if (d.categorias.some((c) => f.cats.includes(c))) return f.nome; } return 'Widgets e outros'; } function secaoIdentidade(deteccoes: Detection[]): string { if (!deteccoes.length) { return ( titulo('IDENTIDADE TECNOLÓGICA') + '\n' + chalk.dim(' Nenhuma tecnologia reconhecida com confiança suficiente.') ); } const ordem = [...FAMILIAS.map((f) => f.nome), 'Widgets e outros']; const grupos = new Map(); for (const d of deteccoes) { const fam = familiaDe(d); (grupos.get(fam) ?? grupos.set(fam, []).get(fam)!).push(d); } const partes: string[] = []; for (const nome of ordem) { const itens = grupos.get(nome); if (!itens?.length) continue; const blocos = itens.map(blocoDeteccao).join('\n\n'); partes.push(chalk.bold.underline(nome) + '\n\n' + blocos); } return titulo('IDENTIDADE TECNOLÓGICA') + '\n\n' + partes.join('\n\n'); } function blocoDeteccao(d: Detection): string { const nome = chalk.bold(d.nome.toUpperCase()) + (d.versao ? chalk.dim(` v${d.versao}`) : ''); const cats = d.categorias.length ? chalk.dim(' ' + d.categorias.join(', ')) : ''; const conf = corConfianca(d.confianca)(`Confiança: ${d.confianca}% — ${d.faixa}`); const linhas = [`${nome}${cats}`, ` ${conf}`, '', chalk.dim(' Evidências encontradas:')]; const mostradas = d.evidencias.slice(0, 6); for (const ev of mostradas) { const trecho = ev.trecho ? chalk.dim(` (${ev.trecho})`) : ''; linhas.push(` ${chalk.green('✓')} ${ev.descricao}${trecho}`); } const extras = d.evidencias.length - mostradas.length; if (extras > 0) linhas.push(chalk.dim(` … e mais ${extras} evidência(s) do mesmo tipo`)); if (d.significado) { linhas.push('', chalk.dim(' O que isso significa:')); for (const l of quebrar(d.significado, LARGURA - 4)) linhas.push(' ' + l); } if (d.implica?.length) { linhas.push(chalk.dim(' Implica: ') + d.implica.join(', ')); } return linhas.join('\n'); } function secaoArquitetura(cadeia: string[]): string { return ( titulo('ARQUITETURA PROVÁVEL') + '\n ' + chalk.dim('(inferência didática, não confirmada)') + '\n ' + cadeia.join(chalk.red(' → ')) ); } function secaoInfraestrutura(infra: InfraResumo): string { const l: string[] = []; if (infra.cdn) l.push(item('CDN', infra.cdn)); if (infra.servidor) l.push(item('Servidor aparente', infra.servidor)); if (infra.protocolo) l.push(item('Protocolo', infra.protocolo)); if (infra.certificado) { const c = infra.certificado; l.push(item('Certificado', `${c.emitidoPara ?? '?'} — emitido por ${c.emitidoPor ?? '?'}`)); if (c.validoAte) l.push(item('Validade', `${c.validoDe ?? '?'} até ${c.validoAte}`)); if (c.protocoloTls) l.push(item('TLS', c.protocoloTls)); } if (infra.dns?.a?.length) l.push(item('DNS A', infra.dns.a.join(', '))); if (infra.dns?.cname?.length) l.push(item('CNAME', infra.dns.cname.join(', '))); if (infra.dns?.ns?.length) l.push(item('NS', infra.dns.ns.slice(0, 4).join(', '))); if (infra.emailNoDns && infra.dns?.mx?.length) { l.push(item('E-mail (MX)', infra.dns.mx.map((m) => m.servidor).slice(0, 3).join(', '))); } if (infra.dominiosExternos.length) { l.push(item('Domínios externos', infra.dominiosExternos.slice(0, 12).join(', '))); } return titulo('INFRAESTRUTURA OBSERVÁVEL') + '\n' + (l.join('\n') || chalk.dim(' —')); } function secaoAnatomia(a: AnatomiaFrontend): string { const js = [ `${a.javascript.arquivos} arquivo(s)`, a.javascript.bytesTransferidos ? `${formatarBytes(a.javascript.bytesTransferidos)} transferidos` : undefined, a.javascript.provavelChunking ? 'provável divisão em chunks' : undefined, a.javascript.assincrono ? 'carregamento assíncrono detectado' : undefined, ]; const css = [ `${a.css.arquivos} arquivo(s)`, a.css.inlineCritico ? 'estilos críticos embutidos no HTML' : undefined, ]; const img = [ `${a.imagens.total} no HTML`, a.imagens.formatos.length ? `formatos: ${a.imagens.formatos.join(', ')}` : undefined, a.imagens.lazyLoading ? 'lazy loading utilizado' : undefined, ]; const fontes = [ `${a.fontes.familias} família(s)`, a.fontes.locais ? 'hospedadas localmente' : undefined, a.fontes.externas ? 'também externas' : undefined, ]; return ( titulo('ANATOMIA DO FRONTEND') + '\n' + subbloco('JavaScript', js) + subbloco('CSS', css) + subbloco('Imagens', img) + subbloco('Fontes', fontes).replace(/\n$/, '') ); } function secaoSegurancaPrivacidade(rep: Report): string { const l: string[] = []; if (rep.seguranca) { const s = rep.seguranca; l.push(item('Cabeçalhos', `nota ${corNota(notaEmNumero(s.nota))(s.nota)} (${s.pontos}/${s.total})`)); if (s.presentes.length) l.push(' ' + s.presentes.map((p) => chalk.green('✓ ') + p).join(' ')); if (s.ausentes.length) l.push(' ' + s.ausentes.map((p) => chalk.red('✗ ') + p).join(' ')); } if (rep.rastreamento) { const r = rep.rastreamento; l.push( item( 'Rastreamento', `${r.rastreadores} rastreador(es) · ${r.dominiosTerceiros} domínio(s) de terceiros`, ), ); } if (rep.alertasLegado.length) { l.push(chalk.dim(' Legado:')); for (const a of rep.alertasLegado) l.push(` ${chalk.yellow('•')} ${a}`); } if (!l.length) return ''; return titulo('SEGURANÇA E PRIVACIDADE') + '\n' + l.join('\n'); } function notaEmNumero(nota: string): number { return { A: 95, B: 80, C: 65, D: 50, E: 35, F: 20 }[nota] ?? 20; } function secaoLighthouse(l: NonNullable): string { const linha = (rot: string, v?: number) => (v == null ? undefined : item(rot, corNota(v)(`${v}/100`))); const linhas = [ linha('Performance', l.performance), linha('Acessibilidade', l.acessibilidade), linha('SEO', l.seo), linha('Boas práticas', l.boasPraticas), ].filter((x): x is string => !!x); return titulo('QUALIDADE TÉCNICA (Lighthouse)') + '\n' + linhas.join('\n'); } function secaoNaoDeterminado(itens: string[]): string { if (!itens.length) return ''; const linhas = itens.map((i) => ` ${chalk.yellow('•')} ${i}`); return titulo('NÃO FOI POSSÍVEL CONFIRMAR') + '\n' + linhas.join('\n'); } function secaoAvisos(avisos: string[]): string { return chalk.dim(titulo('AVISOS') + '\n' + avisos.map((a) => ` - ${a}`).join('\n')); } function titulo(t: string): string { return chalk.bold.red(`▐ ${t}`); } function item(rotulo: string, valor: string): string { return ` ${chalk.dim(rotulo + ':')} ${valor}`; } function subbloco(nome: string, linhas: Array): string { const uteis = linhas.filter((l): l is string => !!l); if (!uteis.length) return ''; return ` ${chalk.bold(nome + ':')}\n` + uteis.map((l) => ` ${l}`).join('\n') + '\n'; } function corConfianca(score: number) { if (score >= 90) return chalk.green; if (score >= 65) return chalk.cyan; if (score >= 40) return chalk.yellow; return chalk.gray; } function corNota(v: number) { if (v >= 90) return chalk.green; if (v >= 50) return chalk.yellow; return chalk.red; } function quebrar(texto: string, largura: number): string[] { const palavras = texto.split(/\s+/); const linhas: string[] = []; let atual = ''; for (const p of palavras) { if ((atual + ' ' + p).trim().length > largura) { if (atual) linhas.push(atual.trim()); atual = p; } else { atual += ' ' + p; } } if (atual.trim()) linhas.push(atual.trim()); return linhas; } function formatarBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }