# Dia 0 — Remediação em PRODUÇÃO

**Estado em 21/08/2026: passos 1 e 2 EXECUTADOS em produção.**

| Passo | Situação |
|---|---|
| 1 — arquivos fora do webroot | **Feito.** Os quatro em `~/quarentena-dia0/20260821-141226`, e as URLs devolvem 404 |
| 2 — apuração LGPD | **Feito. Zero acesso externo** nos quatro, no período retido |
| 3 — `local.php` | pendente |
| 4 — deploy | pendente, e sem caminho de entrega (**D37**) |
| 5 — rotação de credenciais | pendente |
| 6 — bloquear a rota | pendente — `scripts/bloquear-migrar-senhas.sh` |
| 7 — 401 nos crons | **código pronto em 03/10/2026** (D4): vai com o deploy da migração; o crontab de hoje continua servindo |

> ## DECISÃO DE 11/09/2026: os passos 3 a 7 entram na MIGRAÇÃO FINAL
>
> *"nao faremos nenhuma modificação no servidor de produção, pule estas etapas e
> faremos apenas na mingração final, apos todas as validações"*
>
> **Ninguém executa nada deste documento agora.** Os passos 1 e 2 já foram feitos
> em 21/08/2026; os passos 3 a 7 ficam prontos e validados no ambiente de
> desenvolvimento e são aplicados **juntos**, na janela da migração, depois de
> todas as validações.
>
> **As marcas de urgência abaixo continuam valendo como AVALIAÇÃO, e não como
> instrução.** O passo 6 é urgente: a rota `/autenticacao/migrar-senhas` está viva
> em produção e sobrescreve o hash de senha de todos os usuários ativos sem pedir
> autenticação. Dizer "urgente" e "só na janela" ao mesmo tempo não é
> contradição — é a descrição honesta de um risco aceito com data. O que seria
> desonesto é apagar o "urgente" para o documento parecer tranquilo.
>
> **O que continua ativo em produção, e segue ativo até a janela:** aquela rota, as
> oito credenciais em texto claro no código implantado, e o `local.php`
> inexistente.

**Sobre a apuração:** nenhum acesso externo aos quatro arquivos nem à rota de
migração de senhas. A afirmação correta é "nenhum acesso **no período retido
pelos logs**", que são **14 dias** — não "nunca houve acesso". Nem robô de busca
encontrou os arquivos em duas semanas, o que é o melhor indício disponível de que
nunca foram descobertos.

Com base nisso, **não há dever de notificação à ANPD** pelos dados que estavam
expostos. O relatório com a data está em `docs/incidente-lgpd-dia0.md`, gerado no
próprio servidor — é a evidência de que a apuração foi feita, e precisa vir para
o repositório.

**O que continua ativo em produção:** a rota `/autenticacao/migrar-senhas`, as
oito credenciais em texto claro no código, e o `local.php` inexistente.

Ordem importa: **passo 3 antes do 4**. O código novo lê as credenciais de `local.php`; se o deploy for feito antes de criar o arquivo, o sistema para. (Este parágrafo dizia "passo 2 antes do 3" — numeração errada, sobrada de uma renumeração anterior. A dependência sempre foi `local.php` antes do deploy.)

## Dois scripts fazem a parte mecânica

```bash
sh scripts/remediacao-dia0.sh              # audita: passos 1, 2 e 6. Não move nada
sh scripts/remediacao-dia0.sh --aplicar    # move os arquivos para quarentena datada
php scripts/verificar-local-php.php        # portão do passo 4
```

Rode o primeiro **sem** `--aplicar`, leia, e só então aplique. Nada é apagado: os arquivos vão para `~/quarentena-dia0/<data>` com permissão 700, e a apuração do passo 2 é gravada em `docs/incidente-lgpd-dia0.md` nos dois modos.

O que continua manual, porque exige julgamento: passo 3 (preencher credenciais), passo 4 (deploy) e passo 5 (rotação).

---

## Passo 1 — Estancar a exposição — FEITO em 21/08/2026

*O cabeçalho dizia "fazer AGORA, independe do deploy". Ficou como está abaixo
porque o roteiro é o registro de como foi feito — e o "independe do deploy" é o
fato que permitiu fazê-lo sem esperar os outros passos. O quadro de estado no topo
é quem diz a situação.*

Estes quatro arquivos estão dentro do webroot e o nginx os serve diretamente: o `try_files $uri ... /index.php` entrega qualquer arquivo existente antes de cair no PHP. (O `public/.htaccess` do projeto é inerte — o nginx não lê esse arquivo.)

```bash
cd /var/www/franquiabv.com.br/public_html
sh scripts/remediacao-dia0.sh --aplicar
```

Confirme que as URLs passaram a responder 404:

```bash
for u in clientes.txt phpinfofile.php log_scripts.txt logs/; do echo -n "$u -> "; curl -s -o /dev/null -w "%{http_code}\n" "https://franquiabv.com.br/$u"; done
```

## Passo 2 — Apuração do incidente LGPD

`clientes.txt` continha nome e telefone de clientes e esteve publicamente acessível. É preciso determinar se houve acesso externo — isso define se há dever de notificação à ANPD e aos titulares.

```bash
cd /var/log/nginx
LOGS=$(ls -1 *.log *.log.* 2>/dev/null | grep -v error)
echo "$LOGS"                                                  # o log do site (o do vhost) tem de estar aqui
for f in $LOGS; do case $f in *.gz) gzip -t "$f" ;; *.bz2|*.xz|*.zst) false ;; *) [ -r "$f" ] ;; esac || echo "NAO LIDO: $f"; done   # nenhum NAO LIDO
zgrep -hF "/api/versao/app" $LOGS | wc -l                     # O CONTROLE: tem de dar mais que zero
zgrep -hE "clientes\.txt|phpinfofile|log_scripts" $LOGS \
  | grep -v "^127\.0\.0\.1 " | grep -v "^::1 " \
  | awk '{print $1}' | sort | uniq -c | sort -rn
```

**Todo `*.log` e `*.log.*`, e não só o `access.log*`** (desde 08/11/2026, o **D332**): o site tem log próprio por vhost
-- a coleta do **D36** que olhou só `access.log*` achou zero pedidos a `/api/versao/app`, e a que varreu `*.log` achou
11.032. O `zgrep` lê o `.gz` rotacionado e o arquivo comum; os de erro ficam de fora (não registram pedido atendido, e a
primeira coluna deles é a data). **O controle vem antes:** se `/api/versao/app` der zero, estes não são os logs do site,
e "sem retorno" abaixo não quer dizer nada -- não escreva "nenhum acesso".

O `remediacao-dia0.sh` já faz esta apuração, com o mesmo controle, e grava `docs/incidente-lgpd-dia0.md` (os arquivos
lidos e o número do controle vão no relatório; com o controle em zero ele escreve "não li o log do site" e sai com **2**).
**Desde a correção da fatia 128 (08/11/2026)**, também sai com **2** quando o diretório não existe ou não se lê (o
relatório de uma rodada anterior é trocado pelo "não li"), e quando algum arquivo não se lê inteiro -- o `.gz` truncado
(o `gzip -t` recusa), o sem permissão para quem roda, o de compressão que o `zgrep` não abre (`.bz2`, `.xz`): esses vão
à lista "Arquivos que NÃO li" do relatório, fora dos lidos, e a conclusão diz que não cobre o período deles. O controle
é um só, somado sobre os arquivos, e não prova que cada um foi lido; por isso a conferência de cada arquivo, que à mão é
a linha do `gzip -t` acima: um `NAO LIDO` ali é um arquivo que a busca não cobriu. E o erro do `zgrep` fica na tela.
Precisa enxergar `/var/log/nginx` — rode como root, ou aponte com `LOGS_NGINX=`.

Sem retorno, **com o controle acima de zero** = nenhum acesso registrado **no período retido pelos logs**, que não é o mesmo que "nunca houve acesso". Registre há quanto tempo os logs são mantidos; é isso que define o alcance da afirmação.

Com retorno, separe os IPs de robôs conhecidos dos demais e **escale para o jurídico** — o prazo da ANPD é curto.

O relatório é gravado mesmo quando dá vazio, de propósito: é a ausência de acesso que sustenta a decisão de não notificar.

## Passo 3 — Criar `local.php` no servidor

```bash
cd /var/www/franquiabv.com.br/public_html
cp config/autoload/local.php.dist config/autoload/local.php
chmod 640 config/autoload/local.php
nano config/autoload/local.php   # preencher com as credenciais REAIS de produção
php scripts/verificar-local-php.php
```

O `verificar-local-php.php` confere se **toda** chave que o código lê está preenchida, e classifica o que falta: `fatal` (banco — o sistema inteiro sai do ar), `grave` (push ao cliente, e-mail, a cobrança do Asaas, as rotinas do crontab, o Yungas) e `medio` (SMS, visagismo, web push). Sai com código 1 no caso fatal, então serve de portão num script de deploy. Nunca imprime o valor de uma credencial.

Sem ele, uma chave faltando só aparece **depois** do deploy — e no caso do banco, aparece como sistema fora do ar.

**Desde 03/10/2026 o `local.php` de produção precisa de mais duas** (SEGREDOS, D3 e D4). Até então o código as trazia escritas, e por isso o arquivo não precisava delas:

| Chave | O que pôr | Sem ela, depois do deploy |
|---|---|---|
| `asaas.api_key` | a chave do Asaas **rotacionada** no passo 5 (linha 7), nunca a antiga; `asaas.url` fica vazia (vale a de produção) | `relatoriosRoyaltiesFaturasGerar` e `relatoriosRoyaltiesFaturasAtualizar` lançam exceção antes de falar com o Asaas — o cron recebe HTTP 500, e nenhuma fatura de royalties é emitida nem atualizada |
| `cron.token` | o **mesmo valor que o crontab de produção manda** no `?token=` (o `crontab -l` mostra) — o de hoje, ou o novo, se a linha 9 do passo 5 já o rotacionou | as 26 rotinas por URL — royalties, dashboards, NPS, envios — **recusam todo pedido com 401** (desde 03/10/2026, D4): param, e só o log de acesso do nginx mostra — o crontab não lê a resposta (D331) |

O verificador acusa as duas como `grave` (e as três do Yungas, do **D89**, que ele também não conhecia). `grave` não reprova o deploy — o sistema sobe —, então **leia a lista**: o deploy liberado com essas duas ausentes deixa a cobrança e as rotinas paradas.

**O valor de hoje está no repositório**, e por isso o token das rotinas entrou na rotação do passo 5 (linha 9): está no histórico do `CronJobController` e segue escrito no `VisagismoController` — exceção declarada do portão de segredos, porque o visagismo é futuro por decisão sua. Rotacioná-lo **não tira** o valor do repositório: faz o valor do repositório deixar de abrir as rotinas. E é o crontab e o `cron.token` **no mesmo passo**: um sem o outro é o silêncio da linha de cima. **O visagismo fica como está até a migração final** (decisão de 06/10/2026, lote de perguntas, pergunta 2, a opção (a)): com a rotação, o valor escrito nele passa a abrir só o visagismo.

**Desde 30/08/2026 ele confere também o outro lado: chaves que precisam estar AUSENTES.**

| Chave | O que vaza se estiver ligada |
|---|---|
| `view_manager.display_exceptions` | a página de erro publica o rastro de pilha — e a mensagem do Doctrine leva o SQL **e os parâmetros ligados** junto, então uma consulta de cliente que falhe imprime o telefone do cliente na tela (**D16**) |
| `view_manager.display_not_found_reason` | a página de 404 conta por que a rota não casou |

Estas duas viviam em `module/ISBase/config/module.config.php`, que carrega em
**todo** ambiente. Saíram de lá em 30/08 e passaram a viver em `local.php`, que é
por máquina. Em desenvolvimento elas são desejáveis; **em produção, não** — e é
por isso que o portão confere.

Chave indevida também sai com código 1: vazamento não espera janela de
manutenção para ser consertado.

Atenção: as credenciais de produção do banco podem ser as que estavam **comentadas** no arquivo antigo (`franquiabv_user`), não as ativas. Confirme antes.

## Passo 4 — Deploy do código corrigido

Arquivos alterados nesta rodada:

| Arquivo | Mudança |
|---|---|
| `config/autoload/doctrine.global.php` | credenciais removidas |
| `config/autoload/global.php` | senha SMTP e bearer token removidos |
| `config/autoload/local.php.dist` | passa a documentar as chaves necessárias |
| `module/ISBase/src/Util/OneSignal.php` | chaves lidas de `local.php` |
| `module/ISRelatorio/src/Controller/ClientesController.php` | export TXT sem gravar em disco |
| `module/ISRelatorio/src/Controller/PesquisasNpsController.php` | export TXT sem gravar em disco |
| `module/ISBase/src/Util/IdealizeUtil.php` | log vai para `data/logs/` |
| `module/ISApi/src/Controller/AplicativoController.php` | log JSON vai para `data/logs/agenda_json/` |

Após o deploy, crie o diretório de logs e valide:

```bash
mkdir -p data/logs/agenda_json && chmod 775 data/logs data/logs/agenda_json
php -l config/autoload/doctrine.global.php && php -l module/ISBase/src/Util/OneSignal.php
```

**Testes de fumaça obrigatórios** (o sistema deixa de funcionar se `local.php` estiver errado):

1. Login no ERP — valida a conexão com o banco
2. Relatório de Clientes → exportar TXT — valida a correção do vazamento (o arquivo deve baixar e **não** aparecer em `public/`)
3. Envio de uma notificação push — valida as chaves OneSignal
4. "Esqueci minha senha" — valida o SMTP
5. Uma tela de visagismo — valida o bearer token Zoomtec

## Passo 5 — Rotação de credenciais

Todos os segredos abaixo estavam em texto claro no código e **estão presentes em todo backup e snapshot já feito** do projeto. Rotacionar é obrigatório; movê-los para `local.php` não os torna secretos retroativamente.

| # | Credencial | Onde rotacionar | Cuidado |
|---|---|---|---|
| 1 | Senha MySQL (`franquiabv`) | painel do banco | **Antes:** mapear consumidores com `SHOW PROCESSLIST` por alguns dias — o sistema de BI usa o banco direto e vai quebrar. Aproveite para dar a ele um usuário **read-only** próprio |
| 2 | Chave REST OneSignal (app mobile) | painel OneSignal | atualizar `local.php` no mesmo deploy |
| 3 | Chave REST OneSignal (web push) | painel OneSignal | idem |
| 4 | Senha SMTP `franquiabv@barbeariavip.com.br` | painel de e-mail | idem |
| 5 | Bearer token Zoomtec | fornecedor (Zoomtec) | o JWT **não tem `exp`** — não expira sozinho. Peça reemissão |
| 6 | Usuário/senha da API iAgente SMS | painel iAgente | trafegam na **query string**, então estão também nos logs do provedor. Estava no D3 e faltava aqui |
| 7 | **Chave de API do Asaas** | painel Asaas | é a de **produção** (`api.asaas.com`), não sandbox. Move dinheiro — rotacione com o financeiro avisado. Desde 03/10/2026 a nova vai no `asaas.api_key` do `local.php` (passo 3): o código não a traz mais, e a antiga continua no histórico do git |
| 8 | **API key, e-mail e senha do Yungas** | fornecedor (Yungas) | a **senha** está em texto puro. Peça troca dela, não só do token |
| 9 | **Token das rotinas por URL** (`cron.token`) | você mesmo: um valor aleatório novo (`openssl rand -hex 24`) | abre as 26 rotinas por URL, **inclusive a que gera as faturas de royalties no Asaas**. Estava escrito no `CronJobController` até 03/10/2026, e o mesmo valor segue escrito no `VisagismoController`. Troque o crontab e o `cron.token` do `local.php` **no mesmo passo** — um sem o outro para as rotinas em silêncio (D4) — e, antes, confira no `crontab -l` e no log do nginx se mais alguém chama `/api/cronjobs/` com esse token. **No corte, os dois são os passos 7 e 9 da janela**, com o cron desligado entre eles (o passo 1): o 7 sorteia o valor novo no `local.php`, e o 9 refaz as 25 linhas com ele **no cabeçalho**, com `curl -fsS -m <limite>` (decisão de 06/10/2026, D331, pergunta 5, a opção (b)) e, **sem e-mail**, o aviso de cada linha no log do sistema: `MAILTO=""` no crontab e `|| logger -t rotinas "falhou (curl $?): <a rota>"` no fim de cada linha (a resposta 128 (b), de 12/10/2026, que tirou o `MAILTO` da pergunta 5; escrito em 07/11/2026, a fatia 113). O visagismo fica como está (pergunta 2, a opção (a)) |

**A linha 9 entrou em 03/10/2026** (SEGREDOS, revisão): o token das rotinas é segredo escrito no código como os outros oito, e não estava em lista nenhuma. **As linhas 6 a 8 entraram em 31/08/2026.** A 6 já estava no D3 e faltava aqui;
as 7 e 8 não estavam em lugar nenhum, e apareceram ao aplicar os tempos limite
do D67. Uma lista de rotação incompleta é pior que nenhuma: ela é percorrida
até o fim e dá a sensação de terminada.

**Não rotacionar agora:** o token fixo da API do aplicativo (`AplicativoController`). Trocá-lo derruba o app na mão dos clientes. Fica como débito registrado, a ser tratado junto com a migração do app.

## Passo 6 — Endpoint público de migração de senhas (URGENTE — e adiado para a janela)

*A avaliação de urgência continua certa e fica registrada. A aplicação entra na
migração final, por decisão de 11/09/2026 — ver o aviso no topo. `scripts/bloquear-migrar-senhas.sh`
está pronto para quando a janela existir.*

Descoberto durante a W1. A rota abaixo existe em produção, **não exige autenticação** e sobrescreve o hash de senha de **todos os usuários ativos** de uma vez:

> **Correção de 17/08/2026 — o comando de verificação que estava aqui era perigoso.**
>
> Este passo mandava conferir a rota com um `curl` para
> `/autenticacao/migrar-senhas`. Isso **dispara a ação**: um GET executa o
> controller e sobrescreve as senhas. Trocar por `curl -I` não salva — o PHP
> executa o script igual e só descarta o corpo da resposta. O comando causava
> exatamente o incidente que pretendia detectar.
>
> A verificação segura não passa pela web. Olhe o código implantado no servidor:
>
> ```bash
> grep -rn "migrar-senhas" module/ config/
> ```
>
> Com retorno, a rota está viva. Sem retorno, não existe. O `remediacao-dia0.sh`
> faz essa checagem.

Era um script de uso único da migração para PHP 8.3 que ficou publicado. O deploy do Passo 4 já remove a rota e a ação; se o deploy for demorar, bloqueie a URL no nginx imediatamente:

```nginx
# Dentro do server {} do site, antes do "location /"
location = /autenticacao/migrar-senhas {
    deny all;
}
```

Depois: `nginx -t && systemctl reload nginx`

Verifique também se ela chegou a ser executada por terceiros:

```bash
cd /var/log/nginx
LOGS=$(ls -1 *.log *.log.* 2>/dev/null | grep -v error)
for f in $LOGS; do case $f in *.gz) gzip -t "$f" ;; *.bz2|*.xz|*.zst) false ;; *) [ -r "$f" ] ;; esac || echo "NAO LIDO: $f"; done   # nenhum NAO LIDO
zgrep -hF "/api/versao/app" $LOGS | wc -l                     # O CONTROLE: tem de dar mais que zero
zgrep -hF "migrar-senhas" $LOGS | grep -v "^127\.0\.0\.1 " | grep -v "^::1 "
```

Os mesmos arquivos, a mesma conferência de cada um e o mesmo controle do passo 2 (o **D332**): um `NAO LIDO` fica fora
da busca, e com o controle em zero a falta de retorno não diz que
ninguém chamou a rota -- diz que o log do site não foi lido.

Se houver acessos externos, há risco de que senhas de usuários tenham sido revertidas para um estado anterior — nesse caso force a redefinição de senha de todos os colaboradores.

## Passo 7 — Fragilidade na autenticação dos crons

**Correção de 16/08/2026:** este passo dizia que as rotas não tinham proteção. Estava errado — 26 delas conferem um token na query string e as 3 de exportação de vendas conferem o `hash` da unidade. O que existe é fragilidade, não ausência: o token é fixo no código, viaja na URL (logo, aparece nos logs) e, quando ausente, a rota responde `{"sucesso": true}` como se tivesse funcionado. Ver débito D4.

**Atualização de 03/10/2026 (SEGREDOS):** o token **não é mais fixo no código** — vem do `cron.token` do `local.php` (passo 3), e o configurado vazio, ou só de espaços, recusa todo pedido. O valor antigo segue no repositório (linha 9 do passo 5). Continuam valendo o token na URL e a resposta `{"sucesso": true}` à recusa.

**Atualização de 03/10/2026 (D4): o código deste passo está pronto**, e vai com o deploy da migração. Toda recusa — sem token, token que não confere, `cron.token` vazio — responde **401** em JSON (`{"erro": {"codigo": "nao_autenticado", ...}}`, com `WWW-Authenticate: Bearer`), e não mais `{"sucesso": true}`. O token vale por **`Authorization: Bearer <token>`** — o cabeçalho que o `/api/v4` já usa — **e continua valendo por `?token=`**, que é o que o crontab de hoje manda.

**O crontab de produção tem de mandar o token certo — o mesmo `cron.token` do `local.php` (passo 3) —, ou as rotinas param com 401.** Hoje ele o manda na query, e isso continua funcionando: nada no crontab precisa mudar para o deploy. **Passá-lo para o cabeçalho e fazer o cron avisar foi decidido em 06/10/2026** (o **D331**; lote de perguntas, pergunta 5, a opção (b)): no passo 9 do corte as 25 linhas passam a `curl -fsS -m <limite> -H "Authorization: Bearer <token>" "<url>"`, sem e-mail — `MAILTO=""` no crontab e o `|| logger -t rotinas` no fim de cada linha, o aviso no log do sistema (a resposta 128 (b), de 12/10/2026; a forma de 06/10/2026 trazia o `MAILTO` com endereço, e o endereço nunca veio) —, junto com a rotação da linha 9 do passo 5. O roteiro da troca, que não imprime o token, está lá.

O teste abaixo mostra que o código novo está no ar: **antes do deploy ele responde 200** (a recusa de antes, com cara de sucesso); **depois, 401**. Ele não traz token, então não roda rotina nenhuma:

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://franquiabv.com.br/api/cronjobs/npsContabilizar
```

E, depois de religar o cron, **nenhuma rotina pode estar recebendo 401, e alguma tem de estar recebendo 200**. A conta está no **passo 9 do `docs/procedimento-de-corte.md`**, e só lá, de propósito — duas cópias de uma receita divergem no primeiro dia em que alguém corrigir só uma. Ela marca os logs do nginx **antes** de religar e conta **depois**, em todos os `*.log` (o site tem log próprio por vhost, **D36**), e mostra só o código HTTP de cada pedido do crontab, nunca o token. Fora do corte — o cron sem parar —, a marca vale do mesmo jeito: tire-a logo depois do deploy e conte 10 minutos depois.

**Até a revisão de 03/10/2026 este passo trazia um `grep -c '" 401 '` no `/var/log/nginx/access.log`, com "tem de dar 0"** — e dava 0 também com as 25 rotinas recusadas, porque os pedidos do crontab vão para o log do vhost. O zero do sucesso e o zero do arquivo errado eram o mesmo número.

Se a conta mostrar 401: o crontab manda um token que não é o `cron.token` do `local.php`, e as rotinas estão paradas. Confira o `cron.token` com `php scripts/verificar-local-php.php` e o crontab com `crontab -l` — sem colar a saída em lugar nenhum.

---

## Checklist

- [ ] Passo 1 — arquivos fora do webroot, URLs retornando 404
- [ ] Passo 2 — logs apurados e `docs/incidente-lgpd-dia0.md` versionado
- [ ] Passo 3 — `local.php` criado, permissão 640, `verificar-local-php.php` sem falha fatal
- [ ] Passo 4 — deploy feito e os 5 testes de fumaça passaram
- [ ] Passo 5 — credenciais 1 a 9 rotacionadas (a 9, o token das rotinas: crontab e `cron.token` no mesmo passo)
- [ ] Passo 6 — endpoint /autenticacao/migrar-senhas bloqueado e logs apurados
- [ ] Passo 7 — crons respondendo 401 sem token (o código está pronto desde 03/10/2026: confira com o `curl` do passo 7, e o crontab mandando o mesmo `cron.token` — a conta do passo 9 do corte mostrando só 200, e mais de zero)
