OpenCyber Academy
Engenharia · Capítulo 4 · Português
Código do curso: OCA-137Capítulo 04: OCA-137-C004

Capítulo 4 — Programação I

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

Mapa da aula

Aula 04 publicada. Mapa, exemplo, EX010.

flowchart TD
  N0["4.1 Decisões como funções"]
  N1["4.2 Tipos exatos"]
  N2["4.3 Testes em quadros decodificados sintéticos"]
  N3["4.3b Registrando o que a função decidiu"]
  N4["Síntese"]
  N0 --> N1 --> N2 --> N3 --> N4
  N4 --> EX["Exemplo resolvido"]
  EX --> Q["Exercício EX010"]

Desenho lógico

flowchart LR
  N0["4.1 Decisões como funções"]
  N1["4.2 Tipos exatos"]
  N2["4.3 Testes em quadros decodificados sintéticos"]
  N3["4.3b Registrando o que a função decidiu"]
  N4["Síntese"]
  N0 --> N1 --> N2 --> N3 --> N4 --> Q["EX010"]
[4.1 Decisões como funções]
    |
    v
[4.2 Tipos exatos]
    |
    v
[4.3 Testes em quadros decodificados sintéticos]
    |
    v
[4.3b Registrando o que a função decidiu]
    |
    v
[Síntese]
    |
    v
[Exemplo resolvido] --> [Exercício EX010]

Objetivos de aprendizagem

Você vai (1) escrever funções pequenas e puras que decidem se uma mensagem de TO é permitida; (2) representar endereços industriais com tipos exatos; (3) testar com tráfego decodificado sintético, nunca com tráfego real da planta.

4.1 Decisões como funções

Uma lista de permissões (allow-list) para Modbus é uma função permit(source, function_code, register) -> (bool, reason). Torná-la pura — entradas entram, decisão sai, sem configuração oculta — permite testá-la num laptop com registros sintéticos. A string de motivo importa: "register-out-of-range" diz a um revisor que a IHM tentou escrever fora do bloco de setpoints, o que é uma história diferente de uma estação desconhecida usando uma função de diagnóstico.

Lena mantém as strings de motivo curtas e fixas, para que os logs possam ser contados por motivo.

4.2 Tipos exatos

Armazene os códigos de função como inteiros, as faixas de registradores como objetos range e as identidades das estações como strings fixas vindas de um inventário. Um bug frequente trata a notação 4xxxx de "holding register" como string e casa por prefixo, de modo que 400500 parece 40050. Converta os endereços uma única vez, no momento da análise, em inteiros com deslocamentos documentados.

Lena escreve uma única função de conversão para os endereços de registradores e a testa nas duas pontas da faixa gravável da IHM, 40001 e 40010, e um passo fora de cada uma. Os nomes das estações vêm do inventário, então um erro de digitação na lista de permissões falha numa consulta, em vez de criar em silêncio uma nova estação que não casa com nada.

4.3 Testes em quadros decodificados sintéticos

O laboratório abaixo nunca abre um socket; ele verifica tuplas já decodificadas. Isso é deliberado. O código do curso não pode enviar tráfego Modbus a nenhum dispositivo. Escreva um teste por regra, mais os registradores de fronteira 40001 e 40010.

"""Lab: Modbus function-code allow-list on synthetic, already-decoded requests (no network)."""
ALLOW = {("HMI-1", 3), ("HMI-1", 6), ("HIST-1", 3), ("HIST-1", 4)}  # (source, function code)
WRITE_REGS = {("HMI-1", 6): range(40001, 40011)}                     # HMI may write setpoints only
reqs = [("HMI-1", 3, 40001), ("HMI-1", 6, 40003), ("HMI-1", 6, 40050),
        ("HIST-1", 3, 30001), ("HIST-1", 16, 40001), ("ENG-WS", 8, 0), ("HIST-1", 4, 30010)]
for src, fc, reg in reqs:
    if (src, fc) not in ALLOW:
        d = "BLOCK fc-not-allowed"
    elif (src, fc) in WRITE_REGS and reg not in WRITE_REGS[(src, fc)]:
        d = "BLOCK register-out-of-range"
    else:
        d = "PASS"
    print(f"{src:7} fc={fc:2} reg={reg:5} -> {d}")

Saída esperada:

HMI-1   fc= 3 reg=40001 -> PASS
HMI-1   fc= 6 reg=40003 -> PASS
HMI-1   fc= 6 reg=40050 -> BLOCK register-out-of-range
HIST-1  fc= 3 reg=30001 -> PASS
HIST-1  fc=16 reg=40001 -> BLOCK fc-not-allowed
ENG-WS  fc= 8 reg=    0 -> BLOCK fc-not-allowed
HIST-1  fc= 4 reg=30010 -> PASS

Os testes são agrupados por regra, para que uma falha aponte direto para a cláusula que quebrou. O teste de escrita do historiador, que pegou a função 16, é mantido como caso permanente. A tabela de testes de Lena lista cada tupla, a decisão esperada e a string de motivo, e um revisor pode lê-la sem executar nada. Novas estações ou registradores acrescentados à lista de permissões chegam com as suas próprias linhas no mesmo pull request.

4.3b Registrando o que a função decidiu

Toda decisão deve ser registrada com a origem, o código de função, o registrador, a decisão, o motivo e a versão do arquivo da lista de permissões. Quando um operador pergunta por que uma escrita da IHM foi bloqueada às 03:12, o log responde numa linha. Sem o campo de versão, ninguém consegue dizer se o bloqueio veio da lista de ontem ou da de hoje.

Caso sintético — Ashford Batch Plant. A primeira lista de permissões de Lena deixava o HIST-1 usar a função 16 (escrever múltiplos registradores) porque ele "só lê". A tabela de testes pegou isso: o historiador não tem nada que escrever, então a função 16 foi removida e o teste agora espera BLOCK.

4.4 Erros e limites

Uma lista de permissões num monitor só é tão boa quanto o decodificador. Quadros malformados precisam ser contados e alarmados, não descartados em silêncio. Os dispositivos reais de aplicação precisam de validação do fornecedor antes da implantação.

O monitor de Lena conta os quadros malformados por minuto e gera um alarme se a contagem subir, porque uma rajada repentina pode significar um dispositivo falhando ou um bug no decodificador. Tratar esses quadros como permitidos transformaria cada lacuna do decodificador numa porta aberta. O monitor deste curso apenas observa; qualquer dispositivo que de fato bloqueie tráfego na ABP é um produto de fornecedor validado antes na bancada.

Exercícios

Síntese

Funções de permissão puras, tipadas e testadas são a menor unidade confiável de software de segurança de TO.

Respostas comentadas

EX010. permit(src, fc, reg): se src == "HIST-1" e fc em {3, 4} → PASS; se src == "HIST-1" em qualquer outro caso → BLOCK "fc-not-allowed". Nenhum código de escrita (5, 6, 15, 16) é listado.

EX011. Teste 40001 e 40010 com HMI-1 fc=6; ambos PASS. 40011 está fora de range(40001, 40011) e precisa dar BLOCK "register-out-of-range".

EX012. Não há tupla (ENG-WS, 8) em ALLOW, então ela é bloqueada com fc-not-allowed. A função 8 é de diagnóstico; uma lista que nega por padrão obriga a declarar e aprovar as necessidades da estação de engenharia, em vez de presumi-las.