Modelo de ameaças do Anotae
Versão: 1.0 — 2026-05-08
Audiência: mantenedores, auditores externos, profissionais de
saúde com interesse técnico, ANPD ao avaliar conformidade LGPD.
Este modelo combina STRIDE (security threats) com LINDDUN
(privacy threats). Acompanha
docs/CRYPTO-DESIGN.md e
docs/PRIVACY-IMPACT-ASSESSMENT.md.
1. Sistema sob análise
Escopo
Anotae v0.9.0-beta4 — aplicação desktop Python/PySide6 + extensão
de navegador MV3 que assiste profissionais SUS na digitação de
atendimentos. 100% local — sem servidor central, sem nuvem.
Componentes
┌─────────────────────────────────┐
│ Profissional SUS (operador) │ trust boundary
└────────────┬────────────────────┘
│ senha-mestra (digitada)
▼
┌─────────────────────────────────┐
│ App desktop Anotae (Python) │
│ ┌────────────────────────────┐ │
│ │ UI PySide6 │ │
│ ├────────────────────────────┤ │
│ │ Vault SQLite cifrado │ │ ← dados em repouso
│ │ (sqlite3mc + ChaCha20-Poly1305) │
│ ├────────────────────────────┤ │
│ │ NMH stdin/stdout │ │
│ └─────────┬──────────────────┘ │
└────────────┼────────────────────┘
│ JSON via stdin/stdout
│ (ou HTTP loopback 127.0.0.1)
▼
┌─────────────────────────────────┐
│ Extensão de navegador (TS MV3) │
│ ┌────────────────────────────┐ │
│ │ Service worker │ │
│ ├────────────────────────────┤ │
│ │ Content script │ │ → injeta em
│ └────────────────────────────┘ │ formulários SUS
└─────────────────────────────────┘
│
▼ (página do SUS no DOM)
e-SUS APS PEC / SISREG / SEI
Trust boundaries
- TB-1: humano ↔ app desktop (senha-mestra digitada).
- TB-2: app desktop ↔ extensão (NMH process boundary).
- TB-3: extensão isolated world ↔ página do SUS (page world).
- TB-4: filesystem local ↔ disco/backup (cifragem em repouso).
Atores
- Operador legítimo: profissional SUS com vault + senha-mestra.
- Atacante local privilegiado: alguém com acesso físico ao PC.
- Atacante local não-privilegiado: outro processo na mesma
máquina (malware, app comprometido, JS malicioso em outra aba).
- Atacante de rede: man-in-the-middle, phishing, etc.
(relevante apenas se o usuário viola a regra "100% local"
enviando vault.db para nuvem).
- Atacante de supply chain: modificação maliciosa de release
ou dependência.
Suposições (não modeladas)
- O sistema operacional é confiável (kernel não comprometido).
- A conta do usuário no SO não foi comprometida (sessão limpa).
- O navegador está em versão suportada e atualizada.
- O hardware não tem keylogger físico.
- A senha-mestra tem entropia ≥ 60 bits (passphrase 4+ palavras
diceware ou ≥16 chars aleatórios).
2. STRIDE — Ameaças de segurança
S — Spoofing
| ID |
Ameaça |
Mitigação |
Status |
| S-1 |
Usuário malicioso assume identidade do operador real |
Vault cifrado por senha-mestra; CFM 2.454/2026 obriga revisão humana antes de assinar |
✅ Mitigada |
| S-2 |
Extensão maliciosa se passa pela Anotae no NMH |
NMH allowlist ID determinístico (extension/chrome/manifest.json key MV3); origens validadas em manifest.json:allowed_origins |
✅ Mitigada |
| S-3 |
App desktop falso responde no HTTP loopback fallback |
Token efêmero gerado por boot; rejeita requests sem header correto; binding apenas em 127.0.0.1 |
✅ Mitigada |
| S-4 |
Site malicioso assume identidade do e-SUS/SISREG/SEI no host_permissions |
Mappings só ativam em domínios na whitelist do manifest.json |
✅ Mitigada |
T — Tampering
| ID |
Ameaça |
Mitigação |
Status |
| T-1 |
Adulteração do vault.db em disco |
ChaCha20-Poly1305 AEAD detecta tampering em descriptografia; SHA-256 do encounter individual em hash_sha256 (Migration 001) |
✅ Mitigada |
| T-2 |
Modificação de mensagens NMH em trânsito |
NMH usa stdin/stdout do processo filho — sem rede, sem MITM possível |
✅ Mitigada por design |
| T-3 |
Adulteração de release binário em distribuição |
Sigstore attestation (PEP 740) keyless via OIDC do GitHub (PR #32). Verificação por terceiros via Rekor |
✅ Mitigada |
| T-4 |
Adulteração de dependência via supply chain |
pip-audit + Dependabot + lock file com hashes (pyproject.toml) |
⚠️ Parcial — Renovate em paralelo pendente |
| T-5 |
Adulteração do audit log local |
Append-only por contrato (sem UPDATE/DELETE expostos pela API). Detalhes cifrados field-level. Não impede atacante com acesso ao SQLite raw — limitação aceita |
⚠️ Defesa em profundidade |
| T-6 |
Encounter hash não-verificado ao ler |
Rotina de verificação automática (parcial) ao carregar; testes em tests/security/test_crypto_invariants.py |
⚠️ Cobertura parcial |
R — Repudiation
| ID |
Ameaça |
Mitigação |
Status |
| R-1 |
Profissional nega ter editado encounter |
Audit log no vault registra encounter.{created,updated,deleted} com timestamp + actor_cns (PR #41). Histórico append-only visível em Vault → Histórico (Ctrl+H) |
✅ Mitigada |
| R-2 |
Atacante apaga audit log para esconder rastro |
Audit log no mesmo vault que os dados — se apagado, vault inteiro fica suspeito (operador percebe). Não há servidor independente — limitação aceita do modelo offline |
⚠️ Limitação aceita |
| R-3 |
Profissional nega ter usado IA na geração do texto |
CLAUDE.md §2.3 obriga registro de uso de IA no SOAP; CFM 2.454/2026. Disclaimer obrigatório em sei_inject_dialog.py |
✅ Mitigada |
| ID |
Ameaça |
Mitigação |
Status |
| I-1 |
Vault.db cai em mãos erradas |
ChaCha20-Poly1305 com chave derivada via Argon2id (m=64MiB t=3 p=1 default; fallback m=19MiB t=2 p=1) — força bruta inviável para senha com entropia adequada |
✅ Mitigada |
| I-2 |
Logs/stack traces expõem PII em mensagens de erro |
friendly_error_text() redige paths com username (PR #22); CLAUDE.md §2.1 proíbe logging de PII |
✅ Mitigada |
| I-3 |
Backup contém PII em texto claro |
Backups são cópias byte-a-byte do vault cifrado (PR #18) — mantém proteção. Hash SHA-256 adjacente para integridade |
✅ Mitigada |
| I-4 |
Telemetria local expõe PII |
Whitelist categórica de 29 eventos (anotae/core/telemetry.py); valores em extra limitados a string ≤64 chars + escalar; default OFF |
✅ Mitigada |
| I-5 |
Memory dump expõe master key descifrada |
secure_zero_bytes zera master key após derivar (best-effort em Python). Limitação reconhecida — Python não controla GC |
⚠️ Best-effort |
| I-6 |
Side channel timing em verificação de senha |
Comparação por hmac.compare_digest ao verificar token (PR de hardening anterior). Argon2id é constant-time |
✅ Mitigada |
| I-7 |
Anonimização inadequada em export |
3 níveis (none/weak/strong) com hash + salt (LGPD Art. 12); strong generaliza data e CIAP/CID. tests/unit/test_anonymizer.py cobre invariantes |
✅ Mitigada |
| I-8 |
Strings traduzíveis (.pot) expõem PII |
Análise estática só pega literais; runtime nunca entra no catálogo. Política em docs/explanation/i18n-roadmap.md |
✅ Mitigada |
D — Denial of Service
| ID |
Ameaça |
Mitigação |
Status |
| D-1 |
Argon2id parameters travam hardware modesto |
Detecção automática de RAM em crypto.detect_kdf_params() cai para FALLBACK em <4 GB; benchmark CLI anotae kdf benchmark ajuda diagnóstico |
✅ Mitigada |
| D-2 |
Vault.db gigante torna abertura inviável |
Vault tipicamente <100MB para 5 anos de produção. Sem limite hardcoded — operador é responsável |
⚠️ Aceita |
| D-3 |
OOM via payload absurdo na NMH/HTTP loopback |
Validação de tamanho em nmh/protocol.py (limite por mensagem); HTTP loopback rejeita >1MB |
✅ Mitigada |
| D-4 |
Lock SQLite congela por write longo |
journal_mode WAL + retry implícito do sqlite3 stdlib |
✅ Mitigada |
| D-5 |
Atacante força fallback Argon2id mais fraco |
Parâmetros KDF gravados em vault_meta no momento da criação — não negociáveis em open |
✅ Mitigada |
E — Elevation of Privilege
| ID |
Ameaça |
Mitigação |
Status |
| E-1 |
Plugin de mapping malicioso executa código arbitrário |
Plugins são apenas dados JSON (PR #35). Sem eval, sem expressão arbitrária. Engine de injeção interpreta selectors fixos |
✅ Mitigada por design |
| E-2 |
Template renderer com expressão maliciosa |
template_renderer.py usa whitelist de variáveis permitidas — sem code execution; usa string.Template ou regex próprio (não Jinja2) |
✅ Mitigada |
| E-3 |
Extensão escala privilégios ao app desktop |
NMH limita ações a uma whitelist de mensagens em nmh/protocol.py; sem comando shell arbitrário |
✅ Mitigada |
| E-4 |
Subprocess no smoke E2E vaza arg injection |
Argumentos hardcoded com subprocess.run([list, ...]) — sem shell=True |
✅ Mitigada |
3. LINDDUN — Ameaças de privacidade
LINDDUN cobre privacy threats complementares a STRIDE.
L — Linkability
| ID |
Ameaça |
Mitigação |
Status |
| L-1 |
Encounter linkado a paciente específico em export anonimizado |
Hash com salt aleatório por export (anonymizer._make_handle_hash) — IDs ANON-XXXXXXXX não são reutilizáveis entre exports |
✅ Mitigada |
| L-2 |
display_handle do paciente vaza identidade em vault.db |
Documentado em RFC-003 — handle é controlado pelo profissional (ex.: PAC-001); recomendação não usar nome real |
⚠️ Contrato com usuário |
| L-3 |
Audit log linka ações ao operador via actor_cns |
Por design — auditoria interna requer identificação. CNS do operador (não do paciente) é PII fraca |
✅ Aceita |
I — Identifiability
| ID |
Ameaça |
Mitigação |
Status |
| Id-1 |
Vault descriptografado revela identidade real do paciente |
Exigir senha-mestra para descriptografar — controle de acesso forte |
✅ Mitigada |
| Id-2 |
Backup gera nome de arquivo com data identificável |
Nomes vault_<timestamp>.db — apenas data, não conteúdo |
✅ Mitigada |
| Id-3 |
Texto SOAP em audit details revela identidade indiretamente |
Audit details são metadados estruturados (counts, IDs) — texto livre não é gravado |
✅ Mitigada por design |
| Id-4 |
Telemetria local + cross-reference identifica usuário |
Telemetria é só categórica + opt-in + local — atacante já no PC do usuário sabe quem é |
✅ Aceita |
N — Non-repudiation
| ID |
Ameaça |
Mitigação |
Status |
| N-1 |
Audit log impede o profissional de "negar" edição que fez |
Por design — em contexto clínico isso é desejável (CFM exige rastreabilidade) |
✅ Aceita |
| N-2 |
Sigstore attestation impede mantenedor de "negar" release |
Por design — Rekor é log público append-only de identidade do signer (workflow do GitHub) |
✅ Aceita |
D — Detectability
| ID |
Ameaça |
Mitigação |
Status |
| D-1 |
Atacante detecta uso do Anotae por arquivo ~/.anotae/vault.db em disco |
Path padronizado intencional para suporte. Avançado: usuário pode customizar via config.toml:vault_path |
⚠️ Aceita |
| D-2 |
Tráfego de rede revela uso (extensão fala com app local) |
Sem rede — NMH é stdin/stdout; HTTP loopback nunca sai de 127.0.0.1 |
✅ Mitigada por design |
| D-3 |
Atacante observa horários de uso via timestamps de modificação do vault |
Recomendação no RIPD: criptografar disco inteiro (FileVault/BitLocker/LUKS) para esconder padrão |
⚠️ Defesa em profundidade |
Já coberto em STRIDE/I (Information Disclosure). Sem novas ameaças
únicas a LINDDUN.
U — Unawareness
| ID |
Ameaça |
Mitigação |
Status |
| U-1 |
Usuário não sabe que telemetria local existe |
Default OFF; aba "Privacidade" no AboutDialog informa explicitamente; anotae telemetry status na CLI |
✅ Mitigada |
| U-2 |
Usuário não sabe que audit log existe dentro do vault |
Vault → Histórico (Ctrl+H, PR #37) expõe interface; docs/reference/schema.md documenta a tabela |
✅ Mitigada |
| U-3 |
Usuário não sabe quais dados saem ao gerar export |
Aviso amarelo no ExportEncounterDialog: "Você está exportando este atendimento para um arquivo... O Anotae nunca envia para servidor — você escolhe onde salvar" |
✅ Mitigada |
| U-4 |
Usuário não sabe das implicações de sincronizar vault para nuvem |
RIPD seção "Riscos" + aviso no BackupsDialog: "Não use serviços de nuvem genéricos sem revisar a política de privacidade" |
✅ Mitigada |
N — Non-compliance
| ID |
Ameaça |
Mitigação |
Status |
| Nc-1 |
LGPD Art. 12 (anonimização) inadequada em pesquisa |
3 níveis explícitos com explicação no UI; testes invariantes em tests/unit/test_anonymizer.py |
✅ Mitigada |
| Nc-2 |
LGPD Art. 7º (base legal) — uso pessoal só com consentimento |
Anotae é ferramenta-do-profissional, não tem usuário paciente — base legal é "exercício regular de direitos" do profissional. Documentado no RIPD |
✅ Mitigada |
| Nc-3 |
CFM 2.454/2026 — uso de IA exige registro |
Disclaimer fixo no editor; campo de metadata metadata.ai_assisted: bool em encounters; sei_inject_dialog reforça humano-no-loop |
✅ Mitigada |
| Nc-4 |
Anvisa SaMD classification — Anotae não é dispositivo médico |
CLAUDE.md §0 + UI explicitamente marca "Anotae não é PEP, não é SaMD, não substitui julgamento clínico" |
✅ Mitigada por declaração |
| Nc-5 |
AGPL §6 (acesso ao código-fonte) |
AboutDialog → Licença AGPL-3.0 com texto integral + link GitHub. Sigstore prova proveniência |
✅ Mitigada |
4. Resumo executivo
| Severidade |
Ameaças mitigadas |
Aceitas/Limitadas |
Total |
| Alta |
24 |
2 |
26 |
| Média |
9 |
4 |
13 |
| Baixa |
3 |
1 |
4 |
| Total |
36 |
7 |
43 |
Top 5 ameaças não-totalmente-mitigadas (em ordem de risco)
- I-5 (memory dump da master key) — Python não controla GC,
secure_zero_bytes é best-effort. Mitigação: documentar para
usuário não rodar Anotae em sessão suspeita.
- R-2 (audit log apagável por atacante com SQLite raw) —
limitação do modelo offline sem servidor independente.
Aceita; usuário percebe (vault corrompido).
- T-5 (audit log adulterado) — mesma raiz que R-2. Defesa em
profundidade com cifragem de detalhes.
- T-4 (supply chain) — pip-audit + Dependabot, mas Renovate em
paralelo ainda pendente.
- D-1 (detecção de uso por path padrão) — usuário pode mudar.
Próximos passos para reduzir risco
5. Status de auditoria externa
A auditoria externa de segurança ainda não foi realizada
(item v0.9 do BACKLOG, dependente de financiamento).
A tabela agregada de status por ameaça vive em
docs/explanation/auditoria-checklist.md
seção "Status agregado da auditoria" — formato pronto para o
auditor preencher quando a auditoria começar:
— não auditado (default)
✅ verificado
⚠ gap parcial
❌ falha
🔄 refinamento sugerido
Após a auditoria, atualize este documento adicionando uma seção
"Histórico de auditorias" listando data, escopo, resultado.
6. Como atualizar este documento
Este threat model é vivo. Atualize quando:
- Adicionar novo componente (revise trust boundaries)
- Mudar protocolo NMH/HTTP loopback
- Mudar parâmetros KDF
- Adicionar nova fonte de plugins (mapping_plugin.py)
- Auditoria externa apontar gap
Bump da versão no topo. Adicione data + assinatura GPG do mantenedor
em git tag quando promover novo modelo a release.