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
3 changes: 1 addition & 2 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1713,6 +1713,5 @@ Backlog priorizado na seção final do relatório; conferência por `python3 scr
- D5 tem consequência entre UBS: o pedido de um coordenador revoga os tokens e apaga as visitas de **todos** os donos daquele `deviceId`, inclusive ACS de fora da UBS dele que tenham usado o mesmo aparelho. É do desenho (a revogação é do aparelho, não da conta), mas o diálogo de confirmação do admin não diz isso; o texto foi fixado pelo plano e não mudou.
- O diário é de uma vaga só: um pedido mais novo sobrescreve a confirmação pendente de um mais antigo (o `wipe_confirmed` do antigo se perde). Se a gravação de `erased` e a confirmação falharem juntas, o diário fica em `erasing`, mas a retomada não apaga de novo: o pedido já apagado nesta execução, ou um `deviceId` diferente do diário (o apagador o troca no último passo), contam como limpeza feita, e só a confirmação é reenviada (`remote_wipe_test.dart`, `remote_wipe_ui_test.dart`, "diário preso em erasing").
- Escritas tardias do cache da microárea e do cursor de `pull`: o apagador sela `MicroAreaDirectory` e o `VisitPullService` de cada escopo (`LocalWriteSeal`) antes de destruir o banco, e espera (até 5 s) a gravação do cache que já tinha começado; a resposta que volta da rede depois não lê, não grava e não reabre o arquivo, e o selo do cache só abre no próximo login (`remote_wipe_late_writes_test.dart`, banco real FFI: nenhuma chave nova, arquivo ausente, e o cache volta a gravar depois de "Entrar de novo"). Fica aberto: `MicroAreaCacheStore` tem a própria instância de `SecureStorageDatabaseKeyStore`, e no primeiro uso depois de uma destruição (ou da instalação) o `readOrCreate` dele pode correr com o do `VisitDatabase` e gerar duas chaves; na prática a lista vem da rede antes de gravar, mas nada garante a ordem.
- Teste de dispositivo que já falhava antes desta branch (medido na base dela): "banco v6 ... migra para v7" em `apps/acs/integration_test/encrypted_storage_test.dart` espera `user_version` 7 e o schema é 8.
- O motivo escolhido não aparece no histórico do app admin (só no servidor); se a recarga da lista falha depois de um pedido bem-sucedido, a tela mostra a falha genérica.
- Conferidor `scripts/qa/cobertura_prd.py --final` falha por uma linha do RNF04 sem negrito no status; falhava igual com as mudanças desta issue guardadas (medido na base da branch).
- Duas falhas anteriores a esta branch foram corrigidas depois do merge (2026-10-17): o teste de aparelho "banco v6 … migra" em `encrypted_storage_test.dart` esperava o schema 7 e agora acompanha a v8 (8/8 no `emulator-5554`); e `scripts/qa/cobertura_prd.py --final` recusava as linhas RNF04, CRIPTO-TRAEFIK-BACKEND e R2 da auditoria do PRD, que tinham texto depois do status em negrito (o texto foi para a coluna de evidência; 64 IDs, ok).
6 changes: 3 additions & 3 deletions apps/acs/integration_test/encrypted_storage_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ void main() {
await EncryptedLocalDatabase.deleteDatabaseFile(databaseName);
});

test('banco v6 criado no aparelho migra para v7: as linhas vão para a QUARENTENA, cache e cursor ficam', () async {
test('banco v6 criado no aparelho migra para a versão atual: as linhas vão para a QUARENTENA, cache e cursor ficam', () async {
final key = generateDatabaseKey();
await createV6Database(
databaseName: databaseName,
Expand Down Expand Up @@ -310,10 +310,10 @@ void main() {
expect(await storage.legacy.load(), hasLength(2), reason: 'o save de um dono não apaga a quarentena');
expect(await storage.countsByOwner(), isEmpty);

// O schema ficou na v7, com `owner` nulo nas linhas antigas; o resto da v6 intacto.
// O schema ficou na versão atual (v8, #79), com `owner` nulo nas linhas antigas; o resto da v6 intacto.
final db = await storage.database.open();
expect(await db.getVersion(), EncryptedLocalDatabase.schemaVersion);
expect(EncryptedLocalDatabase.schemaVersion, 7);
expect(EncryptedLocalDatabase.schemaVersion, 8);
final semDono = await db.rawQuery('SELECT COUNT(*) AS n FROM offline_visits WHERE owner IS NULL');
expect(semDono.single['n'], 2);
expect((await db.query('micro_area_cache')).single['patient_id'], seedPatientId);
Expand Down
6 changes: 3 additions & 3 deletions docs/auditoria-prd/2026-10-15-lacunas-prd.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ próprio; o conferidor exige apenas os IDs do PRD.
| CRIPTO-APP-LOCAL | SQLCipher com chave derivada de PIN/biometria | **parcial** | SQLCipher com chave aleatória de 32 bytes guardada no Keystore (`apps/acs/lib/core/security/database_key_store.dart:31`); o arquivo documenta por que não usa PBKDF2 de PIN (`:8`). | Divergência do PRD (§4.2.3 pede chave derivada de PIN/biometria por PBKDF2): atualizar o PRD com a decisão ou amarrar a chave à biometria. |
| CRIPTO-APP-CACHE | SQLCipher com chave rotativa | **entregue** | Rotação por evento (#81, D6): `VisitDatabase.destroy` troca a chave no "Limpar este aparelho" e na limpeza remota; decisão de não rotacionar por calendário registrada em `spec/lgpd_design.md` §5.1; `device_wipe_test.dart`, e2e `acs_remote_wipe_e2e.sh` (chave trocada, banco reaberto vazio). | |
| CRIPTO-APP-TRAEFIK | TLS 1.3 app ↔ Traefik | **entregue** | Traefik com `minVersion: VersionTLS13` (`infra/docker/traefik/dynamic/tls.yaml:22`); `scripts/qa/tls_invariants.sh` OK e `openssl s_client -tls1_2` na 443 recusado (2026-10-15). | Certificado Let's Encrypt em produção (hoje CA de dev). |
| CRIPTO-TRAEFIK-BACKEND | mTLS Traefik ↔ backend | **parcial** (2026-10-16, #78) | Traefik → backend em TLS 1.3 de mão única com a cadeia verificada: o backend escuta TLS na 8080 interna (`API_TLS_*`, `ApiTlsConfig`), o Traefik disca por `https` com o `serversTransport` `backend-tls` (`rootCAs` + `serverName`, sem `insecureSkipVerify`). Provado pelo Grupo E de `tls_invariants.sh`. | mTLS: bloqueado pelo Serverpod 3.4.13, que sobe o socket pelo `relic_io` (`HttpServer.bindSecure` sem `requestClientCertificate`); decisão registrada em `PROGRESS.md`. |
| CRIPTO-TRAEFIK-BACKEND | mTLS Traefik ↔ backend | **parcial** | #78 (2026-10-16): Traefik → backend em TLS 1.3 de mão única com a cadeia verificada: o backend escuta TLS na 8080 interna (`API_TLS_*`, `ApiTlsConfig`), o Traefik disca por `https` com o `serversTransport` `backend-tls` (`rootCAs` + `serverName`, sem `insecureSkipVerify`). Provado pelo Grupo E de `tls_invariants.sh`. | mTLS: bloqueado pelo Serverpod 3.4.13, que sobe o socket pelo `relic_io` (`HttpServer.bindSecure` sem `requestClientCertificate`); decisão registrada em `PROGRESS.md`. |
| CRIPTO-POSTGRES | Criptografia no PostgreSQL | **entregue** | AES-256-GCM na aplicação com `HEALTH_DATA_ENCRYPTION_KEY` (decisão §2.2.1 §6, que substituiu pgcrypto); ver INV-04. | Chave em gerenciador de segredos (Vault) em produção; hoje variável de ambiente. |
| CRIPTO-AUDITORIA | Cadeia HMAC-SHA256 da auditoria | **entregue** | HMAC-SHA256 com `AUDIT_CHAIN_SECRET` fora do Postgres; `audit_chain_test.dart` passa. | Rotação do segredo invalida a história (limite conhecido, #45). |

Expand All @@ -104,7 +104,7 @@ próprio; o conferidor exige apenas os IDs do PRD.
| RNF01 | Latência MQTT < 500 ms (p95) | **parcial** | Entrega de alerta p95 212 ms, 10/10 amostras (meta < 500 ms); `measure_latency.dart --samples 10 --visit-count 100` contra a stack local (2026-10-16 01:27 UTC). | Só stack local, 1 cliente sequencial, 10 amostras (p95 de 10 amostras é quase o máximo): repetir em produção, com carga e mais amostras. |
| RNF02 | Sincronização offline > 99,5 % | **parcial** | Sync 10/10 lotes de 100 visitas sem perda (`measure_latency.dart --samples 10 --visit-count 100` contra a stack local (2026-10-16 01:27 UTC)); `offline_burst_test.dart` prova que nenhuma de 100 se perde em reinício. | A taxa > 99,5 % em campo não é medida: nenhum evento `sync_attempt`/`sync_result` é emitido (ver EVT-*). |
| RNF03 | Criptografia AES-256 em repouso | **parcial** | Ver INV-04 e CRIPTO-*. | Ver CRIPTO-APP-CACHE. |
| RNF04 | TLS 1.3 em todas as comunicações | **parcial** (2026-10-16, #78: os três trechos da issue fechados) | RPC em TLS 1.3 obrigatório (Grupo B); broker MQTT só em TLS 1.3 na 8883 com mTLS (`tls_version tlsv1.3`, Grupo F; `mtls_invariants.sh` OK); Traefik → backend em TLS 1.3 verificado (Grupo E). Medido também no `emulator-5554` (API 36). | Seguem em texto claro na stack local, fora do escopo da #78: backend → Postgres (`SERVERPOD_DATABASE_REQUIRE_SSL: "false"`, 5432 publicado no host), backend → Gorush (`GORUSH_URL: http://gorush:8088`), Insights (8083) e webServer (8082) dentro do container, dashboard do Traefik (8081, só dev). Produção: no Render o TLS termina na borda e o container segue em HTTP (`backend/DEPLOY.md`); sem prova em aparelho físico nem em API 24–29; mTLS interno (CRIPTO-TRAEFIK-BACKEND). |
| RNF04 | TLS 1.3 em todas as comunicações | **parcial** | #78 (2026-10-16) fechou os três trechos da issue: RPC em TLS 1.3 obrigatório (Grupo B); broker MQTT só em TLS 1.3 na 8883 com mTLS (`tls_version tlsv1.3`, Grupo F; `mtls_invariants.sh` OK); Traefik → backend em TLS 1.3 verificado (Grupo E). Medido também no `emulator-5554` (API 36). | Seguem em texto claro na stack local, fora do escopo da #78: backend → Postgres (`SERVERPOD_DATABASE_REQUIRE_SSL: "false"`, 5432 publicado no host), backend → Gorush (`GORUSH_URL: http://gorush:8088`), Insights (8083) e webServer (8082) dentro do container, dashboard do Traefik (8081, só dev). Produção: no Render o TLS termina na borda e o container segue em HTTP (`backend/DEPLOY.md`); sem prova em aparelho físico nem em API 24–29; mTLS interno (CRIPTO-TRAEFIK-BACKEND). |
| RNF05 | Acessibilidade WCAG 2.1 AA | **parcial** | Matriz de contraste e `meetsGuideline` nos três apps (`apps/*/test/contrast_tokens_test.dart`, `touch_targets_test.dart` no admin), avaliação em `spec/ux_accessibility_assessment.md` (WCAG 2.2 AA, superconjunto do 2.1); linha de base verde. | Foco visível no admin (A11Y-2.4.7) e métricas de interface (A11Y-LCP/FID/CLS) não medidas. |

### SLOs (§4.1)
Expand Down Expand Up @@ -211,7 +211,7 @@ próprio; o conferidor exige apenas os IDs do PRD.
| ID | Requisito | Status | Evidência | O que falta |
|----|-----------|--------|-----------|-------------|
| R1 | Colisão de sincronização | **entregue** | Conflito detectado sem sobrescrever (`visit_sync_service.dart`), guardado fora da retentativa com a cópia do servidor (schema v8, `offline_visit_queue.dart`), tela de comparação lado a lado e decisão do ACS (`resolve_conflicts`, `conflict_*`) e `visits.resolveConflict` com compare-and-set. Emulador: `acs_full_e2e.sh`, 6/6 em 2026-10-16 (`docs/superpowers/plans/2026-10-16-issue-79-e2e.log`). | Conflito de visita legada segue sem tela (quarentena); "manter a do servidor" não deixa trilha no servidor; ver abertos em `PROGRESS.md`, #79. |
| R2 | Falha do MQTT em rede 3G | **entregue** (2026-10-17, #80) | Reconexão com backoff e sessão persistente no MQTT; outbox no servidor reentrega (`mqtt_resiliencia_e2e.sh`); visitas já vão por HTTP; reserva por HTTP dos alertas: `alerts.openForMicroArea` consultado pelo ACS a cada 30 s enquanto o broker está fora (`AlertHttpFallback`, `AlertQueue.mergeFromServer`); no `emulator-5554`, alerta sem outbox chegou à tela em 29,6 s e foi confirmado (`scripts/qa/acs_alert_fallback_e2e.sh`). | Consulta só em primeiro plano (sem WorkManager, fora de `spec/stack.md`); nenhum alerta gravado no aparelho (decisão D5 da #80). |
| R2 | Falha do MQTT em rede 3G | **entregue** | #80 (2026-10-17). Reconexão com backoff e sessão persistente no MQTT; outbox no servidor reentrega (`mqtt_resiliencia_e2e.sh`); visitas já vão por HTTP; reserva por HTTP dos alertas: `alerts.openForMicroArea` consultado pelo ACS a cada 30 s enquanto o broker está fora (`AlertHttpFallback`, `AlertQueue.mergeFromServer`); no `emulator-5554`, alerta sem outbox chegou à tela em 29,6 s e foi confirmado (`scripts/qa/acs_alert_fallback_e2e.sh`). | Consulta só em primeiro plano (sem WorkManager, fora de `spec/stack.md`); nenhum alerta gravado no aparelho (decisão D5 da #80). |
| R3 | Vazamento de PII no dispositivo | **parcial** | Limpeza do aparelho pelo próprio ACS (`device_wipe_test.dart`); chave do SQLCipher no Keystore; limpeza remota pelo backoffice e rotação da chave por evento (#81, 2026-10-17): `admin.orderDeviceWipe`, `devices.pendingWipe`/`confirmWipe`, `RemoteWipeCoordinator`; prova no emulador API 36 em `./scripts/qa/acs_remote_wipe_e2e.sh` (log `docs/superpowers/plans/2026-10-17-issue-81-e2e.log`). | Certificado de cliente por aparelho (issue #94); chave derivada de biometria, que o PRD pede (a chave do SQLCipher fica no Keystore sem amarração de autenticação, por decisão do `spec/lgpd_design.md` §5.1); aparelho offline nunca alcançado e JWT vivo até 15 min; API 24–29 e aparelho físico sem prova. |
| R4 | Rejeição do ACS | **externo** | Nada no código. | NPS semanal e treinamento (produto + UBS). |
| R5 | Performance do PostgreSQL | **parcial** | 46 índices na migração `20261014155304871`, incluindo `alerts_micro_area_status_idx` e `users_micro_area_idx`. | Particionamento por microárea não existe; sem teste de carga que mostre necessidade (M3.3). |
Expand Down
Loading