Blogue ·
Uma análise que fechava a sua janela
Cada análise agendada bem sucedida marcava a própria janela como expirada, por causa de uma palavra da enumeração errada. Dois defeitos só visíveis a medir.
Por Provibr
A nossa análise de exposição tem um planeador. Reclama uma janela, envia um comando de análise ao agente que alcança a máquina e espera por duas coisas: a medição e a conclusão do comando que a transportou. Quando uma janela termina sem resposta é marcada como expirada, e a máquina aparece como «sem medição recente».
Cada análise bem sucedida marcava a própria janela como expirada. Quatro em quatro, medidas contra um agente real, enquanto as quatro medições em si eram lidas e guardadas sem problema. O painel anunciava que quatro máquinas não tinham sido analisadas recentemente, minutos depois de as analisar.
Uma palavra, vinda de outra tabela
O tratador que fecha uma janela sem resposta começava com uma saída antecipada: se o comando teve êxito não há nada a fechar, portanto sai-se.
A palavra com que comparava era succeeded. Essa palavra não existe na enumeração dos estados de comando, que contém pending, sent, completed, failed, timeout e cancelled. É a palavra de outra tabela por completo, a que segue as instalações de agentes, e chegou aqui porque alguém escreveu o tratador olhando para os estados dessa outra tabela e porque o parâmetro estava tipado como texto simples.
O texto simples é toda a história. A comparação compila contra qualquer texto, logo nada objeta, e a falha não é uma exceção mas um ramo que nunca é tomado. O tratador percorreu o seu caminho de «não chegou resposta» para cada análise bem sucedida e marcou obedientemente cada janela como expirada.
Nenhum teste unitário poderia ter encontrado isto. Numa imitação não há ciclo de vida de comando: a imitação é o que entrega o estado, logo o estado é o que escreveu quem redigiu o teste, e essa pessoa escreveu a mesma palavra errada. Só um agente real, a enviar conclusões reais pelo distribuidor real, produz o estado que o revela. O parâmetro está agora tipado nos três estados que podem mesmo chegar a esta função, tal como o tratador equivalente de monitorização sempre esteve. Duas funções, a mesma pergunta, e apenas uma escrita com cuidado.
E as duas mensagens chegam fora de ordem
Corrigir a comparação não bastava, e a razão é uma segunda coisa que só um agente real mostra.
O agente envia duas mensagens distintas: o resultado da análise e a conclusão do comando. Viajam de forma independente. Medido, o resultado chegou primeiro, catorze milissegundos antes da conclusão. Esse intervalo é pequeno o bastante para que ninguém pense nele e grande o bastante para ocorrer sempre.
Quando o tratador de conclusão corre, a janela está portanto quase sempre já fechada pelo resultado. Uma atualização sem condição reabre então uma janela terminada e marca-a de novo expirada, que é o defeito original com outro chapéu. A atualização está por isso limitada aos dois estados em que uma janela ainda espera resposta. Se já está resolvida, o tratador não muda nada.
É uma forma geral: quando um facto pode chegar por dois caminhos, o escritor que não é o principal tem de declarar de que estados lhe é permitido sair.
Um achado fechado que parou toda a frota
O segundo defeito é mais calado e causou mais estragos.
Quando uma fonte de vulnerabilidades se atualiza, os achados abertos são reavaliados face aos novos dados. Um achado cuja correspondência desapareceu é fechado, e fechado como não verificado e não como resolvido, porque nada foi medido: o que mudou é a nossa inferência, não a máquina. Essa distinção é a razão de aquele estado existir.
A reavaliação obtinha os achados derivados existentes para distinguir «atualiza este» de «cria um novo». Obtinha apenas os abertos. É um filtro de aspeto razoável e está errado, porque a restrição de unicidade por baixo nada sabe de estados: assenta no servidor e numa impressão digital do achado, e ponto.
A sequência é esta: uma base nacional de vulnerabilidades corrige um intervalo de versões, a nossa correspondência desaparece, o achado fecha. Semanas depois essa mesma base revê o intervalo ao contrário, o que acontece mesmo, e a correspondência volta. A consulta não vê a linha fechada. O código toma o ramo «isto ainda não existe». A inserção bate no índice único e levanta uma violação de restrição.
E então essa violação escapa. Quem chama recolhe-a como «a ronda de avisos falhou», e essa ronda não reavalia absolutamente nada, para todos os clientes da plataforma. A condição que a causou não desaparece sozinha, portanto cada ronda seguinte falhou do mesmo modo. Um achado que fechara e reabrira congelou a reavaliação de uma frota inteira, de forma permanente, com uma linha de registo como único sintoma.
A correção é pequena: incluir as linhas fechadas e, quando um achado fechado volta a corresponder, reabri-lo com a sua cronologia intacta em vez de começar um novo. Um achado reaberto é um facto diferente de um novo, e quem o triar merece ver que isto voltou e não que chegou.
Um ciclo de fontes, medido três vezes
O processo das fontes foi escrito antes de existir maneira de o correr sem tocar numa base de dados real, e essa é a armadilha em que caiu a ronda de segurança anterior: importar o pacote de base de dados lê o ambiente e liga-se, pelo que um teste unitário com uma função de descarga injetada escreveu em silêncio quatro avisos numa base de desenvolvimento enquanto anunciava verde.
O lado do armazenamento é agora uma costura, e o ciclo pode ser posto à prova contra uma contabilidade em memória enquanto o código de produção usa a real sem alterações. Depois correu três vezes contra as fontes públicas reais.
| Ronda | Pedidos | O que aconteceu | Tempo |
|---|---|---|---|
| A frio | 4 | 7.066 avisos guardados, 31,4 MB após descompressão | 18,7 s |
| Sem alterações | 1 | 304 Not Modified, validadores guardados mantidos, zero escritas | 0,3 s |
| Cursor dois dias atrás | 3 | 355 avisos atualizados, nenhum novo | segundos |
O 304 é todo o sentido do exercício. Um catálogo de avisos servido de forma estática responde a um pedido condicional com «nada mudou», e o comportamento correto é manter os validadores guardados, não escrever nada e contar a ronda como bem sucedida.
Alguns tamanhos merecem ser sabidos, porque é justamente aí que se adivinha: o catálogo das vulnerabilidades ativamente exploradas pesa cerca de 188 kB no fio por vir comprimido, e uma página de dois mil registos da fonte maior anda pelos 18,7 MB. Ambos são lidos atrás de um tecto rígido de bytes e não pela ajuda JSON vulgar, que não tem limite nenhum; são as duas maiores respostas que a plataforma ingere e vêm de máquinas que não gerimos.
Uma janela que se parte ao meio em vez de saltar
A fonte maior percorre-se por páginas, dentro de uma janela temporal móvel. Duas coisas estavam mal nesse ciclo, e ambas perdiam dados sem nunca assinalar nada.
Se uma janela continha mais registos do que o tecto de páginas permitia, o ciclo parava e mesmo assim movia o cursor para o fim da janela. Tudo o que ficava além do tecto era saltado em silêncio, e só seria recolhido se outra alteração voltasse a tocar nesses mesmos registos.
E uma página vazia a meio de uma janela, quando ainda se declaravam mais registos, avançava o cursor uma página inteira. Dois mil registos perdidos.
Ambos são agora tratados pelo mesmo princípio: se a resposta não cabe, fazer uma pergunta mais pequena, e se a resposta é impossível, não mover nada.
Uma janela demasiado grande é partida ao meio em vez de truncada, no máximo seis vezes, até um chão de uma hora. Seis partições transformam uma semana em pouco mais de duas horas, o que chega para qualquer atraso realista. Abaixo de uma hora pára: uma janela de minutos que ainda devolve vinte mil registos não é um problema de janela mas uma avaria, e não escrever deixando o cursor onde está acaba por ser a única resposta honesta disponível. Uma página vazia a meio da janela é tratada da mesma forma, como um erro que deixa o cursor em paz, para que a ronda seguinte volte a tentar em vez de passar por cima de um buraco.
A partição acontece depois de a página ser escrita, não antes. O que já lemos pertence à janela original e é válido; deitá-lo fora trocaria um tipo de perda de dados por outro.
Uma coisa pequena do mesmo canto, digna de repetição porque sob pressão sai mal com facilidade: a chave de acesso vai num cabeçalho e nunca na cadeia de consulta. Os URL acabam em registos, em caches intermédias e em relatórios de erro. Os cabeçalhos não.
O que anotaríamos
- Tipar o parâmetro, sobretudo quando é um estado. Uma comparação de texto com uma palavra da enumeração de outra tabela compila, corre e toma para sempre o ramo errado.
- Duas mensagens sobre um mesmo acontecimento chegam fora de ordem. Limitar o escritor secundário aos estados de que pode sair.
- Uma restrição de unicidade que ignora estados encontrará código que não os ignora. Obter também as linhas fechadas, ou a primeira reabertura torna-se uma violação.
- Perguntar o que um único erro envenena. Uma exceção que escapa de um ciclo por elemento transforma uma linha má numa paragem de toda a plataforma, e a condição não se resolve sozinha.
- Um pedido condicional respondido com 304 é um êxito. Manter os validadores, não escrever nada e não o contar como falha.
- Se uma janela é grande demais, parti-la; se uma página é impossível, parar. Avançar um cursor sobre dados não lidos é o único desfecho sem volta.
- Um processo que não corre sem base de dados será testado contra a vossa base de dados. Uma costura de armazenamento fica mais barata do que a limpeza.
Para que fique claro: a análise de exposição está construída e atrás de um interruptor de implantação, portanto nenhum cliente a tem à frente. A reavaliação, as fontes e a distribuição por vários endereços estão exercitadas; fica por demonstrar o estreitamento de janela contra uma fonte viva em vez de injetada, o comportamento do índice sobre um catálogo verdadeiramente cheio, e uma análise lançada a partir de uma máquina diferente da que a pediu.
Do trabalho de plataforma na Provibr. Porque é que o comparador se recusa a ter certeza de uma versão está em uma correspondência que se recusa a ter certeza, e porque é que a sonda por detrás quase nada envia está em porque um scanner de vulnerabilidades deve enviar zero bytes.