Cola de auditoría B (task #286): CNS e insights de IA — WS sin gate de permiso, apply/rollback sin Agente, prompts sin sanear
Cuándo
04-09-2026 · commit fbcfec01 (PR #497, v1.105.0). Segunda entrega de la “cola de auditoría” del task #286 sobre el dominio monitoring — la primera (v1.104.0, “cola auditoría A”) cubrió sondas y targets; esta cierra CNS e insights de IA con 11 hallazgos MEDIA propios más 11 adicionales de una revisión en Opus sobre el mismo PR. 34 tests nuevos en tests/api/test_audit_monitoring_b.py.
Síntomas visibles
- WebSocket de Observatory (
MonitoringConsumer.connect) aceptaba cualquier usuario de la organización sin comprobarobservatory:view, y el handlerinsight_updatereenviaba diagnósticos de CNS (resumen, nivel de riesgo) a usuarios sincns:view— el aislamiento por tenant no sustituye el nivel de permiso dentro de la org, mismo patrón que la Tanda 1 de la Auditoría Suprema 2 (ver relacionada) pero esta vez en un canal WS que no pasa por el decorador REST habitual. AIInsight.save()asignabacase_numbercon un patrón leer-luego-escribir sin bloqueo: dos insights creados a la vez en la misma organización podían chocar contraunique_case_number_per_org.apply/rollbackmarcaban el insight comoEXECUTINGy solo DESPUÉS intentaban despachar el comando al Agente — si no había Agente conectado, o el envío fallaba entre el gate y el despacho, el insight quedabaEXECUTINGpara siempre: ni reaplicable ni caducable, con una respuesta 200 que mentía sobre el resultado.- El endpoint
/resultaceptaba resultados de ejecución sin comprobar que el insight estuviera realmente enEXECUTING, y el output no tenía límite de tamaño. explain/reviseconstruían el prompt al LLM concatenando a mano campos deAIInsight(diagnóstico, nombre del target, IP) sin pasar porwrap_untrusted— esos campos vienen de telemetría SNMP/SSH no confiable o de un nombre de dispositivo manipulable, así que un dato malicioso podía inyectar directivas en el prompt de seguimiento.receive_insight(la ingesta del Agente) no aplicabaai_cost_guarden la sesión, y varios endpoints de listado no acotaban la paginación.
Causa raíz
Cada punto tiene su causa puntual (falta de gate en el WS, falta de lock, orden de operaciones optimista, falta de saneado), pero todos comparten el patrón ya visto en tandas anteriores: código que asume el camino feliz — Agente siempre conectado, LLM siempre bien portado, WS con el mismo nivel de confianza que la sesión HTTP — sin comprobar el caso adverso antes de mutar estado o de construir el prompt.
Fix aplicado
Commit fbcfec012d46a7e3c4c244f67936954701d52d09 (PR #497):
- WS:
has_permission(user, "observatory", "view")al conectar;cns:viewcacheado por conexión y comprobado eninsight_updateantes de reenviar el diagnóstico. case_number:pg_advisory_xact_lockpor organización (namespace4272, distinto del4271de la flota enterminal/fleet_lifecycle.py) conlock_timeout='3s', sin tocar la fila real decore_organization— detalle enentity--monitoring--model--aiinsight.apply/rollback: gate409si no hay Agente conectado ANTES de mutar el insight; si el despacho falla después del gate, se revierte el estado (PENDING/rollback_executed=False) y se devuelve409en vez de dejarlo colgado enEXECUTING./result: solo acepta resultados si el insight está enEXECUTING; output truncado a 64 KB.explain/revise: nuevobuild_insight_context()ensanitize.pycentraliza el saneado conwrap_untrustedde target/diagnóstico/comandos/anomalía, mismo patrón quebuild_user_prompt.receive_insight: exigecns:edity aplicaai_cost_guarden sesión; clamps de paginación en los listados; timeout en las 4 llamadas al LLM; el feedback loop toma el confirmado más reciente en vez del primero; toast traducible para el nuevo 409.
Lecciones
El gate por nivel de permiso en WebSocket es el mismo hueco de siempre —aislamiento por tenant no es lo mismo que nivel de permiso—, pero esta vez en un canal que no hereda el chequeo del decorador REST: cada AsyncWebsocketConsumer de Channels necesita su propio gate explícito. Y el saneado de prompts (wrap_untrusted) tiene que aplicarse en TODOS los puntos donde se construye contexto para el LLM, no solo en el primero que se auditó — explain/revise llevaban meses construyendo el prompt a mano sin pasar por él.
Preventivos futuros
- Revisar el resto de
AsyncWebsocketConsumerdel proyecto por el mismo hueco de gate-por-permiso al conectar. - Al añadir un nuevo punto de entrada al LLM con datos de
AIInsight/telemetría, usarbuild_insight_context()obuild_user_prompt()— no reconstruir el prompt a mano. - Continuar la cola de auditoría del task #286 sobre los dominios de
monitoringque falten.
Véase también
- [[entity—monitoring—model—aiinsight]]
- [[entity—monitoring—service—insight-service]]
- [[concept—monitoring—cns]]
- [[crearack—conceptos—usuarios-y-permisos]]
- [[incident—20260831—auditoria-suprema-2-tanda1-monitoring-racks]]
- [[feature—monitoring—auditoria-suprema-sa3-cns-insights-ia]]