Java-зависимости и транзитивный Log4j: начинаем инвентаризацию у клиентов
Security-сообщество активно обсуждает сложность аудита транзитивных Java-библиотек. Мы начали инвентаризацию Log4j в enterprise-стеках клиентов - превентивно.
Security community активно обсуждает риски Java-зависимостей и сложность аудита транзитивных библиотек в enterprise-приложениях
В последние недели в security-чатах и рассылках неожиданно оживилась тема Java-зависимостей. Не конкретный CVE, не инцидент - просто несколько обстоятельных постов про то, как сложно понять, что именно тащит в себе enterprise Java-приложение. Транзитивные зависимости, вложенные jar-файлы, артефакты, собранные полгода назад и задеплоенные без пересборки. Тема не новая, но разговор пошёл конкретный - с примерами и инструментами. Мы решили, что это хороший повод разобраться, как обстоят дела у наших клиентов.
Начали с самого простого вопроса: какие версии Log4j используются в production? Звучит невинно. На практике - отдельное приключение.
Почему транзитивные зависимости - это проблема именно в Java
В экосистеме npm или PyPI зависимость - это обычно отдельный каталог в node_modules или site-packages, и её версию видно сразу. В Java мир устроен иначе: зависимости упаковываются внутрь jar и war файлов. Fat jar, uber jar, shadow jar - артефакт содержит всё нужное внутри себя, и это удобно для деплоя. Но означает, что для ответа на вопрос «какая версия Log4j?» нужно либо смотреть в pom.xml исходников (если они есть и актуальны), либо лезть внутрь jar и разбирать вложенные архивы.
У enterprise-приложений ситуация ещё веселее. Типичный стек: несколько WAR-файлов на одном Tomcat, каждый из которых собирался независимой командой в разное время, часть - вендорские продукты без исходников. pom.xml к ним может и не прилагаться. Спрашивать разработчиков «а какая у вас Log4j?» - занятие с непредсказуемым результатом.
Что мы используем для инвентаризации
Первый инструмент - jar tf и рекурсивный поиск. Грубо, но работает:
# найти все jar внутри war
jar tf application.war | grep "log4j"
# если упакованы вложенные jar - распаковываем и смотрим внутри
find . -name "*.jar" -o -name "*.war" | while read f; do
jar tf "$f" 2>/dev/null | grep -i "log4j" | sed "s|^|$f: |"
done
Этот подход находит log4j-*.jar в очевидных местах. Но если Log4j зашит внутрь fat jar как набор классов, а не как отдельный jar - этот метод его не видит.
Второй инструмент - анализ манифестов и pom.properties. Внутри каждого jar, собранного Maven, есть META-INF/maven/<groupId>/<artifactId>/pom.properties с версией. Это надёжнее:
find . -name "*.jar" -o -name "*.war" | while read f; do
unzip -p "$f" "*/pom.properties" 2>/dev/null \
| grep -A2 "log4j" | grep version
done
Третий вариант - Syft. После того как мы использовали его для SBOM container-образов, попробовали прогнать по файловым системам серверов напрямую. Syft умеет анализировать Java-архивы, включая вложенные jar:
syft dir:/opt/tomcat/webapps -o table | grep -i log4j
Результат аккуратнее ручного поиска: видно groupId, artifactId, версию, путь к файлу. На стеках с десятком WAR-файлов это ощутимо быстрее.
Что нашли у клиентов
Картина, честно говоря, не удивила, но масштаб немного впечатлил.
Разброс версий. Там, где мы ожидали увидеть более-менее единообразную картину, обнаружили Log4j от 1.2.x до 2.14.x в рамках одного сервера. Log4j 1.x официально не поддерживается с 2015 года и имеет свои известные проблемы, но живёт в вендорских компонентах и никуда не торопится.
Транзитивные зависимости фреймворков. Несколько приложений не используют Log4j напрямую - в их pom.xml его нет. Но Log4j приходит транзитивно через Hibernate, Apache Struts, Spring-компоненты старых версий. Разработчики про это не знают, потому что «мы не подключали Log4j».
Вендорские jar без pom. Два продукта - коммерческие, исходников нет. Версию Log4j удалось установить только через анализ манифеста jar вручную. В одном случае Log4j оказался встроен как классы в общий fat jar, и определить точную версию получилось только через хеш LogManager.class и сверку с известными сборками.
Почему это работа, а не скрипт
Автоматика находит большинство случаев, но не все. Несколько ситуаций, которые требуют ручной проверки:
- Переименованные пакеты (shading). Maven Shade Plugin умеет переименовывать пакеты внутри jar.
org.apache.logging.log4jможет оказаться зашейден вcom.vendor.thirdparty.log4j. Автоматический поиск по имени это не найдёт. - Нестандартные пути развёртывания. Некоторые приложения кладут jar в
/var/lib/,/opt/vendor/,/usr/share/java/- нужно знать инфраструктуру, чтобы не пропустить. - Классы без jar. Redeploy на Tomcat иногда оставляет extracted классы в
work/иwebapps/<app>/WEB-INF/classes/- они не в архивах, их нужно искать отдельно.
По этим причинам мы делаем инвентаризацию как аудит, а не как автоматизированный скан: инструменты дают 80%, остаток требует понимания конкретной инфраструктуры.
Где сейчас
Инвентаризацию прошли на трёх клиентских средах. Реестр компонентов составлен: что, какой версии, где лежит, как туда попало. По результатам - несколько рекомендаций обновить зависимости, одна рекомендация поднять вопрос с вендором про обновление их компонента.
Сама по себе картина не катастрофическая - Log4j 2.x в основном свежих версий. Но точное знание того, что именно и где установлено, само по себе ценно: когда что-то случается, не нужно срочно бегать по серверам с find. Реестр уже есть.
Выльется ли текущее оживление в security-сообществе во что-то конкретное или останется академическим разговором - неясно. Но инвентаризировать в спокойном режиме приятнее, чем в режиме аврала.