Capítulo 3 — Fundamentos de programación
Mapa de la lección
Lección 03 publicada. Mapa, ejemplo, EX007.
flowchart TD
N0["Resultados de aprendizaje y conexión"]
N1["3.1 Entradas no confiables"]
N2["3.2 Capas de validación"]
N3["3.3 Rechazar, poner en cuarentena, aceptar"]
N4["3.4 Disciplina del código pequeño"]
N5["3.6 Actividad práctica: validador y casos de prueba"]
N6["Síntesis del capítulo"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6
N6 --> EX["Ejemplo resuelto"]
EX --> Q["Ejercicio EX007"]
Dibujo lógico
flowchart LR
N0["Resultados de aprendizaje y conexión"]
N1["3.1 Entradas no confiables"]
N2["3.2 Capas de validación"]
N3["3.3 Rechazar, poner en cuarentena, aceptar"]
N4["3.4 Disciplina del código pequeño"]
N5["3.6 Actividad práctica: validador y casos de prueba"]
N6["Síntesis del capítulo"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> Q["EX007"]
[Resultados de aprendizaje y conexión]
|
v
[3.1 Entradas no confiables]
|
v
[3.2 Capas de validación]
|
v
[3.3 Rechazar, poner en cuarentena, aceptar]
|
v
[3.4 Disciplina del código pequeño]
|
v
[3.6 Actividad práctica: validador y casos de prueba]
|
v
[Síntesis del capítulo]
|
v
[Ejemplo resuelto] --> [Ejercicio EX007]
Resultados de aprendizaje y conexión
Al terminar este capítulo, usted debería ser capaz de:
- escribir una herramienta pequeña cuyo comportamiento ante entradas erróneas se decide de antemano, no se descubre en producción;
- validar entradas no confiables mediante una lista de permitidos de estructura, tipo, rango y orden;
- separar los resultados rechazar, poner en cuarentena y aceptar, y explicar por qué un sistema en operación necesita los tres;
- probar una herramienta con casos deliberadamente erróneos y registrar los resultados como evidencia.
Los implementadores de servicios críticos escriben mucho código pequeño: analizadores, código de unión entre sistemas, comprobaciones, generadores de informes. Casi nunca se revisa. Este capítulo trata de escribir ese código de modo que falle de forma segura, porque en LANTERN el código que lee los mensajes de las estaciones decide lo que el historiador (y, por tanto, los operadores) cree sobre la red de agua.
3.1 Entradas no confiables
Una entrada no confiable es cualquier dato cuyo contenido usted no controla: mensajes de dispositivos de campo, archivos de proveedores, campos que teclean los usuarios, registros del sistema de otra organización. Incluso un dispositivo propio pasa a ser no confiable en cuanto recuerda que puede estar mal configurado, averiado, sustituido o suplantado. La regla es sencilla: compruebe la entrada frente a lo que espera antes de actuar sobre ella, y decida de antemano qué ocurre cuando no supera la comprobación. La guía del proyecto OWASP sobre validación de entradas recomienda usar listas de permitidos (aceptar solo formas conocidas como válidas) en lugar de listas de bloqueo (bloquear formas conocidas como maliciosas), validar tanto la sintaxis como el significado y hacerlo lo antes posible (OWASP Input Validation Cheat Sheet).
3.2 Capas de validación
Para un mensaje de telemetría con la forma STATION|TAG|VALUE|UNIX_TIME|SEQ, la validación tiene capas, y el orden importa:
- Estructura: exactamente cinco campos. Un número de campos erróneo significa que el mensaje no puede interpretarse en absoluto.
- Identidad y vocabulario: el identificador de estación coincide exactamente con el patrón permitido (
PS-y dos dígitos) y la etiqueta es una que el sistema conoce. Las etiquetas desconocidas se rechazan, no se almacenan «por si acaso». - Tipo: el valor, la hora y la secuencia se interpretan como números. Los valores que se interpretan pero no son finitos (
nan,inf) se rechazan; muchos lenguajes los aceptan sin problema. - Significado físico: el valor está dentro de lo que el proceso físico puede producir (un nivel de depósito entre 0 y 12 metros; cloro entre 0 y 5 mg/L en este diseño sintético). Un valor fuera de ese rango no es necesariamente malicioso (los sensores fallan), así que se pone en cuarentena para que una persona lo revise, en lugar de descartarse o almacenarse en silencio.
- Tiempo: la marca de tiempo del mensaje está cerca del reloj del receptor. Una diferencia grande indica un problema de reloj o un mensaje antiguo; póngalo en cuarentena.
- Orden: el número de secuencia es mayor que el último aceptado para esa estación. Un número repetido o menor es una reproducción o una reordenación; rechácelo.
3.3 Rechazar, poner en cuarentena, aceptar
Una función de validación que solo devuelve «válido» o «no válido» obliga a una mala elección con los datos dudosos: o se tira una lectura que un operador podría necesitar, o se almacena una que podría inducir a error. Los sistemas en operación necesitan un tercer resultado. Rechazar significa que la entrada no puede considerarse fiable ni interpretarse, y se registra y descarta. Poner en cuarentena significa que la entrada es interpretable pero inverosímil; se guarda aparte, se marca y se revisa. Aceptar significa que superó todas las comprobaciones y puede usarse para actuar. Cada resultado se cuenta: un aumento repentino de rechazos es en sí mismo una señal de monitorización (capítulo 19).
3.4 Disciplina del código pequeño
Cuatro hábitos hacen fiables las herramientas pequeñas. Mantenga una función por decisión, para poder probarla por separado. Haga explícito el estado: la última secuencia vista por estación se pasa como argumento, no se esconde en una variable global. Falle de forma cerrada: cualquier camino que no previó termina en rechazo, no en aceptación. Y pruebe primero con casos erróneos: para cada comprobación, escriba al menos una entrada que deba fallarla, y ejecútelas todas después de cada cambio. El Lab 3 sigue los cuatro a pequeña escala: todas las decisiones están en una única función corta, validate(), que puede probarse por separado. La comprobación de secuencia recibe como argumento el último número aceptado por estación, y por eso el 102 repetido puede probarse por sí solo; y la regla de plausibilidad del cloro es un paso separado, guiado por la tabla LIMITS después de interpretar el valor, y por eso 9,80 mg/L puede ponerse en cuarentena en lugar de rechazarse sin tocar el analizador.
3.5 Ejemplo resuelto: el validador de mensajes de las estaciones (sintético)
El Lab 3 implementa las seis capas. Al ejecutarlo sobre once mensajes sintéticos se obtienen tres aceptaciones, seis rechazos y dos cuarentenas. Fíjese en los casos que un analizador ingenuo resolvería mal: nan se interpreta como número de coma flotante y se almacenaría; 210.5;DROP se detecta solo porque el valor se interpreta como número en lugar de pasarse como texto; PS-7 falla porque el patrón se compara completo; un número de secuencia 102 repetido se rechaza como reproducción aunque el mensaje sea por lo demás perfecto; y un valor de cloro de 9,80 mg/L se pone en cuarentena, no se rechaza, porque una persona necesita saber si el analizador de PS-07 ha fallado.
3.6 Actividad práctica: validador y casos de prueba
Lab 3 (guárdelo como lab03.py y ejecútelo con python3 lab03.py):
# Lab 3 - validating telemetry messages before they reach the historian (offline, standard library only)
# Synthetic message format from Fallowmere pumping stations: STATION|TAG|VALUE|UNIX_TIME|SEQ
import re
LIMITS = {"LEVEL_M": (0.0, 12.0), "FLOW_LS": (0.0, 450.0), "CL2_MGL": (0.0, 5.0)}
STATION = re.compile(r"PS-\d{2}")
def validate(line, now, last_seq):
parts = line.strip().split("|")
if len(parts) != 5:
return "reject: field count"
st, tag, val, ts, seq = parts
if not STATION.fullmatch(st):
return "reject: station id"
if tag not in LIMITS:
return "reject: unknown tag"
try:
v, t, q = float(val), int(ts), int(seq)
except ValueError:
return "reject: not numeric"
if v != v or v in (float("inf"), float("-inf")):
return "reject: not finite"
lo, hi = LIMITS[tag]
if not lo <= v <= hi:
return "quarantine: out of physical range"
if abs(now - t) > 300:
return "quarantine: clock skew >300 s"
if q <= last_seq.get(st, -1):
return "reject: replay or reorder"
last_seq[st] = q
return "accept"
now = 1_790_000_000
msgs = ["PS-04|LEVEL_M|3.42|1789999990|101", "PS-04|LEVEL_M|3.44|1789999995|102",
"PS-04|LEVEL_M|3.44|1789999995|102", "PS-07|CL2_MGL|0.61|1789999801|55",
"PS-07|CL2_MGL|9.80|1789999990|56", "PS-07|FLOW_LS|nan|1789999990|57",
"PS-7|FLOW_LS|120|1789999990|58", "PS-09|PRESS_BAR|2.1|1789999990|9",
"PS-09|FLOW_LS|210.5|1789990000|10", "PS-09|FLOW_LS|210.5;DROP|1789999990|11",
"PS-04|LEVEL_M|3.47|1789999999"]
state = {}
counts = {}
for m in msgs:
r = validate(m, now, state)
counts[r.split(":")[0]] = counts.get(r.split(":")[0], 0) + 1
print(f"{m:<38} {r}")
print(counts)
Salida esperada (Python 3.13.5):
PS-04|LEVEL_M|3.42|1789999990|101 accept
PS-04|LEVEL_M|3.44|1789999995|102 accept
PS-04|LEVEL_M|3.44|1789999995|102 reject: replay or reorder
PS-07|CL2_MGL|0.61|1789999801|55 accept
PS-07|CL2_MGL|9.80|1789999990|56 quarantine: out of physical range
PS-07|FLOW_LS|nan|1789999990|57 reject: not finite
PS-7|FLOW_LS|120|1789999990|58 reject: station id
PS-09|PRESS_BAR|2.1|1789999990|9 reject: unknown tag
PS-09|FLOW_LS|210.5|1789990000|10 quarantine: clock skew >300 s
PS-09|FLOW_LS|210.5;DROP|1789999990|11 reject: not numeric
PS-04|LEVEL_M|3.47|1789999999 reject: field count
{'accept': 3, 'reject': 6, 'quarantine': 2}
Antes de cambiar nada, anote lo que espera que imprima cada línea. Después, ejecútelo. Cualquier línea en la que su expectativa fue errónea es un lugar donde su modelo mental del código era erróneo, y ese es el propósito del ejercicio.
Tres líneas suelen sorprender a los lectores: que nan se rechace en lugar de almacenarse, que PS-7 no supere el patrón del identificador aunque parezca inofensivo, y que la lectura de cloro de 9,80 mg/L acabe en cuarentena en lugar de en rechazo. Si predijo mal alguna, anote por qué; después añada a la lista de mensajes un caso erróneo propio, como un número de secuencia menor que 102 o un identificador con un espacio al final, y confirme que el validador lo rechaza antes de fiarse del cambio.
3.7 Errores frecuentes y límites
- Validar con una lista de bloqueo («rechazar si contiene un punto y coma») en lugar de con una lista de permitidos de lo que es aceptable.
- Usar una coincidencia parcial para los identificadores, de modo que
PS-7,XPS-04oPS-04-testpasen. - Olvidar
nany el infinito. - Descartar en silencio los valores fuera de rango, de modo que un sensor averiado parezca una estación tranquila.
- Actualizar el estado de reproducción antes de que se hayan superado todas las demás comprobaciones: un mensaje autenticado pero mal formado o inverosímil puede entonces bloquear los mensajes legítimos que lo siguen (lo verá en el capítulo 29).
- Escribir pruebas solo para el camino feliz.
La validación protege al receptor de los datos mal formados e inverosímiles. No prueba que los datos sean auténticos: un mensaje perfectamente formado y verosímil puede seguir siendo falso. La autenticación se añade en el capítulo 15.
Ejercicios — capítulo 3
EX007 — Prediga y después ejecute. Objetivo: poner a prueba su modelo del orden de validación. Entradas: el Lab 3. Restricciones: escriba las predicciones antes de ejecutar. Pasos: (1) añada tres mensajes: PS-04|LEVEL_M|inf|1789999999|103, PS-04|LEVEL_M|12.0|1789999999|103, PS-04|LEVEL_M|-0.0|1789999999|104; (2) prediga cada resultado; (3) ejecute y explique cualquier sorpresa. Evidencia esperada: predicciones, salida y explicación. Rúbrica: 3 = inf rechazado por no finito, 12.0 aceptado (límite inclusivo), -0.0 aceptado porque −0,0 es igual a 0,0 y la secuencia avanzó, con un comentario sobre si −0,0 debería normalizarse; 2 = resultados correctos, explicación escasa; 1 = no se ejecutó; 0 = nada.
EX008 — Diseño de la cuarentena. Objetivo: diseñar el camino de cuarentena. Entradas: el capítulo 3 («Rechazar, poner en cuarentena, aceptar») y el caso del cloro de PS-07. Restricciones: no se requiere código. Pasos: (1) indique dónde se almacenan los mensajes en cuarentena y durante cuánto tiempo; (2) indique quién los revisa, con qué rapidez y qué desencadena el escalado a Operaciones de Agua; (3) indique qué debe mostrar la pantalla del operador mientras un valor está en cuarentena. Evidencia esperada: diseño de media página. Rúbrica: 3 = almacén separado con conservación, revisor nombrado y tiempo de respuesta, regla de escalado ligada al proceso físico (por ejemplo, dos lecturas de cloro inverosímiles consecutivas → llamar a la sala de control), la pantalla muestra «valor retenido: en revisión» en lugar del último valor bueno como si fuera actual; 2 = falta un elemento; 1 = «lo comprueba un administrador»; 0 = nada.
EX009 — Falle de forma cerrada. Objetivo: encontrar un camino que falla de forma abierta. Entradas: este fragmento: try: v = float(val) except: pass; if v > hi: return "quarantine"; return "accept". Restricciones: identifique, explique y corrija. Pasos: (1) muestre una entrada para la que devuelve accept erróneamente; (2) explíquelo; (3) reescríbalo. Evidencia esperada: entrada, explicación y código corregido. Rúbrica: 3 = muestra que un valor no numérico deja v sin asignar o con un valor antiguo (un NameError en el primer mensaje, o el valor del mensaje anterior reutilizado después), señala la falta de límite inferior y el except desnudo, y lo reescribe para devolver rechazo ante cualquier fallo de interpretación, con ambos límites y una comprobación de finitud; 2 = encuentra un problema; 1 = vago; 0 = nada.
Síntesis del capítulo
El código pequeño decide lo que ven los operadores. Trate cada mensaje como no confiable, valídelo por capas (estructura, identidad, tipo, significado físico, tiempo, orden) y dé a los datos dudosos un camino de cuarentena, de modo que los fallos no se oculten ni se almacenen como verdad. Mantenga las decisiones en funciones pequeñas que puedan probarse, haga explícito el estado, falle de forma cerrada y pruebe primero los casos erróneos. La validación no es autenticación; esta llega más adelante.
Siga estudiando — capítulo 03 de 30
← Capítulo 02 · Índice de capítulos del curso · Capítulo 04 →
Continúe con la siguiente lección escrita.