A maior dificuldade na segurança de aplicações Java (AppSec) raramente reside em encontrar mais um scanner. As equipes geralmente já possuem um conjunto de ferramentas, como SonarQube para análise de código, OWASP Dependency-Check para verificação de dependências, CycloneDX para SBOM (Software Bill of Materials), e ferramentas de cobertura como JaCoCo ou Kover. Além disso, plataformas de CI/CD como GitLab CI, Jenkins, TeamCity ou GitHub Actions são amplamente utilizadas para orquestrar essas verificações.
No entanto, o processo tende a se fragmentar com o tempo. Um serviço pode gerar relatórios do Dependency-Check em um diretório, enquanto outro gera apenas HTML. Um pode integrar corretamente os metadados de merge request ao SonarQube, enquanto outro os trata como análise de branch regular. Um projeto pode gerar SBOM apenas para dependências de runtime, enquanto outro inclui dependências de teste, resultando em relatórios ruidosos. Em projetos multi-módulo, exceções surgem, e pedaços de YAML são copiados de repositórios vizinhos, modificados superficialmente, e assim continuam. Inicialmente, isso pode não parecer um problema, mas em poucos meses, o processo de build de segurança sofre um desvio considerável. Foi para resolver essa questão que o autor desenvolveu o secure-build-gradle-plugin, não como um novo scanner, mas como uma camada de build tooling que padroniza a integração das ferramentas de AppSec existentes em projetos Gradle.
A abordagem convencional para DevSecOps frequentemente começa com arquivos YAML de CI/CD. Para um único serviço Java, um script como este é compreensível:
yamlscript: - ./gradlew test - ./gradlew dependencyCheckAnalyze - ./gradlew cyclonedxBom - ./gradlew sonar
Isso é claro, as etapas são visíveis, o pipeline é executado e os relatórios são gerados. O problema surge quando essa abordagem é replicada em dezenas de serviços. Cada serviço gradualmente se torna responsável por sua própria integração de segurança. As equipes copiam trechos de pipelines antigos, as versões de plugins e scanners divergem, os caminhos dos relatórios se tornam inconsistentes. Alguns adicionam SARIF, outros esquecem. Alguns configuram o SonarQube para receber XML de cobertura, outros não. Alguns usam análise de branch, outros análise de pull request. Onde antes era possível replicar a verificação localmente, agora é preciso apenas fazer o push e esperar o pipeline. Externamente, isso pode parecer automação, mas na prática, transforma-se em um conjunto de integrações manuais ligeiramente diferentes. O mais frustrante é que as equipes começam a discutir não sobre riscos, mas sobre a configuração das ferramentas: "Por que o relatório não está aqui?", "Por que o SonarQube não vê a cobertura?", "Por que o Dependency-Check falha em um serviço, mas apenas gera um relatório em outro?", "Por que o projeto multi-módulo é especial novamente?". Nesse ponto, o problema não é a falta de ferramentas, mas a ausência de um local centralizado para convenções.
Manter toda a lógica de segurança apenas no CI/CD é inconveniente. O CI/CD é excelente como um ambiente de execução geral, fornecendo runners limpos, logs, artefatos, gates e um trilho de auditoria comum. No entanto, não é o local ideal para residir toda a lógica de verificação de segurança. Se toda a lógica estiver apenas em .gitlab-ci.yml ou Jenkinsfile, o desenvolvimento local torna-se secundário. O desenvolvedor faz o push do código não apenas para abrir um merge request, mas também para entender o que o pipeline de segurança pensa. Este é um loop de feedback ruim. Idealmente, o desenvolvedor deveria ser capaz de executar a mesma verificação básica antes do merge request, por exemplo, com ./gradlew clean securityAnalyze --no-daemon. O CI/CD, por sua vez, deveria executar o mesmo workflow e coletar artefatos previsíveis. Ou seja, o CI/CD deve executar o workflow de segurança, e não descrevê-lo novamente em cada repositório. Essa é uma distinção importante.
A ideia por trás da solução é simples: o comportamento de segurança deve residir mais perto do código, e o CI/CD deve executar o mesmo comportamento sem reimplementação. Para Gradle, isso se encaixa naturalmente em um plugin de convenção. O Gradle já é o local onde o projeto descreve como ele é construído, testado, publicado e quais tarefas estão disponíveis. Portanto, as convenções repetíveis de AppSec também podem ser colocadas lá. Não a política da empresa, não a aceitação final de risco, não o gerenciamento de vulnerabilidades, mas sim a camada de tempo de build: como executar o Dependency-Check, onde armazenar relatórios, quais formatos gerar, como criar SBOM, como passar cobertura para o SonarQube, como diferenciar análise de branch e merge request, como lidar com projetos multi-módulo e como fornecer um comando claro para o desenvolvedor.
O plugin é aplicado uma vez no projeto:
gradleplugins { id "java" id "io.github.niki1337.securebuild.gradle-java" version "0.1.0" }
Em seguida, o arquivo build.gradle contém apenas os valores específicos do serviço:
gradlesecurityConventions { serviceName = "payment-api" sonarProjectKey = "payment-api" allowLocalSonar = false }
Para um projeto multi-módulo, o plugin é geralmente aplicado no nível raiz:
gradleplugins { id "io.github.niki1337.securebuild.gradle-java" version "0.1.0" } securityConventions { serviceName = "payments-platform" sonarProjectKey = "payments-platform" includedModules = ["api", "service"] excludedModules = ["test-fixtures"] }
Após isso, o modelo de propriedade muda. O CI/CD ainda executa as verificações, a segurança ainda é responsável pelos requisitos e triagem, e os desenvolvedores ainda corrigem os achados. No entanto, a configuração repetível do scanner reside no sistema de build, em vez de ser copiada de repositório para repositório como um conhecimento tácito.
Por baixo dos panos, o plugin não substitui as ferramentas existentes. Ele as integra e padroniza: análise SonarQube, OWASP Dependency-Check, CycloneDX SBOM, cobertura JaCoCo ou Kover, comportamento Gradle single-module e multi-module, metadados Git branch e metadados GitLab merge request. O comando principal para o desenvolvedor é ./gradlew clean securityAnalyze --no-daemon. securityAnalyze torna-se o ponto de entrada normal para a verificação AppSec local, podendo executar testes, cobertura, geração de SBOM e análise de dependências. Se for necessário analisar partes individuais, as tarefas subjacentes não são ocultadas: ./gradlew cyclonedxDirectBom --no-daemon, ./gradlew dependencyCheckAnalyze --no-daemon, ./gradlew sonarHelp --no-daemon. Para multi-módulo: ./gradlew dependencyCheckAggregate --no-daemon. O objetivo não é ocultar as ferramentas, mas tornar o caminho normal óbvio.
Poder-se-ia argumentar: "Por que um plugin? Não podemos simplesmente escrever um .gitlab-ci.yml normal?". Sim, é possível, e para um único repositório, isso geralmente é suficiente. Mas em escala, os problemas usuais começam a surgir: caminhos de relatório divergentes, configurações de scanner divergentes, metadados SonarQube esquecidos, execução local diferente do pipeline, projetos multi-módulo exigindo soluções improvisadas, novos serviços copiando boilerplate antigo, a equipe de segurança recebendo formatos de artefato diferentes, e desenvolvedores sem entender qual comando executar antes do MR. Um plugin Gradle de convenção fornece um local versionado para essa lógica. O CI/CD permanece importante, mas torna-se uma camada de execução: o GitLab CI/Jenkins executa ./gradlew securityAnalyze, coleta artefatos e aplica gates. Enquanto o comportamento de build reside mais perto do projeto: o plugin Gradle conhece as tarefas, os caminhos de relatório, a estrutura multi-módulo e as convenções de metadados SonarQube. Isso é particularmente útil em ambientes fechados, onde muitas empresas da CEI não podem simplesmente usar SaaS e integrá-lo facilmente pela documentação. Existem GitLab internos, Nexus próprio, Harbor, SonarQube auto-hospedado, proxies, CAs internas, restrições de internet e runners separados. Nesse ambiente, a previsibilidade é mais importante do que uma bela imagem na documentação.
O SonarQube pode ser configurado "quase corretamente" com facilidade, o que é uma zona perigosa. O pipeline fica verde, a análise é enviada, algo aparece na UI. Mas os binários Java não foram passados, o XML de cobertura não foi transmitido, os metadados de merge request não foram enviados. O resultado parece existir, mas é mais fraco do que deveria ser. O plugin resolve essa rotina de forma centralizada. Ele lê configurações de variáveis de ambiente, propriedades Gradle ou securityConventions:
bashexport SONAR_HOST_URL="https://sonarqube.example.com" export SONAR_PROJECT_KEY="payment-api" export SONAR_TOKEN="token-value"
E prepara propriedades típicas do SonarQube para Java:
propertiessonar.sources sonar.tests sonar.java.binaries sonar.java.test.binaries sonar.java.libraries sonar.java.test.libraries sonar.coverage.jacoco.xmlReportPaths sonar.exclusions sonar.test.exclusions sonar.cpd.exclusions sonar.coverage.exclusions
Para pipelines de merge request do GitLab, ele pode mapear variáveis de CI:
CI_MERGE_REQUEST_IID -> sonar.pullrequest.key
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME -> sonar.pullrequest.branch
CI_MERGE_REQUEST_TARGET_BRANCH_NAME -> sonar.pullrequest.base
Para pipelines de branch, metadados de análise de branch são definidos. Este é um trabalho tedioso, mas importante, e é exatamente o tipo de trabalho que deve ser resolvido uma vez.
O Dependency-Check é útil quando seus relatórios são previsíveis. Não se quer que um serviço gere JSON, outro apenas HTML, um terceiro SARIF em um diretório obscuro, e um quarto armazene tudo em um caminho customizado. O plugin padroniza os formatos: HTML, JSON, SARIF, XML e os grava em build/reports/dependency-check. Por padrão, analisadores de rede que podem tornar o pipeline instável em infraestrutura fechada são desativados: OSS Index, RetireJS, Node audit, Node package analyzer, suppressions hospedados, analisador KEV da CISA. No mundo ideal, todos teriam internet rápida e acesso às fontes externas necessárias. No mundo real corporativo, frequentemente há proxies, firewalls, mirrors, registros internos e proibição de saída direta. Portanto, o build de segurança não deve se tornar acidentalmente lento ou instável devido a um endpoint externo. Se houver um mirror interno, ele pode ser usado:
bashDT_API_URL=https://dependency-track.example.com \ ./gradlew dependencyCheckAnalyze --no-daemon
Por padrão, o build não falha por CVSS:
failBuildOnCVSS = 11
Como o CVSS máximo é 10, isso significa que o relatório é gerado, mas o build não é derrubado apenas pelo score. Esta é uma escolha consciente. Na primeira etapa, a equipe geralmente precisa de visibilidade e triagem. Se gates rígidos forem ativados imediatamente em dados ruidosos, os desenvolvedores rapidamente começarão a ver a segurança como um bloqueador sem sentido. Primeiro, dados, validação e redução de falsos positivos. Depois, políticas de bloqueio.
SBOM é útil apenas quando descreve um artefato útil. Se um projeto inclui dependências de teste e outro não, comparar os resultados é difícil. Se a raiz de um projeto multi-módulo é apenas um agregador, o SBOM de nível raiz pode descrever mal a aplicação real. O plugin foca em dependências de runtime e reduz o ruído desnecessário: dependências de runtime são incluídas, dependências de teste não são adicionadas, o texto da licença não é incorporado, o número de série pode ser desativado, e metadados desnecessários são reduzidos. Comandos típicos: ./gradlew cyclonedxDirectBom --no-daemon, ./gradlew cyclonedxBom --no-daemon. Relatórios: build/reports/cyclonedx, build/reports/cyclonedx-direct. Para projetos multi-módulo Spring Boot, o plugin tenta encontrar o módulo implantável, como um módulo com bootJar, e criar um SBOM mais próximo da aplicação real, em vez de um agregador raiz vazio. Isso parece um detalhe, até que você comece a enviar SBOMs para o Dependency-Track e a analisar por que metade dos achados se refere a algo que não entra em runtime.
A configuração de cobertura é necessária para análise normal do SonarQube, mas os caminhos frequentemente divergem. O plugin suporta JaCoCo e Kover. No modo auto, ele usa Kover se já estiver presente, caso contrário, conecta JaCoCo:
gradlesecurityConventions { coverageProvider = "auto" }
Para JaCoCo, o plugin usa JaCoCo 0.8.13, torna jacocoTestReport dependente de tests, inclui saída XML e passa o caminho XML para o SonarQube. O caminho típico é build/reports/jacoco/test/jacocoTestReport.xml. Novamente, isso não é ciência de foguetes, é apenas a configuração repetível que não se quer corrigir em cada serviço separadamente.
Projetos Gradle multi-módulo rapidamente revelam a maturidade da integração. Para um projeto single-module de demonstração, quase qualquer scanner parece bonito. E então vem o repositório real: root, api, service, domain, client, test-fixtures. E começa: onde está o source? Onde estão os tests? Onde estão os binários? Qual módulo é implantável? Quais módulos incluir na análise? O que fazer com a raiz? O plugin identifica subprojetos Java com java, java-library e suporta filtros:
gradlesecurityConventions { includedModules = ["api", "service"] excludedModules = ["test-fixtures"] }
No modo multi-módulo, ele configura a cobertura por módulos Java, coleta caminhos de XML de cobertura, configura a análise SonarQube de nível raiz, define caminhos de source/test/binary/library de nível de módulo, executa a agregação do Dependency-Check, tenta não gerar um SBOM de nível raiz ruidoso, usa o módulo implantável para SBOM quando possível, e mantém um securityAnalyze unificado de nível raiz. Essa é a diferença entre "nós conectamos um scanner" e "temos uma convenção de build para repositórios Java reais".
Com isso, o CI/CD se torna mais simples. Um exemplo de GitLab CI:
yamlsecurity:gradle: image: eclipse-temurin:17 stage: test variables: GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle" script: - ./gradlew clean securityAnalyze --no-daemon artifacts: when: always expire_in: 7 days paths: - build/reports/dependency-check/ - build/reports/cyclonedx/ - build/reports/cyclonedx-direct/ - "**/build/reports/jacoco/" sonarqube:gradle: image: eclipse-temurin:17 stage: test script: - ./gradlew sonar --no-daemon rules: - if: '$SONAR_TOKEN'
A ideia importante é: o CI/CD chama tarefas de build, não reimplementa convenções de segurança. O pipeline se torna mais legível, os artefatos mais previsíveis e a execução local mais próxima do CI.
O que este plugin não tenta resolver é intencionalmente limitado. Ele não substitui SonarQube, OWASP Dependency-Check, CycloneDX, DefectDojo ou Dependency-Track, triagem manual de vulnerabilidades, aceitação de risco, secret scanning, DAST, container scanning, IaC scanning ou processo de aprovação de release. Não é "todo o DevSecOps em uma dependência". É uma camada de tempo de build para AppSec Java: SCA, SBOM, cobertura, metadados SonarQube, comportamento local e CI/CD, convenções de relatório. O secret scanning é conscientemente atribuído a uma camada anterior, antes do commit e push, pois envolve lógica e loop de feedback diferentes.
Em resumo, o secure-build-gradle-plugin não resolve o problema de "falta de scanners". Ele resolve o problema de "scanners conectados de forma diferente em todos os lugares". Em vez de cada serviço reinventar SonarQube, Dependency-Check, CycloneDX e a configuração de cobertura, o projeto recebe uma camada Gradle única: ./gradlew securityAnalyze. Os desenvolvedores obtêm feedback mais cedo, o CI/CD recebe artefatos previsíveis, a equipe de segurança obtém formatos de relatório mais estáveis, e projetos multi-módulo recebem um comportamento que entende a estrutura do repositório. E o mais importante: a ferramenta de segurança se torna parte do fluxo de trabalho normal de engenharia Java, em vez de um conjunto de scripts ao redor dele.





