CreaRack-SL

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

  1. WebSocket de Observatory (MonitoringConsumer.connect) aceptaba cualquier usuario de la organización sin comprobar observatory:view, y el handler insight_update reenviaba diagnósticos de CNS (resumen, nivel de riesgo) a usuarios sin cns: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.
  2. AIInsight.save() asignaba case_number con un patrón leer-luego-escribir sin bloqueo: dos insights creados a la vez en la misma organización podían chocar contra unique_case_number_per_org.
  3. apply/rollback marcaban el insight como EXECUTING y 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 quedaba EXECUTING para siempre: ni reaplicable ni caducable, con una respuesta 200 que mentía sobre el resultado.
  4. El endpoint /result aceptaba resultados de ejecución sin comprobar que el insight estuviera realmente en EXECUTING, y el output no tenía límite de tamaño.
  5. explain/revise construían el prompt al LLM concatenando a mano campos de AIInsight (diagnóstico, nombre del target, IP) sin pasar por wrap_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.
  6. receive_insight (la ingesta del Agente) no aplicaba ai_cost_guard en 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:view cacheado por conexión y comprobado en insight_update antes de reenviar el diagnóstico.
  • case_number: pg_advisory_xact_lock por organización (namespace 4272, distinto del 4271 de la flota en terminal/fleet_lifecycle.py) con lock_timeout='3s', sin tocar la fila real de core_organization — detalle en entity--monitoring--model--aiinsight.
  • apply/rollback: gate 409 si 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 devuelve 409 en vez de dejarlo colgado en EXECUTING.
  • /result: solo acepta resultados si el insight está en EXECUTING; output truncado a 64 KB.
  • explain/revise: nuevo build_insight_context() en sanitize.py centraliza el saneado con wrap_untrusted de target/diagnóstico/comandos/anomalía, mismo patrón que build_user_prompt.
  • receive_insight: exige cns:edit y aplica ai_cost_guard en 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 AsyncWebsocketConsumer del 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, usar build_insight_context() o build_user_prompt() — no reconstruir el prompt a mano.
  • Continuar la cola de auditoría del task #286 sobre los dominios de monitoring que 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]]