Ir para o conteúdo

51 · O caçador de bugs como gate de qualidade no CI

Um gate de qualidade no CI funciona como uma equipe que usa o SonarQube espera: um projeto por repositório, uma linha de base no branch padrão, um perfil que diz o que reprova, todo pull request varrido e anotado, o merge bloqueado enquanto o gate reprova, e as decisões de uma pessoa guardadas de uma varredura para a seguinte. Este capítulo monta isso do início, para GitHub, GitLab e Jenkins. O capítulo 28 traz as condições do gate por completo, e o capítulo 46 a verificação de agentes por completo.

Idiomas

O seletor lê turnos escritos em inglês e em português do Brasil (pt-BR). O idioma é escolhido, nunca adivinhado a partir do turno: na página Settings do console (Language the selector reads), em language numa requisição de plano, ou no settings.language do seu arquivo de vocabulário; sem nenhum deles, inglês. Um turno em português pode ser digitado com os acentos ou sem eles. O português europeu não é uma gramática que o seletor tenha, e um turno em qualquer outra língua não é lido.

Dois gates

Um agente: stalwart verify Uma política ou um roteador: as varreduras de um projeto
Verifica o grafo dos agentes, o rulebook e o código das suas ferramentas (QC) uma política de guardrails, uma planilha de regras, um roteador construído a partir do seu catálogo
Comparado com nada: todo achado é lido como novo a linha de base do projeto, a última varredura do branch padrão
Reprova com qualquer achado QC, e as condições do gate sobre os achados do grafo as condições do gate sobre o que é novo, pelo perfil do projeto
Lembra nada o histórico de cada achado e as decisões de uma pessoa
Roda a partir de qualquer CI, com o comando stalwart a Action do GitHub, ou a API HTTP a partir de qualquer CI

Um agente declarado no agent.yaml é verificado com stalwart verify. Uma equipe que guarda uma política de guardrails ou um roteador num repositório, e quer uma linha de base e uma trilha de revisão, o guarda num projeto. Um repositório pode ter os dois.

1. Um projeto e a sua linha de base

POST /v1/projects com um name e o default_branch (main onde nenhum é dado) cria um projeto. A primeira varredura no branch padrão é a sua linha de base: todo achado nela é existente, então nenhum reprova o gate. A partir daí:

  • uma varredura no branch padrão avança a linha de base, e os achados que ela não tem mais ficam fixed;
  • uma varredura em qualquer outro branch relata contra a linha de base, como new, existing, fixed ou reintroduced, e não move nada;
  • um achado que foi corrigido e volta fica reintroduced, e o gate o lê como novo.

Então o branch padrão é varrido a cada push nele, e todo pull request é varrido contra ele.

2. O perfil de qualidade

Sem perfil, o gate tem as suas condições padrão (capítulo 28): reprova com um achado Critical novo, uma linha quebrada das suas próprias tabelas, uma política nova que não pode ser publicada, um achado não reproduzido e uma divergência do motor. Um perfil acrescenta condições, ou as remove:

{
  "gate": {
    "add": ["no_new_severity_at_least:High", "no_new_rule:QG101"]
  }
}

no_new_severity_at_least:High reprova com um achado novo de severidade High ou maior, e no_new_rule:QG101 com um achado novo sob essa regra. PUT /v1/projects/{id}/profile com {"profile": ...} o define, e {"profile": null} volta ao padrão; a próxima varredura o lê. Um perfil precisa de uma chave com o entitlement hunt_details (capítulo 40).

3. Todo pull request

No GitHub, a Action do Guardrail Bug Hunter faz cada passo a cada execução:

  1. envia os arquivos que as suas entradas nomeiam -- a política, a configuração de rails, o modelo extraído de um host;
  2. submete uma varredura do projeto no branch e no commit do pull request, e a acompanha até o fim;
  3. envia o SARIF da varredura ao code scanning, para cada achado aparecer na linha de que trata;
  4. publica um comentário no pull request -- achados novos, corrigidos e existentes, o resultado do gate e um link para o projeto -- e atualiza esse comentário numa nova execução em vez de acrescentar outro;
  5. reprova o check quando o gate reprova.

Torne esse check obrigatório nas regras de proteção do branch, e um pull request cujo gate reprova não pode ser mergeado. Rode o mesmo workflow nos pushes ao branch padrão, para a linha de base ficar em dia.

Em qualquer outro CI, a mesma varredura são três requisições à API HTTP (capítulo 18):

  1. POST /v1/uploads para cada arquivo;
  2. POST /v1/projects/{id}/scans com o branch, o commit e uma request hunt como POST /v1/solves a recebe, com o operando gate: true;
  3. GET /v1/solves/{job_id} até o job terminar: o gate do documento de achados traz passed, as conditions que leu e as que falharam (failed).

Saia com falha onde passed for false, e torne o job um check obrigatório do merge request. O SARIF está entre os artefatos do job, para uma visão de code scanning ou para o SonarQube (abaixo).

4. Agentes em todo pull request

stalwart verify agent.yaml sai com 1 onde o gate reprova, 2 onde o arquivo não pode ser lido ou falta a chave ou a licença, e 3 onde a caça ou as verificações de código não puderam rodar (capítulo 46). Todo CI bloqueia um merge com um passo que falhou, então o comando é o gate. O runner é uma máquina nova a cada vez, então a licença é ativada primeiro e liberada por último, aconteça o que acontecer no meio.

GitHub Actions:

# .github/workflows/agent.yml: the shop agent checked on every pull request.
name: agent
on: [pull_request]
jobs:
  verify:
    runs-on: ubuntu-latest
    env:
      QLS_API_KEY: ${{ secrets.QLS_API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - run: pip install stalwart-agents
      - run: stalwart licence activate          # a runner is a new machine every time
      - run: stalwart verify agent.yaml --replay --keep findings/
      - if: always()                            # the place is released whatever happened above
        run: stalwart licence release
      - if: always()
        uses: actions/upload-artifact@v4
        with:
          name: findings
          path: findings/

GitLab CI, como .gitlab-ci.yml, com a chave numa variável de CI/CD mascarada:

# .gitlab-ci.yml: the shop agent checked on every merge request.
verify:
  image: python:3.13
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  variables:
    QLS_API_KEY: $QLS_API_KEY                 # a masked CI/CD variable holding a key of your account
  script:
    - pip install stalwart-agents
    - stalwart licence activate               # a runner is a new machine every time
    - stalwart verify agent.yaml --replay --keep findings/
  after_script:
    - stalwart licence release                # the place is released whatever happened above
  artifacts:
    when: always
    paths: [findings/]

Em Settings > Merge requests, Pipelines must succeed bloqueia o merge enquanto o job falha.

Jenkins, como um Jenkinsfile, com a chave numa credencial de texto secreto chamada qls-api-key:

// Jenkinsfile: the shop agent checked on every change request.
pipeline {
    agent { docker { image 'python:3.13' } }
    environment {
        QLS_API_KEY = credentials('qls-api-key')   // a secret text credential holding a key of your account
    }
    stages {
        stage('verify') {
            when { changeRequest() }
            steps {
                sh 'python -m venv .venv && .venv/bin/pip install stalwart-agents'
                sh '.venv/bin/stalwart licence activate'   // an agent is a new machine every time
                sh '.venv/bin/stalwart verify agent.yaml --replay --keep findings/'
            }
        }
    }
    post {
        always {
            sh '.venv/bin/stalwart licence release || true'   // the place is released whatever happened above
            archiveArtifacts artifacts: 'findings/**', allowEmptyArchive: true
        }
    }
}

Um pipeline multibranch relata o resultado do estágio no change request, e a proteção de branch do repositório o torna obrigatório.

5. Triagem

O ciclo de vida de um achado é a decisão de uma pessoa sobre ele, guardada no projeto e lida por toda varredura seguinte:

Ciclo de vida Quer dizer O que o gate faz
open ninguém decidiu ainda o lê pelas suas condições
confirmed é um problema real, a ser corrigido o lê pelas suas condições
false_positive não é um problema o deixa fora das condições que uma decisão resolve: um achado não reproduzido, uma divergência do motor, uma linha quebrada das suas tabelas. Um achado reproduzido não é um falso positivo: continua aberto
accepted_risk é um problema que a equipe aceita até uma data, com uma justificativa como para um falso positivo, até a data; depois o achado fica open de novo

Uma decisão resolve um achado onde ele está; não esconde um que volta. Um achado corrigido e depois reintroduzido é novo de novo, e reprova as condições sobre achados novos -- um Critical, um piso que o perfil define --, qualquer que tenha sido a decisão antes.

PATCH /v1/projects/{id}/findings/{fingerprint} com o lifecycle, uma justification e, para um risco aceito, expires como YYYY-MM-DD registra a decisão; um risco aceito sem data dura 90 dias. As páginas Findings do console mostram as varreduras e os achados de cada projeto e registram as mesmas decisões, e GET /v1/projects/{id}/findings/{fingerprint} lê o histórico inteiro de um achado: quando apareceu, cada decisão e quem a tomou. Registrar uma decisão e ler o histórico de um achado precisam de hunt_details, como o perfil. Um achado QC não tem ciclo de vida: só mudar o código o resolve (capítulo 50).

SonarQube

Uma equipe que já lê os seus achados no SonarQube importa o SARIF da varredura como issues externas (sonar.sarifReportPaths), com as mesmas regras, severidades, locais e fingerprints. Guarde as decisões no projeto: o gate lê a linha de base e os ciclos de vida do projeto, não os do SonarQube.

Os limites

  • Um gate só é tão bom quanto o que recebe. Uma varredura de uma política sem o catálogo classifica toda ação como High; envie o catálogo ou o modelo extraído com ela (capítulo 28).
  • O stalwart verify não tem linha de base. Todo achado de um agente é lido como novo a cada execução; um Critical reprova o gate até ser corrigido.
  • Uma caça que não pôde rodar nunca é uma aprovação. Sem rota até o serviço é o status de saída 3, não 0.