Engenharia

A CVE no seu build estava pública por meses antes de chegar a você

Upgrades de dependências são a tech debt que ninguém possui, então se acumulam até uma virar um incidente. A correção é contínua: observe o advisory, abra o PR, rode os testes, revele a breaking change.

ASR

Apollo Space Research

Apollo Space

· 11 min de leitura

Abra o lockfile do serviço principal de quase qualquer empresa e você vai encontrar um package três major versions atrás, fixado no lugar por um comentário que diz # don't touch, breaks the build. Ninguém decidiu rodar aquela versão. Ninguém a escolheu. Ela simplesmente sobreviveu à atenção de todo mundo, e agora fica lá, load-bearing, sem patch, e exatamente o tipo de coisa que um security advisory vai nomear em seis meses.

O advisory, a essa altura, não vai ser notícia. Vai ter estado público o tempo todo.

Esse é o formato estranho do risco de dependências. A vulnerabilidade que te derruba quase nunca é um zero-day que ninguém viu chegar. É uma antiga, divulgada há muito tempo, que você poderia ter dado patch em qualquer terça tranquila, e não deu, porque dar patch nela não era trabalho de ninguém.

O upgrade que ninguém possui é o que possui você

Aqui está a tese, e o resto deste post é só seu mecanismo: o upgrade que ninguém possui é o que possui você. Manutenção de dependências é a forma mais pura de tech debt, invisível enquanto acumula, catastrófica quando vence, e atribuída a ninguém nesse meio-tempo. O trabalho não é difícil. O trabalho não tem dono. E trabalho sem dono não fica menor; ele espera.

O que segue é como torná-lo possuído, não por uma pessoa que vai esquecer, e não por uma correria trimestral, mas por um sistema que observa continuamente e faz as partes chatas antes que elas parem de ser chatas.

A versão ingênua: o sprint de upgrade trimestral

Todo time de engenharia conhece o ritual. Uma vez por trimestre, alguém nota que a situação das dependências ficou ruim, e um sprint é separado para “fazer os upgrades”. Por duas semanas, o trabalho real de feature para. Um engenheiro mergulha em cem packages desatualizados de uma vez, sobe versões num lote gigante, e gasta a maior parte do tempo não fazendo upgrade mas desembaraçando, qual destas quarenta mudanças quebrou a suíte de testes, e qual das quarenta é inocente.

Parece responsável. É o oposto.

A primeira falha é timing. Quando o sprint trimestral chega, cada dependência está meses obsoleta, então cada upgrade é um salto major cheio de breaking changes, todos aterrissando de uma vez. Você não está fazendo quarenta upgrades pequenos e revisáveis. Está fazendo um enorme e irrevisável, onde uma falha em qualquer package envenena o lote inteiro e você não consegue dizer qual.

A segunda falha é pior, e é sobre a janela. Uma vulnerabilidade é divulgada na semana três do trimestre. Seu próximo sprint de upgrade está a nove semanas. Por nove semanas você está conscientemente, ou, mais frequentemente, inconscientemente, fazendo deploy da versão vulnerável, não porque você decidiu que o risco era aceitável, mas porque ninguém estava observando o feed onde a divulgação aterrissou. A cadência trimestral não reduziu o risco. Ela só definiu o relógio de quanto tempo você o carregaria.

A terceira falha é humana. O sprint de upgrade é a tarefa que todos temem e ninguém se voluntaria. Não produz feature, nem demo, nem história para a standup. Então escorrega. “Faremos os upgrades no próximo sprint” é uma das promessas mais confiavelmente quebradas em software, e cada vez que quebra, o lote fica maior e a próxima tentativa fica mais assustadora. O upgrade que ninguém possui é o que possui você, e o sprint trimestral é só um calendário fingindo ser um dono.

Duas maneiras de lidar com upgrades de dependências. À esquerda, um sprint trimestral deixa a debt e um advisory divulgado se acumularem por semanas até um lote gigante e irrevisável aterrissar de uma vez. À direita, um observador contínuo pega cada advisory e package obsoleto conforme aparecem e abre um upgrade pequeno de cada vez.

A correção é um loop, não um sprint

A cadência ingênua trata upgrades como uma coisa que você faz. A correção os trata como uma coisa que o sistema observa.

Comece pelo observar, porque é a parte que o sprint pula inteiramente. Em algum lugar do mundo, todo dia, feeds de advisory publicam novas divulgações, registries de packages empurram novos releases, e projetos anunciam datas de end-of-life para as versões que você roda. Essa informação já existe. A pergunta é só se algo na sua empresa está lendo. No modelo ingênuo, a resposta é “um engenheiro, quando ele lembra”. Isso não é um observador. É uma esperança.

Um sistema contínuo lê esses feeds do jeito que um detector de fumaça lê o ar, constantemente, em segundo plano, com um propósito: disparar no momento em que algo que ele observa aparece. Um novo advisory nomeando um package no seu lockfile. Uma versão de que você depende chegando ao end-of-life mês que vem. Um release mantido que fecha um buraco no que você está. Nenhum desses deveria jamais te surpreender, porque todos foram anunciados antes de importarem.

Mas saber é só a primeira metade. Um observador que só abre um ticket moveu o trabalho, não o fez, e “tem um ticket para isso” é como o upgrade sem dono continua sem dono. O sistema tem que agir.

Então o loop continua: o observador detecta o package obsoleto ou vulnerável, um agent abre o upgrade como um pull request real, a suíte de testes roda contra ele, e o resultado volta como um de dois desfechos limpos. Ou os testes passam, caso em que você tem um upgrade pequeno, revisável, já-verde esperando um humano dar uma olhada e fazer merge, ou eles falham, caso em que o sistema acabou de te dizer algo preciso e valioso: este upgrade específico tem uma breaking change, e aqui está o teste que a pegou.

Esse segundo desfecho é o que as pessoas subestimam. Um PR de upgrade falhando não é um problema. É o diagnóstico que você de outra forma teria pagado no lote trimestral, entregue um package de cada vez, isolado, com o teste falhando exato anexado.

Por que “abra o PR” é o truque inteiro

Há uma versão dessa ideia que já existe e funciona em parte, o bot automatizado que abre pull requests de bump de dependências. É uma melhoria real sobre o sprint trimestral, e vale ser honesto sobre onde ele para, porque esse gap é a parte interessante.

O bot abre o PR. Aí espera por um humano, e o humano é o mesmo engenheiro esgotado e cheio de pavor do sprint trimestral, agora recebendo a mesma tarefa em pedaços menores. Uma pasta de quarenta PRs de upgrade abertos, cada um vermelho ou amarelo, nenhum trabalho de ninguém, é só o lote trimestral vestindo uma fantasia diferente. O bot automatizou o abrir. Não automatizou o julgamento, e julgamento sempre foi a parte cara.

Aqui está a diferença. Quando um PR de upgrade volta verde, o julgamento é trivial: um humano confirma que a mudança é o que ela diz ser e faz merge. Esse caso deveria exigir quase nada de uma pessoa, e num sistema contínuo não exige.

Quando volta vermelho, o julgamento é real, e é exatamente aí que o trabalho deveria se concentrar. Uma breaking change significa que um comportamento mudou por baixo de você. Talvez uma função foi renomeada. Talvez um default virou. Talvez um return type estreitou e seu código silenciosamente dependia do formato antigo. O fluxo ingênuo te entrega um checkmark vermelho e um stack trace e diz boa sorte. Um sistema feito para isto lê o teste falhando, lê o changelog da dependência, e te entrega a frase de verdade: esta versão renomeou a chamada que seu código usa na linha quarenta; aqui está o rename, aqui está o único lugar onde ele morde. A breaking change para de ser um mistério que você faz engenharia reversa e vira um diagnóstico que você aprova.

Essa é a jogada. O ponto nunca foi fazer merge de upgrades automaticamente, tocar em dependências de produção sem um humano é exatamente o tipo de coisa que deveria continuar com gate. O ponto é fazer todo o trabalho chato até a decisão do humano: observar o feed, abrir a mudança, rodar os testes, isolar a quebra, explicá-la em palavras simples. O humano mantém o único trabalho que vale a pena manter, decidir, e perde todo trabalho que era só labuta.

Um loop contínuo de dependências. Um observador lê feeds de advisory e release, um agent abre um pull request de upgrade, a suíte de testes roda, e o resultado se divide de dois jeitos: uma mudança verde espera um merge humano rápido, enquanto uma mudança vermelha volta com a breaking change exata nomeada e o teste falhando anexado.

Este é um observador entre muitos

Dê um passo atrás e o loop de dependências para de parecer especial. É uma instância específica de um formato geral: algo acontece no mundo, um sistema nota, e faz a labuta até o ponto em que um humano decide.

O feed de advisory é uma fonte. Mas o mesmo loop observa um calendário de end-of-life, uma licença que mudou os termos num release, uma dependência transitiva três camadas abaixo que ninguém escolheu e ninguém sabe que está lá. Cada uma dessas é uma coisa que, hoje, alguém deveria estar observando e ninguém de fato está, porque observar é o tipo de trabalho que não tem prazo até subitamente ter um muito caro.

Imagine a superfície. Digamos que um serviço típico puxe algumas centenas de packages quando você conta as dependências das suas dependências. Nenhum humano lê algumas centenas de changelogs. Nenhum humano rastreia algumas centenas de datas de end-of-life. A razão de a dependency debt ser universal não é que engenheiros são descuidados, é que o observar excede o que a atenção consegue cobrir, e atenção é a única coisa que o modelo ingênuo aloca nisso. O upgrade que ninguém possui é o que possui você, e em algumas centenas de packages, ninguém é o dono padrão de quase todos eles.

Um sistema não cansa no package duzentos. Ele lê o changelog que você pularia. Rastreia a data que você esqueceria. Abre o PR que você adiaria. E faz isso na terça chata, meses antes de o advisory virar notícia, que é o único momento em que o upgrade é barato.

A virada: tech debt é uma questão de quem está observando, não de quão duro você trabalha

Aqui está a parte que não é sobre dependências de jeito nenhum.

A gente fala de tech debt como se fosse uma medida de quão bagunçado o código é, ou quão apressado o time estava, ou quanto atalho foi tomado sob prazo. Às vezes é. Mas a debt que de fato dói, o package sem patch, o certificado vencido, a API deprecada que para de funcionar no dia em que o provedor finalmente a desliga, não é código bagunçado. É código não observado. Estava bem quando você fez o deploy. Apodreceu porque o mundo se moveu e nada na sua empresa estava atribuído a notar.

Essa é uma coisa estranha de pedir a uma pessoa para fazer bem. “Continuamente observe algumas centenas de feeds em busca do único item que vai importar algum dia” é um trabalho em que humanos são estruturalmente ruins, não por falta de habilidade, mas porque vigilância sem um evento é o trabalho mais sem-recompensa que existe. Você não consegue elogiar alguém pelo incidente que não aconteceu porque ele deu patch na coisa que ninguém mais viu. Então o engenheiro diligente que mantém as dependências atualizadas está fazendo labuta invisível, ingrata, e facilmente adiável, e no momento em que ele fica ocupado, ou sai, o observar para e o relógio começa.

A promessa não é um bot de upgrade mais esperto. É que o observar para de depender de alguém lembrar de ser diligente. O advisory é lido no dia em que é publicado. O package obsoleto recebe um PR antes de virar história. A breaking change chega já nomeada, numa terça, enquanto ainda há todo o tempo do mundo para lidar com ela, para que a pessoa mais cuidadosa do seu time pare de ser o detector de fumaça da empresa e consiga ir construir algo no lugar.


Isto é parte do que estamos construindo na Apollo Space, não um jeito mais rápido de fazer o sprint de upgrade, mas um sistema que observa os feeds que sua empresa não consegue, abre a mudança pequena antes que vire uma grande, e revela a quebra em palavras simples para que um humano só precise fazer o decidir. Se você já achou um package três versões atrás fixado no lugar por um comentário don't touch, você já sabe que o upgrade nunca foi a parte difícil. Lembrar de fazê-lo era.

A Apollo cuida da operação repetitiva da sua empresa pro seu time não precisar.

Entre na lista de espera: acesso antecipado, preço de usuário fundador e um lugar na primeira fila enquanto a gente constrói.

Entrar na lista de espera