From 385249a867b2a5fb6add00b2e3b9806d0b5cf146 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 17 Oct 2026 19:31:16 -0400 Subject: [PATCH 1/2] =?UTF-8?q?test(acs):=20migra=C3=A7=C3=A3o=20do=20banc?= =?UTF-8?q?o=20v6=20confere=20o=20schema=20atual=20(v8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acs/integration_test/encrypted_storage_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/acs/integration_test/encrypted_storage_test.dart b/apps/acs/integration_test/encrypted_storage_test.dart index 1f29b86..5e9593d 100644 --- a/apps/acs/integration_test/encrypted_storage_test.dart +++ b/apps/acs/integration_test/encrypted_storage_test.dart @@ -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, @@ -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); From 27202d5cb518245318c39b4ba49bbc82ae7838c4 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Sat, 17 Oct 2026 19:31:16 -0400 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20status=20da=20auditoria=20do=20PRD?= =?UTF-8?q?=20s=C3=B3=20em=20negrito=20(RNF04,=20CRIPTO-TRAEFIK-BACKEND,?= =?UTF-8?q?=20R2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROGRESS.md | 3 +-- docs/auditoria-prd/2026-10-15-lacunas-prd.md | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 6367997..e5516c7 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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). diff --git a/docs/auditoria-prd/2026-10-15-lacunas-prd.md b/docs/auditoria-prd/2026-10-15-lacunas-prd.md index 90e1254..ebc1579 100644 --- a/docs/auditoria-prd/2026-10-15-lacunas-prd.md +++ b/docs/auditoria-prd/2026-10-15-lacunas-prd.md @@ -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). | @@ -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) @@ -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). |