◇ OpenCyber Academy
Tecnólogo · Capítulo 3 · Português

Capítulo 3 — Fundamentos de programação

Ensino gratuito e aberto para quem deseja proteger pessoas e tecnologias. Não é preciso criar uma conta para estudar ou consultar as respostas.

Capítulo 3 — Fundamentos de programação

Mapa da aula

Aula 03 publicada. Mapa, exemplo, EX007.

flowchart TD
  N0["3.1 Entrada não confiável"]
  N1["3.2 Camadas de validação"]
  N2["3.3 Rejeitar, quarentenar, aceitar"]
  N3["3.4 Disciplina do código pequeno"]
  N4["3.6 Atividade prática: validador e casos de teste"]
  N5["Síntese do capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5
  N5 --> EX["Exemplo resolvido"]
  EX --> Q["Exercício EX007"]

Desenho lógico

flowchart LR
  N0["3.1 Entrada não confiável"]
  N1["3.2 Camadas de validação"]
  N2["3.3 Rejeitar, quarentenar, aceitar"]
  N3["3.4 Disciplina do código pequeno"]
  N4["3.6 Atividade prática: validador e casos de teste"]
  N5["Síntese do capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> Q["EX007"]
[3.1 Entrada não confiável]
    |
    v
[3.2 Camadas de validação]
    |
    v
[3.3 Rejeitar, quarentenar, aceitar]
    |
    v
[3.4 Disciplina do código pequeno]
    |
    v
[3.6 Atividade prática: validador e casos de teste]
    |
    v
[Síntese do capítulo]
    |
    v
[Exemplo resolvido] --> [Exercício EX007]

Objetivos de aprendizagem e conexão

Depois deste capítulo, você deve ser capaz de:

Os implementadores em serviços críticos escrevem muito código pequeno: parsers, cola entre sistemas, verificações, geradores de relatórios. A maior parte nunca é revisada. Este capítulo trata de escrever esse código de modo que ele falhe com segurança, porque, no LANTERN, o código que lê as mensagens das estações decide no que o historiador — e, portanto, os operadores — acreditam sobre a rede de água.

3.1 Entrada não confiável

Entrada não confiável é qualquer dado cujo conteúdo você não controla: mensagens de dispositivos de campo, arquivos de fornecedores, campos digitados por usuários, registros do sistema de outra organização. Mesmo um dispositivo que é seu se torna não confiável quando você lembra que ele pode estar mal configurado, com defeito, ter sido substituído ou estar sendo personificado. A regra é simples: confira a entrada contra o que você espera antes de agir sobre ela, e decida de antemão o que acontece quando ela falha na verificação. A orientação do projeto OWASP sobre validação de entradas recomenda o allowlisting (aceitar apenas formas sabidamente boas) em vez do denylisting (bloquear formas sabidamente ruins), validar tanto a sintaxe quanto o significado, e fazê-lo o mais cedo possível (OWASP Input Validation Cheat Sheet).

3.2 Camadas de validação

Para uma mensagem de telemetria no formato STATION|TAG|VALUE|UNIX_TIME|SEQ, a validação tem camadas, e a ordem importa:

  1. Estrutura — exatamente cinco campos. Um número errado de campos significa que a mensagem não pode ser interpretada de forma alguma.
  2. Identidade e vocabulário — o identificador da estação corresponde exatamente ao padrão permitido (PS- e dois dígitos) e a tag é uma que o sistema conhece. Tags desconhecidas são rejeitadas, e não guardadas "por via das dúvidas".
  3. Tipo — o valor, o tempo e a sequência são interpretados como números. Valores que são interpretados, mas não são finitos (nan, inf), são rejeitados; muitas linguagens os interpretam sem reclamar.
  4. Significado físico — o valor fica dentro do que o processo físico pode produzir (um nível de reservatório entre 0 e 12 metros; cloro entre 0 e 5 mg/L neste projeto sintético). Um valor fora dessa faixa não é necessariamente malicioso — sensores falham —, então ele é quarentenado para revisão por uma pessoa, em vez de ser descartado ou guardado em silêncio.
  5. Tempo — o carimbo de tempo da mensagem está próximo do relógio do receptor. Uma diferença grande significa um problema de relógio ou uma mensagem antiga; quarentene-a.
  6. Ordem — o número de sequência é maior que o último aceito para aquela estação. Um número repetido ou menor é uma reprodução ou uma reordenação; rejeite-o.

3.3 Rejeitar, quarentenar, aceitar

Uma função de validação que só retorna "válido" ou "inválido" força uma má escolha para os dados limítrofes: ou jogar fora uma leitura de que um operador pode precisar, ou guardar uma que pode enganar. Os sistemas em operação precisam de um terceiro desfecho. Rejeitar significa que a entrada não pode ser confiada nem interpretada, e é registrada em log e descartada. Quarentenar significa que a entrada é interpretável, mas implausível; ela é mantida à parte, sinalizada e revisada. Aceitar significa que ela passou em todas as verificações e pode servir de base para ação. Cada desfecho é contado: um aumento súbito de rejeições é, por si só, um sinal de monitoramento (Capítulo 19).

3.4 Disciplina do código pequeno

Quatro hábitos tornam as pequenas ferramentas confiáveis. Mantenha uma função por decisão, para que ela possa ser testada sozinha. Torne o estado explícito — a última sequência vista por estação é passada como argumento, e não escondida numa global. Falhe fechado — qualquer caminho que você não previu termina em rejeição, e não em aceitação. E teste primeiro com casos ruins: para cada verificação, escreva pelo menos uma entrada que deveria falhar nela, e execute todas depois de cada mudança.

O Laboratório 3 segue os quatro em pequena escala: todas as decisões ficam em uma única função curta, validate(), que pode ser testada sozinha. A verificação de sequência recebe como argumento o último número aceito por estação, e é por isso que o 102 repetido pode ser testado sozinho; e a regra de plausibilidade do cloro é uma etapa separada, guiada pela tabela LIMITS depois que o valor é convertido, e é por isso que 9,80 mg/L pode ser quarentenado em vez de rejeitado sem mexer no parser.

3.5 Exemplo resolvido: o validador de mensagens das estações (sintético)

O Laboratório 3 implementa as seis camadas. Executá-lo sobre onze mensagens sintéticas produz três aceitações, seis rejeições e duas quarentenas. Olhe para os casos que um parser ingênuo erraria: nan é interpretado como float e seria guardado; 210.5;DROP só é pego porque o valor é interpretado como número, em vez de repassado como texto; PS-7 falha porque o padrão é casado por inteiro; um número de sequência 102 repetido é rejeitado como reprodução, embora a mensagem seja perfeita em todo o resto; e um valor de cloro de 9,80 mg/L é quarentenado, e não rejeitado, porque uma pessoa precisa saber se o analisador da PS-07 falhou.

3.6 Atividade prática: validador e casos de teste

Laboratório 3 (salve como lab03.py, execute com 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)

Saída 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 mudar qualquer coisa, anote o que você espera que cada linha imprima. Depois execute. Toda linha em que a sua expectativa estava errada é um ponto em que o seu modelo mental do código estava errado — e esse é o objetivo do exercício.

Três linhas costumam surpreender os leitores: nan ser rejeitado em vez de guardado, PS-7 falhar no padrão de identificador embora pareça inofensivo, e a leitura de cloro de 9,80 mg/L terminar em quarentena em vez de rejeição. Se você previu errado alguma delas, anote por quê; depois acrescente à lista de mensagens um caso ruim seu, como um número de sequência menor que 102 ou um identificador com um espaço no final, e confirme que o validador o rejeita antes de confiar na mudança.

3.7 Erros comuns e limites

A validação protege o receptor de dados malformados e implausíveis. Ela não prova que os dados são autênticos: uma mensagem perfeitamente formatada e plausível ainda pode ser forjada. A autenticação é acrescentada no Capítulo 15.

Exercícios — capítulo 3

EX007 — Preveja, depois execute. Objetivo: testar o seu modelo da ordem de validação. Entradas: Laboratório 3. Restrições: escreva as previsões antes de executar. Etapas: (1) acrescente três mensagens: PS-04|LEVEL_M|inf|1789999999|103, PS-04|LEVEL_M|12.0|1789999999|103, PS-04|LEVEL_M|-0.0|1789999999|104; (2) preveja cada desfecho; (3) execute e explique qualquer surpresa. Evidência esperada: previsões, saída, explicação. Rubrica: 3 = inf rejeitado por não ser finito, 12.0 aceito (limite inclusivo), -0.0 aceito porque −0,0 é igual a 0,0 e a sequência avançou, com um comentário sobre se −0,0 deveria ser normalizado; 2 = desfechos certos, explicação rasa; 1 = não executado; 0 = nada.

EX008 — Projeto da quarentena. Objetivo: projetar o caminho da quarentena. Entradas: Capítulo 3 ("Rejeitar, quarentenar, aceitar") e o caso do cloro da PS-07. Restrições: nenhum código é exigido. Etapas: (1) diga onde as mensagens quarentenadas são guardadas e por quanto tempo; (2) diga quem as revisa, com que rapidez, e o que dispara o escalonamento para a operação de Água; (3) diga o que a tela do operador deve mostrar enquanto um valor está em quarentena. Evidência esperada: projeto de meia página. Rubrica: 3 = armazenamento separado com retenção, revisor nomeado e tempo de resposta, regra de escalonamento ligada ao processo físico (por exemplo, duas leituras consecutivas de cloro implausíveis → ligar para a sala de controle), tela mostrando "valor retido — em revisão" em vez do último valor bom como se fosse atual; 2 = falta um elemento; 1 = "um administrador confere"; 0 = nada.

EX009 — Falhe fechado. Objetivo: encontrar um caminho que falha aberto. Entradas: este fragmento: try: v = float(val) except: pass; if v > hi: return "quarantine"; return "accept". Restrições: identifique, explique, corrija. Etapas: (1) mostre uma entrada para a qual ele retorna accept indevidamente; (2) explique; (3) reescreva-o. Evidência esperada: entrada, explicação, código corrigido. Rubrica: 3 = mostra que um valor não numérico deixa v sem definição ou obsoleto (um NameError na primeira mensagem, ou o valor da mensagem anterior reaproveitado depois), aponta a falta do limite inferior e o except sem tipo, e reescreve para retornar reject em qualquer falha de interpretação, com os dois limites e uma verificação de finitude; 2 = encontra um problema; 1 = vago; 0 = nada.

Síntese do capítulo

O código pequeno decide o que os operadores veem. Trate toda mensagem como não confiável, valide-a em camadas — estrutura, identidade, tipo, significado físico, tempo, ordem — e dê aos dados limítrofes um caminho de quarentena, para que as falhas não sejam nem escondidas nem guardadas como verdade. Mantenha as decisões em funções pequenas e testáveis, torne o estado explícito, falhe fechado e teste primeiro os casos ruins. Validação não é autenticação; essa vem depois.

Continue estudando — capítulo 03 de 30

← Capítulo 02 · Índice de capítulos do curso · Capítulo 04 →

Siga para a próxima lição escrita.