mvzeppelin/super-recon

GitHub: mvzeppelin/super-recon

面向渗透测试与安全审计的自动化侦察平台,将多款 Kali 工具编排为流水线并统一索引扫描结果。

Stars: 0 | Forks: 0

# super-recon 🇧🇷 Português | 🇺🇸 [English](README.en.md) Plataforma de reconhecimento (recon) automatizado: dispara ferramentas de segurança do Kali Linux contra domínios/IPs de um cliente, normaliza a saída de cada ferramenta e indexa tudo no OpenSearch, com um dashboard web para explorar os achados por cliente e por ferramenta. Tudo roda em Docker. O container do Kali é efêmero — sobe só para executar uma ferramenta e morre logo em seguida. ## Arquitetura ┌──────────────┐ │ frontend │ React + Nginx │ :3000 (host) │ proxy /api/ -> backend └──────┬────────┘ │ recon-net ┌──────▼────────┐ │ backend │ FastAPI │ :8000 (host) │ POST /scans, GET /clients/... └──────┬────────┘ │ enqueue ┌──────▼────────┐ │ redis │ fila (Celery broker/backend) └──────┬────────┘ │ ┌──────▼────────┐ │ worker │ Celery — concorrência = RECON_CPUS │ (docker.sock) │ └──────┬────────┘ │ docker run --rm --cpuset-cpus=N (efêmero) ┌──────▼────────┐ │ kali-tools │ assetfinder, subfinder, sublist3r, amass, │ (sobe e morre) │ dnsenum, dnsrecon, dnsx, httpx, waybackurls, └──────┬────────┘ gau, rdap, masscan, nmap, gobuster, nikto, │ parse + index nuclei, katana, theHarvester ┌──────▼────────┐ │ opensearch │ :9200 (host, só 127.0.0.1) └───────────────┘ - **Rede**: todos os serviços da aplicação ficam na rede docker `recon-net`. As portas publicadas no host (`3000`, `8000`, `9200`) são todas bindadas em `127.0.0.1` — nunca em `0.0.0.0`. Só acessíveis do host local ou de outros containers da rede. - **Kali efêmero**: o `worker` cria containers "irmãos" da imagem `kali-tools:1.0` via `docker.sock` (docker-outside-of-docker) para cada execução de ferramenta, com `--cpuset-cpus` fixando o core usado. O container morre (`--rm`) assim que a ferramenta termina. - **Paralelismo**: a quantidade de CPUs usada é `RECON_CPUS` (`.env`); vazio = usa todos os cores do host. A concorrência do worker Celery é ajustada para esse valor no boot (`backend/entrypoint-worker.sh`). ## Pipeline de recon (grafo de dependências) Fase 1 (passivo, paralelo) Fase 2 (consolidação) Fase 3 Fase 4 (paralelo) domínio: assetfinder, subfinder, ─┐ sublist3r, amass, dnsenum, ├─> consolida subdomínios ─> httpx ┐ dnsrecon, rdap, wayback, gau, │ + portas do masscan dnsx ├─> gobuster + nikto + nuclei theHarvester │ nmap │ + katana (URL viva) IP: rdap (bloco), masscan, ──┘ rdap (bloco)┘ + masscan/nmap/rdap/ shodan, censys (se configurados) shodan, censys shodan/censys (IP de cada subdomínio) Várias ferramentas da Fase 1 fazem enumeração de subdomínio por fontes diferentes (passiva, brute-force de DNS, certificado, etc.) — a soma tende a ser positiva, cada uma encontra algo que as outras não encontram; todas gravam no mesmo índice `subdomains`, só o campo `tool` diferencia a origem. `dnsx` na Fase 3 resolve/valida a lista consolidada (índice `dns`), sem depender de brute-force. Para alvos de domínio, a Fase 3 também resolve o IP do domínio raiz e roda `rdap` nesse IP (índice `rdap-network`) — o RDAP devolve o bloco (CIDR) que contém o IP diretamente, sem precisar calcular a máscara. **`nmap`/`masscan`/`rdap_network` também rodam por subdomínio, não só no IP do domínio raiz.** Assim que o `dnsx` (Fase 3) resolve o IP de cada subdomínio, a Fase 4 pega o conjunto de IPs únicos (deduplicado — vários subdomínios atrás do mesmo host/CDN não geram scan repetido), exclui o IP do domínio raiz (já coberto na Fase 3) e dispara `rdap_network` + `masscan` em paralelo pra cada IP restante; o callback usa as portas que o `masscan` achou pra direcionar o `nmap` (em vez do top-1000 padrão), o mesmo padrão já usado pra alvo IP puro. `httpx` não roda de novo nesse IP — o hostname que resolve pra ele já foi testado na Fase 3. IPs privados/loopback/reservados (ex: um subdomínio tipo `localhost.exemplo.com` apontando pra `127.0.0.1`, visto na prática) são descartados antes de escanear — sem esse filtro, um subdomínio malicioso ou mal-configurado apontando pra dentro escanearia a própria infraestrutura em vez do alvo do cliente. Com `SHODAN_API_KEY`/`CENSYS_API_KEY` configuradas, Shodan e/ou Censys também são consultadas nesses mesmos três pontos (IP do domínio raiz, IP de cada subdomínio, alvo IP puro) — os dois podem ficar ligados ao mesmo tempo, são motores de varredura independentes com cobertura diferente. Ver "Dados do Shodan" e "Dados da Censys" abaixo. Como um IP de subdomínio pode estar numa infraestrutura totalmente diferente da raiz (PTR que não bate com o domínio do cliente, por exemplo), cada achado por IP (nmap, masscan, shodan, censys) tem um botão "ⓘ" ao lado do valor do IP na tabela — mostra de forma curta se aquele IP é o do domínio raiz, veio da resolução de um subdomínio específico (dnsx), ou foi informado direto como alvo do scan (`GET /clients/{client}/ip-provenance?ip=...&scan_id=...`). A Fase 4 roda em qualquer URL que o httpx conseguiu obter resposta (`alive`), não só nas que devolveram `200` — um 404 na raiz não quer dizer "morto", só que não tem index, e é exatamente esse tipo de host que o gobuster existe para investigar (acha caminho vivo que não está linkado em lugar nenhum). `httpx` roda com `-fr` (segue redirects) para que o `status_code` reflita o destino final, e o `gobuster` roda com `-r` (segue redirect) pelo mesmo motivo. O gobuster tem três perfis de wordlist selecionáveis por scan (campo `gobuster_wordlist` em `POST /scans`, ou o seletor no formulário do dashboard): `common` (dirb/common.txt, ~4.6k palavras, padrão — mais rápido), `big` (dirb/big.txt, ~20k palavras — mais completo, porém bem mais lento; o timeout do job sobe de 300s para 900s nesse perfil) e `custom` (wordlist enviada pelo próprio usuário — ver seção abaixo). ### Wordlists customizadas do gobuster Upload por cliente (`POST /clients/{client}/wordlists`, multipart/form-data, ou pelo formulário "Novo recon" no dashboard, ao escolher "Personalizada"). Como é um recurso de upload, é tratado com os seguintes cuidados: - **Nunca confia no nome do arquivo para gravar em disco** — o arquivo é salvo com um id opaco gerado no servidor (`uuid4`); o nome enviado só é usado, sanitizado (charset restrito, sem separador de path), para exibição. Isso fecha qualquer tentativa de path traversal via nome de arquivo. - **Tamanho e conteúdo validados antes de gravar**: o upload é lido em blocos (nunca materializa o arquivo inteiro em memória antes de checar o tamanho), rejeitando acima de `MAX_WORDLIST_BYTES` (padrão 5 MiB). O conteúdo precisa ser texto UTF-8 puro (rejeita byte nulo/caracteres de controle — sinal de binário), sem linhas acima de `MAX_WORDLIST_LINE_CHARS` (padrão 512) nem mais que `MAX_WORDLIST_LINES` (padrão 200.000) linhas válidas. nginx também rejeita corpos grandes antes de chegar no backend (`client_max_body_size`), como uma camada extra. - **Limite por cliente**: no máximo `MAX_WORDLISTS_PER_CLIENT` (padrão 5) wordlists simultâneas — evita acúmulo sem controle no disco. - **Isolamento entre clientes**: um scan só pode referenciar uma wordlist que pertença ao mesmo cliente (checado tanto na criação do scan quanto na hora de rodar o gobuster de fato) — um cliente não acessa o upload de outro. - **Limpeza automática**: excluir um cliente (ou "limpar dados") remove também os arquivos das wordlists customizadas em disco, não só o índice de metadados — sem isso, ficariam órfãos no volume indefinidamente. - No container efêmero do gobuster, o arquivo é montado **somente leitura**, só aquele arquivo específico (não o diretório inteiro de wordlists). Todos os limites são configuráveis via `.env` (`MAX_WORDLIST_BYTES`, `MAX_WORDLIST_LINES`, `MAX_WORDLIST_LINE_CHARS`, `MAX_WORDLISTS_PER_CLIENT`, `GOBUSTER_CUSTOM_TIMEOUT_SECONDS`) — ver `.env.example`. curl -X POST http://localhost:8000/clients/acme/wordlists -F "file=@minha-wordlist.txt" curl http://localhost:8000/clients/acme/wordlists curl -X DELETE http://localhost:8000/clients/acme/wordlists/ **Limitação conhecida**: o limite por cliente é checado com uma leitura seguida de escrita (não é atômico) — sob upload verdadeiramente concorrente (duas requisições simultâneas), é possível passar o limite por 1-2 unidades. Não é um problema para o uso normal (um operador ou time pequeno), só não é uma trava dura sob concorrência adversarial. ## Requisitos - Docker + Docker Compose v2 - Linux com `vm.max_map_count >= 262144` (exigido pelo OpenSearch — a maioria das distros já vem assim; se o `opensearch` não subir, rode `sudo sysctl -w vm.max_map_count=262144`) ## Subindo o stack cp .env.example .env # ajuste as senhas antes de ir para produção docker build -t kali-tools:1.0 -f kali/Dockerfile kali/ # kali-tools não é um "service" do compose (ver nota abaixo) docker compose up -d Acompanhe a subida: docker compose ps Ordem esperada: `redis` + `opensearch` ficam `healthy` → `opensearch-init` roda os templates de índice e sai (`exited 0`) → `backend` + `worker` ficam `healthy` → `frontend` fica `healthy`. Todos os cinco serviços têm healthcheck (`worker` via `celery inspect ping` — confirma conexão real com o broker, não só o processo vivo; `frontend` via `curl` na página servida pelo nginx). ## Uso ### Pelo dashboard Abra **http://localhost:3000** — formulário "novo recon" (nome do cliente + lista de domínios/IPs, um por linha, + perfil de wordlist do gobuster), depois navegue pelos clientes e pelas ferramentas. No topo, as bandeirinhas 🇧🇷/🇬🇧 trocam o idioma da interface (padrão: português). A troca é só de interface — rótulos, botões, mensagens; os dados descobertos pelas ferramentas (subdomínios, URLs, descrições de achados etc.) continuam exatamente como foram encontrados, em nenhum idioma específico. A escolha fica salva no navegador (`localStorage`), então persiste entre sessões. Implementado em `frontend/src/i18n/` — um dicionário (`translations.js`) mapeando cada texto em português para o inglês; textos sem entrada lá continuam em português mesmo com o inglês selecionado (uma rede de segurança, não o comportamento esperado — toda string de interface nova deve ganhar uma entrada ali). ### Pela API # Disparar um recon (gobuster_wordlist é opcional, default "common") curl -X POST http://localhost:8000/scans \ -H "Content-Type: application/json" \ -d '{"client": "acme", "targets": ["acme.com", "203.0.113.10"], "gobuster_wordlist": "big"}' # Mesma chamada, com API_KEY definida no .env (header X-API-Key obrigatório # em toda rota exceto /health — ver "Segurança" para quando isso é necessário) curl -X POST http://localhost:8000/scans \ -H "Content-Type: application/json" \ -H "X-API-Key: " \ -d '{"client": "acme", "targets": ["acme.com", "203.0.113.10"]}' # Acompanhar as execuções de um scan curl "http://localhost:8000/scans/?client=acme" # Listar clientes com dados curl http://localhost:8000/clients # Índices/ferramentas de um cliente (com contagem de docs) curl http://localhost:8000/clients/acme/indices # Achados de uma ferramenta, paginado e filtrável curl "http://localhost:8000/clients/acme/nuclei?severity=critical&page=1&size=25" # Limpar dados: apaga achados + histórico de execuções, mas o cliente # continua na lista (zerado, como se fosse recém-criado) curl -X POST http://localhost:8000/clients/acme/clear # Excluir cliente: some da lista de clientes curl -X DELETE http://localhost:8000/clients/acme "Limpar dados" (botão ↺ no dashboard do cliente) e "excluir cliente" (botão ⚠) resolvem propósitos diferentes: o primeiro reseta o cliente para um estado zerado sem remover seu nome da lista — útil para descartar um recon antigo e começar de novo no mesmo cliente; o segundo remove o cliente por completo. Nenhum dos dois cancela scans em andamento na fila — só afeta o que já foi indexado (para cancelar execuções, use o botão "■ cancelar scans em andamento" antes). `GET /clients/{client}/{suffix}` aceita: `q` (busca livre), `page`, `size`, `sort` (ex: `-@timestamp`), e qualquer outro parâmetro vira filtro exato sobre o campo — ex: `?tool=assetfinder`, `?status_code=200`. `q` busca por "contém a string" em qualquer parte do valor (não só match exato) — buscar `xxx` encontra `xxx.acme.com`, não precisa digitar o valor inteiro. Caracteres especiais da sintaxe do query_string (`: / * ( ) ...`) são escapados automaticamente, então a busca trata o texto digitado como literal, não como uma expressão de busca. ### Excluindo achados específicos (falsos positivos) Cada linha da tabela de achados tem um checkbox de seleção; marcando uma ou mais, aparece uma barra "N selecionado(s)" com o botão "excluir selecionados". Útil para descartar um falso positivo pontual sem apagar o resto do índice (isso é diferente de "limpar dados", que zera o cliente inteiro). Não vale para as tabelas de metadados (`jobs`/`scans`) — um job em andamento precisa ser cancelado (botão "cancelar" na própria linha), não apagado direto, senão o container/task fica órfão sem registro para parar. curl -X POST http://localhost:8000/clients/acme/nuclei/delete \ -H "Content-Type: application/json" \ -d '{"ids": ["<_id do achado>", ""]}' ### Exportando dados Em JSON, CSV ou PDF, no nível do cliente (todos os índices) ou de um único índice/ferramenta. Também disponível como botões no dashboard. # Todos os achados do cliente curl "http://localhost:8000/clients/acme/export?format=json" -o acme.json curl "http://localhost:8000/clients/acme/export?format=csv" -o acme.zip # um CSV por índice, dentro de um .zip curl "http://localhost:8000/clients/acme/export?format=pdf" -o acme.pdf # Só um índice/ferramenta curl "http://localhost:8000/clients/acme/nuclei/export?format=csv" -o acme-nuclei.csv # Exportação por índice aceita os mesmos filtros da tela (q + qualquer # outro campo do índice vira filtro exato) curl "http://localhost:8000/clients/acme/nuclei/export?format=pdf&severity=critical" -o acme-nuclei-critical.pdf O PDF tem um teto de 500 linhas por seção (índices grandes como `katana` passam de 10 mil documentos — não cabe num relatório legível); use JSON/CSV para o dado completo. As colunas do PDF são curadas por ferramenta (ex: nuclei prioriza `severity`/`template_id`/`cve`) — um índice novo sem curadoria cai num fallback automático. A exportação por índice/ferramenta respeita os filtros aplicados na tela (busca livre, ferramenta de origem, severidade, status etc.) — os botões de exportar no frontend já levam em conta o que está filtrado no momento. O PDF filtrado registra "Filtros aplicados: ..." no cabeçalho do relatório, para não ficar ambíguo se aquele PDF é um recorte ou o total. A exportação em nível de cliente (todos os índices) não aceita filtros, já que cada índice tem seu próprio schema de campos. ### Exportando valores únicos (sem duplicidade) Múltiplas ferramentas achando o mesmo subdomínio (a "soma positiva" do recon) ou o mesmo scan rodado de novo em outro dia geram repetições esperadas nos índices — mas às vezes você só quer a lista enxuta (ex: todos os subdomínios distintos, sem duplicar por ferramenta/execução). O checkbox "exportar únicos" (ou `?unique=true` na API) resolve isso do lado do servidor, sem precisar deduplicar no Excel depois: curl "http://localhost:8000/clients/acme/subdomains/export?format=csv&unique=true" -o acme-subdominios.csv Dois achados são considerados "o mesmo" se todo o conteúdo bate, ignorando só os campos que naturalmente variam entre repetições (`tool`, `scan_id`, `@timestamp`). Ao agrupar, o achado mantido junta as ferramentas que confirmaram aquele dado no campo `tool` (ex: `["assetfinder", "subfinder"]`) e soma as ocorrências em `_dedup_count` — nenhuma informação de "quantas fontes concordam" é descartada, só a repetição de linhas. Combina com os demais filtros (`q`, `severity`, `scan_id` etc.) e funciona nos três formatos (JSON/CSV/PDF); só não se aplica à exportação em nível de cliente (mesma razão dos filtros: cada índice tem seu próprio schema). ## Índices no OpenSearch Um índice por ferramenta/tipo de achado, nomeado `{cliente}-{sufixo}`: | Sufixo | Ferramentas | Principais campos | |---|---|---| | `subdomains` | assetfinder, subfinder, sublist3r, amass, dnsenum, dnsrecon | `subdomain`, `domain`, `sources[]` | | `httpx` | httprobe, httpx | `url`, `status_code`, `alive` | | `dns` | dnsx | `subdomain`, `ips[]`, `resolved` | | `wayback` | waybackurls, gau | `url`, `path`, `has_params` | | `katana` | katana | `url`, `domain` | | `harvester` | theHarvester | `type` (email/host/ip/asn/url), `value` | | `rdap-domain` | rdap | `domain`, `nameservers[]`, `registrant`, `events[]` | | `rdap-network` | rdap | `handle`, `start_address`, `end_address`, `cidr`, `org` | | `masscan` | masscan | `ip`, `port`, `proto`, `state` | | `nmap` | nmap | `ip`, `port`, `service`, `product`, `version`, `cpe[]` | | `shodan` | shodan | `ip`, `port`, `product`, `version`, `cpe[]`, `org`, `isp`, `vulns[]`, `hostnames[]` | | `censys` | censys | `ip`, `port`, `protocol`, `software[]`, `asn`, `org`, `labels[]` | | `nikto` | nikto | `host`, `uri`, `description`, `references` | | `nuclei` | nuclei | `template_id`, `severity`, `matched_at`, `tags[]`, `cve` | | `gobuster` | gobuster | `url`, `path`, `status_code`, `size` | | `jobs` | (metadados) | `tool`, `target`, `status`, `scan_id`, `doc_count`, `error` | | `scans` | (metadados) | `scan_id`, `targets[]`, `gobuster_wordlist`, `@timestamp` | | `wordlists` | (metadados) | `wordlist_id`, `filename`, `line_count`, `size_bytes`, `@timestamp` | Templates definidos em `opensearch/templates/`, aplicados pelo container `opensearch-init`. ### Identificando de qual scan veio um achado Todo documento indexado (achado ou job) já carrega `scan_id` e `@timestamp` desde sempre — o que faltava era uma forma amigável de saber *qual* scan_id corresponde a qual execução, já que ele é só um hex opaco. `POST /scans` agora também grava um registro em `{cliente}-scans` (alvos originais + data/hora), consultável via: curl http://localhost:8000/clients/acme/scans No dashboard, isso alimenta um seletor "scan" nos filtros de achados e de execuções (aparece só quando o cliente tem mais de um scan registrado), mostrando "dd/mm/aaaa hh:mm — alvos" em vez do hex. Escanear o mesmo alvo de novo em outro dia não mistura os achados de forma indistinguível: o filtro "scan" (e a exportação, que respeita esse mesmo filtro) deixam claro de qual execução cada achado veio. O PDF exportado com esse filtro mostra "Filtros aplicados: scan=dd/mm/aaaa hh:mm — alvos" no cabeçalho pelo mesmo motivo. O painel do cliente também tem uma seção "Scans" própria, listando cada execução com data/hora, os alvos exatos informados naquele disparo e o perfil de wordlist usado — útil para responder diretamente "quais alvos entraram no scan de ontem vs. no de hoje" (ex: o escopo de um cliente cresceu e um IP foi adicionado numa segunda execução, mantendo o domínio da primeira). Cada linha tem um link "ver execuções" que abre a aba de execuções já filtrada por aquele `scan_id` específico. ### Comparando scans ("o que mudou desde a última vez") Cada linha da seção "Scans" (exceto a mais antiga) tem um link "ver mudanças desde a anterior", que compara aquele scan com o imediatamente anterior. Também dá para marcar dois scans quaisquer (checkbox) e clicar em "comparar selecionados" — útil para comparar execuções não-consecutivas. Isso abre uma tela por índice/ferramenta mostrando: - **Novos** — achados que só existem no scan mais recente; - **Resolvidos** — achados que só existiam no mais antigo (ex: vulnerabilidade corrigida, subdomínio desativado); - **Inalterados** — achados presentes nos dois. Clique no bloco pra abrir/fechar a lista completa (fica recolhida por padrão — normalmente esse é o grupo menos interessante numa comparação, já que a ideia é focar no que mudou). curl "http://localhost:8000/clients/acme/nuclei/compare?from_scan=&to_scan=" Dois achados são considerados "o mesmo" pela mesma regra usada em "exportar únicos": todo o conteúdo bate, ignorando só `tool`/`scan_id`/`@timestamp`/ `client` (os campos que naturalmente variam entre execuções). Não se aplica a `jobs`/`scans` (metadados, não achados) — mesma restrição do "excluir achados específicos". ### Excluindo um scan específico Selecionar exatamente um scan (checkbox) na seção "Scans" faz aparecer o botão "excluir scan selecionado". Diferente de "limpar dados" (que zera o cliente inteiro), isso apaga só aquela execução: o registro do scan e todos os achados/jobs com aquele `scan_id`, em todos os índices do cliente — os achados dos demais scans não são afetados. curl -X DELETE "http://localhost:8000/clients/acme/scans/" Não dá pra saber de antemão quais índices um scan tocou (depende de quais fases/ferramentas rodaram para aquele alvo), então a exclusão varre todos os índices existentes do cliente filtrando por `scan_id` — a resposta traz `deleted_by_suffix` com a contagem removida de cada um. ## Estrutura do projeto super-recon/ ├── docker-compose.yml ├── .env.example ├── kali/ # Dockerfile da imagem kali-tools:1.0 ├── opensearch/ # templates de índice + policies de ILM (ISM) + script de init │ ├── backup.sh # tira snapshot (backup) do OpenSearch │ └── restore.sh # restaura um snapshot ├── backend/ │ ├── app/ # FastAPI + Celery (orquestrador) │ ├── parsers/ # json/xml/txt -> documento normalizado, por ferramenta │ └── tests/ # pytest, amostras de saída de cada ferramenta embutidas nos próprios testes ├── frontend/ # dashboard React + Nginx ├── data/exchange/ # diretório de troca worker <-> containers kali (nikto) ├── data/wordlists/ # uploads de wordlist customizada (gobuster) └── data/opensearch-snapshots/ # arquivos dos backups (ver "Backup do OpenSearch") ## Variáveis de ambiente (`.env`) | Variável | Descrição | |---|---| | `RECON_CPUS` | Nº de CPUs para paralelismo do worker. Vazio = todos os cores do host. | | `REDIS_PASSWORD` | Senha do Redis (fila). | | `OPENSEARCH_ADMIN_USER` / `OPENSEARCH_ADMIN_PASSWORD` | Credenciais do OpenSearch. | | `OPENSEARCH_HOST_BIND` | IP de bind da porta 9200 no host (padrão `127.0.0.1` — não altere para `0.0.0.0`; é o banco cru, com as credenciais acima). | | `BACKEND_HOST_BIND` / `FRONTEND_HOST_BIND` | IP de bind das portas 8000/3000 no host. `127.0.0.1` (padrão) = só local; `0.0.0.0` = acessível de qualquer IP que alcance a máquina (LAN ou internet, se tiver IP público). Só troque para `0.0.0.0` depois de definir `API_KEY`. | | `API_KEY` | Vazio (padrão) = API sem autenticação. Definida = toda rota (exceto `/health`) exige o valor em `X-API-Key` (header) ou `?api_key=` (query string, usado pelos links de exportação). Gere com `openssl rand -hex 32`. | | `ILM_SHORT_RETENTION_DAYS` / `ILM_LONG_RETENTION_DAYS` | Dias até um índice expirar automaticamente (ILM/ISM). Vazio (padrão) = nunca expira. Ver seção "Retenção de dados" abaixo. | | `NOTIFY_SEVERITIES` | Severidades que disparam notificação (padrão `critical`). Lista separada por vírgula, ex: `critical,high`. | | `SLACK_BOT_TOKEN` / `SLACK_CHANNEL` | Token de bot do Slack (`xoxb-...`, escopo `chat:write`) e ID do canal onde o bot foi adicionado. Vazios (padrão) = Slack desligado. | | `NOTIFY_WEBHOOK_URL` | URL que recebe um POST JSON a cada achado crítico. Vazio (padrão) = desligado. Pode ser usado junto com o Slack, não é um ou-outro. | | `PUBLIC_BASE_URL` | Opcional — URL pública do dashboard (ex: `http://minha-vps.example.com:3000`), só para montar um link clicável na notificação. | | `HEALTH_CHECK_INTERVAL_SECONDS` | Intervalo (segundos) do monitor de saúde da plataforma (padrão 60). `<= 0` desliga o monitor. Ver "Monitor de saúde da plataforma". | | `HEALTH_QUEUE_BACKLOG_THRESHOLD` | Nº de tarefas pendentes na fila do Celery acima do qual soa alarme de fila represada (padrão 50). | | `HEALTH_STUCK_JOB_MINUTES` | Minutos que um job pode ficar "running" antes de ser considerado travado (padrão 60). | | `SHODAN_API_KEY` | Vazio (padrão) = Shodan desligada. Definida = consulta a Shodan Host API pro IP do domínio raiz, de cada subdomínio e de alvo IP puro. Ver "Dados do Shodan". | | `CENSYS_API_KEY` | Vazio (padrão) = Censys desligada. Definida = consulta a Censys Platform API nos mesmos pontos que a Shodan. Ver "Dados da Censys". | ## Retenção de dados (ILM) Por padrão os dados ficam guardados indefinidamente. Se quiser expiração automática, defina `ILM_SHORT_RETENTION_DAYS`/`ILM_LONG_RETENTION_DAYS` no `.env` e rode `docker compose up opensearch-init` (reaplica a configuração num cluster já no ar, sem precisar derrubar o resto do stack): - **Retenção curta** (`ILM_SHORT_RETENTION_DAYS`) — `wayback`/`katana`: alto volume de documentos e baixo valor de longo prazo (URLs históricas/ crawling), os índices que mais pesam no cluster. - **Retenção longa** (`ILM_LONG_RETENTION_DAYS`) — todo o resto: achados de mais peso (`nuclei`, `subdomains`, `nmap` etc.) e os metadados de execução (`jobs`, `scans`). Pega qualquer índice que não bater com a retenção curta, então uma ferramenta nova adicionada no futuro já nasce coberta por essa política sem precisar mexer em nada. Implementado via ISM (Index State Management, plugin já incluso na imagem do OpenSearch — não é um recurso pago). Cada grupo é uma policy com uma única transição, `active -> delete` após `min_index_age` dias; `ism_template` no corpo da policy faz o attach automático em índices *novos* que baterem o pattern, e o `opensearch-init` aplica retroativamente aos *já existentes* via `_plugins/_ism/add`. Deixar a variável vazia pula a criação daquela policy inteiramente — nenhum índice desse grupo fica sob gestão do ISM. # Ver se um índice está sob alguma policy, e o estado atual dela curl -sk -u admin: "https://localhost:9200/_plugins/_ism/explain/acme-nuclei" ## Backup do OpenSearch Usa a [Snapshot API](https://opensearch.org/docs/latest/tuning-your-cluster/availability-and-recovery/snapshots/index/) nativa do OpenSearch — sem ferramenta externa. O `opensearch-init` já registra, a cada boot do stack, um repositório de snapshot chamado `recon-backups` (tipo `fs`, ou seja, arquivo em disco) apontando para `data/opensearch-snapshots/` no host; esse registro é só "deixar pronto pra usar", tirar o snapshot em si é sempre uma ação explícita (ver abaixo). # Tirar um backup (nome automático: backup-AAAAMMDD-HHMMSS) ./opensearch/backup.sh # Ou com nome escolhido ./opensearch/backup.sh antes-da-migracao # Listar os snapshots existentes ./opensearch/restore.sh # Restaurar um snapshot (pede confirmação y/N antes de restaurar) ./opensearch/restore.sh backup-20260705-165144 # Restaurar só alguns índices do snapshot (padrão de nome, entre aspas) ./opensearch/restore.sh backup-20260705-165144 "acme-*" Os dois scripts rodam do host (fora de container), usando as credenciais do `.env` — não precisam de nada além de `curl`. `backup.sh` inclui todos os índices de dados do projeto (achados, jobs, scans, wordlists) e exclui índices internos do OpenSearch/plugins (`.opendistro-*`, `security-auditlog-*`, `top_queries-*`). **Restauração é intencionalmente não-destrutiva**: o OpenSearch recusa restaurar por cima de um índice que já existe (erro `snapshot_restore_exception`), então `restore.sh` nunca apaga nada sozinho. Se o objetivo é realmente substituir um índice existente por uma versão antiga do snapshot, apague-o (ou feche-o) manualmente antes: curl -sk -u admin: -X DELETE "https://localhost:9200/acme-nuclei" **Isso não é um backup fora do host.** Os arquivos do snapshot ficam em `data/opensearch-snapshots/`, no mesmo disco dos dados originais (`opensearch-data`) — protege contra erro humano (índice apagado/corrompido sem querer), mas não contra falha de disco ou perda da máquina inteira. Para disaster recovery de verdade, copie essa pasta pra fora do host depois de cada backup (outro disco, outra máquina, S3, rsync para outro servidor etc.): rsync -av data/opensearch-snapshots/ usuario@outro-host:/backups/super-recon/ ## Notificação em achado crítico "Fecha o loop" sem precisar ficar olhando a tela: quando um achado recém- indexado tem severidade em `NOTIFY_SEVERITIES` (padrão `critical` — hoje só o `nuclei` grava esse campo), o worker manda uma notificação por Slack e/ou webhook genérico, o que estiver configurado no `.env`. Desligado por padrão (vazio nos dois canais). - **Uma mensagem por execução de ferramenta**, não uma por achado — uma ferramenta que encontra vários de uma vez não inunda o canal. A mensagem lista até 10 achados (template + host/URL) e resume o resto ("+ N"). - **Slack**: usa a API real (`chat.postMessage`), não um "incoming webhook" — precisa de um bot token (`xoxb-...`) com escopo `chat:write`, adicionado ao canal de destino (o ID do canal, não o nome — pegue em "Copiar link do canal" no próprio Slack). - **Webhook genérico**: POST de JSON (`{"text", "client", "tool", "target", "findings"}`) pra qualquer URL — compatível com um "Incoming Webhook" do Slack, ou um endpoint próprio. Pode ser usado ao mesmo tempo que o Slack. - **Nunca derruba o pipeline de recon**: uma falha de rede, token inválido ou canal errado só gera um log de erro — o job da ferramenta continua marcado como "ok" normalmente. # Disparar via Slack Web API diretamente (útil pra validar token/canal antes # de configurar no .env) curl -X POST https://slack.com/api/chat.postMessage \ -H "Authorization: Bearer $SLACK_BOT_TOKEN" -H "Content-Type: application/json" \ -d '{"channel":"'"$SLACK_CHANNEL"'","text":"teste"}' **Cuidado com o token**: é um segredo — fica só no `.env` (já ignorado pelo `.gitignore`), nunca em código ou log. Se o token vazar (ex: colado num chat, commitado por engano), revogue/gere um novo nas configurações do app Slack. ## Monitor de saúde da plataforma Além de achado crítico, o backend também monitora a própria saúde da plataforma — fila, worker, cluster — e reaproveita o mesmo canal de notificação (Slack e/ou webhook) configurado acima. Roda em background dentro do próprio processo do backend (uma thread, checagem a cada `HEALTH_CHECK_INTERVAL_SECONDS`, padrão 60s) — não precisa de container, serviço ou Celery Beat novo. Quatro checagens, cada uma isolada (uma falhar não impede as outras de rodar): - **Cluster OpenSearch** — status (`green`/`yellow`/`red`) só dos índices do projeto (`*,-.*,-security-auditlog-*,-top_queries-*`, o mesmo padrão de exclusão do backup). Índices internos do próprio OpenSearch/plugins ficam `yellow` pra sempre num cluster single-node (esperam réplica que nunca vai ser alocada) — sem excluí-los o monitor acusaria problema o tempo todo, mesmo com os dados do projeto 100% saudáveis. - **Worker Celery** — `control.inspect().ping()`, chamado do processo do *backend* (não do worker) — testa de fora se existe algum worker vivo e respondendo, não só "o container está de pé". - **Fila represada** — `LLEN` na fila do Celery no Redis; acima de `HEALTH_QUEUE_BACKLOG_THRESHOLD` (padrão 50) soa alarme de fila que não está sendo consumida. - **Jobs travados** — jobs com status `running` (em qualquer cliente) há mais de `HEALTH_STUCK_JOB_MINUTES` (padrão 60) — ex: o worker morreu no meio de uma execução sem atualizar o status. **Notifica só na transição de estado** (bom→ruim ou ruim→bom), nunca a cada checagem — um problema persistente (cluster amarelo por horas, por exemplo) geraria uma mensagem só, não uma por minuto. O resultado do último ciclo também aparece em `GET /health` (`platform_problems`), sem custo extra — não dispara uma checagem nova, só lê o resultado já calculado. `HEALTH_CHECK_INTERVAL_SECONDS <= 0` desliga o monitor inteiramente (a thread nem chega a subir). ## Dados do Shodan Enriquecimento passivo por IP (org/ISP, portas/banners que a Shodan já tinha indexado, CVEs conhecidos) — sem gastar tempo de scan ativo, é só uma consulta HTTPS direta à Host API da Shodan (não sobe container Kali). Desligado por padrão; defina `SHODAN_API_KEY` (mesmo do plano free, em https://account.shodan.io/) para ativar. Roda nos mesmos três pontos onde `nmap`/`masscan`/`rdap_network` já rodam: IP do domínio raiz, IP de cada subdomínio (dedup, excluindo IPs privados/loopback — ver "Pipeline de recon"), e alvo IP puro. **No plano gratuito ("Membership"), nem todo IP com dado na Shodan é acessível** — testado na prática contra IPs reais, não só pela documentação: IP com achado normal -> HTTP 200, dado retornado normalmente IP sem dado na Shodan -> HTTP 404 "No information available for that IP." IP COM dado na Shodan, -> HTTP 403 "Requires membership or higher to access" mas fora do plano free O terceiro caso (a Shodan *tem* informação sobre aquele IP, mas o plano da API key não dá acesso) é registrado como job `status: error` com a mensagem da própria Shodan — de propósito, para não passar a impressão enganosa de "verificamos e não achamos nada" quando na verdade "não conseguimos nem verificar". Não existe um padrão previsível de antemão sobre quais IPs caem em cada caso no plano free; um plano pago (Freelancer ou superior) dá acesso consistente ao Host Lookup para qualquer IP. O campo `vulns` (lista de CVEs que a Shodan já associou àquele host) vem em formatos diferentes dependendo da resposta (às vezes lista simples de CVE-ids, às vezes dict CVE→detalhes) — o parser normaliza os dois formatos pra uma lista simples de strings. ## Dados da Censys Mesma ideia do Shodan acima (enriquecimento passivo por IP, sem scan ativo, consulta HTTPS direta — nesse caso à Censys Platform API), mas outro motor de varredura: cobertura diferente, um acha o que o outro não acha, por isso os dois ficam bem juntos ligados ao mesmo tempo. Desligado por padrão; defina `CENSYS_API_KEY` (token de acesso em https://platform.censys.io/) para ativar. Roda nos mesmos três pontos que a Shodan: IP do domínio raiz, IP de cada subdomínio, e alvo IP puro. Diferenças relevantes em relação à Shodan, testadas na prática: - **Sempre responde 200** para IP válido, mesmo sem nenhum serviço encontrado (`"services": []`, inclusive pra faixas reservadas tipo TEST-NET) — não existe o "403 por causa do plano" visto na Shodan. Um IP sem serviço simplesmente não gera documento (mesmo comportamento do nmap sem portas abertas), sem virar erro. - **Não tem campo de CVE/vulnerabilidade** na resposta do host lookup — o valor aqui é outro: ASN/organização, WHOIS, e o software identificado por serviço (`vendor:product`, ex: `apache:http_server`), incluindo às vezes mais de uma identificação por serviço (ex: o servidor web e o framework por trás dele). - **Rate limit baixo em key de teste/trial**: 3 chamadas simultâneas já produziram `429 Too Many Requests` em parte delas durante os testes — tratado como job `status: error` normalmente (a Shodan/Censys do mesmo IP não dependem uma da outra, então uma falhar não afeta a outra). Um plano pago tem limites mais altos. ## Segurança / limitações conhecidas - O `worker` tem `/var/run/docker.sock` montado (necessário para criar os containers efêmeros do Kali) — equivale a acesso root no host. É um trade-off aceito para permitir orquestração via Docker; não exponha esse container além do ambiente local. - `rdap_domain` só funciona para o domínio registrável (ex: `nmap.org`), não para subdomínios arbitrários (ex: `scanme.nmap.org`) — limitação do protocolo RDAP em si, não da implementação. O job aparece como `status: error` no índice `jobs` quando isso acontece; as demais ferramentas do pipeline não são afetadas. - **Exposição além de localhost**: por padrão, todas as portas publicadas no host (`opensearch`, `backend`, `frontend`) usam bind explícito em `127.0.0.1`. `BACKEND_HOST_BIND`/`FRONTEND_HOST_BIND` (`.env`) tornam isso configurável — útil para acessar via LAN ou rodar numa VPS com IP público. **Antes de trocar para `0.0.0.0` (ou um IP não-loopback), defina `API_KEY`.** Sem isso, qualquer um que alcançar a porta pode disparar scans contra qualquer alvo usando sua infraestrutura, apagar dados de qualquer cliente e ler todos os achados — não existe usuário/senha, só essa chave compartilhada. `OPENSEARCH_HOST_BIND` é independente e deve continuar em `127.0.0.1` sempre (é o banco cru, com as credenciais admin do OpenSearch, não tem relação com `API_KEY`). - Com `API_KEY` definida, o frontend mostra uma tela pedindo a chave na primeira visita (fica salva no navegador via `localStorage` depois disso; um botão "sair" no cabeçalho limpa e pede de novo). É um gate de segredo compartilhado — não é um sistema de usuários/permissões por pessoa. - `amass` roda com `-r 1.1.1.1,8.8.8.8` (resolvers explícitos) — sem isso, a v4 trava indefinidamente fazendo qualificação de dezenas de resolvers públicos, algo observado de forma reprodutível em ambiente containerizado. - `dnsrecon` roda só com `-t std,brt` (padrão + brute-force), sem o módulo `bing`: em teste real, a busca no Bing devolveu subdomínios inventados (fragmentos de URL mal interpretados, ex: `3ascanme.nmap.org`) que passam despercebidos em domínios com DNS wildcard. Ferramentas de subdomínio por fontes diferentes (Findomain, DNSmap, Knock, Naabu, Photon) foram deliberadamente deixadas de fora por redundância com o que já está no pipeline. - Hydra (brute-force de credenciais) foi removido do projeto — destoava do resto do pipeline (que é só leitura/enumeração) e é uma ferramenta intrusiva por natureza (tenta login ativamente contra um serviço real, podendo travar contas por lockout de tentativas). Não existe mais nem como etapa manual/opt-in.
标签:AV绕过, Docker, FastAPI, GitHub, React, Syscalls, 安全测试, 安全防御评估, 实时处理, 密码管理, 插件系统, 搜索引擎查询, 攻击性安全, 自动化侦察, 请求拦截, 逆向工具