CreaRack-SL

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

  1. net-#16 — Una DeviceTask sin ningún Agente conectado nunca caducaba. _schedule_retry decide rendirse mirando task.attempts, pero solo el camino “agente encontrado” incrementa ese contador (attempts=F("attempts")+1 dentro del claim atómico); el camino “sin agente” sale antes sin tocarlo. Con attempts clavado 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 y purge_device_tasks no la borraba jamás — filas queued acumulándose sin límite en cualquier organización con el Agente apagado.
  2. 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) y delete 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 tiene write 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 attempts es 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 constante NO_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_task comprueba timezone.now() - task.created_at >= NO_AGENT_GRACE en el camino sin agente; si se cumple, llama a _give_up y la tarea pasa a STATUS_TIMEOUT con el motivo "no agent online for 5 min". Dentro de la ventana el comportamiento no cambia — la tarea sigue en queued sin contar como intento — así que el contrato de test_dispatch_no_agent_requeues queda intacto. El margen de 5 minutos cubre un Agente reiniciándose por auto-update.
  • monitoring/services/command_validation.py: las tres cadenas retiradas directamente de VENDOR_SAFE_COMMANDS, sin invertir el orden blocklist/whitelist (que habría sido el arreglo más arriesgado — riesgo de reabrir copy running-config tftp://… como vía de exfiltración). write memory sigue 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_whitelist no 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 a timeout) y TestUnreachableWhitelistEntriesRemoved (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 network de 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]]