Как сделать Maven build security-aware: AppSec-проверки без дрейфа CI/CD

Страницы:  1

Ответить
 

Professor Seleznov


Практический разбор Maven core extension, который встраивает Java security checks в Maven lifecycle, а не заставляет копировать scanner-конфигурацию по pipeline-файлам.
Вступление
Проблема никогда не была в том, что Maven-проекты не умеют запускать security-инструменты.
Умеют.
Можно в pipeline вызвать tests, Dependency-Check, CycloneDX и SonarQube. Можно прописать plugin blocks в pom.xml. Можно скопировать рабочую конфигурацию из одного сервиса в другой и назвать это стандартом.
Какое-то время это даже работает.
Потом начинаются маленькие отличия.
В одном сервисе есть JaCoCo, но XML coverage не передается в SonarQube. В другом Dependency-Check делает только HTML. В multi-module проекте SBOM генерируется от root aggregator, который сам не является runtime-приложением. В третьем pipeline забыли merge request metadata, поэтому SonarQube analysis технически прошел, но практически получился неполным.
Это и есть security build drift.
Выглядит как автоматизация. Работает как несогласованность.
Я сделал secure-maven-extension, чтобы закрыть именно эту проблему для Maven-проектов.
Не заменять сканеры.
А заставить Maven lifecycle нести security workflow.
Проект: Secure Build Maven Extension
Как обычно начинается Maven DevSecOps
Типичный pipeline выглядит примерно так:
script:
- ./mvnw test
- ./mvnw org.owasp:dependency-check-maven:check
- ./mvnw org.cyclonedx:cyclonedx-maven-plugin:makeBom
- ./mvnw sonar:sonar
Для одного репозитория это нормально.
На масштабе это превращается в maintenance pattern, которым никто до конца не владеет.
Часть настроек живет в CI/CD. Часть в pom.xml. Часть в документации. Часть в переменных окружения, о которых локальный разработчик узнает только после падения pipeline.
Новый сервис каждый раз заново отвечает на одни и те же вопросы:
  • как включить coverage;
  • куда класть Dependency-Check reports;
  • какие форматы нужны security-команде;
  • как передать SonarQube token;
  • как отличить branch analysis от MR analysis;
  • как сделать SBOM для multi-module проекта;
  • как повторить это локально.
И в какой-то момент становится понятно: build сам по себе не security-aware. Pipeline просто вызывает сканеры рядом с build.
Почему обычный подход неудобен
CI/CD должен быть общей средой выполнения.
Он должен запускать чистый build, публиковать artifacts, включать gates, хранить logs и давать auditability.
Но когда CI/CD еще и владеет всей scanner-конфигурацией, каждый репозиторий становится отдельной custom-интеграцией.
Для разработчика это выглядит так:
mvn verify
локально проходит, но pipeline делает что-то другое:
  • другие goals;
  • другие properties;
  • другие report paths;
  • другой набор форматов;
  • другая SonarQube metadata.
И разработчик уже не доверяет локальному результату.
Эту дыру я и хотел закрыть.
Maven команды должны остаться знакомыми, но lifecycle должен запускать один и тот же AppSec behavior локально и в CI/CD.
Принцип решения
Ключевая идея:
оставить Maven experience нативным,
но внедрить повторяемые security conventions в lifecycle
Разработчику не нужен отдельный security script для каждого сервиса.
CI/CD не должен заново описывать scanner conventions.
Security-команда не должна по каждому репозиторию объяснять, где лежат отчеты и какие properties нужно передать.
Build должен знать скучные детали сам.
Именно поэтому это Maven core extension, а не просто еще одна команда в pipeline.
Почему Maven core extension
Обычный Maven plugin все равно обычно нужно явно конфигурировать в каждом проекте.
Это тоже можно стандартизировать, но copy-paste полностью не исчезает.
Core extension дает более раннюю точку интеграции.
Он подключается через:
.mvn/extensions.xml
Пример:


io.github.niki1337.securebuild
secure-maven-extension
0.1.0

Внутри extension работает на стадии Maven afterProjectsRead.
Это важный момент.
На этой стадии Maven уже прочитал root pom.xml и module POMs. Уже известны packaging, modules, existing plugins и properties. Но lifecycle еще не стартовал.
То есть extension может посмотреть на проект и внедрить нужные security plugins до фаз initialize, package, verify и sonar:sonar.
Это удобное место для conventions.
Что подключается под капотом
Extension работает с инструментами, которые Java-команды и так знают:
  • jacoco-maven-plugin для coverage;
  • sonar-maven-plugin для SonarQube analysis;
  • dependency-check-maven для dependency risk reports;
  • cyclonedx-maven-plugin для SBOM.
Разработчик продолжает использовать Maven:
mvn package
mvn verify
mvn sonar:sonar
Разница в том, что команды становятся security-aware.
Например:
mvn package
может собрать приложение и сгенерировать CycloneDX SBOM.
mvn verify
может запустить tests, JaCoCo coverage и Dependency-Check.
mvn verify sonar:sonar
может отправить SonarQube analysis с branch/MR metadata, binaries и coverage paths.
И это главное: workflow выглядит как Maven, а не как набор сканеров, приклеенных вокруг Maven.
Конфигурация из разных источников
Реальная инфраструктура редко бывает идеальной.
Локально разработчик может передавать -D.... В CI/CD значения приходят через environment variables. Какие-то стабильные настройки удобно хранить в pom.xml.
Extension поддерживает все эти источники:
  • environment variables;
  • Maven user properties;
  • project properties из pom.xml;
  • system properties.
Пример project defaults:

payment-api
payment-api
Payment API
Пример CI variables:
export 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"
Пример локального override:
mvn verify \
-Dsecure.serviceName=payment-api \
-Dsonar.projectKey=payment-api
Смысл не в том, чтобы заставить всех использовать один стиль конфигурации.
Смысл в том, чтобы итоговое поведение было одинаковым.
Coverage без повторяемой проводки
Coverage часто ломает AppSec workflow тихо.
SonarQube может запуститься без coverage. JaCoCo может сгенерировать отчет, но если XML output не включен или path не передан в SonarQube, анализ будет слабее.
Extension inject-ит JaCoCo для Java jar и war проектов, если JaCoCo еще не настроен.
Lifecycle wiring:
initialize -> jacoco:prepare-agent
verify -> jacoco:report
XML отчет:
target/site/jacoco/jacoco.xml
Этот path автоматически передается в:
sonar.coverage.jacoco.xmlReportPaths
Это не самая эффектная часть проекта.
Но именно такие повторяемые детали и создают drift, когда их копируют руками.
SonarQube: токена мало
Частая ошибка: считать, что SonarQube setup это URL, project key и token.
Для Java-проектов нормальный analysis зависит еще от source paths, test paths, compiled binaries, coverage XML, branch metadata и merge request metadata.
Extension готовит properties:
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
В GitLab merge request pipeline он берет:
CI_PIPELINE_SOURCE=merge_request_event
CI_MERGE_REQUEST_IID
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
CI_MERGE_REQUEST_TARGET_BRANCH_NAME
И маппит в:
sonar.pullrequest.key
sonar.pullrequest.branch
sonar.pullrequest.base
Для branch pipeline задается:
sonar.branch.name
Это ровно тот тип логики, который становится хрупким, если он размазан по десяткам .gitlab-ci.yml.
В core extension это версионируется и переиспользуется.
Dependency-Check в одном формате
OWASP Dependency-Check inject-ится в Maven lifecycle.
Для single-module проекта:
verify -> dependency-check:check
Для multi-module:
verify -> dependency-check:aggregate
Форматы отчетов:
HTML
JSON
SARIF
XML
Путь:
target/reports/dependency-check
По умолчанию отключаются network-dependent analyzers:
  • RetireJS;
  • Node audit;
  • Node package analyzer;
  • OSS Index;
  • hosted suppressions.
В закрытых средах это важно.
Если каждый pipeline зависит от внешнего endpoint, то безопасность внезапно начинает зависеть от доступности интернета, proxy и rate limits. Внутренний mirror решает эту проблему лучше.
Пример:
DT_API_URL=https://dependency-track.example.com mvn verify
По умолчанию build не падает по CVSS:
failBuildOnCVSS = 11
Это не потому что vulnerabilities не важны.
Это потому что первая стадия внедрения часто должна дать видимость и данные. Blocking gates лучше включать после triage и настройки noise reduction.
SBOM должен описывать полезный artifact
SBOM не должен быть просто файлом ради файла.
Он должен описывать то, что реально деплоится.
Для single-module проектов extension запускает:
package -> cyclonedx:makeBom
Отчеты:
target/reports/cyclonedx
Включаются:
compile dependencies
runtime dependencies
Исключаются:
test scope
provided scope
system scope
Для multi-module проектов root часто является только aggregator. Генерировать SBOM только от root бывает бесполезно.
Extension пытается найти Spring Boot module по:
org.springframework.boot:spring-boot-maven-plugin
Если находит, inject-ит CycloneDX туда.
Если нет, fallback на aggregate SBOM на root:
package -> cyclonedx:makeAggregateBom
Это практичнее для реальных Maven-репозиториев, где deployable artifact живет не в root.
Multi-module Maven
Multi-module Maven проекты требуют отдельной логики.
Extension считает build multi-module, когда Maven видит больше одного project и secure.forceSimpleMode не включен.
Java modules:
jar
war
Можно фильтровать:

api,service
test-fixtures
В multi-module режиме extension:
  • настраивает SonarQube на root project;
  • inject-ит JaCoCo в Java-модули;
  • добавляет module-level SonarQube paths;
  • inject-ит aggregate Dependency-Check на root;
  • генерирует CycloneDX из Spring Boot module, если возможно;
  • fallback-ится на aggregate SBOM, если deployable module не найден.
Это отличие между “мы вызвали scanner command” и “build понимает структуру Maven-проекта”.
CI/CD становится меньше
После этого pipeline может быть проще.
GitLab CI пример:
security: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/"
SonarQube можно запускать отдельно:
sonarqube:maven:
image: eclipse-temurin:17
stage: test
script:
- ./mvnw -B verify sonar:sonar
rules:
- if: '$SONAR_TOKEN'
Pipeline остается читаемым.
Security wiring живет в Maven extension.
Чем Maven extension отличается от Gradle plugin
Обе идеи решают одну проблему: security build drift.
Но build systems разные.
Gradle task-oriented, поэтому Gradle plugin дает tasks:
securityAnalyze
dependencyCheckAnalyze
dependencyCheckAggregate
cyclonedxDirectBom
sonar
sonarHelp
Maven lifecycle-oriented, поэтому Maven extension inject-ит security tooling в phases:
initialize
package
verify
sonar:sonar
Коротко:
Gradle plugin:
security checks как Gradle tasks и conventions
Maven extension:
обычные Maven lifecycle commands становятся security-aware
Реализация разная.
Цель одна: меньше drift, больше repeatability.
Что extension не решает
Это один build-time слой.
Он не заменяет:
  • централизованный vulnerability management;
  • ручной triage;
  • DefectDojo или Dependency-Track;
  • secret scanning;
  • DAST;
  • container scanning;
  • IaC scanning;
  • release approval policy.
Он отвечает за Maven Java build-time checks:
coverage
SonarQube metadata
SCA reports
SBOM generation
repeatable lifecycle behavior
Secret scanning, например, лучше ставить раньше: до commit и push. Это другой слой Secure SDLC.
Итог
secure-maven-extension превращает разрозненную scanner-конфигурацию в переиспользуемую Maven lifecycle convention.
Вместо того чтобы каждый проект вручную подключал JaCoCo, SonarQube, Dependency-Check и CycloneDX, extension inject-ит их до старта lifecycle.
Разработчики продолжают использовать обычные команды:
mvn package
mvn verify
mvn sonar:sonar
Но build становится security-aware.
И это главное.
Не сделать еще один scanner.
А сделать существующие AppSec tools проще для одинакового внедрения в Maven-проекты.
Ссылки Черновые хабы для Habr
Java, Maven, DevOps, Информационная безопасность, CI/CD-Источник
 
Loading...
Error