Ir para o conteúdo

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.

RondaPedidosO que aconteceuTempo
A frio47.066 avisos guardados, 31,4 MB após descompressão18,7 s
Sem alterações1304 Not Modified, validadores guardados mantidos, zero escritas0,3 s
Cursor dois dias atrás3355 avisos atualizados, nenhum novosegundos

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

  1. 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.
  2. Duas mensagens sobre um mesmo acontecimento chegam fora de ordem. Limitar o escritor secundário aos estados de que pode sair.
  3. 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.
  4. 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.
  5. Um pedido condicional respondido com 304 é um êxito. Manter os validadores, não escrever nada e não o contar como falha.
  6. 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.
  7. 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.

Comece com um host e um agente

Crie uma conta, instale o agente com um único comando e ligue o seu primeiro host Proxmox. O período experimental dura 14 dias e não pede quaisquer dados de pagamento.