Destino Dev

Blog

Escanear imagens Docker no CI com Trivy e GitHub Actions — falhe por CVSS

Por John Pires · 20 de março de 2026

Por que aprender isso em 2026

O desenvolvimento moderno empacota aplicações em imagens Docker e as entrega continuamente. Em 2026, ataques automatizados exploram vulnerabilidades conhecidas com velocidade; integrar uma verificação automática no CI é defesa básica. Não basta “rodar um scanner” — é preciso automatizar a decisão: recusar builds quando uma vulnerabilidade atinge um nível de risco numérico (CVSS).

Este artigo mostra passo a passo como usar Trivy para escanear imagens Docker dentro do GitHub Actions e como fazer o pipeline falhar automaticamente se qualquer vulnerabilidade tiver CVSS maior ou igual a um limiar (ex: 7.0).

Você vai ver blocos de código reais: um Dockerfile para uma aplicação simples, um workflow do GitHub Actions que constrói, escaneia e falha por CVSS, e um script em Python que interpreta o JSON do Trivy — porque Trivy nem sempre expõe diretamente um "fail-by-cvss" numa flag única.

Por que CVSS e não só severidade

Trivy tem flags de severidade (LOW, MEDIUM, HIGH, CRITICAL) que são úteis. Mas o CVSS fornece uma pontuação numérica (0.0–10.0). Para políticas corporativas, muitas equipes preferem um limite numérico porque dá controle fino: por exemplo, falhar apenas quando CVSS >= 7.0 elimina falsos positivos de severidade média que têm CVSS baixo.

Resultados esperados

Ao final você terá: (1) um workflow que constrói a imagem Docker, (2) Trivy gerando relatório JSON, (3) um verificador que sai com código 1 se existir vulnerabilidade com CVSS >= limiar. O job do GitHub Actions falhará automaticamente quando a regra for satisfeita.

Configuração do ambiente

Antes de começar: instale Docker localmente e Trivy para testes locais. No CI (GitHub Actions) não é necessário instalar localmente a ferramenta se você usar a action oficial ou instalar via script. Aqui mostramos ambas abordagens — usar a action simplifica, mas rodar o binário e gerar JSON dá mais controle.

Instalando Trivy localmente

Para testar localmente recomendo usar a release oficial. No Linux/WSL:

Comando para instalar (contexto: instala o binário /usr/local/bin/trivy):

curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin v0.44.0

O comando acima baixa e instala uma versão específica. Depois rode trivy --version para validar. Este passo é opcional se você só vai usar GitHub Actions.

Depois, teste um scan local com uma imagem oficial:

trivy image --format json --output report.json alpine:latest

O comando gera um report.json com estrutura que vamos parsear no verificador Python.

Permissões e tokens no GitHub

No repositório crie um secret (Settings -> Secrets -> Actions) chamado GHCR_PAT se você publicar imagens em GitHub Container Registry, ou mantenha nada se só construir e escanear localmente no runner. Para puxar imagens privadas, configure autenticação adicional.

Se usar a action do Trivy você não precisa de token extra para rodar o scanner; para puxar imagens privadas configure o docker login no workflow antes do scan.

Hello World progressivo

Vamos construir uma imagem Docker mínima que serve uma página estática com nginx. Esse é o "hello world" real para scans: contém uma base (alpine/nginx) com pacotes que Trivy pode analisar.

Dockerfile explicado

Crie um Dockerfile simples que copia um index.html para um nginx leve:

FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html

O que o Dockerfile faz: usa a imagem base nginx:alpine e substitui a página padrão. Depois de construir, a imagem pode ser escaneada pelo Trivy. Base em alpine é intencional: muitos scanners encontram vulnerabilidades em bibliotecas C; você verá resultados reais.

Construir e escanear localmente

Etapas para testar localmente:

  1. Salvar o Dockerfile e um index.html no mesmo diretório.
  2. Construir a imagem: docker build -t meu-nginx-hello:local .
  3. Rodar Trivy e gerar JSON: trivy image --format json --output report.json meu-nginx-hello:local

Depois verifique o report.json. A estrutura contém um array de resultados por target (pacote, tipo, vulnerabilidades). Vamos automatizar a interpretação desse JSON no CI.

Mini-projeto prático

Objetivo: configurar um GitHub Actions workflow que:

  • Constrói a imagem
  • Roda Trivy gerando JSON
  • Executa um verificador que falha se existir CVSS >= LIMIAR (ex: 7.0)

Vamos direto ao arquivo do workflow. Coloque em .github/workflows/ci-trivy-cvss.yml.

Workflow do GitHub Actions

Arquivo completo do workflow (explicação antes e depois): este workflow usa ubuntu-latest, constrói a imagem, instala Trivy via script, roda o scan em JSON e chama o script Python para checar CVSS. Se o script retornar código 1 o job falha.

name: CI - Build & Trivy CVSS Check

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Build image
        run: |
          docker build -t ghcr.io/${{ github.repository_owner }}/meu-nginx-hello:${{ github.sha }} .

      - name: Install trivy
        run: |
          curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin v0.44.0

      - name: Trivy scan (json)
        run: |
          trivy image --quiet --format json --output trivy-report.json ghcr.io/${{ github.repository_owner }}/meu-nginx-hello:${{ github.sha }}

      - name: Check CVSS threshold
        run: |
          python3 .github/scripts/check_cvss.py trivy-report.json 7.0

O que acontece: o job constrói uma imagem e a marca com o SHA do commit. Trivy é instalado e gera trivy-report.json. O script Python interpreta esse JSON e sai com código 1 se algum CVSS >= 7.0.

Script Python: check_cvss.py

Coloque este script em .github/scripts/check_cvss.py. Ele lê o JSON do Trivy e avalia CVSS v3 (quando disponível) ou CVSS v2. O script é intencionalmente simples e robusto para formato real do Trivy.

#!/usr/bin/env python3
import sys
import json

if len(sys.argv) != 3:
    print("Usage: check_cvss.py <trivy-json> <threshold>")
    sys.exit(2)

report_file = sys.argv[1]
threshold = float(sys.argv[2])

with open(report_file, 'r') as f:
    data = json.load(f)

violations = []

# Trivy returns array 'Results'
for result in data.get('Results', []):
    for vuln in result.get('Vulnerabilities', []) or []:
        # Try CVSS v3 first, then v2, then skip
        cvssv3 = vuln.get('CVSS', {}).get('nvd', {}).get('V3Vector', None)
        # Numeric scores may be in 'CVSS' -> 'nvd' -> 'V3Score' or 'V2Score'
        numeric_v3 = vuln.get('CVSS', {}).get('nvd', {}).get('V3Score')
        numeric_v2 = vuln.get('CVSS', {}).get('nvd', {}).get('V2Score')

        score = None
        if isinstance(numeric_v3, (int, float)):
            score = float(numeric_v3)
        elif isinstance(numeric_v2, (int, float)):
            score = float(numeric_v2)

        if score is not None and score >= threshold:
            violations.append({
                'pkg': vuln.get('PkgName'),
                'vuln_id': vuln.get('VulnerabilityID'),
                'score': score,
                'title': vuln.get('Title')
            })

if violations:
    print(f"Found {len(violations)} vulnerabilities with CVSS >= {threshold}:")
    for v in violations:
        print(f"- {v['vuln_id']} {v['pkg']} score={v['score']} title={v['title']}")
    sys.exit(1)
else:
    print(f"No vulnerabilities with CVSS >= {threshold} detected.")
    sys.exit(0)

O que o script faz: percorre o JSON do Trivy, tenta extrair a pontuação CVSS v3/v2 e compara com o limiar. Se encontrar alguma >= threshold, imprime detalhes e retorna 1 — o GitHub Actions registra falha.

Executando localmente o mesmo fluxo

Você pode simular localmente: construa a imagem, rode Trivy gerando JSON e execute o script:

docker build -t meu-nginx-hello:local .
trivy image --format json --output trivy-report.json meu-nginx-hello:local
python3 .github/scripts/check_cvss.py trivy-report.json 7.0

Resultado: o script imprime se encontrou ou não vulnerabilidades com CVSS alto. Use isso para ajustar o limiar antes de colocar em produção.

Erros comuns, armadilhas e dicas práticas

Esta seção mostra problemas reais que você vai encontrar e como resolver rapidamente. Pratique corrigir o pipeline em commits de PR para não bloquear o time sem aviso.

Falso positivo por pacotes sem CVE

Às vezes Trivy relata vulnerabilidades com dados de NVD incompletos (sem V3Score). O script acima tenta V3Score e V2Score. Se sua política exigir só V3, ajuste o script para ignorar V2 ou tratar como menor risco.

Workflow lento por download de base

Instalar Trivy a cada execução aumenta tempo. Use uma cache de runner ou uma action que já tenha Trivy. Outra saída: usar image-scanning actions de marketplace que fazem cache para você, mas perca controle sobre o JSON detalhado.

Erros de autenticação ao escanear imagens privadas

Se seu workflow precisa puxar imagens privadas (por exemplo, base image privada), configure docker/login-action antes do scan e exporte credenciais. Sem isso Trivy pode falhar ao tentar baixar layers para analisar.

Dica: coloque o verificador de CVSS como etapa final e envie o relatório JSON como artifact do job para análise posterior com actions/upload-artifact.

Recursos para continuar estudando

Depois de ter o pipeline básico funcionando, você vai querer: automatizar correções (dependabot/container-image updates), integrar SCA para dependências de linguagem e configurar alertas de segurança quando subir para produção.

Documentação e leituras recomendadas

  • Aqua Security Trivy: documentação oficial e flags avançadas
  • GitHub Actions: conceitos de jobs, secrets e artefatos
  • CVSS documentation: entenda o que cada componente da pontuação significa

Próximos passos práticos

Implemente um mecanismo de aprovação em PRs: se o pipeline falhar por CVSS, crie um comentário automático com instruções de mitigação. Integre com tickets (Jira/GitHub Issues) para rastrear correções.

Considere também usar SBOM (Software Bill of Materials) e scanners para dependências de linguagem (ex: npm audit, pip-audit) em paralelo com o Trivy, que faz SCA e imagem.

Conclusão prática

Automatizar scans de imagens Docker no CI com Trivy e GitHub Actions é uma proteção fundamental. Este artigo forneceu um caminho prático: Dockerfile, workflow GitHub Actions, script de verificação de CVSS e dicas para evitar armadilhas comuns.

Resumo do fluxo: construa a imagem, gere relatório JSON com Trivy, rode um verificador que interpreta CVSS e retorne código 1 quando o limiar for atingido. Isso transforma um scanner passivo em uma regra ativa de gate de qualidade.

Implemente o exemplo no seu repositório, ajuste o limiar para o nível de risco da sua equipe, registre os relatórios como artifacts e configure alertas — assim você transforma segurança em algo previsível, automatizado e escalável.

Se precisar, adapte o script para aceitar múltiplos limiares por severidade ou por ambiente (staging vs production). O importante é começar simples e evoluir com dados reais dos relatórios.

Boa prática: depois de liberar o pipeline, faça PRs experimentais com imagens intencionalmente vulneráveis (ambiente controlado) para verificar se o workflow realmente falha como esperado.

Assuntos: Docker, ci-cd, github-actions, trivy, DevOps

Leia também