open atlas
↑ К треку
Go с нуля до senior GO · 11 · 02

govulncheck и гигиена зависимостей: достижимость на уровне символов, целостность go.sum и MVS без сюрпризов

govulncheck сообщает только об уязвимостях, до которых код реально доходит по графу вызовов — шума в ~10 раз меньше, чем у сканеров образов. go.sum фиксирует хэши содержимого, MVS не обновляет зависимости без явного bump, ночные сканы ловят новые CVE против старого кода.

GO Senior ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Бюллетень прилетел в пятницу в 16:00: CVSS 9.8 в библиотеке парсинга YAML, и go mod graph подтвердил — три уровня в глубину, затянута конфиг-лоадером внутреннего фреймворка, присутствует во всех одиннадцати сервисах. Сканер контейнеров уже раскрасил все дашборды красным. Две команды на этаже отменили планы на выходные и начали аварийные бампы по своим репозиториям. Одна инженерка вместо этого запустила govulncheck ./... и внимательно прочитала вывод: находка была информационной — модуль присутствовал в сборке, но уязвимый символ, рекурсивный резолвер алиасов в глубине Unmarshal, был недостижим ни по одному пути вызовов из их кода. Они использовали строгое типизированное декодирование; опасная функция в графе вызовов не появлялась. Она приложила трассу к тикету инцидента, секьюрити-лид подписал, и команда отгрузила апгрейд в обычном релизном поезде понедельника — то же исправление, ноль выходных. Две другие команды выкатили хотфикс в два ночи и дважды откатывали один сервис. Разница была не в смелости. Она была в знании разницы между зависимостью, которая у вас есть, и функцией, которую вы вызываете.

Достижимость, а не присутствие

Бывало ли у вас, что сканер красил все дашборды CVSS 9.8, а вечер пятницы уходил на споры о том, вызывает ли ваш код ту самую функцию? Есть вопрос острее — и тулчейн Go его задаёт первым.

Сканеры контейнеров и SCA (Software Composition Analysis — анализ состава зависимостей) отвечают на поверхностный вопрос: какие версии модулей лежат в артефакте и с какими диапазонами CVE они пересекаются? govulncheck отвечает на вопрос, по которому вы реально триажите: может ли исполнение дойти до уязвимого кода? Он строит статический граф вызовов программы, сопоставляет его с базой уязвимостей Go (vuln.go.dev), где каждый отчёт называет затронутые символы — конкретные функции и методы, — и выдаёт находку только при существовании пути вызовов из вашего кода до одного из этих символов. Всё остальное понижается до информационного.

$ go install golang.org/x/vuln/cmd/govulncheck@latest
$ govulncheck ./...
=== Symbol Results ===
Vulnerability #1: GO-2022-0969
  ...
  Example traces found:
    #1: api/handler.go:42 handler.Process calls yaml.Unmarshal

Снижение шума — вся ценность инструмента: вывод сканера для типичного сервиса перечисляет десятки CVE уровня модулей, и на практике подавляющее большинство недостижимы — команды обычно видят сокращение требующих действия находок порядка 10:1. Каждая настоящая находка приходит с трассой (цепочкой вызовов от вашей функции до уязвимого символа), что превращает триаж из «спора о баллах CVSS» в «чтение стека». Для уже задеплоенных артефактов есть govulncheck -mode=binary — проверка скомпилированного бинаря по его таблице символов: что реально слинковано, а не что подразумевает дерево исходников.

Честные оговорки, потому что достижимость — это статический анализ: вызовы через рефлексию, plugin или кодогенерацию могут прятать пути, так что «недостижимо» значит деприоритизировано, а не проигнорировано — бамп всё равно случается, просто по расписанию. И govulncheck покрывает только Go-код: ваши C-зависимости и базовый образ остаются работой сканера.

Викторина

Сканер образов сообщает о 23 CVE для Go-сервиса; govulncheck — о 2 находках и 21 информационной записи. Чем объясняется разница?

Где он работает: на каждом PR и каждую ночь

Два триггера — две причины. На каждом PR, потому что новая зависимость или новый вызов в старую зависимость добавляют достижимость в момент мержа. И каждую ночь на main, потому что второй вход меняется без вас: новые CVE публикуются против кода, который вы не трогали месяцами. Репозиторий, сканирующий только PR, слеп между мержами — ровно то окно, которое эксплуатирует пятничный бюллетень. Форма CI скучна намеренно: govulncheck ./... как PR-гейт, падающий на находках уровня символов; та же команда по ночному расписанию, открывающая тикет вместо падения сборки; плюс -mode=binary по выпущенным артефактам, если деплой отстаёт от мержей.

Почему это работает

Почему ночной прогон оправдан, когда код заморожен? Потому что отчёт об уязвимости — это join двух таблиц: вашего графа вызовов и базы уязвимостей, и обе стороны меняются независимо. PR-сканы покрывают изменения вашей стороны. Сторона базы меняется ежедневно: команда безопасности Go публикует отчёты непрерывно, часто про код, который год лежит в вашей сборке. Ночной скан — то, что превращает раскрытие во вторник в тикет во вторник, а не в инцидент от клиента на шестой неделе.

go.mod выбирает, go.sum проверяет

Два файла защищают от разных атак, и их смешение — то, из-за чего команды неверно оценивают свою экспозицию. go.mod — это выбор версий: какие версии модулей сборка хочет. go.sum — это целостность: криптографические хэши содержимого каждой версии модуля, записанные при первом её попадании в сборку. При каждой последующей загрузке тулчейн пересчитывает хэши и отказывается собирать при несовпадении — если та же версия модуля когда-либо приедет с другими байтами (скомпрометированный прокси, перетегнутый репозиторий, подменённое зеркало), сборка громко упадёт вместо компиляции кода атакующего. Первые появления тоже проверяются — по публичной базе контрольных сумм (sum.golang.org), журналу прозрачности, который прокси не может тихо переписать. GOPRIVATE выводит внутренние модули из этого публичного поиска — держите его область узкой: всё, что он матчит, теряет проверку через sumdb.

Слой прокси добавляет гарантию доступности и неизменяемости: как только proxy.golang.org закэшировал версию модуля, она отдаётся неизменно — удаление или перетегивание апстрима не меняет того, что видят сборки. Класс инцидентов left-pad (зависимость исчезла и сломала мир) и класс retag (тот же тег, новое содержимое) закрыты структурно — поэтому отключение прокси не покупает ничего, кроме риска.

MVS: скучный по дизайну

Минимальный выбор версий в Go берёт для каждого модуля максимум из минимумов, заявленных в go.mod по всему графу — никогда «последнюю доступную». Следствие для безопасности стоит проговорить: публикация новой версии модуля ничего не делает с вашей сборкой. Зловредная v1.9.9, запушенная в популярную библиотеку, лежит инертно, пока какой-нибудь go.mod в вашем графе явно её не потребует. Сравните с npm-диапазонами (^1.2.0), где следующая чистая установка резолвится в самую свежую подходящую версию — ровно механизм нескольких крупных supply-chain-атак, где окна между зловредной публикацией и обнаружением хватило на компрометацию тысяч CI-прогонов. В Go этого окна просто нет; апгрейды происходят, когда человек запускает go get.

Трейдофф симметричен, и им нужно управлять: исправления безопасности тоже ждут явного бампа. MVS без сканирования — путь к гниению сборок в непропатченную старость. Рабочая пара: MVS для детерминизма плюс govulncheck для срочности — бампайте по расписанию в норме, бампайте немедленно, когда достижимая находка этого требует.

Викторина

CI-сборка падает с несовпадением контрольной суммы go.sum для версии модуля, которая неделю назад собиралась нормально. О чём это реально говорит?

Меньше зависимостей лучше просканированных зависимостей

Все контроли выше работают по графу зависимостей, который у вас уже есть; самый сильный контроль его сокращает. Зависимость — это постоянное право исполнения в вашем процессе плюс вечная строка в бюджете аудита, поэтому вопрос ревью для новой прямой зависимости не «хороший ли это код», а «стоит ли она вечного аудита и что она затягивает?» (go mod graph | wc -l до и после — отрезвляющий дифф.) Для существующего веса go mod why -m <модуль> отвечает на «почему это здесь» цепочкой импортов, а go mod tidy держит список требований честным. Пространство имён модулей добавляет одну Go-специфичную остроту: пути модулей — это URL, в основном GitHub-пути, и тайпсквоттинг живёт в одной переставленной букве — у github.com/sirupsen/logrus задокументированная история сквоттнутых опечаток. Пути импортов заслуживают того же внимания на ревью, что и код, который их использует.

Вспомните перед уходом
  1. 01
    Объясни механизм, благодаря которому govulncheck выдаёт в 10 раз меньше требующих действия находок, чем сканер образов, и две оговорки, которые с этим приходят.
  2. 02
    Разбери, что в терминах безопасности гарантирует каждый: go.mod, go.sum, прокси модулей и MVS.
Итог

Цепочка безопасности Go триажит по вопросу острее, чем способен задать любой сканер версий: не какие модули присутствуют, а до каких уязвимых функций ваша программа реально может дойти. Теперь, когда видишь отчёт сканера с десятками CVE — первый ход govulncheck ./...: пусть граф вызовов отделит пятничный пожар от понедельничной задачи, и никогда не заглушай находку без трассы, доказывающей недостижимость. govulncheck строит статический граф вызовов, соединяет его с аннотированной по символам базой уязвимостей Go и срабатывает только на находках, подкреплённых конкретной трассой из вашего кода — на практике срезая требующие действия алерты на порядок и превращая пятничные пожары в плановые понедельничные бампы. Его оговорки — часть честного использования: статический анализ не видит сквозь рефлексию и плагины, поэтому «недостижимо» значит «деприоритизировано с тикетом», а охват только Go-кода оставляет сканерам образов их работу на уровне ОС. Запускайте его дважды: как PR-гейт, потому что мержи добавляют достижимость, и ночью, потому что база уязвимостей меняется против вашего замороженного кода. Под сканером лежит стек целостности: go.mod выбирает версии, go.sum проверяет, что каждая загруженная версия модуля совпадает с записанным хэшем содержимого (несовпадение валит сборку — удалить строку ради зелёного CI значит довериться тому, что отдаётся прямо сейчас), база контрольных сумм делает первые появления публично аудируемыми, а proxy.golang.org отдаёт закэшированные версии неизменно, так что ретеги и удаления апстрима до вас не дотягиваются. MVS делает весь граф детерминированным — максимум из заявленных минимумов, никогда «последняя», — что закрывает окно атаки «опубликуй и жди», оставленное npm-диапазонами, по симметричной цене: исправления безопасности тоже приезжают только явным бампом. А контроль, который бьёт всё это, — вычитание: каждая зависимость — вечное обязательство аудита, go mod why называет цепочку, оправдывающую каждую, и пути импортов заслуживают того же ревью с поправкой на тайпсквоттинг, что и код.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.