Incidentedraftcreado Sun Sep 06#network#discovery#auto-provision#security#reliability#django#encryption#audit-rondas
Cuándo
06-09-2026 · commit e44d987187044c1e785232c61ea544fcc208d990 (PR #513, v1.120.0). Cuarto PR de una serie que liquida hallazgos pendientes de las rondas de auditoría del 30-08 y del 06-09 (serie distinta de la cola por dominio de la task #286: los PRs anteriores de esta misma serie cerraron las tasks #292, #279, #277/#293/#294/#296.1). Este cierra #291 y #278 (ALTA), #280 (ALTA), el punto 4 de #281 y los puntos 5 y 6 de #282, todos en el módulo de descubrimiento de red (network/Auto-Provision). 94 tests backend + 90 de frontend en verde.
Síntomas visibles
- #291 — Un host que fallaba el enriquecimiento (
enrich_oneennetwork/api/discovery.py) se guardaba igualmente como ficha condiscovery_method="ping"y confianza 10-20, sinlast_verified— indistinguible en pantalla de un hallazgo real por ping. En PROD había 114 fichas así, creadas en dos ráfagas de 57 el 21-08. - #278 — Con el Agente local sin WebSocket, “Deep Discover” mostraba aviso verde de éxito en unos 2 segundos sin haber hecho ningún sondeo real. 201 de 261 fichas en PROD viven hoy en ese estado no verificado.
- #280 — El editor de ficha de dispositivo (
device_card_editor.js) tenía una listaTYPE_OPTScon 7 valores frente a los 11 que acepta el modeloDevice. Al abrir y guardar la ficha de un tipo no listado (p. ej.media_player), el desplegable quedaba en su primera opción y el guardado la persistía — convirtiendo la ficha en “router” de forma permanente. 105 fichas de cartelería en riesgo. - #281 pt.4 — El enlace automático ficha-dispositivo (matching por IP) nunca encontraba coincidencias, aunque el hallazgo original apuntaba a una causa distinta (lectura de IP sin descifrar). Medido en PROD: 6 dispositivos con
management_config, los 6 con IP descifrable, 0 con la IP en texto claro. - #282 pt.5/pt.6 — La chapa de “Deep discovery completed” siempre citaba “0 MIBs loaded” (dato nunca poblado). Y los valores
discovery_method="http"/device_type="ups"se escribían en producción sin estar declarados en las choices del modelo, conupsaceptado en un endpoint (assign.py) y rechazado con 400 en otro (profiles.py).
Causa raíz
- #291: el bloque
exceptdeenrich_oneno distinguía “no pude verificar este host” de “lo verifiqué y es un ping válido” — ambos casos producían el mismo tipo de ficha. - #278: el modo
rest(Agente sin WebSocket) devuelve tareas para que el navegador las ejecute y solo señala fallo real víaresp.error; el consumidor del asistente de discovery no distinguía “tareas entregadas y agotadas sin respuesta” de “sondeo completado con éxito”. Otros 4 consumidores del mismo patrón sí lo hacían bien — la lógica correcta existía pero no estaba compartida (violación de la Regla 2, no duplicar código). - #280: dos listas independientes describiendo los mismos tipos de dispositivo —
TYPE_OPTSen el frontend yDEVICE_TYPE_CHOICESen el modelo Django — se fueron desincronizando con cada tipo nuevo añadido al modelo sin propagar al frontend. - #281.4: el hallazgo original diagnosticó mal el síntoma. El código ya llamaba a
Device.management_ip, que sí descifra. El defecto real vivía un nivel más abajo: el prefiltro SQLmanagement_config__contains=<ip>buscaba la IP en claro dentro de un campo cuyo valor está cifrado con Fernet — cifrado no determinista, así que unLIKE/containsno puede casar nunca un ciphertext contra un texto plano, sin importar si la IP es correcta. El test que cubría este camino ocultaba el bug porque creaba losmanagement_configde prueba sin cifrar. - #282.6: dos endpoints (
assign.py,profiles.py) validaban tipos de dispositivo contra listas propias en vez de una fuente única (ASSIGNABLE_TYPESduplicabaDEVICE_TYPE_CHOICEScon desfase).
Fix aplicado
Commit e44d987187044c1e785232c61ea544fcc208d990 (PR #513):
- #291:
enrich_oneya no fabrica una ficha “ping” ante una excepción — el host fallido vuelve en una listafailed(con el tipo de excepción) que la pantalla de resultados muestra aparte. Las 114 fichas ya creadas en PROD se dejan tal cual — decisión de Edu, un re-escaneo las corrige. De paso cierra el diferidonetwork-#12. - #278: nuevo módulo compartido
static/js/utils/deepDiscoverAgent.js; los 5 consumidores (incluidoswireless,ups,signage,monitoring) pasan a usarlo. Agotar los reintentos del sondeo del Agente ya no se interpreta como éxito. - #280: nuevo endpoint
device-type-choicesque sirve las opciones directamente desdeDEVICE_TYPE_CHOICESdel modelo;device_card_editor.jsconsulta ese endpoint en vez de mantenerTYPE_OPTScomo lista paralela. - #281.4: se elimina el prefiltro SQL ciego; el matching por IP se hace en Python descifrando
management_configde los dispositivos no borrados de la organización (en PROD acota de 423 dispositivos a 6, coste asumible). El test que ocultaba el bug se corrigió para cifrar losmanagement_configde prueba igual que en producción. - #282.5: se retira del tooltip la cifra de “N MIBs loaded” que nunca se poblaba.
- #282.6: migración
0064_add_ups_http_choicesdeclarahttpyupsen las choices del modelo; se borraASSIGNABLE_TYPESpara queassign.pyyprofiles.pyvaliden contra la misma fuente (DEVICE_TYPE_CHOICES). - Queda abierto — punto 7 de #282 (modal de grupos del Terminal): existe
DeviceManager.js::_applyCommonGroupChecks, un mecanismo que sí intenta premarcar grupos por intersección, y que la descripción del hallazgo original no mencionaba. Sin trazar si el dato le llega poblado, no se pudo confirmar si el bug reproduce tal cual — se deja para revisarlo aparte en vez de aplicar un parche a ciegas.
Lecciones
- Un
exceptque “guarda algo de todos modos” para no perder el intento fabrica un dato indistinguible de uno legítimo. Cuando falla la verificación, el resultado debe volver marcado como fallo, nunca como un hallazgo de baja confianza silencioso. - Cifrado no determinista (Fernet) y filtrado SQL en claro son incompatibles: un prefiltro
contains/LIKEsobre un campo cifrado siempre devuelve vacío, nunca falla ruidosamente — el bug pasa desapercibido porque “no encuentra nada” parece un resultado plausible. - Un test que prepara sus fixtures sin replicar el cifrado real de producción puede dar cobertura falsa: pasaba en verde tapando exactamente el bug que debía atrapar.
- Cuando el diagnóstico de un hallazgo de auditoría resulta incompleto o erróneo, el PR de cierre debe decirlo explícitamente (aquí, #281.4) en vez de aplicar el fix que el hallazgo sugería sin comprobar la causa real.
- Dos fuentes de verdad describiendo el mismo catálogo (choices de modelo vs. lista de frontend, o
ASSIGNABLE_TYPESvs.DEVICE_TYPE_CHOICES) se desincronizan con el tiempo; la corrección estructural es servir la lista desde una única fuente, no parchear la copia.
Preventivos futuros
- Punto 7 de #282 (modal de grupos del Terminal) queda pendiente de trazar antes de tocarlo.
- Las 114 fichas
discovery_method="ping"ya creadas en PROD antes de este fix no se han corregido retroactivamente; quedan a la espera de un re-escaneo natural.
Véase también
- [[feature—network—discovery-audit-sa1]]
- [[entity—network—service—device-config]]
- [[incident—20260904—auditoria-suprema-2-cola-network-agente-offline-y-whitelist-vendor]]
- [[incident—20260904—auditoria-suprema-2-cola-monitoring-d-deep-discovery-wireless-ups]]