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

推荐订阅源

cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
小众软件
小众软件
博客园_首页
博客园 - 聂微东
V
V2EX
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
SegmentFault 最新的问题
J
Java Code Geeks
Last Week in AI
Last Week in AI
The Cloudflare Blog
月光博客
月光博客
雷峰网
雷峰网
宝玉的分享
宝玉的分享
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
博客园 - Franky
腾讯CDC
Jina AI
Jina AI
博客园 - 叶小钗
大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
量子位
爱范儿
爱范儿
美团技术团队
T
Tailwind CSS Blog
博客园 - 【当耐特】
D
Docker
IT之家
IT之家
V
Visual Studio Blog
P
Proofpoint News Feed
L
LangChain Blog
Engineering at Meta
Engineering at Meta
C
Check Point Blog
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Recorded Future
Recorded Future

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Уязвимость пришла из зависимости, которую вы не добавляли: ловим дыры в Spring до прода в GitLab
Sergey Proshchaev · 2026-06-25 · via Все публикации подряд на Хабре

Средний

13 мин

13

Всем привет, меня зовут Сергей Прощаев и в этой статье расскажу, как настроить в GitLab автоматический поиск уязвимостей в зависимостях Spring‑приложения так, чтобы дыры всплывали в merge request до прода, а не на проде, — и при этом пайплайн не падал на каждой устаревшей библиотеке.

Я Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E‑commerce и преподаю на курсах разработки и архитектуры в ОТУС.

Сразу обозначу рамку. Это не обзор «что такое SCA» и не пересказ документации GitLab. Это рабочий маршрут: берём типовой Spring Boot сервис, подключаем dependency scanning на новом движке и настраиваем так, чтобы security‑гейт реально защищал, а не превращался в красный крестик, который все привыкли игнорировать.

Рис. 1. Уязвимость, спрятанная в глубине дерева зависимостей, и контрольная точка в пайплайне, которая ловит её до продакшена

Рис. 1. Уязвимость, спрятанная в глубине дерева зависимостей, и контрольная точка в пайплайне, которая ловит её до продакшена

С чего всё начинается: «у нас же ничего такого не подключено»

Помню историю, которая до сих пор для меня как эталон. Декабрь 2021-го, Log4Shell. Полночь, дежурный канал разрывается, и первый вопрос, который задаёт половина команд по всему рынку, звучит не «как пропатчить», а «а у нас вообще есть этот Log4j?».

И вот это самое страшное. В pom.xml его нет. Никто его не добавлял. Он приехал транзитивно — через стартер, через библиотеку логирования, через зависимость зависимости. Команды тратили дни, чтобы просто понять, затронуты они или нет: не было полной карты того, что собирается в JAR.

Мне как‑то попалась оценка по экосистеме Maven за 2025 год (её приводили в аналитике по supply‑chain рискам): уязвимости затрагивали порядка трети последних релизов библиотек через прямые зависимости и заметно больше — через транзитивные. Точные проценты гуляют от источника к источнику, но порядок один: основной канал риска — не то, что вы написали в pom.xml, а то, что подтянулось следом, на два‑три уровня глубже.

Я бы сформулировал так: если ваш инструмент проверки смотрит только на прямые зависимости, он показывает вам верхушку и молчит про корни. А именно в корнях обычно и сидит то, что вас положит.

Что мы будем делать и в каких условиях

Берём типовой сервис: Spring Boot 4.1.0, Spring Framework 7.0.8 под капотом, Java 21, сборка через Maven. Пример протестирован на Spring Boot 4.1 и проверен на GitLab 19.1. Используемые возможности относятся к GitLab, поэтому для Spring Boot 3.x конфигурация остаётся такой же — меняются только версии зависимостей. Тариф — Ultimate, потому что SBOM‑based dependency scanning живёт именно там; на нём же завязан Security Dashboard и гейт в merge request.

Чтобы было что ловить, добавим в сервис «мину» — зависимость с известной уязвимостью. Возьмём commons‑text 1.9. В ней живёт CVE-2022-42889, она же Text4Shell: при определённых сценариях использования StringSubstitutor с дефолтными интерполяторами она позволяет добиться удалённого выполнения кода (CVSS v3.1 Base Score 9.8). Починили её в 1.10, но 1.9 до сих пор регулярно встречается в проектах — иногда напрямую, чаще транзитивно, приехав через утилитную библиотеку, которую подключили ради одного метода.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Наша "мина": уязвимая версия commons-text (CVE-2022-42889, Text4Shell).
         В реальном проекте такая чаще приезжает транзитивно, а не строкой в pom.
         Здесь объявили явно, чтобы пример был воспроизводимым. -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-text</artifactId>
        <version>1.9</version>
    </dependency>
</dependencies>

Важная оговорка про честность примера. Я объявил уязвимую версию явной строкой, чтобы вы воспроизвели всё один в один. В жизни так бывает реже: обычно commons‑text 1.9 не написан в pom.xml, а подтянут на втором‑третьем уровне через библиотеку, которую подключили для другого. Руками такую мину вы не увидите — поэтому дальше я упираюсь в транзитивные зависимости.

Цель — три вещи:

  • Видеть полный список зависимостей, включая транзитивные, и понимать, какая откуда пришла.

  • Получать новые уязвимости прямо в merge request — до того, как изменение попало в основную ветку.

  • Не ронять пайплайн на каждой найденной CVE, а отделять то, что реально угрожает, от шума.

Важная деталь по версиям. Внутри GitLab сейчас два движка dependency scanning. Старый, на Gemnasium, помечен устаревшим ещё в 17.9 и пойдёт под нож в 20.0. Новый движок использует SBOM в формате CycloneDX, стал generally available в GitLab 19.0 и подключается через V2-шаблон. Делаем сразу на новом: переезжать с Gemnasium через год — то ещё удовольствие, я предпочитаю не плодить технический долг.

Маршрут решения

Дальше — по шагам. Каждый шаг закрывает один конкретный риск, и после каждого я показываю, как проверить, что он сработал.

Шаг 1. Подключаем новый движок сканирования

Базовое подключение — одна строка include в.gitlab‑ci.yml. Отдельных джоб руками писать не нужно, шаблон сам разворачивает всё необходимое.

stages:
  - build
  - test

include:
  - template: Jobs/Dependency-Scanning.v2.gitlab-ci.yml

Ключевое слово здесь — v2. Если по старому гайду с какого‑нибудь Medium подключить Security/Dependency‑Scanning.gitlab‑ci.yml, вы получите старый Gemnasium‑движок. Внешне всё работает, отчёт появляется — но это уже легаси, и части фишек, ради которых мы всё затеваем, там не будет.

Как проверить: после пуша зайдите в Build → Pipelines, откройте последний пайплайн. В стадии.pre должна появиться джоба разрешения зависимостей, а дальше — dependency‑scanning. Если их нет — скорее всего не включена фича в Settings → Security или у проекта не тот тариф.

Шаг 2. Даём сканеру увидеть транзитивные зависимости

Здесь же — главная ловушка, на которую я сам когда‑то наступил.

Анализатор dependency scanning строит SBOM на основе графа зависимостей и метаданных сборки, и для Maven источников несколько: разрешение зависимостей во время сборки, экспортированный граф (maven.graph.json) или, в некоторых случаях, анализ самого pom.xml. Проблема в том, что у Maven, в отличие от условного npm, нет привычного «lock‑файла» в репозитории по умолчанию. И если нет ни графа, ни результатов resolution, анализатор может перейти в fallback‑режим анализа pom.xml. А этот режим, по документации GitLab на момент написания статьи, ограничивается явно объявленными зависимостями и неполной моделью транзитивного графа. Это поведение реализации GitLab, а не свойство Maven, и со временем может измениться.

То есть формально джоба зелёная, отчёт есть, галочка стоит. А часть зависимостей могла не попасть в анализ. Худший вид безопасности — тот, что создаёт ложное ощущение, будто всё проверено.

В GitLab 19.0 это во многом закрыли автоматическим разрешением зависимостей: если коммитнутого графа нет, движок сам пытается сгенерировать его в стадии.pre, поднимая лёгкий образ с JDK и прогоняя Maven. На многих проектах хватает из коробки.

Но для воспроизводимости примера я предпочитаю генерировать maven.graph.json явно. Экспортированный граф зависимостей может использоваться анализатором GitLab как один из источников информации о составе зависимостей, и явная генерация упрощает диагностику и делает пайплайн менее зависимым от внутренних механизмов анализатора. Если GitLab у вас и так строит граф корректно — оставайтесь на автоматике, это полноценный рабочий вариант.

stages:
  - build
  - test

include:
  - template: Jobs/Dependency-Scanning.v2.gitlab-ci.yml

# Явно строим граф зависимостей в контролируемой сборочной стадии.
# Формат именно JSON (maven.graph.json) — его умеет читать анализатор.
build:
  stage: build
  image: maven:3.9-eclipse-temurin-21
  script:
    - mvn dependency:tree -DoutputType=json -DoutputFile=maven.graph.json
    - mvn package -DskipTests
  artifacts:
    paths:
      - maven.graph.json
      - target/*.jar
    expire_in: 1 hour

Как проверить: откройте dependency‑scanning, скачайте артефакт gl‑dependency‑scanning‑report.json или сам SBOM в формате CycloneDX. Найдите в нём нашу commons‑text 1.9 — она должна попасть в отчёт как уязвимая (Text4Shell). Заодно гляньте на любую заведомо транзитивную библиотеку — что‑нибудь из tomcat‑embed или jackson, которое вы руками не подключали. Если и мина, и транзитивные на месте — анализатор увидел полный граф. Если в отчёте только явно объявленное в pom.xml, — похоже, сработал fallback, и стоит разобраться с графом зависимостей.

Шаг 3. Включаем reachability — отделяем опасное от шумного

Теперь решаем проблему, из‑за которой большинство команд в итоге забивают на сканеры. Имя ей — шум.

Если просто включить сканирование на большом легаси‑проекте, вы утром получите десятки, а то и сотни уязвимостей. Critical, High, всё красное. И дальше одно из двух: либо команда героически неделю разгребает, либо — что бывает чаще — все молча договариваются красный гейт игнорировать. И инструмент фактически перестаёт использоваться.

Здесь помогает фича, которая в новом движке мне особенно нравится, — static reachability analysis. Далеко не каждая зависимость из дерева реально используется в коде: что‑то подтянулось транзитивно, но ни одна строчка вашего кода её не импортирует. Анализатор использует статический анализ проекта и определяет, есть ли в коде фактическое использование уязвимых пакетов — их импорты и обращения к символам. Если использование найдено, уязвимости в этой зависимости присваивается значение reachable. Важно понимать рамку: это статическая модель использования, а не гарантия эксплуатации. Reachability сужает список того, на что смотреть в первую очередь, но не выносит вердикт «это точно эксплойтабельно».

Для Java, JavaScript/TypeScript и Python это работает. Включается одной переменной:

include:
  - template: Jobs/Dependency-Scanning.v2.gitlab-ci.yml
    variables:
      DS_STATIC_REACHABILITY_ENABLED: true

Я когда первый раз прогнал это на реальном сервисе, выдохнул. Список «на что смотреть сейчас» сократился в разы. Большая часть красноты оказалась библиотеками, до которых приложение не дотягивается.

Как проверить: в карточке уязвимости появляется поле Reachable. Отфильтруйте дашборд по нему. Уязвимости со значением Yes — ваш реальный backlog на сегодня. Остальное — в очередь по плановому обновлению, без ночных подвигов.

Шаг 4. Ставим гейт в merge request

Сканер, который пишет отчёт, который никто не открывает, бесполезен. Смысл всей затеи — поймать проблему на входе, в merge request, пока изменение ещё не в main.

Тут принципиальная тонкость. Гейт должен реагировать не на «в проекте вообще есть уязвимости» (они есть всегда, это нормальное состояние живого проекта), а на «это конкретное изменение приносит новую уязвимость». GitLab Security Policies позволяют реализовать именно такой сценарий: политика сравнивает находки между исходной и целевой веткой и показывает в merge request дельту — что нового притащил этот MR.

Задаётся это политикой на уровне группы, а не правкой .gitlab‑ci.yml в каждом репозитории. Важный сдвиг: безопасность настраивается один раз и применяется ко всем проектам автоматически, и рядовой разработчик не может её случайно выпилить из своего .gitlab‑ci.yml.

Сам гейт описывается политикой в отдельном security‑проекте. Вот минимальный пример scan result policy, который блокирует мерж, если изменение приносит новую уязвимость уровня Critical:

approval_policy:
  - name: Блок мержа на новые Critical в зависимостях
    description: >
      Если MR приносит новую уязвимость Critical из dependency scanning —
      требуется аппрув security-команды. Состояние проекта в целом не трогаем,
      смотрим только дельту, которую вносит это изменение.
    enabled: true
    rules:
      - type: scan_finding
        scanners:
          - dependency_scanning
        vulnerabilities_allowed: 0
        severity_levels:
          - critical
        vulnerability_states:
          - newly_detected
    actions:
      - type: require_approval
        approvals_required: 1
        role_approvers:
          - maintainer

Разберём важное. vulnerability_states: newly_detected — это и есть дельта: политика срабатывает только на то, что добавил MR, а не на весь ворох существующих находок. vulnerabilities_allowed: 0 по critical — «ни одной новой критичной без аппрува». А require_approval вместо жёсткой блокировки оставляет легальную дверь: security‑команда может осознанно пропустить изменение, приняв риск. Это честнее наглухо закрытого шлагбаума, который начинают обходить.

Я бы предостерёг от частой ошибки — не делайте первый гейт блокирующим по любой High. Получите бунт команды на второй день. Мой подход: на старте гейт реагирует только на новые Critical, а потом, когда команда привыкла, добавляем High и фильтр по Reachable. Это узкий, обороняемый порог — против него тяжело возразить, потому что это буквально «ты прямо сейчас тащишь в прод критичную дыру». А дальше планомерно закручиваете гайки.

Как проверить: заведите тестовый MR, добавляющий в pom.xml ту самую commons‑text версии 1.9. Виджет в merge request должен показать новую уязвимость Critical именно как привнесённую этим MR, а сам мерж — потребовать аппрува по политике. Если показал и потребовал — петля замкнулась, гейт работает.

Почему транзитивные зависимости — это не только про CVE

Тут хочу показать кейс, после которого я стал относиться к глубине дерева зависимостей всерьёз, а не как к формальности.

Есть исследование под названием Java‑Class‑Hijack (его представляли на воркшопе SCORED в 2025-м). Суть атаки красивая и от этого жутковатая. В Java, когда классы с одинаковым полным именем оказываются в разных зависимостях, при определённом порядке разрешения classpath в дело идёт тот класс, что встретился раньше. И вот исследователи показали: можно подсунуть вредоносный класс с тем же именем, что у легитимного, спрятав его глубоко в транзитивной зависимости. При сборке он встаёт в итоговый артефакт раньше настоящего — и подменяет поведение, не трогая ни основной код, ни имена библиотек.

Самое показательное — они воспроизвели это на реальном серверном приложении Corona‑Warn‑App, немецком ковид‑трекере. Через крошечную библиотеку валидации JSON, сидящую где‑то в глубине дерева, удалось дотянуться до логики подключения к базе. Маленькая безобидная зависимость на третьем уровне вложенности — а на выходе контроль над коннектом к БД. Оговорюсь: это не «любой Maven‑проект обречён» — эксплуатация требует ряда условий, связанных с разрешением зависимостей и порядком формирования classpath. Но сам класс атак показывает, почему глубину дерева нельзя игнорировать.

К чему я это. «Видеть транзитивные зависимости и их происхождение» — это не про галочку в комплаенсе. Атакующие всё чаще используют не прямую зависимость, которую вы хоть как‑то смотрите, а ту, что на три уровня глубже и которую вы в глаза не видели. Новый анализатор умеет трассировать транзитивную зависимость до источника: показывает цепочку, через какую библиотеку приехал уязвимый пакет. А зная цепочку, понимаешь, где бить — обновлять родителя или форсить версию.

И это перестало быть теорией ещё и потому, что атаки идут валом. В ноябре 2025-го червь Shai‑Hulud во второй итерации доехал и до Maven Central — компрометировал опубликованные пакеты отдельных разработчиков, тащил секреты, расползался дальше. Важная оговорка: компрометация шла через заражённые пакеты, а не через взлом самой инфраструктуры Maven Central. По следам той кампании счёт утёкших уникальных секретов шёл на десятки тысяч. Так что «у нас закрытый периметр, нам это не грозит» — позиция, которую я больше не принимаю всерьёз.

Как всё это связано в единый поток

Ниже на рис. 2 — как изменение проходит путь от коммита до решения «пускать или нет» и где включается сканирование.

Рис. 2. Путь изменения через пайплайн: сканирование зависимостей встроено в проверку merge request, а гейт реагирует на новую уязвимость, а не на общее состояние проекта

Рис. 2. Путь изменения через пайплайн: сканирование зависимостей встроено в проверку merge request, а гейт реагирует на новую уязвимость, а не на общее состояние проекта

Главная мысль из схемы: проверка стоит не в конце, перед деплоем, а на входе — в merge request. Чем левее в пайплайне вы ловите проблему, тем дешевле она обходится. Поймать уязвимую зависимость на ревью — это правка одной строки версии. На проде после инцидента — ночной созвон, хотфикс и объяснительная.

Где это решение не сработает (и что делать)

Я не люблю гайды, которые заканчиваются на «теперь всё работает». Так не бывает. Честно про ограничения.

  • Если у вас не Ultimate. SBOM‑based dependency scanning и гейт в merge request — это Ultimate. На младших тарифах вы соберёте SBOM и список зависимостей, но полноценного сравнения находок в MR и Security Dashboard не получите. Тогда смотрите в сторону внешних SCA (OWASP Dependency‑Check, Trivy) — их можно прикрутить отдельной джобой, но петлю «дельта в MR» придётся собирать вручную.

  • Reachability и Spring — отдельная осторожность. Важный нюанс, прямо прописанный в документации GitLab. Анализ достижимости поддерживает Java, JavaScript/TypeScript и Python — но для Java ловит явное использование (прямые импорты, рефлексию, строки подключения JDBC) и не видит зависимости, подгружаемые динамически в рантайме. А это ровно то, что делают DI‑фреймворки вроде Spring Boot. На практике — повышенный риск ложноотрицательных результатов: пакет может быть помечен как Not Found, хотя Spring поднимает его через инъекцию. Поэтому на Spring‑проекте я не отношусь к Not Found как к индульгенции и критичные находки перепроверяю руками. Для Go или Rust reachability не работает вовсе.

  • И общая оговорка. Как и любой SCA‑инструмент, GitLab dependency scanning не гарантирует стопроцентную полноту покрытия — особенно в проектах с динамической загрузкой зависимостей. Это не «включил настройки и система видит всё», а смещение вероятностей в вашу пользу: ловите кратно больше, чем без него, но абсолютной гарантии не даёт ни один сканер.

  • Не путайте с container scanning. Dependency scanning смотрит на зависимости приложения по манифестам и графу. Уязвимости в базовом Docker‑образе и системных пакетах ОС — зона container scanning, отдельного сканера. Два разных среза риска, закрывать надо оба.

  • Reachability — помощник, а не индульгенция. То, что код сегодня не дотягивается до уязвимого пакета, не значит, что не дотянется завтра после рефакторинга. Я использую Reachable для приоритизации, но не как разрешение навсегда забить на остальное. Мёртвая сегодня зависимость завтра может ожить.

Что в итоге

Если убрать всё лишнее, рабочая связка такая. Подключаем V2-шаблон dependency scanning, а не легаси. Скармливаем анализатору полный граф зависимостей (maven.graph.json), чтобы он видел транзитивные, а не только прямые из pom.xml. Включаем static reachability, чтобы отделить опасное от фонового шума и не убить доверие команды к гейту. И ставим гейт в merge request на дельту — на новые Critical, — задавая его политикой на уровне группы, чтобы его нельзя было тихо обойти.

Самый частый провал тут не технический. Команда включает сканер «чтобы был», тонет в шуме и через неделю учится не замечать красный крестик. Поэтому я настаиваю: узкий честный гейт, который ловит реальную дыру на входе, работает кратно лучше тотального гейта, который все игнорируют!


Продолжить знакомство с этими практиками можно уже 30 июня в 20:00 на бесплатном открытом уроке «GitLab CI как конструктор workflow». На нём покажем, как собирать в GitLab CI надёжные пайплайны, которые не просто запускают сборку, а помогают выстроить предсказуемый процесс доставки.

Больше полезных материалов по инфраструктуре смотрите в дайджесте.