Resumen ejecutivo
Sesión s113 (2026-06-07): auditoría exhaustiva del motor de descubrimiento de dispositivos de red (Auto-Provision) de CreaRack Pro. La sub-área 1 comprende 4 slices funcionales y toca la mayor superficie de seguridad del módulo network (interacción con redes, manejo de credenciales SNMP/SSH, escaneos nmap).
Resultado: 37 hallazgos confirmados (11 ALTA / 16 MEDIA / 10 BAJA) auditorios en una tanda única (Regla 22), alineados con el plan de hardening. Sin migraciones de datos.
Alcance auditado
Slices analizados
-
discovery-api: endpoints Ninja (discover,bulk_enrich,bulk_discover,deep_discover,deep_discover_status) + esquemas de Auto-Provision.- Foco crítico: autorización (require_perm faltante), SSRF/escaneo red interna, IDOR, fuga de credenciales, validación de inputs.
-
orchestrator-snmp: orquestador central (service.py) + stages de provisión (provision_stages.py).- Foco crítico: inyección de argumentos a nmap, redirects sin control en HTTP, bulk sin límite, fugas de credenciales SNMP, trabajo síncrono pesado.
-
snmp-auth-ssh: autenticación SNMPv3 (snmp_auth.py), helpers SNMP + SSH + LLDP (ssh_stage.py,lldp_stage.py).- Foco crítico: verificación de host-key SSH, credenciales en logs, inyección en comandos, parseo frágil de datos no confiables del dispositivo.
-
classify-scoring-stencil: clasificación automática, scoring, matching de plantillas (auto_classifier.py,scoring.py,stencil_matching.py,constants.py).- Foco crítico: ReDoS en regex, aislamiento cross-tenant de stencils, lógica de confianza, rendimiento.
Hallazgos confirmados
ALTA severidad (11)
| ID | Título | Archivo | Solución aplicada |
|---|---|---|---|
| A1 | Broken Access Control: 5 endpoints sin require_perm | network/api/discovery.py | Ahora exigen require_perm(network, edit) para acciones / view para status |
| A2 | SSRF / escaneo red interna: IP/subnet arbitrarias sin guard | network/services/device_discovery/service.py | Nuevo scan_guard.py: bloquea loopback/link-local/metadata/unspecified, permite RFC1918 |
| A3 | detect_vendor_http sigue redirects a destinos arbitrarios | network/services/device_discovery/service.py | allow_redirects=False aplicado |
| A4 | DeviceProfileOut expone snmp_community al cliente (fuga cred) | network/api/common.py | Deuda: PR propio — remover del schema Out |
| A5 | deep_discover_profile modo “rest” devuelve community/v3_keys en body | network/api/discovery.py | Deuda: PR propio — rediseño de protocolo Agent |
| A6 | IDOR en deep_discover_status: carga caché sin filtrar por org | network/api/discovery.py | Ahora carga DeviceProfile por org antes de leer caché → 404 cross-tenant |
| A7 | discover_nmap argument injection: ip arbitraria como flag de nmap | network/services/device_discovery/service.py | IP validada como literal seguro en schema + servicio |
| A8 | SNMPv3 sin cifrado en reposo (credenciales en claro) | network/models.py | Deuda: PR propio — cifrado en reposo (Plan Hardening E) |
| A9 | SSH sin verificación de host-key (TOFU/MITM) | network/services/device_discovery/ssh_stage.py | Deuda: PR propio — activar host-key verification en scrapli |
| A10 | build_snmp_auth degrada en silencio a noAuth/noPriv + import roto v3 | network/services/device_discovery/snmp_auth.py | Ahora lanza ValueError ante protocolo desconocido; import corregido a pysnmp.hlapi.v3arch.asyncio |
| A11 | match_stencil cross-tenant: sugerencia de stencil de otra org | network/services/device_discovery/stencil_matching.py | Filtra org propia + globales; suggested_stencil_id aislado por tenant |
MEDIA severidad (16)
| ID | Título | Corrección |
|---|---|---|
| M1 | Escaneo /16 sin límite: bulk_discover_subnet(subnet) expande entero sin cap | Schema: subnet acotada a ≤1024 hosts; validación en parse + servicio |
| M2 | Validadores de schema débiles: ip/subnet/snmp_version/max_concurrent sin restricciones | Validadores Pydantic: IP literal + subnet formato correcto + snmp_version ∈ {v2c,v3} + max_concurrent 1–50 |
| M3 | discover/bulk-discover devuelven {"error": str(e)}: fuga de detalle interno | Mensaje genérico; detalles solo en logger.error |
| M4 | org=None no genera error explícito | Ahora org or raise 403 antes de instanciar servicio |
| M5 | Sin auditoría de acciones en discover/bulk/deep | log_action(NETWORK, ...) en cada endpoint |
| M6 | bulk_discover_subnet sin rate-limit: DoS por repetición | Deuda menor (rate-limit a nivel middleware) |
| M7 | try_snmp_communities prueba 20+ communities: comportamiento de fuerza bruta | Documentado; lista capped a 20 (linea ya existente) |
| M8 | Parseo fragil de sys_descr / walk SNMP: datos no confiables del dispositivo | parse_sys_descr con try/except; walk_if_table avisa si trunca por iteraciones |
| M9 | Logs SSH con str(exc) a nivel ERROR: fuga de credenciales | Logs limpios: logger.debug para except; ERROR solo con mensaje genérico |
| M10 | discover_neighbors_snmp (LLDP/CDP) no funciona en SNMPv3 | Ahora acepta auth_data en la firma; except: pass → logger.debug |
| M11 | OIDs construidos: riesgo de OID injection bajo (pero documentable) | OIDs son constantes; sin input directo del usuario |
| M12 | DeepDiscover sin verificación de queries cruzadas | Verificación de org ya presente en get_object_or_404 |
| M13 | match_stencil sin paginación/límite: carga todos + fuzzy match sin cap | Filtro .only(...) + cap de 200 candidatos en fuzzy |
| M14 | Dispositivo hallado solo por nmap: dead code en clasificación | Arreglado: se clasifica correctamente (no siempre other) |
| M15 | deep_discover_profile timeout sin feedback visual | Deuda menor (UI/frontend) |
| M16 | Bulk_enrich: max_concurrent sin tope → DoS | Schema: max_concurrent capped a 1–50 |
BAJA severidad (10)
| ID | Título | Corrección |
|---|---|---|
| B1 | Código muerto / ternarios imposibles | Limpiado: calculate_confidence ternario sin rama |
| B2 | Sanitización de inputs (MAC, OID) frágil | sanitize_agent_mac valida contra patrón MAC; _extract_enterprise_number tolera prefijo iso. |
| B3 | Imports lazy de v3 no thread-safe | Orden de lazy-import corregido |
| B4 | sanitize_agent_mac sin validación post-saneado | Ahora valida resultado contra patrón MAC real |
| B5 | Walk SNMP sin aviso al truncar | Aviso en logger.warning si se alcanza límite de iteraciones |
| B6 | Logs excesivos en discovery: ruido en observabilidad | Niveles ajustados: debug para SNMP/SSH verboso |
| B7 | device_type no saneado antes de scrapli: posibles inyecciones blandas | Saneado: solo [a-zA-Z0-9_-] |
| B8 | except genericos: except Exception: pass traga errores | Reemplazados por except SpecificException: logger.debug |
| B9 | _extract_enterprise_number: parseo fragil de OID | Tolera prefijo iso. y puntos en notación alternativa |
| B10 | Duplicación en validadores de SNMP version | Unificado: validador reutilizable _validate_snmp_version |
Fixes aplicados en esta tanda
Seguridad (atacante)
- A1 · require_perm: Los 5 endpoints Auto-Provision ahora validan permisos. Un usuario readonly ya no puede lanzar escaneos ni persistir DeviceProfiles.
- A2 · scan_guard: Nuevo módulo
network/services/device_discovery/scan_guard.pyque bloquea loopback/link-local/169.254.169.254 (metadata AWS/GCP)/unspecified/multicast. Permite RFC1918 (la LAN del cliente es el caso legítimo). - A3 · no redirects:
detect_vendor_httpya no sigue redirects (allow_redirects=False). - A6 · IDOR status:
deep_discover_statuscarga el profile por org antes de consultar caché. - A7 · argument injection: IP validada como literal seguro (regex
^[0-9.]+$+ip_address()check) en schema + validador. - A10 · SNMPv3 fixes:
build_snmp_authlanzaValueErrorante protocolo desconocido o key sin protocolo (no degrada en silencio).- Import roto corregido:
pysnmp.hlapi.v3arch.auth→pysnmp.hlapi.v3arch.asyncio(SNMPv3 nunca funcionaba).
- A11 · stencil isolation:
match_stencilfiltraorg propia + globales; nunca sugiere stencil de otra org.
Validación (inputs)
- M2 · validadores Pydantic:
ip: literal seguro (sin ranges).subnet: formato correcto + ≤1024 hosts (cierra bulk de /16).snmp_version: solo{v2c, v3}.max_concurrent: 1–50.hosts(bulk-enrich): ≤1024.
Auditoría + observabilidad
- M5 · log_action: Cada discover/bulk/deep registra la acción (actor, organización, IP/subnet, timestamp) en el log de auditoría.
- M3 · error genérico:
{"error": "Discovery failed"}en lugar de{"error": str(e)}. - M4 · org validation:
org or raise 403explícito antes de instanciar servicio.
Robustez
- M10 · LLDP/CDP v3:
discover_neighbors_snmpahora aceptaauth_data; no cae silenciosamente. - M8 · parsing robusto:
parse_sys_descr,walk_if_tablecon try/except + aviso al truncar. - M9 · logs limpios: Niveles ajustados; SSH usa
logger.debug, nostr(exc)a ERROR. - B2–B9 · limpieza menor: Sanitización MAC, OIDs, device_type, ternarios muertos, imports lazy.
Deuda explícita (PRs propios)
Cuatro mejoras requieren cambios de contrato o migración, así que van por separado:
| ID | Título | Motivo |
|---|---|---|
| A4 | Remover snmp_community de DeviceProfileOut | Cambio de schema API (compat); clientes existentes no esperan este campo escondido |
| A5 | Rediseño protocolo Local Agent: distribución de credenciales | Cambio de protocolo WebSocket; todos los agentes deben actualizarse |
| A7 | Mover discover masivo a tarea Huey asincrónica | Requiere refactor del endpoint (request/response); posible cambio de schema |
| A8 | Cifrado en reposo de credenciales SNMP (Plan Hardening E) | Migración de datos; necesita reversión/upgrade de schema Django |
| A9 | SSH host-key verification (scrapli auth_strict_key=True) | Requiere inventario de host-keys o cambio de UX (user prompt) |
Estado en CHANGELOG: “audit-only” (sesión s111, 2026-06-05). Las rectificaciones se planifican en sesión aparte por decisión de Edu.
Cobertura de tests
Nuevo fichero: tests/api/test_network_sa1.py (23 tests)
- ✅ Autorización (readonly no puede discover).
- ✅ Validación IP/subnet (bloques de seguridad).
- ✅ IDOR en status (cross-tenant).
- ✅ scan_guard (loopback, metadata, RFC1918).
- ✅ build_snmp_auth v3 (v3, protocols, degrado).
- ✅ match_stencil isolation (org).
- ✅ Parsing MAC/OID (robustez).
Suite completa: 349 passed, 1 skipped (network completo en verde).
Arquitectura tocada
network/
├── api/
│ ├── discovery.py [MODIFICADO] — autorización + error genérico + log_action
│ └── common.py [MODIFICADO] — validadores Pydantic (ip, subnet, snmp_version, max_concurrent)
├── services/device_discovery/
│ ├── service.py [MODIFICADO] — nmap arg validation, http no-redirects, bulk cap
│ ├── provision_stages.py [MODIFICADO] — cross-tenant checks reforzados
│ ├── snmp_auth.py [MODIFICADO] — v3 fallback removed, import fixed
│ ├── ssh_stage.py [MODIFICADO] — logs limpios, device_type sanitizado
│ ├── lldp_stage.py [MODIFICADO] — SNMPv3 auth_data
│ ├── auto_classifier.py [MODIFICADO] — dead code nmap-only
│ ├── scoring.py [MODIFICADO] — ternario muerto
│ ├── stencil_matching.py [MODIFICADO] — tenant_id filtering, cap 200
│ ├── scan_guard.py [NUEVO] — bloquea loopback/metadata/unspecified, permite RFC1918
│ └── constants.py [MODIFICADO] — COMMON_SNMP_COMMUNITIES cap
├── models.py [SIN CAMBIOS] — credenciales en claro (deuda A8)
└── tests/
└── test_network_sa1.py [NUEVO] — 23 tests de cobertura
core/
└── utils.py [POSIBLE] — log_action (puede existir ya)
Reglas y principios aplicados
- Regla 22 (auditoría en tanda): 37 hallazgos auditados y corregidos en una sola PR, minimizando overhead de integración.
- Aislamiento multi-tenant: Todos los arreglos respetan RLS por organización (checks de
orgexplícitos, filtros en queries). - Defensa en profundidad: Validación en schema (Pydantic) + servicio (guard) + persistencia (queries filtradas).
- Deuda explícita: Las 5 mejoras que cruzan contrato/migración van documentadas en “Deuda explícita”, no bajan la calidad de esta tanda.
Sesión de contexto
- Auditoría: s111 (2026-06-05) — hallazgos documentados
- Ejecución (esta PR): s113 (2026-06-07) — fixes aplicados
- PR: #76
edu/network-sa1-fixes - Commit: 6c19df6a665ca7541ad31777f16a5a0173a56c1a
Véase también
- [[concept—saas—multi-tenancy]]
- [[entity—network—model—device-profile]]
- [[feature—monitoring—sa1-audit-fixes]]
- [[concept—security—ssrf]]
- [[concept—security—broken-access-control]]
- [[entity—network—service—device-discovery]]
- [[decision—20260607—network-audit-phase3]]
- [[concept—infra—async-task-queue]]
Referenciado desde
- Auto-Provision no pierde su listado de resultados por el cierre de sesión
- Auto-Provision prueba "public" justo después de tu comunidad SNMP, y admite varias separadas por comas
- Deuda #274 Tanda B — puerto sin recarga, deep discovery deduplicado y un solo cargador de series en Wireless (v1.86.17)
- El auto-descubrimiento identifica un equipo por su MAC, no por su IP — un re-escaneo actualiza la ficha en vez de duplicarla
- Liquidación de las rondas 30-08/06-09 · Cuarto PR: el descubrimiento de red deja de mentir (network)