Como tornar o build Maven ciente de segurança: Verificações AppSec sem desvio no CI/CD
Introdução
A questão nunca foi se projetos Maven conseguem executar ferramentas de segurança. Eles conseguem. É possível chamar testes, Dependency-Check, CycloneDX e SonarQube no pipeline. É possível escrever blocos de plugin no pom.xml. É possível copiar a configuração de trabalho de um serviço para outro e chamar isso de padrão. Por um tempo, isso até funciona.
Então começam as pequenas diferenças. Em um serviço, há JaCoCo, mas a cobertura XML não é transmitida para o SonarQube. Em outro, o Dependency-Check faz apenas HTML. Em um projeto multi-módulo, o SBOM é gerado a partir do agregador raiz, que não é um aplicativo em tempo de execução. Em um terceiro, o pipeline esqueceu os metadados da solicitação de mesclagem, então a análise do SonarQube passou tecnicamente, mas praticamente foi incompleta.
Isso é o que chamamos de security build drift (desvio de build de segurança). Parece automação. Funciona como inconsistência. Eu criei o secure-maven-extension para resolver exatamente esse problema para projetos Maven. Não para substituir os scanners, mas para fazer com que o ciclo de vida do Maven carregue o fluxo de trabalho de segurança.
Projeto: Secure Build Maven Extension
Como o DevSecOps Maven geralmente começa
Um pipeline típico se parece com isto:
bashscript: - ./mvnw test - ./mvnw org.owasp:dependency-check-maven:check - ./mvnw org.cyclonedx:cyclonedx-maven-plugin:makeBom - ./mvnw sonar:sonar
Para um repositório, isso é normal. Em escala, isso se transforma em um padrão de manutenção que ninguém domina completamente. Parte das configurações vive no CI/CD. Parte no pom.xml. Parte na documentação. Parte nas variáveis de ambiente, sobre as quais o desenvolvedor local só aprende após a falha do pipeline.
Um novo serviço responde às mesmas perguntas repetidamente:
- Como ativar a cobertura?
- Onde colocar os relatórios do Dependency-Check?
- Quais formatos a equipe de segurança precisa?
- Como passar o token do SonarQube?
- Como distinguir a análise de branch da análise de MR?
- Como fazer um SBOM para um projeto multi-módulo?
- Como repetir isso localmente?
E em algum momento fica claro: o build em si não é security-aware (consciente de segurança). O pipeline apenas chama os scanners próximos ao build.
Por que a abordagem usual é inconveniente
O CI/CD deve ser um ambiente de execução comum. Ele deve executar um build limpo, publicar artefatos, incluir gates, armazenar logs e fornecer auditabilidade. Mas quando o CI/CD também possui toda a configuração do scanner, cada repositório se torna uma integração customizada separada.
Para o desenvolvedor, isso se parece com:
mvn verify passa localmente, mas o pipeline faz outra coisa:
- Outras metas;
- Outras propriedades;
- Outros caminhos de relatório;
- Outro conjunto de formatos;
- Outros metadados do SonarQube.
E o desenvolvedor não confia mais no resultado local. Eu queria resolver essa lacuna. Os comandos Maven devem permanecer familiares, mas o ciclo de vida deve executar o mesmo comportamento AppSec localmente e no CI/CD.
Princípio da solução
A ideia principal é:
- Deixar a experiência Maven nativa,
- Mas introduzir convenções de segurança repetíveis no ciclo de vida.
O desenvolvedor não precisa de um script de segurança separado para cada serviço. O CI/CD não deve reescrever as convenções do scanner. A equipe de segurança não deve explicar para cada repositório onde os relatórios estão e quais propriedades devem ser passadas. O build deve conhecer os detalhes chatos por conta própria. É por isso que é uma extensão do núcleo Maven, e não apenas outro comando no pipeline.
Por que a extensão do núcleo Maven
Um plugin Maven normal ainda precisa ser explicitamente configurado em cada projeto. Isso também pode ser padronizado, mas a cópia e colagem não desaparecem completamente. A extensão do núcleo oferece um ponto de integração mais cedo. Ele se conecta via: .mvn/extensions.xml
Exemplo:
xml<extensions> <extension> <groupId>io.github.niki1337.securebuild</groupId> <artifactId>secure-maven-extension</artifactId> <version>0.1.0</version> </extension> </extensions>
Dentro da extensão, ela funciona no estágio Maven afterProjectsRead. Este é um ponto importante. Neste estágio, o Maven já leu o pom.xml raiz e os POMs do módulo. O empacotamento, os módulos, os plugins existentes e as propriedades já são conhecidos. Mas o ciclo de vida ainda não começou. Ou seja, a extensão pode olhar para o projeto e introduzir os plugins de segurança necessários antes das fases initialize, package, verify e sonar:sonar. Este é um bom lugar para convenções.
O que é conectado por baixo
A extensão funciona com as ferramentas que as equipes Java já conhecem:
jacoco-maven-pluginpara cobertura;sonar-maven-pluginpara análise SonarQube;dependency-check-mavenpara relatórios de risco de dependência;cyclonedx-maven-pluginpara SBOM.
O desenvolvedor continua usando o Maven:
bashmvn package mvn verify mvn sonar:sonar
A diferença é que os comandos se tornam security-aware. Por exemplo:
mvn package pode construir o aplicativo e gerar o CycloneDX SBOM. mvn verify pode executar testes, cobertura JaCoCo e Dependency-Check. mvn verify sonar:sonar pode enviar a análise SonarQube com metadados de branch/MR, binários e caminhos de cobertura. E isso é o principal: o fluxo de trabalho se parece com o Maven, e não com um conjunto de scanners colados em torno do Maven.
Configuração de diferentes fontes
A infraestrutura real raramente é perfeita. Localmente, o desenvolvedor pode passar -D.... No CI/CD, os valores vêm por meio de variáveis de ambiente. Algumas configurações estáveis são convenientes para armazenar em pom.xml. A extensão suporta todas essas fontes:
- Variáveis de ambiente;
- Propriedades do usuário Maven;
- Propriedades do projeto de
pom.xml; - Propriedades do sistema.
Exemplo de padrões do projeto:
xml<properties> <secure.serviceName>payment-api</secure.serviceName> <sonar.projectKey>payment-api</sonar.projectKey> <sonar.projectName>Payment API</sonar.projectName> </properties>
Exemplo de variáveis CI:
bashexport SERVICE_NAME="payment-api" export SONAR_HOST_URL="https://sonarqube.example.com" export SONAR_PROJECT_KEY="payment-api" export SONAR_TOKEN="token-value" export DT_API_URL="https://dependency-track.example.com"
Exemplo de substituição local:
bashmvn verify \ -Dsecure.serviceName=payment-api \ -Dsonar.projectKey=payment-api
O ponto não é forçar todos a usar um estilo de configuração. O ponto é que o comportamento resultante seja o mesmo.
Cobertura sem roteamento repetível
A cobertura geralmente quebra o fluxo de trabalho AppSec silenciosamente. O SonarQube pode ser executado sem cobertura. JaCoCo pode gerar um relatório, mas se a saída XML não estiver ativada ou o caminho não for passado para o SonarQube, a análise será mais fraca. A extensão injeta JaCoCo para projetos Java jar e war se JaCoCo ainda não estiver configurado.
Ciclo de vida de fiação:
initialize -> jacoco:prepare-agentverify -> jacoco:report
Relatório XML:
target/site/jacoco/jacoco.xml
Este caminho é automaticamente passado para:
sonar.coverage.jacoco.xmlReportPaths
Esta não é a parte mais espetacular do projeto. Mas são esses detalhes repetíveis que criam o drift quando são copiados manualmente.
SonarQube: token não é suficiente
Erro comum: pensar que a configuração do SonarQube é uma URL, chave do projeto e token. Para projetos Java, a análise normal depende também dos caminhos de origem, caminhos de teste, binários compilados, cobertura XML, metadados de branch e metadados de solicitação de mesclagem.
A extensão prepara propriedades:
sonar.sources
sonar.tests
sonar.java.binaries
sonar.java.test.binaries
sonar.coverage.jacoco.xmlReportPaths
sonar.exclusions
sonar.test.exclusions
sonar.cpd.exclusions
sonar.coverage.exclusions
No pipeline de solicitação de mesclagem do GitLab, ele pega:
CI_PIPELINE_SOURCE=merge_request_eventCI_MERGE_REQUEST_IIDCI_MERGE_REQUEST_SOURCE_BRANCH_NAMECI_MERGE_REQUEST_TARGET_BRANCH_NAME
E mapeia para:
sonar.pullrequest.key
sonar.pullrequest.branch
sonar.pullrequest.base
Para o pipeline de branch, é definido:
sonar.branch.name
Este é exatamente o tipo de lógica que se torna frágil se estiver espalhado por dezenas de .gitlab-ci.yml. Na extensão do núcleo, isso é versionado e reutilizado.
Dependency-Check em um formato
OWASP Dependency-Check é injetado no ciclo de vida do Maven. Para um projeto de módulo único:
verify -> dependency-check:check
Para multi-módulo:
verify -> dependency-check:aggregate
Formatos de relatório:
- HTML
- JSON
- SARIF
- XML
Caminho:
target/reports/dependency-check
Por padrão, os analisadores dependentes da rede são desativados:
- RetireJS;
- Node audit;
- Node package analyzer;
- OSS Index;
- supressões hospedadas.
Em ambientes fechados, isso é importante. Se cada pipeline depender de um endpoint externo, a segurança de repente começa a depender da disponibilidade da Internet, proxy e limites de taxa. Um espelho interno resolve esse problema melhor.
Exemplo:
DT_API_URL=https://dependency-track.example.com mvn verify
Por padrão, o build não falha no CVSS:
failBuildOnCVSS = 11
Isso não é porque as vulnerabilidades não são importantes. É porque o primeiro estágio de introdução deve frequentemente fornecer visibilidade e dados. É melhor incluir gates de bloqueio após a triagem e a configuração da redução de ruído.
SBOM deve descrever um artefato útil
SBOM não deve ser apenas um arquivo pelo bem do arquivo. Ele deve descrever o que realmente está sendo implantado. Para projetos de módulo único, a extensão executa:
package -> cyclonedx:makeBom
Relatórios:
target/reports/cyclonedx
Incluído:
- dependências de compilação
- dependências de tempo de execução
Excluído:
- escopo de teste
- escopo fornecido
- escopo do sistema
Para projetos multi-módulo, a raiz geralmente é apenas um agregador. Gerar SBOM apenas da raiz pode ser inútil. A extensão tenta encontrar o módulo Spring Boot por:
org.springframework.boot:spring-boot-maven-plugin
Se encontrar, injeta CycloneDX lá. Caso contrário, fallback para o SBOM agregado na raiz:
package -> cyclonedx:makeAggregateBom
Isso é mais prático para repositórios Maven reais, onde o artefato implantável não reside na raiz.
Maven multi-módulo
Projetos Maven multi-módulo exigem lógica separada. A extensão considera o build multi-módulo quando o Maven vê mais de um projeto e secure.forceSimpleMode não está ativado.
Módulos Java:
- jar
- war
Você pode filtrar:
xml<properties> <secure.includedModules>api,service</secure.includedModules> <secure.excludedModules>test-fixtures</secure.excludedModules> </properties>
No modo multi-módulo, a extensão:
- Configura o SonarQube no projeto raiz;
- Injeta JaCoCo nos módulos Java;
- Adiciona caminhos SonarQube no nível do módulo;
- Injeta Dependency-Check agregado na raiz;
- Gera CycloneDX a partir do módulo Spring Boot, se possível;
- Faz fallback para o SBOM agregado se o módulo implantável não for encontrado.
Esta é a diferença entre “nós chamamos o comando do scanner” e “o build entende a estrutura do projeto Maven”.
CI/CD fica menor
Depois disso, o pipeline pode ser mais simples. Exemplo do GitLab CI:
yamlsecurity:maven: image: eclipse-temurin:17 stage: test script: - ./mvnw -B verify artifacts: when: always expire_in: 7 days paths: - target/reports/dependency-check/ - target/reports/cyclonedx/ - "**/target/reports/dependency-check/" - "**/target/reports/cyclonedx/" - "**/target/site/jacoco/"
O SonarQube pode ser executado separadamente:
yamlsonarqube:maven: image: eclipse-temurin:17 stage: test script: - ./mvnw -B verify sonar:sonar rules: - if: '$SONAR_TOKEN'
O pipeline permanece legível. A fiação de segurança vive na extensão Maven.
Como a extensão Maven difere do plugin Gradle
Ambas as ideias resolvem o mesmo problema: security build drift. Mas os sistemas de build são diferentes. Gradle é orientado a tarefas, então o plugin Gradle fornece tarefas:
securityAnalyze
dependencyCheckAnalyze
dependencyCheckAggregate
cyclonedxDirectBom
sonar
sonarHelp
Maven é orientado ao ciclo de vida, então a extensão Maven injeta ferramentas de segurança nas fases:
initialize
package
verify
sonar:sonar
Em resumo:
- Plugin Gradle:
- verificações de segurança como tarefas e convenções do Gradle
- Extensão Maven:
- comandos normais do ciclo de vida do Maven se tornam security-aware
A implementação é diferente. O objetivo é o mesmo: menos drift, mais repetibilidade.
O que a extensão não resolve
Esta é uma camada de tempo de build. Não substitui:
- gerenciamento centralizado de vulnerabilidades;
- triagem manual;
- DefectDojo ou Dependency-Track;
- varredura de segredos;
- DAST;
- varredura de contêiner;
- varredura IaC;
- política de aprovação de lançamento.
Ele é responsável pelas verificações de tempo de build do Maven Java:
- cobertura
- metadados do SonarQube
- relatórios SCA
- geração de SBOM
- comportamento repetível do ciclo de vida
A varredura de segredos, por exemplo, é melhor colocada mais cedo: antes do commit e push. Esta é outra camada do Secure SDLC.
Conclusão
A secure-maven-extension transforma a configuração dispersa do scanner em uma convenção reutilizável do ciclo de vida do Maven. Em vez de cada projeto conectar manualmente JaCoCo, SonarQube, Dependency-Check e CycloneDX, a extensão os injeta antes do início do ciclo de vida. Os desenvolvedores continuam usando os comandos normais:
bashmvn package mvn verify mvn sonar:sonar
Mas o build se torna security-aware. E isso é o principal. Não fazer outro scanner. Mas tornar as ferramentas AppSec existentes mais fáceis para uma implementação consistente em projetos Maven.
Links
- Secure Build Maven Extension: https://github.com/Niki-1337/secure-build-maven-extension
- Secure Build Gradle Plugin: https://github.com/Niki-1337/secure-build-gradle-plugin






