惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
博客园_首页
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
有赞技术团队
有赞技术团队
阮一峰的网络日志
阮一峰的网络日志
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
酷 壳 – CoolShell
酷 壳 – CoolShell
Y
Y Combinator Blog
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
博客园 - 司徒正美
Engineering at Meta
Engineering at Meta
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
N
Netflix TechBlog - Medium
Martin Fowler
Martin Fowler
宝玉的分享
宝玉的分享
G
Google Developers Blog
Last Week in AI
Last Week in AI

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Хватит копировать security YAML: AppSec-слой для Java-про...
NicholasKuzy · 2026-05-07 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение9 мин

Охват и читатели630

Кейс

Практический разбор того, как я вынес security-проверки Java-проектов из разрозненных CI/CD-скриптов в переиспользуемый Gradle convention plugin.

Вступление

Самая сложная часть Java AppSec обычно не в том, чтобы найти еще один сканер.

Сканеры у команд и так часто есть.

Есть SonarQube для анализа кода. Есть OWASP Dependency-Check для проверки зависимостей. Есть CycloneDX для SBOM. Есть JaCoCo или Kover для покрытия. Есть GitLab CI, Jenkins, TeamCity, GitHub Actions или какой-нибудь внутренний CI, который все это запускает.

И все равно процесс начинает разъезжаться.

Один сервис кладет Dependency-Check-отчеты в одну директорию. Второй генерирует только HTML. Третий правильно передает merge request metadata в SonarQube. Четвертый гоняет все как обычную branch analysis. Один проект делает SBOM только по runtime-зависимостям. Другой включает test-зависимости и получает шумный отчет. В multi-module проекте появляется исключение, кто-то копирует кусок YAML из соседнего репозитория, чуть правит, и так оно живет дальше.

В первый день это не выглядит проблемой.

Через несколько месяцев это уже нормальный такой дрейф security build-процесса.

И вот эту проблему я хотел закрыть через secure-build-gradle-plugin.

Не новым сканером.

А слоем build tooling, который стандартизирует, как существующие AppSec-инструменты подключаются к Gradle-проекту.

Проект: Secure Build Gradle Plugin

Проблема, которую я хотел убрать

Обычно DevSecOps начинается с CI/CD YAML.

Для одного Java-сервиса это нормальный старт:

script:
  - ./gradlew test
  - ./gradlew dependencyCheckAnalyze
  - ./gradlew cyclonedxBom
  - ./gradlew sonar

Все понятно. Команды видны. Pipeline запускается. Отчеты появляются.

Проблема начинается, когда такой подход размножается на десятки сервисов.

Каждый сервис постепенно становится ответственным за свою security-интеграцию. Команды копируют старые куски pipeline. Версии плагинов и сканеров расходятся. Пути отчетов расходятся. Где-то добавили SARIF, где-то забыли. Где-то SonarQube получает coverage XML, где-то нет. Где-то branch analysis, где-то pull request analysis. Где-то локально можно повторить проверку, а где-то только пушить и ждать pipeline.

Снаружи это выглядит как автоматизация.

На практике это превращается в набор чуть разных ручных интеграций.

И самое неприятное: команды начинают спорить не про риск, а про проводку инструментов.

Почему отчет не там?

Почему SonarQube не видит coverage?

Почему в одном сервисе Dependency-Check падает, а в другом только пишет отчет?

Почему multi-module проект опять особенный?

В этот момент проблема уже не в том, что “нам нужны еще tools”.

Проблема в том, что нет одного места для conventions.

Почему держать всю security-логику только в CI/CD неудобно

CI/CD хорош как общая среда выполнения.

Он дает чистый runner, логи, artifacts, gates, approvals, общий audit trail. Это все нужно.

Но CI/CD не очень хорош как единственное место, где живет вся логика security-проверок.

Если вся логика находится только в .gitlab-ci.yml или Jenkinsfile, локальная разработка становится вторичной. Разработчик пушит код не только чтобы открыть merge request, но и чтобы понять, что вообще думает security pipeline.

Это плохой feedback loop.

В идеале разработчик должен иметь возможность запустить ту же базовую проверку до merge request:

./gradlew clean securityAnalyze --no-daemon

А CI/CD должен запускать тот же workflow и собирать предсказуемые артефакты.

То есть CI/CD должен исполнять security workflow, а не каждый раз заново описывать его в каждом репозитории.

Это важное отличие.

Идея решения

Принцип был простой:

security behavior должен жить ближе к коду,
а CI/CD должен запускать тот же behavior без повторной реализации

Для Gradle это естественно ложится в convention plugin.

Gradle уже является местом, где проект описывает, как он собирается, тестируется, публикуется и какие tasks доступны. Значит, туда же можно вынести повторяемые AppSec conventions.

Не политику компании.

Не финальное risk acceptance.

Не vulnerability management.

А именно build-time слой:

  • как запускать Dependency-Check;

  • куда складывать отчеты;

  • какие форматы генерировать;

  • как делать SBOM;

  • как передавать coverage в SonarQube;

  • как различать branch и merge request analysis;

  • как жить с multi-module проектами;

  • как дать разработчику одну понятную команду.

Как подключается Gradle plugin

Проект применяет plugin один раз:

plugins {
  id "java"
  id "io.github.niki1337.securebuild.gradle-java" version "0.1.0"
}

Дальше в build-файле остаются только значения конкретного сервиса:

securityConventions {
  serviceName = "payment-api"
  sonarProjectKey = "payment-api"
  allowLocalSonar = false
}

Для multi-module проекта plugin обычно подключается на root-уровне:

plugins {
  id "io.github.niki1337.securebuild.gradle-java" version "0.1.0"
}

securityConventions {
  serviceName = "payments-platform"
  sonarProjectKey = "payments-platform"
  includedModules = ["api", "service"]
  excludedModules = ["test-fixtures"]
}

После этого меняется модель владения.

CI/CD все еще запускает проверки. Security все еще отвечает за требования и triage. Разработчики все еще чинят findings.

Но повторяемая scanner-проводка живет в build system, а не копируется из репозитория в репозиторий как устное предание.

Что происходит под капотом

Plugin не заменяет существующие инструменты.

Он подключает и стандартизирует:

  • SonarQube analysis;

  • OWASP Dependency-Check;

  • CycloneDX SBOM;

  • JaCoCo или Kover coverage;

  • Gradle single-module и multi-module behavior;

  • Git branch metadata;

  • GitLab merge request metadata.

Главная команда для разработчика:

./gradlew clean securityAnalyze --no-daemon

securityAnalyze становится нормальной точкой входа для локальной AppSec-проверки. Она может запускать тесты, coverage, SBOM generation и dependency analysis.

Если нужно разобрать отдельные части, underlying tasks никуда не прячутся:

./gradlew cyclonedxDirectBom --no-daemon
./gradlew dependencyCheckAnalyze --no-daemon
./gradlew sonarHelp --no-daemon

Для multi-module:

./gradlew dependencyCheckAggregate --no-daemon

Смысл не в том, чтобы спрятать инструменты.

Смысл в том, чтобы нормальный путь был очевидным.

Почему это лучше голого CI/CD

Можно сказать: “А зачем plugin? Можно же просто написать нормальный .gitlab-ci.yml”.

Можно.

Для одного репозитория это часто нормально.

Но на масштабе начинаются обычные проблемы:

  • пути отчетов расходятся;

  • scanner settings расходятся;

  • SonarQube metadata забывается;

  • локальный запуск отличается от pipeline;

  • multi-module проекты требуют отдельных костылей;

  • новые сервисы копируют старый boilerplate;

  • security-команда получает разные форматы артефактов;

  • разработчики не понимают, какую команду запускать до MR.

Gradle convention plugin дает одно версионируемое место для этой логики.

CI/CD остается важным, но становится execution layer:

GitLab CI / Jenkins
  запускает ./gradlew securityAnalyze
  собирает artifacts
  применяет gates

А build behavior находится ближе к проекту:

Gradle plugin
  знает tasks
  знает report paths
  знает multi-module structure
  знает SonarQube metadata conventions

В закрытых контурах это особенно полезно.

Во многих СНГ-компаниях нельзя просто взять SaaS и красиво подключить все по документации. Есть внутренний GitLab, свой Nexus, Harbor, self-hosted SonarQube, прокси, внутренние CA, ограничения на интернет, отдельные runners. В такой среде предсказуемость важнее красивой картинки в документации.

SonarQube: проблема почти правильной настройки

SonarQube легко настроить “почти правильно”.

Это как раз опасная зона.

Pipeline зеленый. Analysis ушел. В UI что-то появилось.

Но Java binaries не передались. Coverage XML не передался. Merge request metadata не передалась. В итоге результат вроде есть, но он слабее, чем должен быть.

Plugin решает эту рутину централизованно.

Он читает настройки из environment variables, Gradle properties или securityConventions:

export SONAR_HOST_URL="https://sonarqube.example.com"
export SONAR_PROJECT_KEY="payment-api"
export SONAR_TOKEN="token-value"

И готовит типовые Java SonarQube properties:

sonar.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

Для GitLab merge request pipeline он может маппить CI variables:

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

Для branch pipeline задается branch analysis metadata.

Это скучная, но важная работа. И именно такую работу лучше решать один раз.

Dependency-Check: отчеты должны быть скучными

Dependency-Check полезен тогда, когда его отчеты предсказуемы.

Я не хочу, чтобы один сервис генерировал JSON, второй только HTML, третий SARIF в непонятной директории, а четвертый вообще все складывал в кастомный путь.

Plugin стандартизирует форматы:

HTML
JSON
SARIF
XML

И пишет их в:

build/reports/dependency-check

По умолчанию отключаются сетевые анализаторы, которые могут делать pipeline нестабильным в закрытой инфраструктуре:

  • OSS Index;

  • RetireJS;

  • Node audit;

  • Node package analyzer;

  • hosted suppressions;

  • CISA KEV analyzer.

В идеальном мире у всех быстрый интернет и доступ к нужным внешним источникам.

В реальном enterprise-мире часто есть прокси, firewall, mirror, внутренние registry и запрет на прямой outbound. Поэтому security build не должен случайно становиться медленным или нестабильным из-за внешнего endpoint.

Если есть внутренний mirror, можно использовать его:

DT_API_URL=https://dependency-track.example.com \
./gradlew dependencyCheckAnalyze --no-daemon

По умолчанию build не падает по CVSS:

failBuildOnCVSS = 11

Так как максимальный CVSS равен 10, это означает: отчет генерируем, но build не валим только по score.

Это осознанный выбор.

На первом этапе команде часто нужна видимость и triage. Если сразу включить жесткие gates на шумных данных, разработчики быстро начнут воспринимать security как блокер без смысла. Сначала данные, валидация и false-positive reduction. Потом уже blocking policy.

SBOM без лишнего шума

SBOM полезен только тогда, когда он описывает полезный artifact.

Если один проект включает test dependencies, а другой нет, сравнивать результаты сложно. Если root multi-module проекта является только aggregator, root-level SBOM может плохо описывать реальное приложение.

Plugin фокусируется на runtime dependencies и уменьшает лишний шум:

  • runtime dependencies включаются;

  • test dependencies не добавляются;

  • license text не встраивается;

  • serial number можно отключить;

  • лишняя metadata уменьшается.

Типовые команды:

./gradlew cyclonedxDirectBom --no-daemon
./gradlew cyclonedxBom --no-daemon

Отчеты:

build/reports/cyclonedx
build/reports/cyclonedx-direct

Для multi-module Spring Boot-проектов plugin старается найти deployable module, например модуль с bootJar, и сделать SBOM ближе к реальному приложению, а не к пустому root aggregator.

Это кажется мелочью, пока не начинаешь отправлять SBOM в Dependency-Track и разбирать, почему половина findings относится к тому, что не попадает в runtime.

Coverage wiring

Coverage нужен для нормального SonarQube analysis, но paths часто расходятся.

Plugin поддерживает:

  • JaCoCo;

  • Kover.

В режиме auto он использует Kover, если Kover уже есть, иначе подключает JaCoCo:

securityConventions {
  coverageProvider = "auto"
}

Для JaCoCo plugin:

  • использует JaCoCo 0.8.13;

  • делает jacocoTestReport зависимым от tests;

  • включает XML output;

  • передает XML path в SonarQube.

Типовой путь:

build/reports/jacoco/test/jacocoTestReport.xml

Опять же, это не rocket science. Это просто та самая повторяемая настройка, которую не хочется чинить в каждом сервисе отдельно.

Multi-module Gradle проекты

Multi-module проекты быстро показывают, насколько интеграция зрелая.

Для demo single-module проекта почти любой scanner пример выглядит красиво.

А потом приходит реальный репозиторий:

root
  api
  service
  domain
  client
  test-fixtures

И начинается: где source? где tests? где binaries? какой модуль deployable? какие модули включать в analysis? что делать с root?

Plugin определяет Java subprojects с:

java
java-library

И поддерживает фильтры:

securityConventions {
  includedModules = ["api", "service"]
  excludedModules = ["test-fixtures"]
}

В multi-module режиме он:

  • настраивает coverage по Java-модулям;

  • собирает coverage XML paths;

  • конфигурирует root-level SonarQube analysis;

  • задает module-level source/test/binary/library paths;

  • запускает aggregate Dependency-Check;

  • старается не генерировать шумный root-level SBOM;

  • использует deployable module для SBOM, когда это возможно;

  • оставляет единый root-level securityAnalyze.

Это и есть отличие между “мы прикрутили сканер” и “у нас есть build convention для реальных Java repositories”.

Как выглядит CI/CD после этого

CI/CD становится проще.

Пример GitLab CI:

security: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 можно вынести отдельно:

sonarqube:gradle:
  image: eclipse-temurin:17
  stage: test
  script:
    - ./gradlew sonar --no-daemon
  rules:
    - if: '$SONAR_TOKEN'

Важная мысль:

CI/CD вызывает build tasks,
а не заново реализует security conventions

Pipeline становится читаемее. Артефакты становятся предсказуемее. Локальный запуск становится ближе к CI.

Что этот plugin не пытается решить

Plugin специально ограничен.

Он не заменяет:

  • SonarQube;

  • OWASP Dependency-Check;

  • CycloneDX;

  • DefectDojo или Dependency-Track;

  • ручной vulnerability triage;

  • risk acceptance;

  • secret scanning;

  • DAST;

  • container scanning;

  • IaC scanning;

  • release approval process.

Это не “весь DevSecOps в одной зависимости”.

Это build-time слой для Java AppSec:

SCA
SBOM
coverage
SonarQube metadata
local and CI/CD behavior
report conventions

Secret scanning я сознательно отношу к более раннему слою: до commit и push. Про это лучше писать отдельно, потому что там другая логика и другой feedback loop.

Итог

secure-build-gradle-plugin решает не проблему “нет сканеров”.

Он решает проблему “сканеры подключены везде по-разному”.

Вместо того чтобы каждый сервис заново изобретал SonarQube, Dependency-Check, CycloneDX и coverage wiring, проект получает один Gradle-слой:

./gradlew securityAnalyze

Разработчики получают более раннюю обратную связь.

CI/CD получает предсказуемые artifacts.

Security-команда получает более стабильные форматы отчетов.

Multi-module проекты получают поведение, которое понимает структуру репозитория.

И главное: security tooling становится частью нормального Java engineering workflow, а не набором скриптов вокруг него.

Ссылки

Черновые хабы для Habr

Java, Gradle, DevOps, Информационная безопасность, CI/CD