Auditoría Suprema 2 · Cola network: DeviceTask sin agente que no caduca y comandos de la whitelist por vendor inalcanzables
Cuándo
04-09-2026 · commit 48872fc4 (PR #503, v1.112.0). Cierra los dos hallazgos de la cola MEDIA/BAJA de la Auditoría Suprema 2 para network (task #286) que el PR #502 (v1.111.0, mismo día) había dejado pendientes por ser decisión de producto, no arreglo mecánico. Edu tomó las dos decisiones esa misma tarde y este PR las implementa. 5 tests nuevos en tests/network/test_cola_auditoria_network.py.
Síntomas visibles
- net-#16 — Una
DeviceTasksin ningún Agente conectado nunca caducaba._schedule_retrydecide rendirse mirandotask.attempts, pero solo el camino “agente encontrado” incrementa ese contador (attempts=F("attempts")+1dentro del claim atómico); el camino “sin agente” sale antes sin tocarlo. Conattemptsclavado en 0, la condición de rendición (attempts >= max_attempts) era matemáticamente inalcanzable: el sweeper de cada minuto reencolaba la tarea para siempre, nunca llegaba a un estado terminal ypurge_device_tasksno la borraba jamás — filasqueuedacumulándose sin límite en cualquier organización con el Agente apagado. - net-#21 — Tres comandos declarados “seguros” en la whitelist por fabricante nunca podían ejecutarse:
copy running-config startup-config(Cisco IOS y NX-OS) ydelete interfaces(Juniper Junos) están también en el blocklist genérico, que se evalúa ANTES que la whitelist por prefijo. El operador veía “Blocked command” sobre un comando que la propia tabla marcaba como aprobado para ese fabricante — y en NX-OS, que no tienewrite memory, no quedaba NINGUNA forma aprobada de persistir un cambio de remediación.
Causa raíz
- net-#16: el contador de intentos vive solo en una de las dos ramas posibles. Cualquier condición de parada basada en
attemptses letra muerta en la rama que nunca lo incrementa — el camino correcto para “sin agente” es una comprobación de tiempo transcurrido desde la creación, no de intentos. - net-#21: dos listas independientes (bloqueo genérico por prefijo y permiso específico por fabricante) se evalúan en el orden equivocado, y el bloqueo gana siempre que comparte prefijo con un permiso. Un test de arquitectura (
tests/test_architecture_fitness.py) incluso documentaba la intención de permitir esas tres cadenas sin llamar nunca al validador real, así que la discrepancia entre intención y comportamiento pasó desapercibida.
Fix aplicado
Commit 48872fc4 (PR #503):
network/tasks.py: nueva constanteNO_AGENT_GRACE = timedelta(minutes=5)y función_give_up(task, reason)(extraída del cuerpo que antes solo vivía dentro de_schedule_retry).dispatch_device_taskcompruebatimezone.now() - task.created_at >= NO_AGENT_GRACEen el camino sin agente; si se cumple, llama a_give_upy la tarea pasa aSTATUS_TIMEOUTcon el motivo"no agent online for 5 min". Dentro de la ventana el comportamiento no cambia — la tarea sigue enqueuedsin contar como intento — así que el contrato detest_dispatch_no_agent_requeuesqueda intacto. El margen de 5 minutos cubre un Agente reiniciándose por auto-update.monitoring/services/command_validation.py: las tres cadenas retiradas directamente deVENDOR_SAFE_COMMANDS, sin invertir el orden blocklist/whitelist (que habría sido el arreglo más arriesgado — riesgo de reabrircopy running-config tftp://…como vía de exfiltración).write memorysigue siendo la forma de persistir donde exista; NX-OS y Junos quedan sin comando aprobado de persistencia, a propósito.test_blocked_list_wins_over_vendor_whitelistno cambia de comportamiento, solo su docstring.- 5 tests nuevos en
tests/network/test_cola_auditoria_network.py:TestDeviceTaskNoAgentGrace(tarea reciente sigue en cola sin contar intento; tarea más vieja que la ventana pasa atimeout) yTestUnreachableWhitelistEntriesRemoved(parametrizado sobre los tres comandos retirados, confirma que quedan fuera de la whitelist y siguen bloqueados).
Lecciones
- Un contador de intentos que solo se incrementa en UNA de las ramas posibles deja la rama contraria sin techo. Cuando el evento a acotar no es “cuántas veces lo intenté” sino “cuánto tiempo llevo esperando”, la comprobación correcta es de tiempo transcurrido, no de intentos.
- Dos listas de comandos evaluadas en el orden equivocado no fallan de forma abierta: fallan cerradas pero mudas, denegando algo que el propio código declara seguro para ese fabricante. Un test de “fitness” que compara listas estáticas sin invocar la función real puede documentar una intención que el runtime no cumple.
- Cuando dos hallazgos de una auditoría exigen una decisión de producto (no un arreglo mecánico), el patrón de este proyecto es cerrar primero lo mecánico en un PR grande y, una vez el dueño del producto decide, un segundo PR estrecho que solo toca esas piezas — evita bloquear el resto de hallazgos cerrables mientras se espera la decisión.
Preventivos futuros
- Ninguno pendiente de este cierre concreto — net-#16 y net-#21 quedan resueltos con decisión tomada. La cola de
networkde la Auditoría Suprema 2 conserva otros hallazgos diferidos con motivo documentado (9 diferidos según el PR #502 / v1.111.0).
Véase también
- [[incident—20260904—auditoria-suprema-2-cola-monitoring-a-sondas-y-targets]]
- [[incident—20260904—auditoria-suprema-2-cola-monitoring-d-deep-discovery-wireless-ups]]
- [[incident—20260904—auditoria-suprema-2-cola-racks-libreria-backup-restore]]
- [[incident—20260831—auditoria-suprema-2-tanda1-monitoring-racks]]
- [[entity—network—endpoint—execute-script]]
- [[feature—network—scripts-ssh-audit-s113]]
- [[entity—network—service—device-config]]