#!/usr/bin/env bash
# attack-surface.sh — o que um atacante encontra no seu servidor. SOMENTE LEITURA.
#
# Duas visões que a auditoria de configuração não da:
#   1. DE FORA, sem chave: que portas respondem pela internet e o que a
#      aplicação entrega para quem só sabe o endereco.
#   2. DE DENTRO, por SSH: o que estaria ao alcance de quem já entrou, e o que
#      foi publicado sem querer junto com o código.
#
# Não explora, não tenta senha, não derruba nada, não altera nada. Abre conexão
# e desliga; pede pagina e le o codigo de resposta. E seguro em produção.
#
# So aceita servidor registrado em .sshepherd/servers/. Apontar para máquina de
# terceiro e recusado: isto e ferramenta de defesa do que e seu.
#
# Uso:
#   attack-surface.sh --host <ip> --user <usuário> [--port 22] [--key <chave>]
#                     [--app-path /srv/app] [--url https://seu-dominio/]
#                     [--dir .sshepherd] [--skip-remote]

set -uo pipefail

HOST=""; USER_NAME=""; PORT=22; KEY_PATH=""
APP_PATH="/srv/app"; BASE_URL=""; DIR=".sshepherd"; SKIP_REMOTE=0; THREATS=""

while [ $# -gt 0 ]; do
  case "$1" in
    --host) HOST="$2"; shift 2 ;;
    --user) USER_NAME="$2"; shift 2 ;;
    --port) PORT="$2"; shift 2 ;;
    --key)  KEY_PATH="$2"; shift 2 ;;
    --app-path) APP_PATH="$2"; shift 2 ;;
    --url)  BASE_URL="$2"; shift 2 ;;
    --dir)  DIR="$2"; shift 2 ;;
    --threats) THREATS="$2"; shift 2 ;;
    --skip-remote) SKIP_REMOTE=1; shift ;;
    *) echo "argumento desconhecido: $1" >&2; exit 2 ;;
  esac
done

[ -n "$HOST" ] || { echo "ERRO: --host é obrigatório" >&2; exit 2; }

# --- trava de alvo ----------------------------------------------------------
# Sondar máquina que não e sua não e defesa, e varredura. O alvo precisa estar
# registrado como perfil neste projeto.
if ! ls "${DIR}"/servers/*.json >/dev/null 2>&1; then
  echo "BLOQUEADO: não achei nenhum perfil de servidor em ${DIR}/servers/." >&2
  echo "Esta ferramenta só examina servidor seu, registrado pelo Dia Zero." >&2
  echo "Rode /sshp-setup antes." >&2
  exit 14
fi
if ! grep -lE "\"host\"[[:space:]]*:[[:space:]]*\"${HOST}\"" "${DIR}"/servers/*.json >/dev/null 2>&1; then
  echo "BLOQUEADO: ${HOST} não é um servidor seu." >&2
  echo "" >&2
  echo "Só examino endereço registrado em ${DIR}/servers/. Sondar máquina de" >&2
  echo "terceiro sem autorização é ataque, não auditoria, e eu não faço isso." >&2
  echo "" >&2
  echo "Seus servidores registrados:" >&2
  grep -hoE "\"host\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" "${DIR}"/servers/*.json 2>/dev/null \
    | sed 's/.*"\([^"]*\)"$/  - \1/' >&2
  exit 14
fi

declare -a ACHADOS=()
achado() { ACHADOS+=("$1|$2|$3|$4"); }  # severidade|titulo|por_que|como_corrigir

# --- base de ameaças --------------------------------------------------------
# Lista viva do que procurar, mantida pelo agente com pesquisa na web.
# Vazia ou ausente não impede nada: a lista embutida abaixo continua valendo.
[ -n "$THREATS" ] || THREATS="${DIR}/threats.db"
EXTRA_CAMINHOS=""; EXTRA_PORTAS=""; BASE_IDADE=""
if [ -f "$THREATS" ]; then
  while IFS='|' read -r tipo valor sev oque motivo _fonte _data; do
    case "${tipo:-}" in
      caminho) EXTRA_CAMINHOS="${EXTRA_CAMINHOS}${valor}|${sev}|${oque}|${motivo}"$'\n' ;;
      porta)   EXTRA_PORTAS="${EXTRA_PORTAS}${valor}:${oque}:${sev}:${motivo}"$'\n' ;;
    esac
  done < <(grep -vE '^[[:space:]]*(#|$)' "$THREATS")

  # date -d e do GNU. Onde não existir, a idade fica em branco em vez de mentir.
  ATUALIZADA=$(grep -oE 'atualizado_em:[[:space:]]*[0-9-]+' "$THREATS" | head -n1 | grep -oE '[0-9-]+$')
  if [ -n "$ATUALIZADA" ]; then
    QUANDO=$(date -u -d "$ATUALIZADA" +%s 2>/dev/null || echo "")
    [ -n "$QUANDO" ] && BASE_IDADE=$(( ( $(date -u +%s) - QUANDO ) / 86400 ))
  fi
fi

echo ""
echo "================================================================"
echo " SUPERFICIE DE ATAQUE — ${HOST}"
echo " Modo: somente leitura. Nada foi alterado, nada foi explorado."
if [ -f "$THREATS" ]; then
  N_EXTRA=$(printf '%s%s' "$EXTRA_CAMINHOS" "$EXTRA_PORTAS" | grep -c . || true)
  echo " Base de ameaças: ${N_EXTRA} itens${BASE_IDADE:+, atualizada ha ${BASE_IDADE} dias}"
  if [ -n "$BASE_IDADE" ] && [ "$BASE_IDADE" -gt 60 ]; then
    echo " ATENCAO: base velha. Ataque novo aparece toda semana e eu ainda não"
    echo " sei procurar os desta temporada. Peça: /sshp-attack atualizar base"
  fi
else
  echo " Base de ameaças: nenhuma. Uso só a lista que vem de fabrica."
fi
echo "================================================================"

# ── 1. DE FORA: que portas respondem pela internet ──────────────────────────
# Uma conexão por porta, 3 segundos, e desliga. E o mesmo que qualquer robo de
# varredura faz com a sua máquina a cada poucos minutos, o dia inteiro.

porta_aberta() { timeout 3 bash -c "echo > /dev/tcp/${HOST}/$1" 2>/dev/null; }

# porta:nome:severidade_se_aberta:motivo
PORTAS="
22:SSH:ok:
80:HTTP:ok:
443:HTTPS:ok:
2375:comando do Docker aberto:critica:Esta porta aceita comando sem pedir senha. Quem chegar nela vira dono da máquina inteira, em um comando so.
6379:Redis:critica:Redis abre sem senha por padrão. Da para ler tudo que está em memória e gravar chave de acesso no disco.
27017:MongoDB:critica:Banco inteiro legivel e apagavel por quem chegar na porta.
3306:MySQL:critica:Banco de dados exposto à internet. Só falta a senha, e senha se tenta em série.
5432:PostgreSQL:critica:Banco de dados exposto à internet. Só falta a senha, e senha se tenta em série.
9200:busca Elasticsearch:critica:Vem sem senha de fabrica. Quem chegar na porta le tudo que está indexado.
11211:cache memcached:critica:Vem sem senha, e ainda da para usar o seu servidor como arma contra outras pessoas.
23:telnet:critica:Manda a senha pela rede sem embaralhar: quem estiver no meio do caminho le. Isso não deveria existir hoje.
21:FTP:alta:Manda a senha pela rede sem embaralhar. Use SFTP, que já vem junto com o SSH.
3389:acesso remoto do Windows:alta:Uma das portas mais tentadas da internet, o dia inteiro, por robo.
5900:acesso remoto VNC:alta:Costuma ficar sem senha ou com senha fraca, e da a tela da máquina para quem entrar.
15672:painel do RabbitMQ:alta:Painel de administração que vem com usuário e senha de fabrica: guest e guest.
5672:fila RabbitMQ:media:Fila de mensagem é peça interna do sistema. Não precisa estar aberta para a internet.
9090:medidor Prometheus:media:Mostra por dentro quais rotas existem, quais versões você usa e quanto o sistema roda. Mapa pronto.
8080:sua aplicação, direto:media:Aqui a aplicação atende sem o servidor web na frente, ou seja, sem cadeado e sem limite de acesso.
8000:sua aplicação, direto:media:Aqui a aplicação atende sem o servidor web na frente, ou seja, sem cadeado e sem limite de acesso.
3000:sua aplicação, em porta de teste:media:Esta é a porta que se usa enquanto o sistema está sendo feito. No ar, ela deveria atender pela 80 e 443.
5000:sua aplicação, em porta de teste:media:Esta é a porta que se usa enquanto o sistema está sendo feito. No ar, ela deveria atender pela 80 e 443.
${EXTRA_PORTAS}"

echo ""
echo "== 1. portas abertas =="
echo "   Porta e como uma porta de casa: cada serviço do seu servidor usa uma."
echo "   Aberta = qualquer pessoa da internet consegue bater nela agora."
echo ""
ABERTAS=""
HTTP_ABERTA=0; HTTPS_ABERTA=0
while IFS=: read -r p nome sev motivo; do
  [ -n "${p:-}" ] || continue
  if porta_aberta "$p"; then
    ABERTAS="${ABERTAS}${p} "
    printf '  aberta  %-6s %s\n' "$p" "$nome"
    [ "$p" = "80" ] && HTTP_ABERTA=1
    [ "$p" = "443" ] && HTTPS_ABERTA=1
    if [ "$sev" != "ok" ]; then
      achado "$sev" "porta ${p} aberta para a internet (${nome})" "$motivo" \
        "feche a porta ${p} no firewall (o porteiro do servidor): sudo ufw deny ${p} — e configure esse serviço para atender só quem já está dentro da máquina"
    fi
  fi
done <<< "$PORTAS"
[ -n "$ABERTAS" ] || echo "  nenhuma das portas conhecidas respondeu"

# ── 2. DE FORA: o que a aplicação entrega sem pedir nada ────────────────────
if [ -z "$BASE_URL" ]; then
  [ "$HTTPS_ABERTA" -eq 1 ] && BASE_URL="https://${HOST}"
  [ -z "$BASE_URL" ] && [ "$HTTP_ABERTA" -eq 1 ] && BASE_URL="http://${HOST}"
fi

if [ -n "$BASE_URL" ]; then
  BASE_URL="${BASE_URL%/}"
  echo ""
  echo "== 2. arquivos que qualquer um baixa do seu site =="
  echo "   Peço cada arquivo abaixo como um visitante qualquer pediria."
  echo "   EXPOSTO = ele veio. Ou seja, está publico agora."
  echo ""

  http_code() { curl -sk -o /dev/null -w '%{http_code}' --max-time 8 "$1" 2>/dev/null || echo 000; }
  http_body() { curl -sk --max-time 8 --max-filesize 4096 "$1" 2>/dev/null; }

  # caminho|severidade|o_que_e|por_que
  CAMINHOS="
/.env|critica|arquivo com todas as suas senhas|Ali dentro estão a senha do banco, as chaves de API e o segredo que assina o login dos usuários. Quem baixou entra em tudo. É o pior de todos.
/.git/HEAD|critica|copia do seu codigo-fonte|Com isso da para baixar o projeto inteiro, com todas as versões antigas — inclusive senha que você colocou sem querer e apagou depois. O historico guarda tudo.
/.git/config|critica|endereco do seu repositorio|E quando o endereco tem usuário e senha embutidos, entrega os dois junto.
/.sshepherd/state.json|alta|ficha do seu servidor|Entrega o endereco da máquina, o nome do usuário que opera ela e onde o site fica no disco.
/.reversa/state.json|alta|mapa interno do seu projeto|Descreve como o sistema está organizado por dentro.
/_reversa_sdd/prd.md|alta|documento que explica o seu negocio|Conta as regras, por onde passa o dinheiro e o que o sistema ainda não trata. Escrito para ser entendido, o que ajuda quem ataca tanto quanto ajuda você.
/_reversa_sdd/personas.md|media|material interno de produto|Não faz mal direto, mas não tem motivo para estar publico.
/.claude/settings.json|alta|configuração do seu assistente de IA|Mostra comandos automaticos e caminhos internos do projeto.
/CLAUDE.md|media|manual de como o seu sistema funciona|Escrito para orientar a IA, serve igualmente para orientar quem ataca: diz onde fica cada coisa.
/AGENTS.md|media|manual de como o seu sistema funciona|Escrito para orientar a IA, serve igualmente para orientar quem ataca: diz onde fica cada coisa.
/docker-compose.yml|alta|receita de tudo que roda no servidor|Lista cada serviço, as portas internas e, quase sempre, a senha do banco escrita ali mesmo.
/package.json|media|lista das pecas que o seu site usa|Com a lista e as versões, da para procurar falha conhecida de cada peça.
/backup.sql|critica|copia do seu banco de dados|Os dados dos seus usuários, baixaveis por qualquer pessoa. Se tem dado pessoal ali, isso também é problema com a LGPD.
/dump.sql|critica|copia do seu banco de dados|Os dados dos seus usuários, baixaveis por qualquer pessoa. Se tem dado pessoal ali, isso também é problema com a LGPD.
/phpinfo.php|alta|relatorio completo do servidor|Mostra a configuração inteira da máquina, os caminhos e as senhas guardadas no ambiente.
/server-status|media|painel de uso do servidor|Mostra quais endereços do site estão sendo usados e por quem.
/.DS_Store|baixa|lista de arquivos da pasta|Arquivo que o Mac cria sozinho. Entrega o nome de tudo que existe na pasta.
${EXTRA_CAMINHOS}"

  EXPOSTOS=0
  while IFS='|' read -r caminho sev oque motivo; do
    [ -n "${caminho:-}" ] || continue
    CODE=$(http_code "${BASE_URL}${caminho}")
    case "$CODE" in
      2*)
        EXPOSTOS=$((EXPOSTOS + 1))
        printf '  EXPOSTO %-26s %s\n' "$caminho" "$oque"
        achado "$sev" "qualquer um baixa do seu site: ${oque} (${caminho})" "$motivo" \
          "bloqueie esse endereco no servidor web agora. E confira o .sshepherdignore: isso não deveria nem ter subido"
        ;;
      *) printf '  ok      %-26s HTTP %s\n' "$caminho" "$CODE" ;;
    esac
  done <<< "$CAMINHOS"
  [ "$EXPOSTOS" -eq 0 ] && echo "  nenhum arquivo sensivel respondeu"

  # Listagem de diretorio: entrega o mapa do que existe sem precisar adivinhar.
  RAIZ=$(http_body "${BASE_URL}/")
  if printf '%s' "$RAIZ" | grep -qiE "index of /|<title>Directory listing"; then
    achado alta "seu site mostra a lista de arquivos das pastas, como uma vitrine" \
      "Sem isso, quem ataca precisa adivinhar o nome de cada arquivo. Com isso, recebe a lista pronta e escolhe." \
      "no nginx, escreva: autoindex off;  |  no Caddy, tire a palavra browse da configuração"
  fi

  # Cabecalhos: o que o servidor conta de si mesmo, e o que ele deixa de proteger.
  echo ""
  echo "  cabeçalhos:"
  HDRS=$(curl -skI --max-time 8 "${BASE_URL}/" 2>/dev/null)
  SRV=$(printf '%s' "$HDRS" | grep -i '^server:' | head -n1 | tr -d '\r')
  XPB=$(printf '%s' "$HDRS" | grep -i '^x-powered-by:' | head -n1 | tr -d '\r')
  [ -n "$SRV" ] && echo "    ${SRV}"
  [ -n "$XPB" ] && echo "    ${XPB}"

  if printf '%s' "${SRV}${XPB}" | grep -qE '[0-9]+\.[0-9]+'; then
    achado media "seu servidor conta para todo mundo qual programa e qual versão ele usa" \
      "Com a versão na mao, procurar uma falha ja conhecida daquela versão leva segundos. É como avisar a marca da sua fechadura." \
      "no nginx, escreva: server_tokens off;  — e tire o cabeçalho X-Powered-By da sua aplicação"
  fi
  if [ "$HTTPS_ABERTA" -eq 1 ] && ! printf '%s' "$HDRS" | grep -qi 'strict-transport-security'; then
    achado media "seu site não obriga o navegador a usar o cadeado" \
      "O cadeado funciona, mas nada impede alguem de empurrar o visitante para a versão sem cadeado, onde da para ler o que ele digita." \
      "acrescente esta linha na configuração do servidor web: Strict-Transport-Security: max-age=31536000"
  fi
  if [ "$HTTP_ABERTA" -eq 1 ] && [ "$HTTPS_ABERTA" -eq 0 ]; then
    achado alta "seu site não tem cadeado (o https do navegador)" \
      "Sem ele, tudo que a pessoa digita viaja aberto até o servidor: senha de login e dado de formulario. Quem estiver no meio do caminho le." \
      "use o Caddy, que resolve o cadeado sozinho e de graça. O /sshp-provision instala"
  fi
else
  echo ""
  echo "== 2. o que a aplicação entrega =="
  echo "  nem 80 nem 443 responderam: nada servido pela web neste endereco."
  echo "  se seu site responde por dominio, repita com --url https://seu-dominio/"
fi

# ── 3. DE DENTRO: o que está ao alcance de quem já entrou ───────────────────
if [ "$SKIP_REMOTE" -eq 1 ] || [ -z "$USER_NAME" ]; then
  echo ""
  echo "== 3. de dentro do servidor =="
  echo "  pulado (sem --user ou com --skip-remote)"
else
  SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ForwardAgent=no -o ConnectTimeout=10 -p "$PORT")
  [ -n "$KEY_PATH" ] && SSH_OPTS+=(-i "${KEY_PATH/#\~/$HOME}" -o IdentitiesOnly=yes)

  # Uma unica ida ao servidor. Nenhum valor de segredo e lido: so nome de
  # variavel, permissão e presenca de arquivo.
  REMOTE='
    APP="'"${APP_PATH}"'"
    REL="$APP/current"

    echo "CHK|listen|$( (ss -lntH 2>/dev/null || netstat -lnt 2>/dev/null) | awk "{print \$4}" | grep -E "^(0\.0\.0\.0|\[::\]|\*):" | sed "s/.*://" | sort -un | tr "\n" " ")"

    if [ -e "$APP/shared/.env" ]; then
      echo "CHK|env_perm|$(stat -c "%a %U:%G" "$APP/shared/.env" 2>/dev/null)"
      echo "CHK|env_vars|$(grep -oE "^[A-Z_]+" "$APP/shared/.env" 2>/dev/null | tr "\n" " ")"
    else
      echo "CHK|env_perm|ausente"
      echo "CHK|env_vars|"
    fi
    # O cofre: só o que o helper conta (dono, modo, quantidade), nunca valor.
    if [ -d /var/lib/sshepherd/secrets ]; then
      echo "CHK|cofre|$(sudo -n /usr/local/sbin/sshepherd-secrets status 2>/dev/null | grep -E "^(dono_modo|pasta_modo|variaveis)=" | tr "\n" " ")"
    else
      echo "CHK|cofre|ausente"
    fi
    echo "CHK|unit_user|$(systemctl show -p User --value sshepherd-app 2>/dev/null)"
    echo "CHK|env_na_release|$(find "$APP/releases" -maxdepth 2 -name ".env*" -type f 2>/dev/null | head -n5 | tr "\n" " ")"

    VAZOU=""
    for p in .git .env .sshepherd .reversa .claude .agents _reversa_sdd _reversa_forward _reversa_docs; do
      [ -e "$REL/$p" ] && VAZOU="$VAZOU $p"
    done
    echo "CHK|publicado|$VAZOU"

    echo "CHK|world_readable|$(find "$REL" -maxdepth 2 -type f -perm -o=r -name "*.env*" 2>/dev/null | head -n5 | tr "\n" " ")"
    echo "CHK|nopasswd|$(sudo -n grep -rhE "NOPASSWD" /etc/sudoers /etc/sudoers.d/ 2>/dev/null | grep -cv "^#")"
    echo "CHK|shells|$(getent passwd 2>/dev/null | grep -vE "(nologin|/false)$" | cut -d: -f1 | tr "\n" " ")"
    echo "CHK|falhas|$(sudo -n grep -c "Failed password\|Invalid user" /var/log/auth.log 2>/dev/null || echo indisponivel)"
    echo "CHK|docker_sock|$([ -S /var/run/docker.sock ] && stat -c "%a" /var/run/docker.sock 2>/dev/null || echo ausente)"
    echo "CHK|containers|$(docker ps --format "{{.Ports}}" 2>/dev/null | grep -c "0.0.0.0" || echo 0)"
  '

  echo ""
  echo "== 3. o estrago que alguem faria se já estivesse dentro =="
  RAW=$(ssh "${SSH_OPTS[@]}" "${USER_NAME}@${HOST}" "$REMOTE" 2>&1)
  if [ $? -ne 0 ]; then
    echo "  não consegui entrar por SSH. As verificações de fora acima valem." >&2
    printf '%s\n' "$RAW" | head -n5 | sed 's/^/  /' >&2
    RAW=""
  fi

  get() { printf '%s\n' "$RAW" | grep "^CHK|$1|" | head -n1 | cut -d'|' -f3-; }

  if [ -n "$RAW" ]; then
    LISTEN=$(get listen); ENV_PERM=$(get env_perm); ENV_VARS=$(get env_vars)
    PUBLICADO=$(get publicado); WORLD=$(get world_readable)
    NOPASSWD=$(get nopasswd); SHELLS=$(get shells); FALHAS=$(get falhas)
    DOCKSOCK=$(get docker_sock); CONTAINERS=$(get containers)
    COFRE=$(get cofre); UNIT_USER=$(get unit_user); ENV_REL=$(get env_na_release)

    echo "  portas que atendem qualquer um   : ${LISTEN:-nenhuma}"
    if [ -n "$COFRE" ] && [ "$COFRE" != "ausente" ]; then
      echo "  cofre de segredos                : ${COFRE}"
    else
      echo "  quem pode ler o arquivo de senhas: ${ENV_PERM:-?}"
    fi
    echo "  contas que conseguem entrar      : ${SHELLS:-?}"
    echo "  tentativas de entrar que falharam: ${FALHAS:-?}"

    # Segredo legivel por qualquer usuário da máquina.
    PERM_BITS=$(printf '%s' "$ENV_PERM" | awk '{print $1}')
    if [ -n "$PERM_BITS" ] && [ "$PERM_BITS" != "ausente" ]; then
      ULTIMO=${PERM_BITS: -1}
      if [ "$ULTIMO" != "0" ]; then
        achado alta "o arquivo com as suas senhas pode ser lido por qualquer um dentro do servidor" \
          "É o .env, onde ficam a senha do banco e as chaves. Do jeito que está (${PERM_BITS}), qualquer conta ou programa da máquina abre e le. Não precisa ser o dono." \
          "isso e um comando so, e eu rodo com voce: chmod 600 ${APP_PATH}/shared/.env"
      fi
    fi

    # Servidor com cofre, mas com o .env antigo ainda em shared/: o arquivo que
    # o cofre existe para tirar do alcance do usuário de operação continua lá.
    if [ -n "$COFRE" ] && [ "$COFRE" != "ausente" ] && [ "$PERM_BITS" != "ausente" ] && [ -n "$PERM_BITS" ] && [ "$UNIT_USER" = "sshepherd-app" ]; then
      achado alta "o arquivo de senhas antigo ainda existe em shared/.env, mesmo com o cofre instalado" \
        "A aplicação já lê as chaves do cofre, que só root abre. Esse arquivo antigo é uma cópia das chaves que qualquer conta de operação, inclusive a que o assistente usa, consegue ler." \
        "confira com /sshp-secrets que tudo está no cofre e apague ${APP_PATH}/shared/.env no seu terminal"
    fi
    if [ -n "$COFRE" ] && [ "$COFRE" != "ausente" ] && [ -n "$UNIT_USER" ] && [ "$UNIT_USER" != "sshepherd-app" ]; then
      achado media "o cofre existe, mas a aplicação ainda roda como '${UNIT_USER}', o mesmo usuário que publica e que o assistente usa" \
        "Rodando assim, o processo da aplicação carrega as chaves num usuário que o assistente controla. O cofre só protege de verdade quando a aplicação roda como o usuário de serviço." \
        "rode /sshp-provision de novo: ele troca o serviço para o usuário sshepherd-app"
    fi
    if [ -n "${ENV_REL// /}" ]; then
      achado alta "tem arquivo .env de verdade dentro de uma versão publicada: ${ENV_REL}" \
        "Arquivo de senhas que subiu junto com o código. Ele fica em cada versão guardada, legível por quem publica, e vai para qualquer backup ou cópia dessas pastas." \
        "tire o .env do envio (.sshepherdignore ou .gitignore), troque as chaves que estavam nele e publique de novo"
    fi

    # O que subiu junto com o codigo e não deveria.
    if [ -n "${PUBLICADO// /}" ]; then
      achado alta "material seu de trabalho foi para o servidor junto com o site:${PUBLICADO}" \
        "São as pastas de especificação e de agente de IA. Elas contam como o seu sistema funciona por dentro. Estão no servidor, e basta uma configuração errada para virarem pagina publica." \
        "revise o arquivo .sshepherdignore, que diz o que pode subir, e publique de novo com /sshp-deploy"
    fi

    if [ -n "${WORLD// /}" ]; then
      achado alta "tem arquivo de senha aberto dentro da versão que está no ar" \
        "Encontrei: ${WORLD}. Qualquer conta da máquina consegue abrir e ler." \
        "fecho esses arquivos com chmod 600, e a gente tira eles do envio no .sshepherdignore"
    fi

    # Servico interno escutando para o mundo: e o par de dentro do achado da secao 1.
    for p in $LISTEN; do
      case "$p" in
        3306|5432|6379|27017|9200|11211|2375)
          achado alta "o serviço da porta ${p} aceita conexão de qualquer lugar" \
            "Ele mesmo não filtra quem chega. Hoje quem segura é so o firewall, o porteiro do servidor. No dia que uma regra do firewall cair por engano, ele fica aberto na hora." \
            "configure esse serviço para atender so quem já está dentro da máquina. O firewall e a segunda tranca, não a primeira" ;;
      esac
    done

    if [ "${NOPASSWD:-0}" -gt 0 ] 2>/dev/null; then
      achado media "tem conta que executa comando de dono sem pedir senha (${NOPASSWD} regra(s))" \
        "Quem entrar como essa conta vira dono da máquina direto, sem precisar descobrir mais nada." \
        "revise o arquivo /etc/sudoers.d/ e deixe essa liberação so no que a publicação precisa"
    fi

    if [ -n "$DOCKSOCK" ] && [ "$DOCKSOCK" != "ausente" ] && [ "${DOCKSOCK: -1}" != "0" ]; then
      achado critica "o canal de comando do Docker está aberto para outras contas" \
        "É por esse canal que se manda o Docker fazer as coisas. Quem alcança ele vira dono da máquina inteira, sem exceção e sem senha." \
        "fecho com chmod 660 /var/run/docker.sock, e quem precisa entra pelo grupo docker"
    fi

    if [ "${CONTAINERS:-0}" -gt 0 ] 2>/dev/null; then
      achado media "${CONTAINERS} contêiner(es) abriram porta para a internet por conta própria" \
        "Contêiner e a caixinha onde um serviço roda. Quando ele abre porta, essa porta passa por cima do firewall: a regra que você criou não vale para ela. É por isso que da para ter firewall certo e banco exposto ao mesmo tempo." \
        "no docker-compose, troque 5432:5432 por 127.0.0.1:5432:5432 — assim so quem já está dentro alcança"
    fi

    # ── vetor LLM: o que uma IA atacante colhe e usa ──────────────────────
    LLM_KEYS=$(printf '%s' "$ENV_VARS" | tr ' ' '\n' \
      | grep -E "ANTHROPIC|OPENAI|GEMINI|GROQ|MISTRAL|COHERE|HUGGING|REPLICATE|OPENROUTER" | tr '\n' ' ')
    if [ -n "${LLM_KEYS// /}" ]; then
      echo "  chaves de LLM no ambiente        : ${LLM_KEYS}"
      achado media "há chave de provedor de IA no arquivo de segredos: ${LLM_KEYS}" \
        "Chave de LLM vazada não é só invasão, é conta de consumo: quem pega gasta o seu credito em minutos, e o rombo aparece na fatura." \
        "confirme que o .env não sobe no deploy, que a permissão é 600, e ative limite de gasto no painel do provedor"
    fi
  fi
fi

# ── relatorio ───────────────────────────────────────────────────────────────
echo ""
echo "================================================================"

CRIT=0; ALTA=0; MEDIA=0; BAIXA=0
for a in "${ACHADOS[@]:-}"; do
  [ -n "$a" ] || continue
  case "${a%%|*}" in
    critica) CRIT=$((CRIT + 1)) ;;
    alta) ALTA=$((ALTA + 1)) ;;
    media) MEDIA=$((MEDIA + 1)) ;;
    baixa) BAIXA=$((BAIXA + 1)) ;;
  esac
done

TOTAL=$((CRIT + ALTA + MEDIA + BAIXA))
if [ "$TOTAL" -eq 0 ]; then
  echo " NAO ACHEI NADA no que eu consigo ver."
  echo ""
  echo " Isso não é a mesma coisa que estar seguro. Eu olho o que da para ver"
  echo " de fora e o que está gravado no disco. Erro dentro da sua aplicação"
  echo " — um usuário que enxerga dado de outro, login que nunca expira — eu"
  echo " não alcanço daqui."
else
  echo " ${TOTAL} ACHADO(S): ${CRIT} critico(s), ${ALTA} alto(s), ${MEDIA} medio(s), ${BAIXA} baixo(s)"
fi
echo "================================================================"

for sev in critica alta media baixa; do
  for a in "${ACHADOS[@]:-}"; do
    [ -n "$a" ] || continue
    IFS='|' read -r s titulo motivo fix <<< "$a"
    [ "$s" = "$sev" ] || continue
    echo ""
    echo "  [${s^^}] ${titulo}"
    echo "     por que isso é ruim: ${motivo}"
    echo "     como resolver      : ${fix}"
  done
done

echo ""
echo "CRITICAS=${CRIT}"
echo "ALTAS=${ALTA}"
echo "MEDIAS=${MEDIA}"
echo "BAIXAS=${BAIXA}"
echo "PORTAS_ABERTAS=${ABERTAS:-nenhuma}"
[ "$CRIT" -gt 0 ] && exit 1
exit 0
