Uma imagem de container pode funcionar perfeitamente e, ainda assim, carregar pacotes com vulnerabilidades conhecidas.
Foi exatamente esse tipo de situação que encontrei ao analisar uma aplicação .NET 8: o scan da imagem com Trivy apontava vulnerabilidades classificadas como HIGH e CRITICAL. Durante a investigação, uma das mudanças realizadas foi substituir a imagem de runtime genérica do .NET 8 por uma variante específica baseada em Ubuntu Jammy.
No cenário analisado naquela ocasião, a mudança reduziu significativamente os achados e, naquele teste, eliminou os mais severos que estavam sendo investigados.
Mas esse resultado não deve ser tratado como uma regra do tipo “troque a tag e resolva o problema”.
O ponto mais importante é outro: a imagem base faz parte da superfície de segurança da aplicação e precisa ser analisada como qualquer outra dependência.
Neste artigo, vamos usar o Trivy para entender esse processo.
Em termos operacionais, o fluxo é:
- executar o scan com critérios consistentes;
- interpretar os achados no contexto dos componentes e do workload;
- aplicar a correção ou mitigação sustentada pelas evidências;
- reconstruir a imagem;
- executar um novo scan com os mesmos critérios.
O que o Trivy procura em uma imagem?
O Trivy é um scanner de segurança capaz de identificar vulnerabilidades conhecidas em diferentes tipos de componentes.
Em um scan de imagem de container, ele pode identificar, entre outros itens:
- pacotes do sistema operacional;
- dependências específicas de linguagens suportadas;
- componentes para os quais existem dados de vulnerabilidade conhecidos.
Isso significa que uma aplicação sem alteração de código pode apresentar resultados diferentes quando sua imagem base muda.
Um scan básico é simples:
trivy image <imagem>
Para focar somente em vulnerabilidades e restringir a saída às severidades mais altas:
trivy image \
--scanners vuln \
--severity HIGH,CRITICAL \
<imagem>
A documentação do Trivy define as severidades como UNKNOWN, LOW, MEDIUM, HIGH e CRITICAL. A classificação exibida pode considerar informações publicadas pelos fornecedores dos pacotes, e não apenas uma pontuação genérica de CVSS.
Por isso, o resultado precisa ser interpretado no contexto da imagem analisada.
Primeiro: descubra o que existe na imagem
Encontrar um CVE não significa automaticamente que a aplicação está explorável.
O scan responde uma pergunta mais específica:
Existem componentes nessa imagem associados a vulnerabilidades conhecidas pela base consultada?
A partir daí, ainda precisamos investigar:
- qual pacote foi identificado;
- qual versão está instalada;
- se existe uma versão corrigida;
- de qual parte da imagem o componente veio;
- se a vulnerabilidade é relevante para o cenário da aplicação.
Também é importante separar duas origens comuns.
Pacotes do sistema operacional
A imagem base normalmente contém bibliotecas e utilitários mantidos pela distribuição Linux.
Uma mudança de família de imagem pode mudar:
- distribuição;
- versões dos pacotes;
- conjunto de pacotes instalados;
- alertas de segurança utilizados na avaliação.
Consequentemente, o conjunto de vulnerabilidades encontradas também pode mudar.
Dependências da aplicação
O Trivy também suporta análise de dependências de várias linguagens. Para .NET, por exemplo, há cobertura para pacotes NuGet quando essas dependências estão presentes de forma detectável no alvo.
Por isso, nem todo achado encontrado em uma imagem deve ser atribuído à imagem base.
Um caso real com .NET 8
Na situação que motivou este artigo, o Dockerfile utilizava uma imagem de runtime semelhante a:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM base AS final
Durante o scan, foram encontrados diversos achados HIGH e CRITICAL.
A alteração aplicada à imagem de runtime foi:
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy AS base
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM base AS final
Observe um detalhe importante: o estágio build continuou utilizando:
mcr.microsoft.com/dotnet/sdk:8.0
A mudança explícita ocorreu no estágio base, do qual deriva a imagem final.
Em um build multi-stage, as ferramentas e os pacotes do estágio SDK não são automaticamente carregados para a imagem final. Normalmente, apenas os artefatos explicitamente copiados para o estágio final seguem para produção.
Isso é importante porque o scan da imagem entregue deve representar aquilo que realmente será executado.

Comparando as duas imagens com os mesmos critérios
Para a comparação ser útil, os dois scans precisam usar o mesmo conjunto de parâmetros.
Exemplo:
trivy image \
--scanners vuln \
--severity HIGH,CRITICAL \
skynalytix/trivy-dotnet:before
e:
trivy image \
--scanners vuln \
--severity HIGH,CRITICAL \
skynalytix/trivy-dotnet:after
O primeiro scan pode precisar baixar ou atualizar a base de vulnerabilidades. Depois disso, a saída mostra os targets analisados e os achados que atendem ao filtro informado.
O objetivo não é buscar um número previamente esperado.
É descobrir o resultado daquelas imagens, naquele momento, usando aquela versão do scanner e a base disponível durante o teste.
Resultados do scan da imagem before (antes — aspnet:8.0):



Resultados do scan da imagem after (depois — aspnet:8.0-jammy):


Na saída filtrada, a imagem after não apresenta achados HIGH ou CRITICAL. A saída sem o filtro de severidade ainda registra 17 achados LOW e 16 MEDIUM.
Comparação de vulnerabilidades HIGH e CRITICAL encontradas:
BEFORE
aspnet:8.0
HIGH: 52
CRITICAL: 4
AFTER
aspnet:8.0-jammy
HIGH: 0
CRITICAL: 0
Por que a imagem base pode mudar tanto o resultado?
Uma imagem Docker não contém apenas nossa aplicação.
Ela pode carregar uma cadeia de componentes:
aplicação
↓
runtime
↓
bibliotecas
↓
pacotes do sistema operacional
↓
imagem base
Quando alteramos a base, parte dessa cadeia também muda.
No caso do .NET, a Microsoft publica diferentes famílias e variantes de imagens. Escolher uma variante específica pode significar trabalhar com outro conjunto de pacotes do sistema operacional.
Isso pode diminuir alguns achados, introduzir outros ou simplesmente alterar as versões e os alertas de segurança aplicáveis.
Por esse motivo, seria incorreto transformar o resultado histórico deste caso em uma regra como:
8.0-jammy = mais segura
A conclusão tecnicamente correta precisa ser baseada no scan atual.

Nem toda vulnerabilidade tem a mesma resposta
Uma lista de CVEs não deve ser tratada apenas como um contador.
Ao investigar um achado, observe pelo menos:
- severidade;
- pacote afetado;
- versão instalada;
- versão corrigida, quando disponível;
- origem do pacote;
- exposição real da aplicação;
- possibilidade de atualização ou mitigação.
O Trivy também permite ocultar vulnerabilidades sem correção disponível usando --ignore-unfixed, mas isso muda a pergunta que está sendo feita.
Por exemplo:
trivy image \
--scanners vuln \
--severity HIGH,CRITICAL \
--ignore-unfixed \
<imagem>
Esse comando pode ser útil quando queremos priorizar itens para os quais já existe uma atualização, mas não deve ser confundido com um scan completo dos achados HIGH e CRITICAL.
Uma vulnerabilidade não deixa de existir apenas porque ainda não existe patch disponível.
Zero HIGH ou CRITICAL não significa risco zero
Esse é um dos cuidados mais importantes ao trabalhar com scanners.
Se o Trivy não retornar vulnerabilidades HIGH ou CRITICAL, isso significa apenas que, dentro das fontes, regras, componentes detectados e critérios usados naquele scan, nenhum achado dessas severidades foi apresentado.
Isso não comprova que a imagem seja segura.
Existem outros aspectos que também precisam ser considerados:
- vulnerabilidades ainda desconhecidas;
- configurações inseguras;
- segredos expostos;
- permissões excessivas;
- dependências não cobertas pelo scanner;
- risco real de exploração;
- segurança da aplicação propriamente dita.
O scan é uma camada do processo de segurança, não a validação completa.
O scan precisa fazer parte do ciclo da imagem
Outro ponto importante é que uma imagem considerada adequada hoje pode apresentar um resultado diferente amanhã.
Novos CVEs são publicados, pacotes recebem atualizações e as bases dos scanners mudam.
Por isso, a estratégia não deveria ser:
scan
↓
corrigir
↓
nunca mais verificar
O processo mais saudável é contínuo:
build
↓
scan
↓
análise
↓
correção
↓
rebuild
↓
novo scan
Esse mesmo fluxo pode ser incorporado a pipelines de DevOps e automação:
scan manual
↓
validação recorrente
↓
integração CI/CD
↓
política de bloqueio por severidade, quando apropriado
A decisão de bloquear um pipeline não deveria considerar apenas a quantidade bruta de CVEs. Critérios como severidade, existência de correção, exceções documentadas e contexto do workload também precisam fazer parte da política.
O principal aprendizado
O Trivy torna muito simples começar uma análise:
trivy image <imagem>
A parte mais importante vem depois do comando.
Precisamos entender o que foi encontrado, de onde veio e qual ação realmente reduz o risco.
No caso que vimos, alterar a imagem de runtime foi uma ação relevante e produziu um resultado muito melhor no cenário histórico analisado. Mas o aprendizado que permanece não é “sempre use Jammy”.
É este:
trate a imagem base como uma dependência de segurança da aplicação.
Escolha a variante conscientemente, mantenha as imagens atualizadas, execute scans com critérios consistentes e valide novamente depois de cada mudança.
Esse processo transforma o Trivy de um simples gerador de lista de CVEs em uma ferramenta útil dentro de uma prática contínua de segurança de containers.




