Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ test-results
playwright-report
*.pdf
!examples/sample_input.pdf
# Nunca na imagem: variações de .env (podem ter API_TOKEN), aprendizado do decisor local e estado
.env.*
!.env.example
learning
*.jsonl
features_cache.json
state
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
# Nomes extras aceitos nos cabeçalhos Host/Origin quando não há API_TOKEN (proteção contra DNS rebinding e
# CSRF). Loopback e APP_HOST sempre valem. "*" desliga a checagem (não recomendado). Ex.: sentry.intranet
# ALLOWED_HOSTS=

# --- Local decision model / Decisor local (optional, docs/decisions.md) ------------------------
# Requires / Exige: pip install -e ".[laya]"
# DECISION_ENGINE=off # laya | off
# DECISION_MODE=shadow # shadow (observa) | assist (participa; só com perfil aprovado)
# LAYA_MODEL=convaiinnovations/laya-multilingual
# LAYA_DEVICE=cpu
# LAYA_REVISION=reviewed # fixa os commits revisados do modelo
# LEARNING_ENABLED=0 # 1 = aprende com as correções do revisor (dados ficam nesta máquina)
# PRIVIO_LEARNING_DIR=./learning
# Maximum upload size in MB / Tamanho máximo de upload em MB
# MAX_UPLOAD_MB=500
# Where uploaded PDFs are stored / Onde os PDFs enviados são guardados (default: ./WEB_INPUT)
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ on:
description: "Tag existente a publicar (ex.: v5.1.0)"
required: true

# Mínimo privilégio: só o job que publica recebe escrita. O job de testes instala dependências de
# terceiros e não deve ter um token capaz de alterar o repositório.
permissions:
contents: write
contents: read

jobs:
tests:
Expand All @@ -27,6 +29,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ env.TAG }}
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
Expand Down Expand Up @@ -55,6 +58,8 @@ jobs:
release:
needs: tests
runs-on: ubuntu-latest
permissions:
contents: write
timeout-minutes: 10
env:
TAG: ${{ github.event.inputs.tag || github.ref_name }}
Expand All @@ -63,6 +68,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ env.TAG }}
persist-credentials: false
- name: Extrair notas do CHANGELOG
shell: python
run: |
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,6 @@ playwright-report/
.DS_Store
Thumbs.db
INFORMATION_BASE/

# Automelhoramento do decisor local: exemplos rotulados e perfis (contêm texto de endereços)
learning/
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ existentes em documentação, interface ou mensagens.
| I-07 | Não enfraqueça a verificação pós-tarja nem o aviso permanente de que a detecção por IA é probabilística | Revisão de PR |
| I-08 | Testes **não** podem exigir Ollama, GPU nem rede: o LLM e o YOLO são simulados | `tests/conftest.py`, `tests/sentry_testkit.py` |
| I-09 | Todo alerta exige revisão: use `add_review(página, motivo)` ou `add_document_review(motivo)`, nunca `self.alerts.append` direto. Falha do OCR é detectada por `OCREngine.failure_count` (resultado vazio por erro ≠ página sem CPF) | `tests/test_fail_closed_ocr_address.py` |
| I-10 | O decisor local (`utils/decisions`) **nunca reduz proteção**: só acrescenta tarja ou pede revisão (`policy.combine_address_decision`). Modo `assist` só com perfil aprovado pelo portão; o texto enviado ao modelo e aos exemplos passa por `normalize_state` | `tests/test_decisions.py` |

## 3. Arquitetura: três processos

Expand Down Expand Up @@ -61,6 +62,8 @@ Navegador ─► gatekeeper.py (:8000, opcional) ─proxy─► app_service.py (
| `utils/session.py` | Pastas da tarefa, logs, exportação, reconstrução do PDF e tarja nativa (`apply_native_pdf_redactions`) |
| `utils/verifier.py` | Verificação pós-tarja: relê o PDF final e confronta o original (`find_uncovered_cpfs`) |
| `utils/pii.py` | Mascaramento de CPF em logs |
| `utils/decisions/` | Decisor local (Laya) e automelhoramento: perguntas, motor, regra de combinação, treino com portão de qualidade e versões. Guia: [docs/decisions.md](docs/decisions.md) |
| `utils/auth.py` | Autenticação por `API_TOKEN` com sessão aleatória (app e gatekeeper): `?token=` só abre a sessão e sai da URL |
| `utils/net_guard.py` | Checagem de `Host`/`Origin` (anti *DNS rebinding* e CSRF) usada pelo app e pelo gatekeeper; `ALLOWED_HOSTS` |
| `templates/index.html` | Editor web autocontido; textos no objeto `I18N` (pt-BR padrão, en-US), chaves conforme `docs/brand/UX_SPEC.md`; renderizador Markdown próprio que **nunca** injeta HTML |
| `templates/gatekeeper.html` | Página do painel quando o app está desligado |
Expand Down
38 changes: 30 additions & 8 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ leia o [AGENTS.md](AGENTS.md): invariantes, arquitetura e como rodar os testes.
**P2** (robustez, operação e experiência) · **P3** (melhoria e refatoração).
- Itens marcados como **visão** vêm do roteiro do produto: não têm data nem promessa de entrega.
- As referências `arquivo:linha` valem para o estado de referência e podem se deslocar.
- Estado de referência: `5.2.0`, 2026-10-04. Itens B-40 a B-65 vieram de uma revisão completa do código e da redação do manual nessa data.
- Estado de referência: `5.3.0`, 2026-10-04. Itens B-40 a B-65 vieram de uma revisão completa do código e da redação do manual nessa data.

## Resumo

| Prioridade | Abertos | Foco |
|---|---|---|
| 🔴 P0 | 2 | Proteger o serviço local contra outros sites no mesmo navegador e o token de acesso |
| 🔴 P0 | 0 | Proteger o serviço local contra outros sites no mesmo navegador e o token de acesso |
| 🟠 P1 | 13 | Falhar fechado em mais situações (OCR, respostas da IA, tarjas manuais, rotação) |
| 🟡 P2 | 12 | Robustez dos processos, limites de recursos, Docker e editor |
| 🟡 P2 | 11 | Robustez dos processos, limites de recursos, Docker e editor |
| 🔵 P3 | 5 | Limpeza de código, lint e marca |
| 🧠 Roteiro | 9 | Detecção configurável (nomes, telefones, RG...), GLiNER e decisor Laya que aprende (B-71 a B-79) |
| 🔭 Visão | 5 | Auditoria, políticas e novos módulos SENTRY |

---
Expand All @@ -42,12 +43,29 @@ publicar uma versão que mexa nessas áreas:

---

## 🧠 Detecção configurável e decisor que aprende (roteiro)

Objetivo: o usuário escolhe **o que tarjar** (inclusive nomes), com recurso mínimo (modo leve, sem GPU),
tudo local, e o sistema **melhora com as correções do revisor** sem nunca reduzir a proteção sozinho.
Arquitetura: `detectar (regras + GLiNER + LLM) → decidir (Laya) → revisão → tarja`. Guia do decisor:
[docs/decisions.md](docs/decisions.md).

| ID | Prioridade | Item | Depende de |
|---|---|---|---|
| B-71 | P1 | **Política configurável**: o usuário escolhe os tipos (CPF, endereço, nome, telefone, e-mail, RG...) na interface e na CLI; arquivo de política versionado; registrar qual política foi aplicada em cada tarefa | — |
| B-72 | P1 | **Novos tipos por regras com validação**: RG, CNH, PIS/NIS, título de eleitor, telefone, e-mail, CEP, placa, cartão (dígito verificador quando houver), com testes e casos falsos positivos conhecidos | B-71 |
| B-73 | P1 | **Nomes e rótulos livres com GLiNER** (modo leve na CPU): o usuário escreve o rótulo ("nome de pessoa", "número de processo"); trechos mapeados para as caixas do OCR; confiança baixa → revisão | B-71 |
| B-74 | P2 | **Exceções decididas pelo Laya**: "nome a proteger" × "servidor/signatário público" (LAI), com perguntas próprias, treino e portão | B-73, B-70 |
| B-75 | P2 | **Perfis de política e roteamento**: perfis prontos (Transparência/LAI, Saúde, Jurídico, Só CPF) e o Laya sugerindo o perfil pelo tipo de documento | B-71, B-70 |
| B-76 | P2 | **Ajuste fino dos pesos do Laya** (hoje só a cabeça e os limiares são treinados): quando houver rotina pública de treino, ou com treinador próprio + LoRA, sempre com o mesmo portão | B-70 |
| B-77 | P2 | **Painel de aprendizado na interface**: modo atual, perfil ativo e métricas, botão de treinar e de voltar versão, contador de correções guardadas | B-70 |
| B-78 | P1 | **Avaliação realista do decisor**: conjunto fictício mais difícil que o sintético (endereços ambíguos, OCR ruidoso) para o portão de qualidade | B-70, B-01 |
| B-79 | P3 | **Instalação offline e mais rápida**: modelo em pasta local / `HF_HUB_OFFLINE`, versão ONNX na CPU (`laya.onnx_agent`), cache compartilhado | B-70 |

## 🔴 P0 · Segurança e vazamento

| ID | Item | Referência |
|---|---|---|
| B-41 | **Não aceitar `API_TOKEN` na *query string* nem guardar o token bruto em cookie.** `?token=` vai para o log do uvicorn, o histórico do navegador e o log do proxy do gatekeeper. Direção: trocar por cookie de sessão aleatório (`HttpOnly`, `SameSite`, `secure` com HTTPS), redirecionar sem o `?token=` e mascarar a *query* nos logs | `app_service.py:239-244`, `gatekeeper.py:205-234` |
| B-42 | **Autenticar o painel do gatekeeper e restringir `kill_port_owner`.** Desde B-40 o `/api/toggle` recusa outros sites (`Origin`/`Host`), mas o painel continua sem autenticação própria; `kill_port_owner` faz `taskkill /F /T` em qualquer processo na porta. Direção: aceitar `API_TOKEN` no painel e encerrar só o processo filho conhecido | `gatekeeper.py:88-102`, `gatekeeper.py:189-198` |

## 🟠 P1 · Resultado errado ou tarja a menos

Expand All @@ -70,7 +88,6 @@ publicar uma versão que mexa nessas áreas:

| ID | Item | Referência |
|---|---|---|
| B-52 | **Drenar ou redirecionar stdout/stderr do app no gatekeeper.** O app é aberto com `PIPE` que nunca é lido; quando o buffer enche, o app trava, o *watchdog* o mata e as tarefas ficam "Interrompidas". Direção: `DEVNULL`, arquivo de log ou thread leitora | `gatekeeper.py:114-120` |
| B-53 | **Detectar a morte do worker e permitir cancelar tarefas.** Worker morto (OOM, *kill*) deixa a tarefa parada para sempre; não há rota de cancelamento nem encerramento dos workers no desligamento. Direção: monitorar `exitcode`, `POST /cancel/{id}` e *lifespan* | `app_service.py:96-105`, `app_service.py:205-264` |
| B-54 | **Limitar recursos por documento e fechar *handles*.** `Image.MAX_IMAGE_PIXELS = None` desliga a proteção contra bombas de descompressão; não há limite de páginas nem de pixels; documentos `fitz` e *FileHandlers* da CLI não são fechados em exceção | `utils/session.py:12`, `utils/transform_pdf_to_img.py:21-61`, `main.py:647-662` |
| B-55 | **Tirar o I/O bloqueante do upload do *event loop* e fazer o proxy repassar o corpo em *streaming*.** Uploads grandes bloqueiam o loop (inclusive o `/internal/update` dos workers) e falham pelo gatekeeper (corpo inteiro em memória, *timeout* de 30 s) | `app_service.py:302-329`, `gatekeeper.py:210-222` |
Expand Down Expand Up @@ -98,8 +115,8 @@ publicar uma versão que mexa nessas áreas:
| ID | Item | Referência |
|---|---|---|
| B-30 | **Trilha de auditoria** (SENTRY Audit): hashes e proveniência de modelo/política sem guardar valores sensíveis | [docs/brand/LGPD_PRODUCT_POSITIONING.md](docs/brand/LGPD_PRODUCT_POSITIONING.md) |
| B-31 | **Motor de políticas** configurável (o que detectar e como proteger) | idem |
| B-32 | Novos tipos de dado (SENTRY Detect): nomes, telefones, e-mails, RG, dados bancários | [docs/limitations.md](docs/limitations.md) |
| B-31 | **Motor de políticas** configurável (o que detectar e como proteger). Primeira etapa no roteiro: B-71 e B-75 | idem |
| B-32 | Novos tipos de dado (SENTRY Detect): nomes, telefones, e-mails, RG, dados bancários. Primeira etapa no roteiro: B-72 e B-73 | [docs/limitations.md](docs/limitations.md) |
| B-33 | Mascaramento e pseudonimização (SENTRY Mask / Transform) | [README.md](README.md#-visão-futuro-não-implementado) |
| B-34 | Fronteira de privacidade para IAs/APIs externas (SENTRY Gateway) | idem |

Expand All @@ -109,6 +126,11 @@ publicar uma versão que mexa nessas áreas:

| ID | Item | Versão | Teste |
|---|---|---|---|
| B-41 | Token na URL só abre a sessão (redireciona sem o token); cookie de sessão aleatório (`HttpOnly`, `SameSite=Strict`, `Secure` com HTTPS) no lugar do token cru; `token=` mascarado nos registros (`utils/auth.py`) | 5.3.0 | `tests/test_auth.py`, `tests/test_service_endpoints.py` |
| B-42 | Painel do gatekeeper exige `API_TOKEN` quando definido; `kill_port_owner` só encerra um `app_service.py` órfão e não usa shell | 5.3.0 | `tests/test_auth.py`, `tests/test_gatekeeper.py` |
| B-52 | Saída do app herdada pelo gatekeeper (o PIPE nunca lido travava o app) | 5.3.0 | `gatekeeper.py` |
| B-66 | Revisão de segurança independente (2026-10-04): workflow de Release com privilégio mínimo; `/purge` apaga `decisions.json`; exemplos de treino apagados com a tarefa e pasta padrão fixa; `.dockerignore` sem `.env.*` nem `learning/`; versão exata do `laya` e commit do modelo fixado no projeto; SHA-256 do modelo YOLO conferido antes de carregar; CPF com separadores incomuns mascarado; resposta crua do LLM fora do log; Origem anti-CSRF conferida também pela porta | 5.3.0 | `tests/test_decisions.py`, `tests/test_yolo_engine.py`, `tests/test_pii.py`, `tests/test_net_guard.py`, `tests/test_service_endpoints.py` |
| B-70 | **Decisor local Laya com automelhoramento** para o tipo de endereço: modos sombra/assistido, regra que nunca reduz proteção, texto minimizado, cabeça treinável sobre o Laya congelado, limiares aprendidos, portão de qualidade, perfis versionados com rollback, captura das correções do revisor (opt-in) e CLI `python -m utils.decisions`. Treino real na CPU, conjunto realista: AUC 0,83 → 0,91 | 5.3.0 | `tests/test_decisions.py` |
| B-40 | Validar `Host` (anti *DNS rebinding*) e `Origin`/`Sec-Fetch-Site` (anti CSRF) no app e no gatekeeper quando não há `API_TOKEN`; nova variável `ALLOWED_HOSTS` | 5.2.0 | `tests/test_net_guard.py` |
| B-43 | Falha do Tesseract (nas duas escalas) conta em `OCREngine.failure_count` e vira alerta da página no OCR, nos recortes e na verificação; junção das metades não perde a metade de baixo | 5.2.0 | `tests/test_fail_closed_ocr_address.py` |
| B-49 | Tipo de endereço do LLM normalizado (caixa, acento, sinônimos); rótulo desconhecido é tarjado como pessoal e manda a página para revisão; resposta sem a lista `addresses` falha fechado | 5.2.0 | `tests/test_fail_closed_ocr_address.py` |
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,39 @@ Todas as mudanças relevantes ficam registradas aqui. Formato baseado no

## [Não publicado]

## [5.3.0] - 2026-10-04

Decisor local que aprende (experimental, desligado por padrão) e uma rodada completa de segurança: os dois
itens P0 restantes (B-41, B-42) e todos os achados de uma revisão de segurança independente.

### Segurança
- **Token de acesso não vaza mais por URL, histórico ou registros (B-41).** `?token=` só abre a sessão: o servidor redireciona para a mesma página sem o token e grava um **cookie de sessão aleatório** (`HttpOnly`, `SameSite=Strict`, `Secure` com HTTPS). O token cru nunca vai para o cookie, e `token=` é mascarado nos registros do app, do gatekeeper e do uvicorn. Scripts seguem com o cabeçalho `X-API-Token`. Nova variável `SESSION_TTL_HOURS` (padrão 12). **Atenção:** quem usava o cookie antigo precisa abrir `?token=` de novo.
- **Painel do gatekeeper protegido (B-42).** Com `API_TOKEN`, `/gatekeeper`, `/manage` e `/api/*` exigem o token. O `kill_port_owner` só encerra um `app_service.py` órfão (antes matava qualquer programa na porta) e não usa mais shell.
- **Revisão independente (B-66):**
- workflow de Release com privilégio mínimo (escrita só no job que publica) e `persist-credentials: false`;
- `/purge` apaga também o `decisions.json`;
- exemplos de treino apagados junto com a tarefa de origem (e pela retenção), com pasta padrão fixa no projeto;
- `.dockerignore` exclui `.env.*` e `learning/`;
- versão exata do `laya` e commit do modelo fixado no próprio projeto (modelo remoto sem commit fixo é recusado);
- **SHA-256 do modelo YOLO conferido antes de carregar** (arquivo `.pt` pode executar código): arquivo diferente não é carregado e o documento vai para revisão; `YOLO_MODEL_SHA256` para modelos próprios;
- CPF com separadores incomuns (`529-982-247-25`, `529,982,247-25`...) também é mascarado nos registros;
- a resposta crua do LLM de visão saiu do log da tarefa;
- a Origem anti-CSRF é conferida também pela porta (outro app local não dispara escritas).

### Corrigido
- O gatekeeper abria o app com saída num *pipe* nunca lido: com o buffer cheio, o app travava e as tarefas eram interrompidas (B-52).

### Adicionado
- **Decisor local com automelhoramento (experimental, desligado por padrão).** Nova camada `utils/decisions/` usa o [Laya](https://huggingface.co/convaiinnovations/laya) (modelo de decisão local, Apache-2.0, roda na CPU em ~0,1 s por decisão) para dar uma segunda opinião calibrada sobre o tipo de cada endereço. Ponto central: o decisor **nunca reduz proteção**; só acrescenta tarja ou pede revisão.
- **Modos:** `shadow` (só observa e registra, o padrão) e `assist` (participa, só com perfil aprovado).
- **Treinamento:** o Laya fica congelado e uma cabeça leve (regressão logística por método de Newton sobre 9 perguntas respondidas numa passada) aprende a combinar as respostas; os limiares de decisão também são aprendidos.
- **Automelhoramento:** com `LEARNING_ENABLED=1`, as tarjas finais do revisor viram exemplos rotulados. `python -m utils.decisions train` treina um candidato que só vira ativo se passar no **portão de qualidade** (AUC, precisão, NPV e não piorar o perfil ativo); perfis versionados com `rollback` e `purge`.
- **Privacidade:** tudo local; o texto é minimizado (CPF mascarado, dígitos trocados por `0`); exemplos em `PRIVIO_LEARNING_DIR`, fora do git.
- **Rede corporativa:** o `truststore` faz o download do modelo funcionar atrás de proxy com inspeção TLS.
- **Portão com conjunto realista** (36 endereços fictícios escritos à mão, fora do treino), porque o conjunto gerado superestimava a qualidade: um primeiro perfil aprovado só com ele errou em textos reais.
- Treino real (CPU), conjunto realista: AUC 0,83 sem treino → **0,91** treinado; precisão 0,92 e NPV 0,88 nas decisões firmes; 17% dos casos ficam incertos e vão para revisão.
- Instalação opcional (versões exatas): `pip install -e ".[laya]"` ou `requirements-laya.txt`. Guia: `docs/decisions.md`. Roteiro da detecção configurável (nomes, telefones, RG, perfis) no BACKLOG (B-71 a B-79).

## [5.2.0] - 2026-10-04

Versão de segurança e confiabilidade: o pipeline falha fechado em mais situações e o serviço local passa a
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ authors:
- family-names: Gambati
given-names: Joberth Firmino
alias: Yiuky
version: 5.2.0
version: 5.3.0
date-released: 2026-10-04
license: AGPL-3.0-or-later
repository-code: "https://github.com/Yiuky/PrivioSentry"
Expand Down
3 changes: 2 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ The LGPD separates *personal data* from *sensitive personal data* (health, biome
4. Asks the vision LLM to discover **addresses** on each page and classify them (personal / professional / secondary); only *personal* addresses are redacted, via a deterministic match against the OCR words.
5. Produces a final PDF (native-text redaction of the original, or rasterized pages) plus a JSON of redaction boxes.
6. Runs a **post-redaction verification**: re-reads the output and cross-checks the *original* (OCR at higher DPI) to make sure every CPF found is covered by a redaction.
7. Provides a **web editor** to add, move, delete and approve redactions before generating the final PDF.
7. Optionally, a **local decision model** ([Laya](https://huggingface.co/convaiinnovations/laya), Apache-2.0, CPU-friendly) gives a calibrated second opinion on addresses and **learns from reviewer corrections**; a new profile only goes live after passing a quality gate, and it never removes a redaction. Off by default; see [docs/decisions.md](docs/decisions.md) (Portuguese).
8. Provides a **web editor** to add, move, delete and approve redactions before generating the final PDF.

## Requirements

Expand Down
Loading
Loading