Saltar a contenido

Gates del piloto de captura privada

Estado inicial: NO-GO. Todos los casos usan cuentas, consentimientos, imágenes, marcadores y resultados sintéticos. Una prueba con foto real invalida la evidencia de este checklist.

Convención de evidencia

Crear /var/tmp/eppa-private-capture/<release-id>/ fuera del repo. Cada gate debe guardar comando, timestamp UTC, commit/digest, salida sanitizada y responsable. Nunca guardar cookies, tokens, claves, object keys completos, bytes/base64, nombres ni IDs de sujetos.

G0 — Alcance y aprobaciones

  • [ ] El commit/digest está fijado y el worktree coincide con el release.
  • [ ] EPPA_PRIVATE_CAPTURE_ENABLED=false por defecto.
  • [ ] La adenda sigue bloqueando uso real hasta tener todas las firmas.
  • [ ] El hostname piloto no comparte tráfico, cookies ni config con legacy.
  • [ ] No se modificó .git, .git/lfs, .gitattributes ni fuentes de pacientes.

G1 — Dependencias y configuración fail-closed

docker run --rm "$IMAGE_DIGEST" \
  python3 -c 'import boto3, cryptography, sqlalchemy; print("runtime-deps-ok")'
docker run --rm "$IMAGE_DIGEST" \
  sh -c 'test "${EPPA_PRIVATE_CAPTURE_ENABLED:-false}" = false'
  • [ ] Producción rechaza arranque sin EPPA_ENV=prod, cookie secure, JWT explícito, tres secret files o KEK de exactamente 32 bytes.
  • [ ] TTL mayor a 86400, origen no HTTPS, bucket vacío o límites inválidos rechazan arranque.
  • [ ] Ningún secreto aparece en docker inspect, HTML, bundle Next o logs.

G2 — Bucket y privilegio mínimo

Con un objeto de texto sintético bajo pc/<org>/<session>/probe:

  • [ ] identidad runtime puede Put/Get/Delete y listar sólo pc/;
  • [ ] otro prefijo y cualquier otro bucket responden AccessDenied;
  • [ ] acceso anónimo y URL sin autenticación fallan;
  • [ ] identidad runtime no puede crear bucket/usuario/policy/lifecycle;
  • [ ] identidad admin no está montada en API/worker;
  • [ ] versioning, object lock y replication están desactivados;
  • [ ] lifecycle exportado coincide con minio-lifecycle.json;
  • [ ] bucket y prefijo difieren de todos los destinos de backup.

G3 — Auth, tenant y consentimiento

Ejecutar al menos:

cd fronts/eppa
pytest -q python/tests/auth/test_private_capture_pilot.py
  • [ ] no autenticado: 401 o 404 sin revelar que el piloto existe;
  • [ ] autenticado sin grant, grant vencido o rol incorrecto: fail-closed;
  • [ ] org ajena: 404, nunca 403 revelador;
  • [ ] falta/retiro/contexto distinto de consentimiento: ninguna sesión ni transición, aunque el cliente omita o falsifique headers;
  • [ ] origen distinto del único allowlisted: 403;
  • [ ] ETag/revisión vieja: 409, sin sobrescribir datos;
  • [ ] eventos de auditoría sólo contienen UUID pseudónimo, acción, estado y request ID sanitizados.

G4 — Canonicalización y cifrado

  • [ ] sólo JPEG/PNG decodificables dentro de bytes/pixels permitidos;
  • [ ] MIME, extensión y magic bytes inconsistentes se rechazan;
  • [ ] EXIF/ICC/text chunks y filename original desaparecen al re-encode;
  • [ ] dimensiones/transformación canónica son deterministas;
  • [ ] AES-GCM usa nonce único y AAD con org/session/view/artifact/schema/key;
  • [ ] cambiar un byte, AAD, tag o key version hace fallar autenticación;
  • [ ] el objeto MinIO no contiene JPEG/PNG legible ni metadatos del paciente;
  • [ ] una excepción no registra bytes, base64, AAD completo, key o KEK.

G5 — Flujo sintético completo

El Playwright focal navega a /{locale}/pilot/private-capture y usa una imagen generada en test, nunca un archivo recibido de una persona:

  1. capability autorizada y aviso de consentimiento/TTL visibles;
  2. crear sesión con consentimiento sintético vigente;
  3. guardar en orden anterior, posterior, lateral izquierda y lateral derecha;
  4. validar referencia/calibración y marcadores sintéticos por vista;
  5. finalizar, anonimizar y revisar candidate;
  6. aprobar/rechazar según el fixture;
  7. pedir purge inmediato;
  8. verificar UI purged, DB tombstone y cero objetos/versiones/multipart.

  9. [ ] refresh, doble click, request duplicado y respuesta fuera de orden son idempotentes;

  10. [ ] caída del worker/storage conserva estado recuperable sin doble candidato;
  11. [ ] ni sessionStorage, localStorage, IndexedDB, cache HTTP ni screenshot de Playwright contiene la imagen.

G6 — TTL, purge y no-backup

  • [ ] reloj falso crea una sesión a TTL y el worker la toma al vencer;
  • [ ] consentimiento retirado y purge manual tienen prioridad inmediata;
  • [ ] purge borra original, normalizado, temporales, candidato y multipart;
  • [ ] HEAD y listado administrativo confirman ausencia antes de deleted;
  • [ ] fallo transitorio reintenta idempotente; fallo persistente produce purge_failed sanitizado y alerta;
  • [ ] objeto más viejo que TTL + 30 min dispara alerta;
  • [ ] backup_restore_drill.py rechaza bucket y prefijo privados:
cd fronts/eppa
EPPA_BACKUP_DIR=/tmp/eppa-private-backup-negative \
EPPA_MINIO_BUCKET=eppa-private-capture-pilot \
  python3 scripts/backup_restore_drill.py --dry-run backup
# esperado: exit != 0 y mensaje de exclusión; eliminar el directorio temporal
  • [ ] una restauración de DB no vuelve a crear ni hace descargable un objeto purgado.

G6.1 — Límite de replay MATLAB y fixture

El archive aprobado del piloto demuestra únicamente la cadena de custodia de cuatro imágenes anonimizadas y sus snapshots sanitizados. No es, por sí solo, una entrada del oráculo MATLAB: el oráculo de 61 métricas acepta solamente el atlas sintético versionado y sus coordenadas/provenance verificables.

  • [ ] el validator del archive confirma orden, hashes y ausencia de referencias raw, URL, object key, paciente o consentimiento;
  • [ ] el replay MATLAB se ejecuta exclusivamente contra el atlas sintético guided-landmark-atlas.v1 y su reporte real de 61 métricas;
  • [ ] las cuatro excepciones de ángulos quedan evaluadas sólo mediante el contrato matlab_angle_exceptions, sin copiar ni inferir resultados desde un candidate;
  • [ ] ninguna aprobación de candidate, aun sintético, se presenta como validación clínica ni como paridad MATLAB de un caso de persona.

G7 — Blue/green y rollback

  • [ ] health de eppa.firemandeveloper.com y eppa2.firemandeveloper.com guardados antes del cambio;
  • [ ] candidato por digest inicia sin tráfico y readiness pasa;
  • [ ] migración expand-only funciona con color anterior activo;
  • [ ] smoke sintético directo al candidato pasa con flag off y luego on;
  • [ ] nginx -t pasa antes del switch atómico;
  • [ ] smoke por hostname, métricas y health legados pasan después;
  • [ ] conmutar al color previo tarda menos que la ventana aprobada;
  • [ ] rollback no hace downgrade DB y purga la sesión sintética;
  • [ ] no se ejecutó scripts/deploy.sh.

G8 — Observabilidad e incidente

  • [ ] alertas verificadas: purge atrasado/fallido, orphan, multipart, worker stale, drift de policy/lifecycle/public access y aparición en backup;
  • [ ] búsqueda negativa en logs/export/traces no encuentra base64, filename, object key, subject, consent evidence ni secretos;
  • [ ] revocación de credenciales runtime produce AccessDenied y alerta;
  • [ ] simulacro documenta disable flag, rollback, purge y escalamiento sin copiar la imagen sintética.

Gate final

Un QA independiente firma cada bloque con evidencia. Cualquier casilla abierta mantiene NO-GO. Incluso con todos los gates técnicos verdes, una captura real requiere las aprobaciones legales/clínicas explícitas de la adenda; este checklist por sí solo no la autoriza.