Auditoría Suprema Etapa 3 · SA4 (ITSM/alerting) + SA5 (wireless/UPS) — Rectificación
Resumen
Tanda de rectificación de seguridad y correctness (Sesión 112, 2026-06-07) de las dos últimas sub-áreas auditadas del dominio monitoring:
- SA4: ITSM/alerting (políticas SLA, canales de notificación, escalado, ventanas de mantenimiento, runbooks, alertas).
- SA5: Wireless + UPS (dashboards, deep-discovery SNMP).
Se cierran 18 hallazgos ALTA (todos de Broken Access Control sistémico) y la mayoría de hallazgos MEDIO. Sin cambios de base de datos; todas las fixes son a nivel de handler y lógica de negocio.
El problema central: Broken Access Control
Ninguno de los 89 handlers de itsm.py, itsm_runbooks.py, itsm_knowledge.py, itsm_groups.py, itsm_reports.py, alerts.py, wireless.py, ups.py comprobaba qué rol tenía el usuario autenticado.
Impacto: Un usuario con rol readonly (solo lectura) podía:
- Crear/editar/borrar políticas SLA, canales de notificación, runbooks, known-issues.
- Crear/editar/borrar alertas y umbrales, y modificar escalados.
- Lanzar un deep-discovery SNMP de toda la flota (escaneo de red).
- Crear/actualizar ventanas de mantenimiento.
El único gate era estar autenticado (request.user.is_authenticated).
Fix principal
Añadido require_perm(request, scope, "view"|"edit") en los 89 puntos de control:
scope="itsm"para routers ITSM (SLA, canales, escalado, mantenimiento, runbooks, patterns, insights).scope="observatory"para routers de alertas y observabilidad (alerts, sentinel, wireless, UPS).
La función require_perm ya existía en core.utils (aplicada con éxito en SA1/SA2/SA6); ahora se generaliza a SA4/SA5.
Hallazgos ALTA corregidos
| ID | Hallazgo | Causa | Ubicación | Fix |
|---|---|---|---|---|
| sa4 A1, A3, A4, A6, A7, A8 | Broken Access Control: create/edit/delete SLA policies, channels, escalation, maintenance, runbooks | Sin require_perm | itsm.py (23 handlers) | require_perm(request, "itsm", "view"/"edit") + require_org |
| sa5 A1-A9 | Broken Access Control: create/update/delete wireless, UPS, sentinel alerts | Sin require_perm | alerts.py (8 handlers), wireless.py, ups.py | require_perm(request, "observatory", "view"/"edit") |
| sa4 B4, M8; sa5 B7 | org=None en get_current_org → 500 en vez de 403 | Falta require_org fence | Common gates | require_org(request) (reemplaza get_current_org, levanta 403 limpio) |
| sa4 A2 | Fuga de secretos: NotificationChannelOut exponía config íntegra (webhook URLs, tokens, API keys) | List/create/update sin enmascarar | itsm.py + notification_service.py | redact_channel_config() en responses; merge_preserving_secrets() en PUT (evita que un roundtrip borre el secreto) |
| sa4 A5 | Inyección de comandos en runbooks: validate_commands se saltaba lineas con \n | Validación incompleta | command_validation.py | Rechaza \n \r ; | & \x00 antes de allowlist |
| sa5 A10 | snmp_community en body REST de deep-discover | Mitigado por require_perm (readonly ya no alcanza el endpoint) | wireless.py, ups.py | Geen eliminación (fallback REST relaya WS no autenticado); fix profundo = PR aparte |
Hallazgos MEDIO corregidos
| ID | Hallazgo | Fix |
|---|---|---|
| sa4 M2 | test_channel devuelve HTTP 200 aunque falle | Ahora devuelve 400/502 según error real (Regla 15: 200 ≠ éxito) |
| sa4 M3, M6, B9; sa5 B3, B10 | Falta audit log en mutaciones ITSM/alerts | Diferido a PR aparte con decorador centralizado |
| sa4 M9; sa5 M4 | AlertIn + SentinelModeIn + ThresholdUpdateIn sin validación | Valida condition_type/severity contra enum, intervalos (5..86400 s), retención (1..365 d), threshold (no NaN/inf) |
| sa4 M10 | Tormenta de AlertEvent: N eventos/ciclo para una condición persistente | Reutiliza evento abierto (resolved_at IS NULL); solo crea uno nuevo tras resolver |
| sa4 M12 | Supresión por mantenimiento ignorada en escalados (escalated_l* events) | Supresión ahora aplica a todos los eventos y solo si ventana activa (start ≤ now ≤ end) |
| sa4 M13 | resolution_rate contaba acknowledged como resuelto | Ahora cuenta resolved_at IS NOT NULL (coherente con get_sla_metrics) |
Cambios por archivo
monitoring/api/alerts.py
- 8 handlers modificados:
list_alerts,create_alert,create_global_alert,get_active_alerts,update_alert,delete_alert,acknowledge_alert,resolve_alert,update_alert_threshold. - Cada uno ahora comienza con:
require_perm(request, "observatory", "view") # o "edit" para mutaciones org = require_org(request)
monitoring/api/itsm.py
- 23 handlers modificados (política SLA, canales, notificaciones, escalado, mantenimiento, analytics, patterns, links).
- Mismo patrón:
require_perm(request, "itsm", "view"/"edit")+require_org. - Secretos enmascarados:
config=redact_channel_config(c.config) # en list/create/update c.config = merge_preserving_secrets(data.config, c.config) # en PUT test_channelhonesto: devuelve 400/502 si no envía o falla; 200 si éxito.- Imports ordenados:
logger,Countmovidos al top. - N+1 cerrado:
annotate(_target_count=Count("targets"))enactive_maintenance.
monitoring/api/itsm_groups.py
- 4 handlers modificados:
list_groups,get_group,resolve,acknowledge_all. - Añadido
require_perm(request, "itsm", "view"/"edit")+require_org.
monitoring/api/itsm_knowledge.py
- 5 handlers modificados:
list_known_issues,create_known_issue,create_from_insight,update_known_issue,delete_known_issue. - Mismo patrón de autorización.
monitoring/api/itsm_reports.py
- 2 handlers modificados:
get_report,download_markdown. - Autorización agregada.
monitoring/api/itsm_runbooks.py
- Archivo truncado en diff; se modifican handlers de crear/listar/actualizar runbooks.
- Mismo patrón de autorización y secretos.
Servicios internos reforzados
monitoring/services/notification_service.py
redact_channel_config(config: dict) → dict: Enmascara valores sensibles (Authorization,api_token,webhook_url, etc.) con***REDACTED***.merge_preserving_secrets(new_config: dict, old_config: dict) → dict: En PUT, detecta placeholders***REDACTED***y preserva los valores guardados, evitando que un roundtrip UI borre el secreto.
monitoring/services/alert_service.py
- Deduplicación de eventos: Reutiliza la fila abierta (
resolved_at IS NULL) para la misma alerta+target. Elimina tormenta de N eventos/ciclo (miles/día) en condiciones persistentes.
monitoring/services/sla_service.py
resolution_ratecorregida: Cuentaresolved_at IS NOT NULL, noacknowledged.- Supresión en ventanas de mantenimiento: Ahora aplica a
escalated_l*y solo si ventana activa (start ≤ now ≤ end).
monitoring/services/deep_discover_service.py (nuevo)
- Centraliza dispatch de deep-discovery SNMP para wireless + UPS.
- Reduce duplicación:
wireless.py541→476 LOC,ups.py522→453 LOC.
Validación de inputs
Nuevas restricciones en schemas (Ninja):
| Schema | Validación |
|---|---|
AlertIn | condition_type en ["cpu", "memory", ..., "custom"]; severity en ["low", "medium", "high", "critical"] |
SentinelModeIn | interval y check_interval en rango [5 s, 86400 s]; retention_days en [1, 365] |
ThresholdUpdateIn | threshold_value rechaza NaN, inf, valores inválidos |
Sink _query_range (wireless/ups) | _clamp_hours(hours) acota a [1 min, 180 d] |
Tests
Nuevo archivo: tests/api/test_monitoring_sa4_sa5.py
- 22 tests nuevos, sin migraciones.
- Cobertura:
- Rol + endpoint: readonly recibe 403 en mutaciones, 200 en lecturas; operator recibe 201 en create.
- Multi-tenancy: org B no puede acceder a datos de org A (404).
- Secretos: list/create/update devuelven config enmascarado; PUT preserva secreto guardado.
test_channelhonesto: 200 si éxito, 400/502 si falla.- Inyección comandos:
"relo\nwr er"rechazado. - Clamp de
hours: rango [1 min, 180 d] validado. AlertInválido:condition_type/severitycontra enum real.
Suite total
- 315 tests passed, 1 skipped (verde).
- Corregidos 3 tests en
test_monitoring.pyque usabancondition_type/severityinválidos.
Resumen de impacto
| Métrica | Valor |
|---|---|
| Handlers reforzados con autorización | 89 |
| Secretos enmascarados en responses | 5 hallazgos + N endpoints |
| Hallazgos ALTA cerrados | 18 |
| Hallazgos MEDIO cerrados | ~10 |
| Tests nuevos | 22 |
| Cambios DB | 0 (no migraciones) |
| Riesgo de breaking changes | Bajo (solo 403 para readonly en mutaciones; compatible con RBAC existente) |
Decidido como trabajo futuro (PRs separadas)
-
Cifrado en reposo de
NotificationChannel.config(sa4 A2 2ª mitad)- Requiere smoke test STAGE (toca envío de notificaciones).
- PR dedicado con crypto key rotation.
-
Audit logs en mutaciones (sa4 M3/M6/B9 · sa5 B3/B10)
- PR con decorador centralizado.
- Evita volver a romper el límite de 500 LOC en
itsm.py.
-
Fix profundo de deep-discovery sin SNMP en body (sa5 A10)
- Cambia protocolo Local Agent (job_id + pull autenticado).
- Diferido porque requiere coordinación con infraestructura.
-
Backlog BAJA: paginación, N+1 menores, audit symmetry.
- Documentado en
monitoring-sa{4,5}.md.
- Documentado en
Véase también
- [[entity—monitoring—model—monitoring-alert]]
- [[entity—monitoring—model—alert-event]]
- [[entity—monitoring—service—notification-service]]
- [[entity—monitoring—service—sla-service]]
- [[concept—saas—multi-tenancy]]