Вызываем паранойю у ИИ-агентов: заменит ли MCP AppSec?
Привет, Хабр!
Я не писал на Хабре уже 15 лет, но всё это время хотел. Было много всяких разных проектов, продуктов, утилит и много чего ещё, о чём бы следовало написать. Но либо проекты устаревают быстрее, чем выдастся свободная неделька, либо NDA намекает не раскрывать деталей, без которых не получится забористо, либо просто лень. В общем “хорошие мысли преследовали меня, но я был быстрее”.
И вот появилось время, чтобы… Во-первых, примерить на себя роль владельца продукта, не знаю уж почему, но всегда хотелось попробовать себя в роли повелителя бэклога. Во-вторых, опробовать всякие модные Агенты, LLM и прочую нечисть, поработав над интересной мне задачей.
TLDR
Раньше писали скрипты через vim прямо на проде, потом отлаживали через print i,z,b и кофейную гущу, а теперь запускаешь Cursor — и за вечер появляется веб-приложение, а CI/CD уже катит его на прод. Кода стало больше, он стал запутаннее, и хакеры уже не сидят в зелёном терминале — AI-эра тут. Старая добрая триада SAST/SCA/DAST с потоком кода ещё справляется, а вот AppSec становится узким местом.
Поэтому я собрал себе ИИ-помощника с лёгкой степенью паранойи: локальный MCP-сервер AppSecGate, который запускает сканеры, ведёт базу файндингов и заставляет агента бояться уязвимостей.
В статье: как агенты встраиваются в SSDLC, что происходит под капотом MCP, бенчмарк семи локальных моделей на одной RTX 4060 Ti — без явных победителей, но с явными выводами. И ответ на главный вопрос: заменит ли MCP классический AppSec? Спойлер: нет, но рутину снимет, оставив безопаснику архитектуру и логику.
Итак, как было раньше или ещё сейчас
SSDLC даёт довольно прозрачный, проверенный временем и понятный путь сделать разрабатываемое приложение безопасным. Хотя есть несколько стандартов, все они описывают один и тот же подход, но с разных сторон. Вот пример минимального жизненного цикла разработки.
Рис. 1. Классический SSDLC: люди, инструменты, агрегаторы
Легенда: зелёный — люди и роли, фиолетовый — инструменты безопасности, голубой — системы агрегации и учёта, оранжевый — агенты.
Разработчик (на схеме — Developer) пишет код, используя свою любимую среду разработки (IDE). После написания код попадает в систему контроля версий (Git), где в рамках процесса сборки и тестирования (CI/CD) запускаются сканеры безопасности (SAST/SCA/DAST). Отчёты от сканеров загружаются в агрегатор файндингов (ASOC/ASPM), где обрабатываются, дедуплицируются, обогащаются и вот их наконец-то видит безопасник (AppSec). Он триажит файндинги, тестирует руками и, если ему что-то не понравилось, пишет задачу в систему управления заявками (TaskTracker). Задача доходит до разработчика, он её исправляет и процесс уходит на следующий круг, и так, пока всем не надоест. За всем этим безобразием естественно наблюдают руководители (Leads), они приоритизируют задачи, налаживают коммуникации, а иногда даже могут ходить в ASOC, чтобы посмотреть на красивые дашборды.
Кстати, если в данной схеме заменить ASOC на TMS, а AppSec на QA, то схема не изменится совсем. У такого “наивного” подхода есть много минусов, но самый жирный — это непредсказуемая скорость исправления уязвимостей. Например, сломались сканеры, дырявую библиотеку нечем заменить или AppSec ушёл в отпуск, а надо релизиться. Вот и получается, что время от появления уязвимости в коде до её фикса может варьироваться от почти мгновенного до когда рак на горе свистнет.
Но, несмотря на минусы, у него есть один перекрывающий всё “плюс” — чёткое и понятное разделение ответственности. Разработчик отвечает за код, безопасник за безопасность, руководители за создание ценности. А сопутствующие проблемы — это и не проблемы вовсе, поскольку решаются деньгами. Больше разработчиков, больше AppSec, дорогие сканеры, автоматизация процессов и так далее, пока деньги не закончатся. И я, несмотря на весь сарказм в описании процесса, против такой организации ничего не имею, потому что это работает! И, что самое главное, можно прозрачно и понятно донести до бизнеса, что если хочешь безопасности — плати.
Надо сказать, что есть множество вариантов качественного улучшения процесса, но востребованы они чаще всего в специализирующихся на разработке компаниях, где критически важен TimeToMarket.
И вот на сцену выходят LLM
Несмотря на то, что я довольно давно в ИБ и вообще деревянный по пояс, но старался держать руку на пульсе и возможности LLM меня удивили не больше, чем их создателей. По факту я оказался абсолютно не готов к новой реальности, где агенты захватили умы и бюджеты, в том числе и мой. Статья написана руками, но дальше я опираюсь на данные, полученные от ИИ, и строю прогнозы и предположения, исходя из собственного разумения.
Первым на появление LLM, конечно же, отреагировали разработчики и принялись радостно переходить на них. Сперва просто нажимали TAB, и функция писалась сама, потом просили реализовать такой-то метод в таком-то файле, и моделька сама всё писала, ну и по итогу разработчик просто скармливает агенту ТЗ, которое написала моделька по задаче, которую написала моделька, и следит, чтобы она сильно от него не отклонялась. И всё это привело к тому, что кода стало больше, а сам он стал сложнее и запутанней. Надо бы отметить, что хотя бы документация теперь есть, и она актуальная, и ей, наверное, можно даже верить. Увеличение количества кода увеличивает нагрузку на AppSec, не только увеличивая объём триажа, но и повышая сложность ручного тестирования. И внезапно AppSec становится узким местом, что усложняет и без того напряжённые отношения.
Но всё-таки в индустрии безопасности тоже читают Хабр и не стоят на месте, когда другие интегрируют LLM. На помощь AppSec приходят замечательные ИИ-инструменты — ИИ-триаж, ИИ-пентест, ИИ-SAST, ИИ-SCA — всё это аккуратно упаковывается в продукты и продаётся и, конечно же, покупается и внедряется. Да и агента грех не использовать в работе, очень уж это сокращает время на поиск точек входа. Правда, есть вариант, что публичные LLM запрещены политикой компании, и тем, кто её пишет (это, кстати, мы) по-хорошему надо её соблюдать. В общем, если внедрить всё, что можно для триажа, то часть рутинной работы снимается. А вот сама структура работы перестраивается в сторону поиска логических уязвимостей и проработки безопасной архитектуры.
Кстати, ИИ-бум породил совершенно новую категорию разработчиков — вайб-кодеров (Vibe-coding). Зачастую они реализуют проекты в одного, при этом могут быть далеки от программирования и безопасности, зато у них есть уникальные идеи, востребованные на рынке. Самое забавное, что идея может быть действительно востребованной и денежной. Менее забавно то, что им приходится верить, что LLM пишет безопасный код, не путает дев с продом и вообще не ошибается. Но, к сожалению, пока такие надежды легко разбиваются о реальность и с этим всем приходится мириться.
По итогу получилась забавная ситуация, когда в процесс разработки вдруг вклиниваются агенты и захватывают его полностью.
Рис. 2. Агенты встраиваются в каждую роль процесса
Получается, что теперь процесс изменяется с “Я подумал и сделал” на “Я подумал, написал агенту, он сделал, я проконтролировал”. Давайте “прочитаем” процесс “по ролям”.
Руководители генерят идею, агент скребёт по бэклогу и предлагает создать задачу на разработку
Разработчик просит агента посмотреть бэклог и выбирает задачу. Если задача ок, то отдаётся команда — “фас” и код попадает в Git
CI/CD запускает агента, который сканирует код на уязвимости и исправляет их или просто генерирует отчёт
ASOC/ASPM получает все отчёты, нормализует их и отправляет агенту, чтобы тот протриажил и выдал своё заключение
AppSec просит агента посмотреть файндинги на ревью, и всё, что ему не понравится, отправлять разработчику в виде задач
Руководители в процессе работы просят агента поскрести по бэклогу, находят задачу от AppSec и перекидывают её на ответственного специалиста
И вот петля обратной связи затянулась, план по токенам выполнен, задача закрыта, все довольны и идут пить кофе. Понятно с первого взгляда, что процесс не самый оптимальный. Но при классическом разделении зон ответственности, к которым прилагается бюджет, такой вариант может сложиться “сам собой”.
Взаимодействие Agent-to-Agent
И тут самое время вспомнить про Lean и DevOps (не все знают, но DevOps — это практически адаптация Lean к пайплайну разработки) и прочие прекрасные инструменты из недалёкого прошлого. А они нам предлагают искать максимально короткий путь обратной связи и маленькими шажками двигаться по нему. И такой путь, если взглянуть на схему, становится очевиден — дать агентам возможность работать напрямую друг с другом. Но тут есть нюансы…
Если дать агентам: Разработчик, Руководитель и Безопасник, обсуждать приоритизацию бэклога, то кто из них быстрее поймёт, что с помощью промпт-инъекции договориться проще и быстрее. Но самое интересное — не закончится ли бюджет на токены раньше, чем они придут к этой мысли. Звучит как начало классического анекдота “Заходит в бар…”, кстати такие анекдоты последнее время публикуют в новостях, правда почему-то не в разделе “Юмор”.
Как бы ни хотелось полной автоматизации, пока что она, скорее всего, заведёт нас в тупик. Так что возвращаемся к старому доброму системному подходу и начинаем думать. Собственно, немного подумав, я нарисовал следующую схему.
Рис. 3. Системный вариант: меньше агентов, структурированный обмен, ASOC как система для агентов
Что мы тут видим в отличие от прошлого варианта:
TaskTracker — исключён из процесса обработки файндингов. Потому что раньше он передавал неструктурированную информацию от одного человека к другому, а для агентов лучше сразу обмениваться структурированной информацией. Но при этом трекер может остаться в схеме, например для учёта времени, списания токенов и т.п.
В процессе CI/CD остался только один агент, но он в процессе сканирования получает контекст по файндингам из ASOC. И это единственный автономный агент, и он не обязателен, с его работой может справиться агент, запускаемый Безопасником. Нужен он или нет — зависит от того, готовы ли вы к автономным агентам или нет.
Система ASOC/ASPM трансформируется из системы для людей в систему для агентов. Сохранится ли у неё старое название или будет называться по-другому, пока не ясно.
А вот чего не видно, — это то, что можно лёгким движением руки сделать shift-left и сдвинуть этап сканирования на этап написания кода и заставить агента сканировать проект самостоятельно. В пользу этой гипотезы можно привести следующие тезисы.
Разработчику не нужно париться, так как агент сам будет выполнять сканирование, проверит результаты и попробует исправить. Короче, ничем это не будет отличаться от форматера или юнит-тестов, которые агенты уже научились запускать самостоятельно.
Нет смысла блокировать push и останавливать разработку, если вдруг сканер чего-то там нашёл. Проблема автоматически эскалируется на AppSec/Lead.
Но при всём при том разработчику проще устранить проблему сейчас, нежели ждать, пока эта проблема прилетит сверху.
При сканировании diff можно сильно экономить токены за счёт того, что Агент и так работает с этим файлом.
Весь локальный триаж фиксируется на центральном сервере, так что мы можем контролировать процесс и, что самое главное, доказать эффективность подхода руководству
Побочным продуктом такого сдвига может получиться неплохой инструмент для SOLO-VIBE-CODER’ов, которые, с одной стороны, читают новости и не хотели бы в них попадать в качестве иллюстрации ИИ-провалов, а с другой гонятся за TimeToMarket и не могут потратить вечность на изучение вопросов безопасности.
А будет ли это работать?
Для проверки подхода я решил не размениваться по мелочам и реализовать MVP локального MCP-сервера. Он будет запускать сканеры, возвращать результаты по запросу и хранить базу сканирования и триажа. Получилась вот такая вот архитектура.
Рис. 4. Архитектура MVP AppSecGate MCP: изолированный Docker, сканеры, SQLite, задел на центральный сервер
В процессе реализации хотелось ответить на несколько важных для меня вопросов.
Сможет ли агент самостоятельно разобраться в инструментарии, если я попрошу “А проверь-ка, голубчик, проект наш на безопасность, да AppSecGate используя непременно”?
Сколько времени/денег обойдётся разработка первой версии?
Как это всё будет работать?
Справятся ли с ролью агента локальные модели, доступные на моём железе “NVIDIA RTX 4060 Ti 16GB”?
Словлю ли я “вайб”?
Что дальше?
А проверь-ка голубчик…
И вот спустя 7 дней разработки, ~210 миллионов токенов, ~3000 рублей и ~8 литров кофе у меня на руках оказался собранный образ MCP-сервера. К тому моменту я уже переключился с Continue на Cline, написал AGENTS.md и вообще преисполнился, но об этом позже. Клонирую OWASP Juice Shop, без которого не обходится ни одно тестирование ASOC, подключаю AppSecGate MCP в Cline и в режиме планирования пишу “Проверить безопасность проекта” и наслаждаюсь магией.
По результатам нескольких прогонов получилось выявить следующий путь рассуждений агента.
Агент проверяет конфигурацию через get_config
Запускает сканирование проекта scan_project и получает агрегированный результат сканирования
Проводит анализ находок, используя SQL-доступ к базе файндингов query_findings_db. Не знаю почему, но Агенту нравится пользоваться именно таким вариантом, хотя есть альтернативы.
Результат анализа содержит перечисление файндингов и предложение ничего не исправлять, потому что это OWASP Juice Shop и всё так и задумано.
Приходится доказывать агенту, что я тут инструменты проверяю и придётся фиксить всё.
Получив-таки рабочий план, перевожу его в режим Act и агент принимается за работу
Дальше идёт обычный триаж. Получение контекста по срабатываниям — get_fix_context_batch; если требуется, исправляет и, используя verify_fix_batch, проверяет исправление или подавляет файндинг, если это FALSE_POSITIVE.
Периодически агент проверяет закрытие файндингов через query_findings_db и продолжает их устранять.
По итогу он формирует отчёт о проделанной работе с основными исправлениями
Последний прогон занял ~50 минут с использованием deepseek-v4-flash и стоил ~15 рублей. За это время обработано 200 находок: 56 исправлено (из них 8 critical, 45 high, 3 medium), 27 признано WONT_FIX (в основном учебные codefix-данные и публичный ALB), 117 отмечено как FALSE_POSITIVE (тестовые секреты Gitleaks). Изменено 29 файлов, diff +333 / −115 строк. Расход токенов: 16 212 728 input (из них 15 173 760 — cache reads) и 56 411 output.
Действия агента более чем предсказуемые, а рассуждения оправданны. Отдельно поразило, как ловко модели научились оперировать результатами сканеров и исправлять файндинги. Не могу утверждать точно, но, кажется, модели специально натаскивали под подобные задачи, что в целом неудивительно.
Сценарий с мягким shift-left я тоже пробовал отработать. Изначально была идея использовать хуки, но как они работают понять не смог и нашёл вариант добавить требование в AGENTS.md, можно и через rules, но, кажется, оба варианта по итогу обрабатываются одинаково. И вариант с AGENTS.md в теории универсален для разных типов агентов. Перед тем, как добавлять правила, рекомендую выполнить один прогон вручную, чтобы агент набрал контекста по использованию MCP. Важно помнить, что бы вы ни написали в правилах, модель может это проигнорировать или воспринять неправильно.
Всё хорошо, но проект явно сыроват и требует времени на допил. Во время прогонов я заметил несколько проблем (уверен, что их больше).
Один раз модель потеряла 20 файндингов и нашла, когда я её попросил повторно просканировать проект
Cline не сжимает контекст, пока задача полностью не закончится, почему — непонятно. Это точно неправильное поведение, которое может влиять на расход токенов и скорость сканирования.
Иногда MCP обманывает агента, например, посчитав два дубля файндинга за один, что приводит к дополнительным рассуждениям
Иногда возникает задумчивость при работе с query_findings_db, например, агенту требуется узнать состав полей
Когда тестировал pre-commit, намеренно изменил файл и попросил закоммитить изменения. Агент запустил скан, но пометил находки как WONT_FIX, типа, человек же сам исправил, значит, так надо…
Но самое главное — оно работает. Да, нужно поправить баги, да, нужна центральная база хранения — но это уже дело техники. Прежде чем превращать MVP (а если смотреть на полный функционал — будем честны, скорее PoC) в полноценный продукт, я решил глубже погрузиться в магию MCP и собрать побольше грабель.
Как рождается магия MCP
При запуске IDE агент считывает конфигурацию. Ниже пример конфигурации Cline (.cline/data/settings/cline_mcp_settings.json) для запуска локального MCP-сервера:
{
"mcpServers": {
"appsecgate-jshop": {
"type": "stdio",
"command": "/usr/bin/docker",
"args": [
"run",
"--rm",
"-i",
"--network=none",
"--read-only",
"--tmpfs=/tmp:size=100M,mode=1777",
"--tmpfs=/home/appsecgate/.cache:size=2G,mode=1777",
"--tmpfs=/home/appsecgate/.appsecgate-mcp:size=100M,mode=1777",
"--cap-drop=ALL",
"--security-opt=no-new-privileges:true",
"-v",
"./projects/juice-shop/:/workspace:ro",
"moondoom/appsecgate-mcp:latest",
"--transport",
"stdio",
"--project",
"/workspace"
],
"disabled": false,
"timeout": 600
}
}
}
sourcecraft.dev
В примере используется локальный запуск сервера “type”:“stdio”, что указывает агенту на необходимость самостоятельно запустить сервер, выполнив “command” с “args”. Есть ещё вариант удалённого сервера “type”: “http” и “url” вместо “command”. В варианте “stdio” есть как плюсы, так и минусы:
Плюсы:
Используется образ Docker — это позволяет легко распространять приложение, то есть конфигурация универсальная и не требует установки дополнительного софта, кроме Docker, конечно же.
Для stdio не требуется наличие сети в контейнере, так что его можно запускать в режиме максимальной изоляции. Это в целом снижает риски, связанные с запуском “чужого” кода, что не может не радовать такого параноика, как я.
Можно использовать volume для хранения постоянных данных, а можно и tmpfs прописать, и тогда они сами будут чиститься при каждом запуске, что удобно для отладки. В примере отладочный вариант представлен, для прода с README.md ознакомится ты должен сперва.
Минусы:
Требуется в явном виде пробрасывать исходники проекта в контейнер. Для связки VS Code + Continue — можно использовать переменные окружения, а вот связка VS Code + Cline у меня так и не завелась и пришлось хардкодить путь проекта (судя по всему из-за того, что у меня VS Code установлен через snap, а так-то вроде можно)
Изоляция приводит к тому, что базы правил для SAST/SCA нельзя обновлять в режиме онлайн. Для MVP это не критично, для Enterprise вообще огонь, но вот для массовой аудитории может создать трудности с обновлением библиотек. Пока вижу вариант передавать аргумент “–pull=always”.
Постоянные volume могут вызывать проблемы при обновлении сервера из-за разных версий баз данных, но это проблемы будущих итераций :)
После запуска контейнера Агент начинает своё общение с сервером. Проводит инициализацию (initialize) и получает список доступных на сервере тулов (tools/list). Далее список тулов косвенно встраивается в SYSTEM_PROMPT и передаётся при каждом запросе к модели. Таким образом, модель понимает, что ей доступны tool-ы и сразу получает исчерпывающие инструкции.
Очень удобно для пользователя и в равной степени для злоумышленников. Ведь для успешной атаки на агента через MCP достаточно просто подключить его и отправить хотя бы один запрос, а дальше модель, повинуясь внутреннему голосу, начнёт исполнять волю “Хаоса” и творить “добро” направо и налево. Это известная проблема и современные модели должны изолировать тулы и не воспринимать их как прямые указания к действию, но это не точно и зависит от модели. Так что будьте осторожны при подключении любых MCP, даже если вы запускаете его удалённо или в изолированной среде. Как вариант можно вручную проверить список тулов полученных от сервера в интерфейсе расширения, до первого запроса к агенту.
Соответственно, при получении запроса “Проверить безопасность проекта” модель уже “знает” об инструментарии, доступном ей, и принимает единственно верное решение — использовать инструмент с подходящим описанием. Дальше поведение агента уже зависит от системного промпта и результатов которые вернул MCP.
То есть у нас фактически есть три “крутилки”:
Системный промпт. Определяет общее поведение агента, например — “митигировать, нельзя пофиксить” или “митигировать нельзя, пофиксить”, а третьего не дано.
Описание тулов. Указывает какие инструменты использовать и как обрабатывать их результаты
Результаты сканирования. Это один из самых вариативных инструментов, меняя описание правил и критичность различных категорий, можно регулировать поведение агента в зависимости от типа уязвимости.
Про важность этих “крутилок” я узнал, пока тестировал локальные модели, но эту историю нужно рассказать подробнее.
Тестирование open-source моделей
Мне, как безопаснику, долгое время боровшемуся с утечками корпоративных тайн, горько смотреть, с какой лёгкостью сейчас люди переходят на публичные модели. Поэтому при работе с исходниками компании я всегда использовал только локальные LLM — благо ещё до повышения цен собрал себе неплохой сетап. Но железо ≠ магия, так что без подводных камней не обошлось.
Железо
Всё крутится на одном хосте под Proxmox VE, с включённым IOMMU и обеими GPU, проброшенными в гостей:
Хост (PVE)
VM “Workstation”
VM “LLM”
CPU
i7-13700K, 16C/24T (8P+8E), до 5.4 GHz
8P
8P
RAM
64 GB
24 GB
24 GB
GPU
—
Intel UHD 770
NVIDIA RTX 4060 Ti 16GB
Монитор
только на старте
да (подключён к матери)
headless
Диск
Samsung 980 PRO NVMe
ZFS
ZFS
Почему именно так:
Обе GPU проброшены в гостей — хост “голый”, ни i915, ни nvidia в нём не грузятся.
Workstation на Intel UHD 770 — там монитор, VS Code + Cline, браузер и Docker с MCP-сервером AppSecGate. Модели не крутятся, GPU нужна только чтобы рисовать интерфейс.
LLM на 4060 Ti 16GB — headless, только под вычисление.
Плюс такой конфигурации в том, что видеокарта полностью отдана под вычисления: ollama, hashcat или, например, jupyterlab + pytorch. Причём вся эта радость в докере, так что можно менять конфигурации как перчатки. Ну а если надо поиграться с настоящими ИИ можно загрузить Win и погонять ИИ-скинов в Cyberpunk через Moonlight. Есть и проблемы, но я к ним уже привык.
Изначально хотел тестировать модели руками в Cline и по ходу дела делиться впечатлениями. Полагаясь на свой опыт работы с deepseek-r1:14b, думал, что слабые модельки попросту не смогут нормально работать с инструментами и будут лажать. Но, запустив более-менее современную модель, понял, что всё не так плохо. Пришлось пересматривать свои планы и быстренько собирать стенд для тестирования.
Быстренько, как водится, не получилось, а получилось вот так вот.
Цель для сканирования — микропроект на Flask (app.py) — 8 уязвимостей для Semgrep и Docker+requirements.txt для Trivy
Тестовый Агент — с задачей сканировать, фиксить SAST и эскалировать находки Trivy
AppSecGate MCP — сканирует и предоставляет агенту результаты
Coding Tools MCP — в качестве замены стандартным тулам Cline
Обёртка, которая всё это дело запускает и сохраняет логи и трейсы.
Важный дисклеймер! Цель тестирования — проверить, что MCP вообще заработает! Тесты не претендуют на объективность, а результаты могут не отражать реальную производительность и возможности тестируемых моделей!
Далее попросил DeepSeek выбрать несколько моделей, которые по его разумению сдюжат агентский режим и прогнал по ним предварительные тесты. Контекст выбрал 32768 токенов, это вроде как минимум для агентов. Размер и раскладку CPU/GPU брал из ollama ps. Никакого дополнительного тюнинга не применял, просто загрузил, используя ollama pull. Передварительно решил оценить скорость моделей.
Модель
Параметры
Tools
Thinking
Размер
CPU / GPU
t/s
gemma4:26b
25.2B
да
да
~16 GB
21% / 79%
84.0
gpt-oss:20b
20.9B
да
да
12 GB
0% / 100%
67.8
qwen3-coder:30b
30.5B
да
нет
15 GB
1% / 99%
58.5
glm-4.7-flash
29.9B
да
да
15 GB
1% / 99%
52.0
qwen2.5-coder:14b
14.8B
да
нет
15 GB
0% / 100%
31.0
qwen3.8:27b
27.3B
да
да
17 GB
35% / 65%
12.1
devstral:24b
23.6B
да
нет
20 GB
23% / 77%
9.5
Судя по токенам в секунду, думал, что лидерами будут qwen3-coder:30b или glm-4.7-flash, потому как большие и быстрые, хотя и не полностью на GPU. Первые прогоны выявили лидера — glm-4.7-flash, на нём я прогонял все предварительные тесты и тюнил промпт.
Про то, что у меня есть ещё и gemma4:26b я вообще забыл и в первой табличке долго не замечал её отсутствие, а выплыла она уже когда заканчивал статью… Потому не рассматривал её как серьёзного конкурента.
Писал агента не сам, но пока корректировал действиями Cline, выделил те самые “крутилки” из предыдущего параграфа. А ещё с удивлением для себя обнаружил, что в моём MCP описание инструментов написано на русском, что вроде как не круто, поэтому оперативно поменял. Так что, более-менее приведя агента в рабочее состояние, я запустил тестирование, скормил логи агенту и принялся изучать результаты.
Модель
Recall (mean)
Fixed (med)
Токены (mean)
Время (mean)
Pass
qwen3.8:27b
100%
8
314k
650s
3/3
gemma4:26b
87%
6
879k
419s
3/3
qwen3-coder:30b
73%
5
877k
451s
2/3
glm-4.7-flash
73%
7
1.17M
772s
2/3
gpt-oss:20b
47%
0
254k
89s
1/3
devstral:24b
3%
0
4.7k
25s
0/3
qwen2.5-coder:14b
0%
0
4.2k
5s
0/3
А результаты меня удивили! Во-первых, мой фаворит glm-4.7-flash — залажал в самом конце и ушёл в петлю, а qwen3.8:27b взял и победил, что называется чисто на скиле. Для серьёзных выводов трёх прогонов, конечно, маловато, но у меня и не стояло такой задачи. Агент распределил места следующим образом.
Модель
Вердикт
Обоснование
qwen3.8:27b
🏆 “Победитель”
100% recall × 3 прогона, 8/8 fixed, минимум токенов среди топа, 0 ошибок
gemma4:26b
🥈 Второй
Стабилен (3/3, 80–90%), самый быстрый среди прошедших (419s), но в 2.8× дороже по токенам и медианно 6/8, а не 8/8
qwen3-coder:30b
⚠️ Средний
Recall нестабилен (40–100%), patch_succ 0.57 — часто бьёт мимо после сдвига строк
glm-4.7-flash
⚠️ Нестабилен
Один прогон — 100%/376k, другой — 20%/1.8M с 3 ошибками. Высокий ceiling, но не гарантирует
gpt-oss:20b
⚠️ Слабый
1/3 pass, 6 tool exceptions, два прогона из трёх — 20–30% recall
devstral:24b
❌ Не оценить
0 tool-calls, JSON в тексте вместо вызова
qwen2.5-coder:14b
❌ Не оценить
То же — JSON-описание вместо tool call
Главные выводы прогона.
Небольшие модели могут стабильно работать в роли агента, пусть и с ограничениями.
Скорость по токенам для агентов важна, но может ничего не стоить, если модель плохо ориентируется в инструментах или уходит в петлю.
Лично для меня это означает, что можно автоматически сканировать и размечать большие проекты по цене электричества. При этом я, скорее всего, буду использовать glm-4.7-flash для большинства задач, возможно под контролем более тяжёлого, но стабильного qwen3.8:27b или всё же внимательнее присмотрюсь к gemma4:26b. Ещё думаю поиграться с настройками ollama, квантованием и пошерстить Hugging Face. Нужно больше тестов, больше прогонов, красивых табличек и ещё более красивых графиков. А пока я вдоволь наигрался в тестировщика и готов поделиться своими незабываемыми впечатлениями и далеко идущими выводами.
Лёгкий оффтоп про “вайб”
Если говорить честно, то всё, что написано выше, — это рассуждения, к которым я приходил по ходу реализации первой фазы проекта. А изначально я просто напрягал DeepSeek своими рассуждениями о переработке резюме и предложил оценить меня как Продукта. Потом была мысль, а не сделать ли свой пет-продукт. Слово за слово и вот “architecture.md” уже сохраняется в пустой каталог “docs”, а агенту ставится задача реализовать первую фазу проекта. А дальше всё как в тумане…
И за три недели блужданий из тумана я так и не выбрался, лошадок не видел, ёжиков тоже, но зато вместе с ИИ поиграл в ролевые игры.
Я-Продукт — хочу сделать продукт, чтобы и самому пользоваться и другим не стыдно было поставить. В общем исследуем рынок, смотрим кто там конкуренты… о, кажется, пора уже и запускать.
ИИ-Программист — а чего ждать, сперва одну фичу, потом другую, потом баги, баги и ещё раз баги…
Я-Безопасник — надо накрутить изоляцию в докере, проверить лицензионную чистоту и вот ещё бы изолировать Cline, а то один “rm -rf /” и всё…
Я-Инженер — надо оптимизировать сборку докера, потом подумать, как будем релизиться. А ещё безопасник что-то там писал про изоляцию, но это потом.
Я-Архитектор — так что-то продукт плохо работает, надо подумать о дедупликации файндингов, слышал недавно про какие-то векторные базы, надо прикрутить. DeepSeek говорит, нормальная штука, вот тебе ADR-0001
Я-Продукт — Так, версия 0.1.0 уже, считай, релиз, начинаю писать статью! Так, сперва процессы, потом технины добавить, но тут пусть Инженер с Программистом думают. А ещё надо рекламой заняться, ё-моё!!! оказывается 90% рынка MCP англоязычный… Надо срочно перевести всю документацию на английский…
И так целыми днями, пока Агент фиксит очередной баг или делает рефакторинг, ты уже переключился на другую роль, исследуешь очередную проблему. Я бы не назвал это вайбом, но процесс затягивает и балует мозг изрядным количеством дофамина. Особенно цепляет скорость, с которой можно воплощать свои идеи в жизнь. Причём скорость тут зависит не от привычных всем переменных — сколько программистов, какая у них квалификация и какая атмосфера в команде, а от знаний конкретного человека и от того, сколько у него денег на токены. И скорость — это то, что бросается в глаза, а за скоростью прячется ещё и куча возможностей. Например, в разрезе моего проекта я не вижу смысла засовывать под капот множество сканеров или делать гибкую настройку workflow, это легко может сделать любой желающий одним промптом. Ведь для агента не важно, кто его запустил, изначальный разработчик или просто рандомный чувак, которому нужна какая-то фича, контекст будет одинаковый, а вот результат скорее всего будет ближе к ожиданиям заказчика.
И что дальше
У проекта был изначальный роадмап, который исходил из кратчайшего пути к готовому продукту. Но в процессе работы над проектом и статьёй я получил новые вводные, о части которых даже не догадывался на этапе планирования.
Народ уже строит целые операционные системы из агентов, в которых нет IDE и минимум людей. Я пока не понимаю, насколько такой подход захватит мир, но надо разбираться. Потенциально, это новые клиенты, для которых проект может стать “санитарным минимумом” по внедрению SSDLC. Тут уже главное показать, что продукт готов к автономной работе и не требует больших усилий по настройке, а то и может кастомизироваться агентами на лету.
Локальные LLM могут работать в Агентских сценариях и надо учитывать, что продукт может заинтересовать тех, для кого важна локальность — корпоративного заказчика. Причём, скорее всего, его средняя часть, у кого есть деньги на собственное железо, но нет Директора по ИИ-трансформации. Таким клиентам бывает сложно продать “чистую безопасность”, потому, возможно, стоит добавить больше правил на code-smell и compliance.
Весь проект — это одна неделя разработки, его легко повторить. Конкурентов в данной нише много, от простеньких “обёрток для сканера”, например “Semgrep MCP”, до классических ASPM, уже реализовавших MCP-интерфейс, — например, “Checkmarx One” и “BoostSecurity”, и модных ИИ-native по типу “ZeroPath”. Так что общая ценность проекта стремится к нулю и надо как-то отстраиваться от конкурентов. Например, делать упор на открытость, лёгкость и возможность быстрой доработки под конкретные флоу.
Подумав немного о вводных, решил, что дальше у меня будет три основных направления развития продукта.
Разработка Фазы 2, то есть центрального сервера с возможностью агрегации событий от нескольких агентов, работающих независимо. И чутка захватить Фазу 4, чтобы получить возможность анализировать данные и строить красивые дашборды. Так можно реализовать самый главный функционал и оценить стоит ли развивать проект дальше.
Сделать лёгкий реверанс в сторону корпоративного заказчика и поддержать популярный в корпоративной среде сканер — SonarQube. При этом можно попробовать показать, что с доработкой правил и центральным сервером, продукт работает не хуже SonarQube, но при этом легче и быстрее.
Провести боевое слаживание в сценарии с несколькими агентами, а-ля ИИ-Мастер — ИИ-Слейвы. Это самый интересный кейс, лично для меня. Хочу провести анализ 50-100 открытых проектов и посмотреть, что там по находкам. Тут можно и надёжность самой тулы проверить, и правила потюнить и самому проверить как там с безопасностью OpenSource написанного Vibe-Coder-ами.
Ну и по мелочи ещё куча всяких проблем, которые надо пофиксить. В общем, планов громадьё, кофе и печеньки закуплены. Есть огромное желание поделиться результатами с сообществом — так что если хотите следить за развитием проекта и не пропустить следующую статью (спойлер: она будет не про MCP, а про моё видение безопасности LLM в целом), подписывайтесь на мой канал ArchiSecOps. А пока я готовлю продукт и план для следующей статьи, приглашаю всех желающих попробовать и оценить AppSecGate MCP.
76 of 77 trivy-action Tags Were Repointed in One Night. Some SHA Pins Didn't Help Either.
On March 19, 2026, someone with stolen credentials force-pushed 76 of the 77 version tags on aquasecurity/trivy-action to point at a credential stealer. All 7 tags on aquasecurity/setup-trivy too. The only clean trivy-action tag was 0.35.0, because it was an immutable release.
If your workflow said uses: aquasecurity/trivy-action@0.34.2, you didn't change a line. Your repo looked exactly the same. The next run just executed different code, for about 12 hours (roughly 17:43 to 05:40 UTC per the advisory), and that code read secrets out of the runner's memory and shipped them off.
The part I keep coming back to is in Aqua's own advisory, under "You are affected if you used":
> SHA pinning to a commit prior to 2025-04-09.
So some people who did the "right" thing and pinned trivy-action to a full SHA were still exposed, if their job ran during the ~4 hours setup-trivy was poisoned. Not because the SHA moved. Because the action they pinned called another action by tag.
A lot of small SaaS repos have a CI file that was copied from a README in an afternoon and never opened again. Often a handful of uses: lines, some on @v3 or @main, and a deploy job with a cloud key or an npm token in it. This post is the one-hour version of fixing that.
Three incidents, same shape
These are all public, with advisories you can read:
| When | Action | What moved | Window |
|---|---|---|---|
| Mar 14-15, 2025 | tj-actions/changed-files | multiple version tags repointed to one malicious commit; secrets dumped into build logs | ~1 day; GitHub's advisory says it impacted "over 23,000 repositories" |
| Mar 3-10, 2026 | xygeni/xygeni-action | the v5 tag moved to a commit from an unmerged pull request | ~6 days |
| Mar 19-20, 2026 | aquasecurity/trivy-action + setup-trivy | 76 of 77 tags, all 7 tags | ~12 h and ~4 h |
In none of these did the victim's repository change. That's the whole problem with a tag. @v5 is not a version, it's a pointer that gets looked up every time the job starts.
The xygeni one is worth a second look. Branch protection worked, the bad PRs were never merged. But the commit still existed in the repo's git objects, and moving a tag to it was enough. Merged or not doesn't matter to a tag.
Three ways a pin fails
People argue "tags vs SHAs" like it's one decision. It's really three separate failure modes:
1. The tag moves. @v4, @v4.2.2, @main. All mutable unless the publisher uses immutable releases. This is tj-actions, xygeni and trivy-action.
2. Your pin isn't transitive. You pin owner/action@<sha>. That action's own action.yml says uses: owner/other-thing@v1. You get whatever v1 is today. This is the trivy-action pre-April-2025 case: until a commit on 2025-04-09 ("Pin aquasecuriy/setup-trivy to hash instead of tag"), trivy-action's action.yaml called aquasecurity/setup-trivy@v0.2.2. It's right there in the repo history. GitHub's own docs say it plainly now: lockfiles cover transitive dependencies, "which SHA pinning does not."
3. The SHA is real but it's not from where you think. A full SHA can point at a commit that isn't on any branch or tag of the real repo. In the trivy binary part of the same incident, the attacker swapped an actions/checkout reference to what the advisory calls an "imposter commit". GitHub's docs have a one-liner on this: when you pick a SHA, "verify it is from the action's repository and not a repository fork." It's very easy to copy a SHA from a blog post or a bot PR and never check where it lives.
Here's the order I'd fix things in.
Step 1: list what you actually run (10 min)
From the repo root:
grep -rnE '^\s*-?\s*uses:\s*[^ ./$]' .github/workflows \
| grep -vE '@[0-9a-f]{40}([^0-9a-f]|$)'
This prints every uses: that points outside your repo and isn't pinned to a full 40-character SHA. It skips local ./ actions. It will also show docker:// steps and reusable workflows (jobs.<id>.uses: org/repo/.github/workflows/x.yml@main), which is good, you want to see those too.
On a test workflow it gave me lines like:
.github/workflows/ci.yml:10: - uses: actions/setup-node@v4
.github/workflows/ci.yml:12: - uses: docker/setup-buildx-action@v3
.github/workflows/ci.yml:14: - uses: github/codeql-action/init@v3
.github/workflows/ci.yml:16: uses: docker://alpine:3.20
.github/workflows/ci.yml:18: uses: octo-org/shared/.github/workflows/deploy.yml@main
Same idea as grepping your server for hardcoded renewal days before the Let's Encrypt change. You can't fix what you haven't listed.
Write the list down in three groups:
- GitHub's own (actions/*, github/*)
- Big, well-known publishers (docker, aws, google, cloudflare, vercel...)
- Everything else. Small repos, one maintainer, something you found in a Marketplace search at 1am.
Step 2: delete before you pin (10 min)
The cheapest action to secure is the one you remove.
A lot of third-party actions in small repos do something like "post a comment", "get changed files", "set up a tool", "send a Slack message". Some of those are 3 to 10 lines of shell plus gh or curl, which runs with the same token anyway but without someone else's code in the middle. For example, a lot of what people use tj-actions/changed-files for is a git diff --name-only between two commits.
For the ones you keep, look at them once before you trust them. Open the repo: how many maintainers, how old, does it pin its own dependencies, does the action.yml call other actions by tag (that's failure mode 2 again). It's basically the same spelling, age and real-usage check as checking a package an AI coding agent wants to install. A typo in an action name runs someone else's code with your secrets, same as a typo in npm install.
Step 3: pin, and check the pin is honest (15 min)
To get the commit a tag points to right now:
git ls-remote https://github.com/actions/checkout \
'refs/tags/v4.2.2' 'refs/tags/v4.2.2^{}' | tail -n1 | cut -f1
The ^{} line matters. Some repos use annotated tags, and then the first SHA you see is the tag object, not the commit. tail -n1 takes the peeled commit when it exists. (Before you pin, check that the tag is the release you meant and not already compromised. Pinning a bad commit just makes it permanent.)
Then write it with the version as a comment so humans can read it:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Now the check most people skip. This script reads every @<sha> # <tag> line and asks GitHub what that tag points to today:
#!/usr/bin/env bash
# For every "uses: owner/repo@<full sha> # <tag>" line, ask GitHub
# what that tag points to right now and compare it with the pin.
grep -rhoE 'uses:\s*[^ ./$][^ @]*@[0-9a-f]{40}\s*#\s*\S+' .github/workflows |
while read -r _ ref _ tag; do
sha=${ref##*@}
repo=$(echo "${ref%@*}" | cut -d/ -f1-2)
now=$(git ls-remote "https://github.com/$repo" \
"refs/tags/$tag" "refs/tags/$tag^{}" | tail -n1 | cut -f1)
if [ "$now" = "$sha" ]; then
echo "ok $repo $tag"
else
echo "CHECK $repo $tag pinned=$sha tag-now=${now:-missing}"
fi
done
On my test file, with one deliberately wrong pin:
ok actions/checkout v4.2.2
CHECK actions/checkout v4.2.2 pinned=d632683dd7b4114ad314bca15554477dd762a938 tag-now=11bd71901bbe5b1630ceea73d27597364c9af683
ok pnpm/action-setup v3.0.0
ok github/codeql-action v3.29.0
A CHECK means one of two things, and both are worth five minutes: either your SHA was never that tag (copied wrong, or an imposter commit), or the tag moved after you pinned it. The second one is exactly the event you want to hear about. Your pin protected you, but it's also a signal that upstream had a bad day and you should go read their issues.
It only checks the format @sha # tag, it handles owner/repo/subdir actions like github/codeql-action/init, and it needs nothing but git and grep. You could run it weekly in a scheduled job. It does not look inside the actions for failure mode 2, that's what the next step is for.
One tradeoff that's easy to miss: GitHub's docs say Dependabot "will not create alerts for actions pinned to SHA values", alerts are only for actions using semantic versioning. So if you pin by SHA, turn on Dependabot version updates for github-actions so the pins don't rot. Since there's a default cooldown now (Dependabot waits 3 days after a release before proposing it, on github.com), a freshly poisoned release has some time to get caught before a bot PR shows up. That helps with bad new releases. It does nothing for a tag that gets repointed under you, only the pin does.
Step 4: or try the new lockfile (15 min)
On October 14, 2026, GitHub put workflow dependency lockfiles into public preview, including on the Free plan. It's basically package-lock.json for Actions. A CLI extension writes .github/workflows/actions.lock with the commit for every action your workflows use, including the transitive ones, plus the numeric owner and repo IDs. Once a workflow is onboarded, Actions runs the locked commit even if the tag moves, and every dependency of that workflow has to be in the lockfile.
gh extension install github/gh-actions-lock
gh actions-lock # scan .github/workflows, write actions.lock
gh actions-lock --verify # read-only check, good for a CI job
Your workflow keeps readable refs (it may narrow @v4 to @v4.2.2). GitHub's docs now say that for repos with a lockfile, SHA pinning in the workflow file is "no longer necessary". There's also a "Require lockfile" policy at repo/org level, and Dependabot regenerates the lockfile entry in the same PR when it bumps an onboarded workflow.
Things to know before you rely on it, straight from the README and release notes:
- It's pre-1.0 and in preview. v0.3.0 (Oct 22) changed behavior enough that some lockfiles that used to pass --verify now fail it.
- Reusable workflows called from another repo (jobs.<id>.uses) are not locked by your lockfile.
- Container images are not locked. Anything without an @sha256: digest is only a warning.
- A lockfile change that moves a floating ref like v4 back to an older commit in its history isn't detected. So review lockfile diffs, or pin exact tags.
- Since v0.3.0 it rejects full-SHA pins to fork-only (imposter) commits, every locked commit must be in the history of its recorded tag or branch. That covers failure mode 3.
If you have one repo and a few workflows, I'd honestly try this over hand-pinning. If you're not ready for a preview tool, steps 1 to 3 still get you most of the way.
Step 5: make a stolen run worth less (10 min)
Pinning is about which code runs. This step is about what that code can grab if something slips through anyway. The tj-actions and trivy payloads both went for secrets in the runner's memory. The xygeni one gave the attacker a remote shell on the runner for up to 180 seconds per run, which reaches the same things.
At the top of every workflow:
permissions:
contents: read
Then give extra scopes only to the job that needs them:
jobs:
comment:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
# steps: ...
Once you set any permission, every one you didn't list becomes none. permissions: {} turns them all off.
Why bother if new repos default to read-only? Because that default only applies to things created after February 2, 2023. GitHub's changelog was explicit: the change "will not impact any existing enterprises, organizations or repositories." If your repo or org is older than that, go to Settings → Actions → General → Workflow permissions and look. An older repo can still say "Read and write" there.
Two more:
- Keep prod secrets out of the PR workflow. Your test job doesn't need the production database URL or the live payment key. Put deploy secrets in an environment with required reviewers. Then only jobs that use that environment can read them, and only after someone clicks approve.
- Prefer short-lived cloud creds. If you deploy to AWS/GCP/Azure, OIDC gives the job a token that expires, instead of a long-lived key sitting in repo secrets forever. On GitHub-hosted runners the GITHUB_TOKEN itself already dies with the job, max 6 hours.
Step 6: if you ran a bad version
All three advisories say some version of the same thing: treat every secret the pipeline could read as exposed, and rotate it. Not "probably fine". The trivy payload read the runner's memory, so masked secrets in the logs didn't matter.
The order I'd go in:
1. Find the runs inside the advisory's window. gh run list --created 2026-03-19..2026-03-20 works, or just scroll the Actions tab.
2. Rotate whatever those jobs could reach. Cloud keys, deploy keys, package registry tokens, any API key in repo or org secrets.
3. For trivy specifically, look for a repo named tpcp-docs in your account or org. Per the advisory, the stealer creates it as a fallback upload spot when its normal exfiltration fails.
4. Check what got published from those runs (releases, packages, images) in case something was tampered.
Rotation is the slow part if you don't know where each key is used. Writing that list down before you need it is worth doing just for this.
The rules I'd actually adopt
At under 50 customers, with one or two people touching CI:
- Third-party actions: always locked (lockfile) or SHA-pinned, with the version comment. No exceptions for "it's popular".
- GitHub's own actions/*: I'd pin them too. GitHub's "Require actions to be pinned to a full-length commit SHA" policy (added August 2025) treats them the same, and it makes the rule simple enough to enforce.
- Fewer actions every quarter, not more.
- permissions: contents: read at the top of every workflow, widen per job.
- The pin check, weekly.
Total time is about an hour for a typical small repo. The trivy-action window was about twelve hours, starting on a Thursday evening UTC, and the real scan still ran after the stealer, so the job looked normal. Nobody watching their CI casually would have caught it.
I'm curious how other small teams handle this. Are you pinning SHAs by hand, trying the new gh actions-lock preview, or still on @v4? And if you already pin by SHA, have you ever opened the action.yml of what you pinned to see if it calls other actions by tag?
Mentions
GitHub Actions の依存関係を gh actions-lock で固定する GitHub Actions の依存関係を gh actions-lock で固定する GitHub Actions の workflow で actions/checkout@v4 のようにタグを指定すると、後からタグを付け替えられたとき、workflow を変更せずに実行コードが変わる可能性があります。 このリスクを抑える仕組みが workflow dependency lockfile です。 結論:何が嬉しいか workflow には読みやすいタグを残し、Action と推移的依存はコミット SHA で固定できる Dependabot が登録済み workflow の Action と lockfile を同じ PR で更新する GitHub Enterprise Cloud では Require lockfile policy で、未登録 workflow の実行を防げる ! 大前提として、シェルや GitHub CLI で代替できる処理には外部 Action を使わず、依存を増やさないことが重要です。 可変タグによる侵害の実例 タグを参照している workflow は、上流でタグが付け替えられると、workflow を変更せずに別のコードを実行する挙動を狙ったサプライチェーン攻撃が成立し得ます。 2025年3月、広く使われていた tj-actions/changed-files が侵害され、複数のタグが悪意あるコミットを指すように変更されました。コードは runner のメモリ上の情報を workflow のログに出力し、CI/CD の秘密情報が漏れるおそれがありました。 2026年3月には Trivy の関連 Action も侵害され、攻撃者は aquasecurity/trivy-action の77個中76個のタグと、aquasecurity/setup-trivy の全タグを悪意あるコミットに付け替えました。悪意あるコードは runner のメモリやファイルから認証情報を盗み出そうとしていました。 GitHub は Immutable Release 機能をリリースするなどして対応してきましたが、 @v1 のようなタグ指定では効果が薄いなど、依然リスクが残っていました。 固定 SHA の課題 コミット SHA を固定すればタグに影響されなくなりますが、バージョンが分かりにくく、可読性も低くなるという課題があります。 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 workflow dependency lockfile とは 上記の課題を踏まえ、 GitHub Actions でも lockfile による依存関係の固定が導入されました。 lockfile は workflow の Action と実行するコミットを対応付けるファイルで、Action が依存する Action (推移依存)も含めて固定できます。 例えばワークフロー内で actions/checkout@v4.2.2 を指定して gh actions-lock による lockfile を生成すると、workflow はその参照に対応するコミットを実行します。 .github/workflows/ci.yml steps: - uses: actions/checkout@v4.2.2 .github/workflows/actions.lock には、タグと実行する SHA が記録されます。 version: 'v0.0.3' workflows: '.github/workflows/ci.yml': - 'actions/checkout@v4.2.2' dependencies: 'actions/checkout@v4.2.2': ref: 'v4.2.2' commit: 'sha1-11bd71901bbe5b1630ceea73d27597364c9af683' owner_id: 44036562 repo_id: 197814629 また、 Dependabot も対応済みで、バージョンアップ時に lockfile も同じ PR で更新します。 Require lockfile ポリシー Require lockfile policy は、対象 workflow が lockfile に登録されていない場合、その実行をブロックするポリシーです。 これにより、lockfile に基づいた安全な依存関係の管理を徹底することができます。 ! lockfile 自体は Free、Team、GitHub Enterprise Cloud で GA ですが、Require lockfile policy の強制機能は GitHub Enterprise Cloud 向けです。 まとめ gh actions-lock は読みやすい参照を残したまま、Action とその依存をコミット SHA で固定できる Dependabot で更新を追える GitHub Enterprise Cloud では Require lockfile policy で登録を徹底できる 参考: GitHub Actions workflow lockfiles の GA Workflow dependency lockfiles workflow の依存関係を lock する Require lockfile policy を適用する CVE-2025-30066: tj-actions/changed-files の侵害 Trivy 関連 Action の侵害 CISA: Supply Chain Compromise of Third-Party GitHub Action
WordPressからnpm・AIツールまで。直近1年の脆弱性と侵害12事例 修正版を入れたあと、何をすれば対応を終えたと言えるのだろう。 この問いは製品が変わると答えも変わる。SiteGuard Server Editionでは更新する前に攻撃の痕跡を保全するよう案内された。Metabaseでは更新に加えて、利用者のセッションや接続先データベースの認証情報まで確認するよう求められた。Trivyでは安全性を調べるために動かしたツール自体が侵害され、実行した環境の秘密情報を点検する事態になった。Axiosではアプリから使う前にも、依存の導入時に不正なコードが動く仕組みが仕込まれた。 どれも「更新が大事」という一文には収まる。でも、その一文だけでは作業の順番も、確認する範囲も分からない。 前回のPHP2Shellの記事では、WordPressの緊急点検を通じて、設定の意味や残すべき機能を見直した。今回はそこから範囲を広げ、2025年11月1日から2026年10月31日までに公表・悪用・対応が報告された脆弱性と供給網侵害を調べた。WordPressやWebサービスの開発・運用に近い12の事例群を、実際の対応と一緒に追っていく。 公表された悪用・対応の記録をもとに、どの構成が対象になり、更新後にどんな作業が残るのかをたどる。 解説図:サイトの運用では公開ページ以外にも、自動化、接続先、開発環境、作業する人の端末が関係する。 自分に関係する事例から読む 読みたい場所へ進めるよう、まず今回の12事例を並べた。CVEは脆弱性を識別する番号で、一つの製品に複数付くこともある。関連する問題をまとめ、AxiosやLiteLLMのような配布物の侵害も扱うため、12事例とCVEの数は一致しない。 使っているもの・担当する範囲 事例 特に確かめたいこと WordPress wp2shell 同じ系列で更新が実際に完了しているか Reactを使うサーバー React2Shell Server Componentsを処理しているか 自動化ワークフロー n8n 編集権限と公開フォームを別々に確認したか AIフローの公開 Langflow フローの実行と変更を区別できているか BI・分析基盤 Metabase 保存された認証情報まで調査したか 自分で管理するDB MongoBleed 認証より前の処理も修正されているか サーバー用WAF SiteGuard Server Edition 更新前に診断情報を保全したか 機器へのクラウドSSO FortiCloud SSO 追加告知と設定流出への対処を追ったか CI/CDの脆弱性検査 Trivy 実行した配布物と露出した秘密を確認したか PythonのAIゲートウェイ LiteLLM 3月と10月で異なる配布物と経路を確認したか npmの依存とビルド環境 Axios 導入時の実行と、端末・CIの秘密情報まで調べたか 普段使うブラウザー Chrome 更新の取得と適用を両方確認したか ! 情報の基準日は2026年10月31日。本文中の修正版は原則として「その問題への修正が最初に入ったバージョン」を示す。現在導入するバージョンは、後続の修正とサポート状況を含めて公式の現行情報で選んでほしい。図は説明用で、攻撃ログや被害画面を再現したものではない。 時系列も一度見ておきたい。告知が出た日、攻撃を観測した日、悪用を確認した一覧へ追加された日は、それぞれ意味が違う。後から出た調査報告を、攻撃の始まった日と読み替えると、対応に使える時間を見誤ってしまう。 解説図:選んだ事例の主な公表時期。すべての脆弱性を網羅した年表でも、被害件数の比較でもない。 WordPressのwp2shell PHP2Shellと名前が似ているが、こちらはWordPress本体の別の問題だ。2026年7月17日、WordPressは7.0.2のセキュリティリリースで二つの脆弱性を修正した。SQLインジェクションのCVE-2026-60137と、REST APIのバッチ処理におけるルートの取り違えに関するCVE-2026-63030だ。WordPressのリリース告知 二つがつながったときの影響 WordPress 6.9以降では、この二つを組み合わせると遠隔でのコード実行(RCE)につながり得る。管理画面に入るためのパスワードが先に盗まれていなくても問題になる、という点が重い。IPAは7月22日の注意喚起で、標準構成で管理者権限などを要さず成立する可能性と、CISAの悪用確認済み脆弱性カタログへの登録を伝えている。IPAの注意喚起 ここで、WordPressのバージョンをまとめて扱うと話がずれる。6.8系列ではSQLインジェクションの問題が対象になり、6.9系列以降で二つを組み合わせた影響が問題になる。同じ「WordPressに脆弱性」という見出しでも、中身は一段違う。 開発元のアドバイザリもSQLインジェクションと組み合わせによるコード実行を分けている。一つ目の深刻度だけを見て軽く扱ったり、逆にすべての旧バージョンが同じ条件でコード実行まで到達すると決めたりせず、系列と組み合わせを読む必要がある。SQLインジェクションの開発元情報、バッチ処理の開発元情報 解説図:単独の問題と攻撃チェーンの影響範囲を分ける。古い系列すべてを同じ条件として扱わない。 自動更新の案内と更新完了 この事例で注目したい対応は、WordPress.orgが影響するバージョンへ強制自動更新を有効化したことだ。利用者全員がニュースに気づき、手動で更新してくれることだけに頼らない対応だった。 ただし、提供側が更新を配信したことと、自分のサイトで更新が終わったことは別に確かめる。IPAも、設定や構成、更新失敗によって適用されない場合があるため、管理画面などで実際のバージョンを確認するよう案内している。管理を委託しているなら、「自動更新の対象です」という回答の先に、稼働しているバージョンと確認時点まで聞きたい。 当時の系列 この問題への修正境界 6.8 6.8.6。SQLインジェクションへの修正 6.9 6.9.5。二つの問題への修正 7.0 7.0.2。二つの問題への修正 この表のバージョンを、10月末に新しく導入するバージョンの指定にはしない。同じ系列にも後続リリースがある。公式のWordPressのバージョン一覧と現在の告知を合わせて確認する。 解説図:更新を配信する側の対応と、利用するサイトでの適用確認をつなぐ。 運用へ持ち帰るなら、WordPress本体のバージョンを記録し、使っている系列の修正状況を確認する。そのうえでログイン、記事の表示、フォーム、管理画面の更新など、通常の操作を通す。REST APIの名前を見て一括で閉じる前に、そのAPIを使う公開機能や編集機能が何かを確かめたい。入口を塞ぐ判断にも、止まる機能との対応が要る。 React2Shell 2025年12月3日に公表されたCVE-2025-55182は、React Server Components(RSC)の処理に関係する認証前のコード実行の脆弱性だ。名前を見て「Reactを使っているから全部同じ」と受け取ると、必要な点検を外してしまう。React公式の告知 ブラウザーとサーバーを分ける 今回問題になったのは、Server Componentsを処理するサーバー側のパッケージだ。ブラウザー内だけで動くReactアプリまで一律に対象になるわけではない。一方、自分でServer Functionを書いていなくても、サーバー側でRSCを扱う構成なら確認が必要になる。 「画面を作るライブラリ」と思っていたものが、サーバーへ届く入力を処理している。その構成を把握していないと、フロントエンド担当とサーバー担当の間で更新の担当が浮いてしまう。アプリの名前より、どこでどのパッケージが動くのかをたどる方が役に立つ。 当初はreact-server-dom-webpack、react-server-dom-parcel、react-server-dom-turbopackの19.0、19.1.0、19.1.1、19.2.0が影響し、このRCEへの修正は19.0.1、19.1.2、19.2.1に入った。ただし、その後のDoSや情報露出などの追加問題とは区別する。最初の修正版の数字だけを、今の推奨バージョンとしてコピーしない。 解説図:確認するのは、名前にReactがあるかだけでなく、サーバーで何を処理しているか。 公開から適用までの時間 AWSは12月4日の報告で、公表から数時間以内に攻撃試行を観測したと説明した。Google Threat Intelligence Groupは12月12日、侵害調査で確認したバックドアや暗号資産マイナーの配備などを報告している。攻撃試行の観測と、侵害後の調査結果は分けて読む。AWSの観測報告、Googleの調査報告 この短さを見ると、週末にまとめてニュースを読む運用だけでは間に合わない場合がある。とはいえ、焦って別のアプリを更新しても解決しない。まず影響する構成と外部からの到達可否を押さえ、更新が必要なアプリとデプロイ先を担当者に伝える。速さと対象の特定は両方必要になる。 Vercelの振り返りにはWAFへの対応だけでなく、影響するデプロイへの通知、更新を支援するCLI、修正PRの作成といった手段が出てくる。修正コードを公開したあと、利用者が自分の環境へ届けるまでに作業があることがよく分かる。Vercelの対応記録 解説図:依存ファイルを直した状態と、その修正を含むアプリが公開環境で動く状態を区別する。 自分の運用ではpackage.jsonの記載だけで判断せず、解決された依存のバージョンとデプロイ済みの成果物を確認する場面になる。更新のPRがマージされても、以前のコンテナーが動いたままなら、公開環境への適用確認は残っている。WAFで一部の通信を遮断できることも、脆弱な処理自体が修正された証拠にはならない。 n8nの編集権限と公開フォーム n8nのような自動化ツールには、APIキーや接続情報を預け、複数のサービスをまとめて動かすことがある。ワークフローを作れる人は、見た目以上に強い権限を持つ。その一方で、公開フォームはログインしていない人にも使わせたい。この二つを分けて考えさせられるのが、年末から翌年に続いた一連の問題だ。 三つの問題を条件と修正版で分ける CVE 開発元の公表日 主な条件と当該問題の修正境界 CVE-2025-68613 2025-12-19 認証済み利用者の式評価に関係。1.120.4、1.121.1で修正 CVE-2026-21858 2026-01-07 一部のフォーム系ワークフローを通じた未認証のファイルアクセス。1.121.0で修正 CVE-2026-25049 2026-02-04 編集権限を持つ認証済み利用者による式評価の追加の回避。1.123.17、2.5.2で修正 出典は開発元の告知と、開発元が登録したCVEの公開レコードだ。68613の修正バージョンはCVEレコードでも確認できる。CVE-2025-68613の告知、CVE-2026-21858、CVE-2026-25049 この三つを「n8nで未認証RCE」と一行にまとめると、個々の条件が消えてしまう。式を設定する利用者の権限が関係する問題と、公開されたフォームから届く問題では、確認する処理が違う。一つの問題の修正版が後から公表されたすべての問題まで直しているわけでもない。 解説図:ワークフローを編集する人と、フォームを使う人では、必要な権限と到達する処理が違う。 組み合わせた悪用も報告された VulnCheckは2026年3月27日、CVE-2026-21858とCVE-2025-68613を組み合わせた、未認証でのコード実行につながる攻撃試行を観測したと報告した。カナダ政府の告知でも、CVE-2025-68613のCISAカタログへの追加が伝えられている。VulnCheckの観測、カナダ政府の注意喚起 「認証済みの人しか使えない問題だから、外部の人には関係しない」と区切っていたところへ、別の入口がつながる。WordPressの事例と同様、脆弱性は一つずつの説明だけでなく、同じ環境で組み合わさる条件も見る必要がある。 更新までの対処としては、開発元が示す公開フォームやWebhookへの制限、編集できる利用者の制限、OS権限やネットワークの到達先の制限を検討する。ただし、これらは適用条件を確認して使う暫定的な対処であり、修正の適用と同じ状態にはならない。 解説図:ツールを更新する作業と、預けている権限・接続情報を確認する作業を合わせて考える。 自動化は便利になるほど、連携先が増えていく。だから、n8n自身の管理者パスワードだけを点検して終わらせたくない。影響を受けた可能性のあるワークフローが、どの認証情報を利用でき、どのサーバーへ接続できたかをたどる。秘密情報が露出したと判断した場合は、接続先側での失効や再発行も対応に含める。これは、このツールを使うときの権限設計にもそのまま返ってくる話だ。 Langflowの公開フロー AIのフローを公開するとき、外から使わせたいのは、用意した処理の実行だ。その処理の中身まで外部の人が差し替えてよいわけではない。LangflowのCVE-2026-33017は、この区別が崩れる問題だった。 実行する権限と変更する権限 開発元のアドバイザリでは公開フローを構築・実行する経路で、保存済みの定義だけでなく外部入力のフローデータを受け付け、サーバー側でPythonの処理へ渡してしまう問題が説明されている。影響欄は1.8.2以下、当該問題への修正境界は1.9.0だ。公開機能の使い方や認証設定などを含めて条件を確認する。Langflowの開発元情報 たとえば「このフォームから質問してよい」と「フォームの裏で動くプログラムを渡してよい」は、同じ許可ではない。公開した処理を利用させる機能に、処理の定義そのものを受け取る機能が混ざれば、許可した操作の範囲が変わってしまう。 点検したいのは、ログイン画面があるかだけではない。公開しているフロー、その識別子が使われる経路、外部から受け取るデータ、認証を通る場所を対応させる。管理画面が閉じていても、公開実行用のAPIは別に動いていることがある。 解説図:用意した処理を呼び出す許可と、サーバーで動く処理を差し替える許可を分けて考える。 約20時間という観測 Sysdigは3月19日の報告で、脆弱なLangflowを置いた観測環境に、アドバイザリ公開から約20時間後には攻撃試行が届いたと説明した。3月17日20:05 UTCのアドバイザリ公開に対し、最初の観測は3月18日16:04 UTCごろだ。Sysdigの観測報告、公開日時を示すAdvisory API ここで「約20時間」の起点はCVEレコードの公開ではない。CVEレコードは3月20日で、アドバイザリや観測報告と日が違う。また、この報告が示すのはハニーポットで観測した攻撃試行であり、実在する利用企業が何社侵害されたかという件数ではない。 この短い間隔を、単に「攻撃が速くて怖い」で終わらせたくない。公開フローが外から使えること、そこに秘密情報が置かれること、利用者自身がインスタンスに更新を適用することが重なっている。ニュースの購読先と、影響を受けるインスタンスを見つける手段がつながっているかを考える材料になる。 解説図:約20時間はアドバイザリ公開からの間隔。報告日と出来事の日を分けると、経緯を取り違えにくい。 当面の露出を減らすための公開経路の制限や前段での認証と、ソフトウェアを修正する作業は分けて扱う。Sysdigは保持していたシークレットの点検・交換や、不審な外向き通信の確認も挙げている。Langflowを修正版に更新しただけで、既に利用できた接続情報まで無効になったとは判断できない。 なお、LangflowにはCVE-2025-3248という別の認証不足の問題もある。初回公表は今回の期間より前だが、2026年7月にも悪用の観測が報告された。新しい問題の修正に追われるなかで、以前から動く別インスタンスが残っていないかも見たい。新しいニュースだけを読むことと、手元の古い環境を把握することは、別の作業になる。以前の問題の開発元情報、7月の観測報告 Metabaseと接続先データベース BIツールを一台更新する話だと思って読むと、対応範囲の広さに目が止まる。MetabaseのCVE-2026-72898では、BIツール自身への侵入から、そのツールが接続しているデータベースへ影響が広がり得る。 二つのデータベースを分ける Metabaseは2026年8月6日、認証なしで届くAPIのSQLインジェクションを公表した。攻撃者がMetabaseのアプリケーションデータベースに不正なSQLを実行し、Metabaseの管理者権限を得る可能性がある問題だ。ここでいうアプリケーションデータベースは、Metabase自身の利用者や設定などを持つ側を指す。開発元アドバイザリ その管理権限を取られると、アプリに保存された接続情報や設定へ手が届く可能性がある。そこから、分析用に接続している別のデータベースの情報が問題になる。「BIの画面を見られた」で止めず、アプリ自身のDBと、アプリが接続する先のDBを分けてたどりたい。 開発元はMetabase Cloudが未知の脆弱性を使った攻撃を受け、対象エンドポイントを遮断したうえで修正したと説明した。公表時点でCloud利用者の環境は修正済みと案内し、自己管理している利用者には更新と追加の点検を求めている。Metabaseの対応告知 解説図:アプリの設定を持つDBと、業務データを読む接続先DBを分ける。侵害調査では両方が関係する。 更新後に残った作業 OSS版の当時の修正境界は、0.58.24、0.59.21、0.60.17、0.61.11、0.62.9、0.63.5だった。利用するバージョンの種類と系列を、開発元の対応表に照らして確認する。そのうえで、対象の/api/session/reset_passwordが公開されていた場合、開発元は次の点検も案内した。JPCERT/CCの注意喚起 有効な利用者セッションを失効させる。 APIキーと管理者アカウントに見覚えのない変更がないか調べる。 接続先データベースの認証情報を交換する。 Metabaseと接続先DBのログや操作履歴を調べる。 「更新後にパスワードを変えましょう」という一般論より、対象がずっと具体的だ。アプリを更新する担当と、データベースの認証情報を管理する担当が別なら、後半の仕事を誰が持つかも決める必要がある。クラウドの修正完了の案内を読んだだけでは、接続先側の確認まで終えたことにはならない。 解説図:公開APIの修正と、利用できた権限や認証情報への対処を別の項目として進める。 ログの読み方も具体的だった。開発元が示したパターンには、パスワード再設定のAPIへのPOSTがHTTP 400となり、続く利用者情報の取得がHTTP 200になる組み合わせがある。一つ目がエラーだからといって、そこで攻撃も失敗したとは決められない。エンドポイント、順番、その後の操作を合わせて調べるための手がかりだ。 もちろん、その通信を一度見つけたことだけで、影響の全範囲まで判明するわけではない。管理者の追加、接続設定の変更、DBの読み出しなど、次の調査へつなげる。JPCERT/CCもインターネットから届く状態だった環境では、更新だけで完了せず侵害有無を確認するよう促している。 MongoBleedと認証前の処理 DBには強いパスワードを付け、利用者ごとの権限も絞る。その対策は必要だが、通信が認証にたどり着く前の処理にも不備は起こる。MongoDB ServerのCVE-2025-14847、通称MongoBleedはその点を確かめる事例になる。 認証より手前で読むデータ 問題はzlib圧縮を扱う通信処理にあり、認証前のクライアントから、未初期化のヒープメモリーを読み出される可能性があった。MongoDBに到達できることが関係し、認証を有効にしただけでこの処理が修正されるわけではない。一方、この問題をそのまま「任意のコードを実行できる」「DBの全件を確実に持ち出せる」と書き換えるのも違う。MongoDBのセキュリティ情報、公式の修正チケット ここで確認するのは、DBのネットワーク処理と認証の順番だ。DBへ接続できる送信元を絞ること、通信処理の修正を適用すること、利用者の認証を設けることは、それぞれ担当する場所が違う。どれか一つを済ませたことで、ほかの対処まで不要になったとは考えない。 解説図:認証があることと、認証前の通信処理が修正されていることを区別する。 Atlasと自己管理版の対応 MongoDBの振り返りでは、社内で問題を発見したのは12月12日。Atlas全体には12月18日までに修正を適用し、12月19日にCVEを公表したと説明されている。管理サービスでは提供側が先に修正を進められるが、自己管理のサーバーでは利用者が更新する範囲が残る。MongoDBの対応時系列 当該問題への修正境界には8.2.3、8.0.17、7.0.28、6.0.27、5.0.32、4.4.30が挙げられた。更新がすぐできない場合の暫定策としては、サーバーの圧縮設定からzlibを明示的に外す方法も案内された。利用中の構成と公式手順を確かめ、設定変更をソフトウェアの修正と取り違えないようにする。 ここで「マネージドだから確認しなくてよい」とまでは言えない。提供元の修正対象に、自分が使っているサービスや構成が含まれているかは見る必要がある。逆にAtlasについての修正完了の説明を、自分で立てたMongoDBサーバーにも適用済みだと読んではいけない。製品名が同じでも、更新を実施する担当は変わる。 解説図:管理を誰に任せているかで確認先が変わる。悪用確認済み一覧への追加日は攻撃開始日を示さない。 当初のMongoDBの説明には、悪用や顧客データ侵害の証拠はないという記述があった。その後、CISAは12月29日に悪用確認済みカタログへ追加した。この二つを並べたとき、初期の説明を現在も続く「悪用はない」という断定として読むと誤る。情報が更新された順番を残すことが大切だ。当時のMongoDBの案内、CISAの公式カタログデータ 運用へ置き換えると、DBを置いている場所、管理サービスか自己管理か、公開範囲、圧縮設定、適用されたバージョンを確認する作業になる。「DBには認証がある」という回答の先を、通信が届く位置から見直せる事例だ。 SiteGuard Server Editionと痕跡の保全 守るために置いたWAFも、更新や侵害調査の対象になる。これはWAFが役に立たないという話ではない。防御製品にもソフトウェアと権限があり、その製品が何を処理するかまで管理する話だ。 統計機能の脆弱性 EGセキュアソリューションズは2026年10月14日、SiteGuard Server Editionの統計機能にあるCVE-2026-106931を公表した。OSコマンドインジェクションの問題で、製品が動くサーバー上でrootまたはSYSTEM権限のコマンド実行につながり得る。同社は悪用を確認しているとも説明した。開発元の告知 対象バージョンはVer2.20以降で、統計機能が有効な構成が影響する。同機能は初期値でも有効とされている。製品名、バージョン、機能を合わせて照合する必要がある。WordPress用のSiteGuard WP Pluginなど、名前にSiteGuardが付くすべての製品へ、この影響を広げてはいけない。 また、公式に示された評価にはローカルの攻撃経路と低い権限の前提がある。この説明を、WAFの公開URLへ誰でも一回アクセスすれば侵入できる話に置き換えない。影響が大きいことと、成立条件を省いてよいことは別だ。JVNの情報 解説図:製品名だけで判断せず、バージョンと機能を照合する。別製品や異なる攻撃条件へ一般化しない。 更新より先に残すもの この事例で特に覚えておきたいのは、更新やパッチの適用前に「診断情報」を取得して保管するという案内だ。更新によって攻撃の痕跡が消える可能性があるため、調査に使う情報を先に残す必要があった。JPCERT/CCもこの順序を注意事項として取り上げている。JPCERT/CCの注意喚起 「一刻も早く更新する」と「調査の材料を残す」は、どちらかを無視すればよい関係ではない。この製品では公式に案内された診断情報の取得を先に行い、そのあとに更新・パッチへ進むという、具体的な順番が示されている。 当該修正はApache版・Nginx版・IIS版では8.00 Update5、FreeBSDのApache版・Nginx版では7.00 Update6に入った。IIS版の3.10から4.00 Update4は影響対象から除かれる。更新が難しい利用者向けには対象ファイルを差し替えるパッチ、回避策としては統計機能の無効化も案内された。バージョンの文字列をざっと読まず、OSと製品版を組み合わせて選ぶ部分だ。 解説図:この製品では更新によって失われ得る診断情報を先に残すよう公式に案内された。 開発元はサポート契約がある利用者には9月29日に個別通知したとも説明している。一般向けの公表日と、契約先へ通知した日は同じではない。運用側ではニュースだけでなく、契約先に届く保守通知が誰へ届き、誰が判断するのかも点検したい。 レンタルサーバーに付属するWAFを使っている場合も、「自分が製品を直接契約していないから関係がない」とは判断せず、提供元の告知で対象と対応状況を確認する。利用者が変更できる設定と、提供側が更新する製品本体を分けて聞くと、問い合わせも具体的になる。 FortiCloud SSOの連続した告知 「先月、言われたバージョンへ更新した」。その記録は大事だが、翌月に別の入口の問題が公表されれば、もう一度確認することになる。FortiCloud SSOの事例は、更新履歴を残すことと、追加告知を追うことの両方を考えさせる。 12月と翌1月の問題 2025年12月9日に公表されたCVE-2025-59718とCVE-2025-59719は、FortiCloud SSOが有効な対象装置への認証回避に関係する。続く2026年1月、Fortinetは当時の最新リリースに更新済みの装置にも類似の不正ログインがあったと報告し、別の攻撃経路を調査した。Fortinetの調査と更新履歴 1月27日に公表されたのがCVE-2026-24858だ。Fortinetが登録したCVEの公開レコードには、攻撃者自身がFortiCloudアカウントと登録済みデバイスを持つ条件が記載されている。標的の装置へ正規にログインできる資格が先に必要、という意味ではない。ただし、12月の問題と同じ「未認証の攻撃者」という一文で、条件をまとめて落とすのも正確ではない。Fortinetが登録したCVEレコード また、対象は訂正後のFortiCloud SSOの範囲で読む。Fortinetは1月28日、第三者のSAML IdPとFortiAuthenticatorはこの問題の対象ではないと明示した。「SSOを使っているから全部同じ」でも、「SSOなら認証があるから心配ない」でも判断できない。 解説図:前の修正の有無と、あとから判明した別の経路を分ける。告知の訂正まで時系列に残す。 提供側の停止と利用側の点検 Fortinetの更新履歴では1月23日に悪用されたアカウントを無効化し、1月26日にFortiCloud SSOを停止した。1月27日には脆弱な装置からのアクセスをサポートしない条件でSSOを再開したと説明されている。これは提供側が実際に行った対応だ。 一方、利用者側には対象装置のファームウェア更新、想定外の管理者アカウントや設定変更の点検が求められる。侵害の兆候がある場合は、装置と設定を侵害済みとして扱い、正常な設定への復旧や変更の監査、認証情報の交換へ進むよう勧められている。提供側でSSOが再開したことだけで、個々の装置の設定まで元に戻ったわけではない。Fortinet公式の復旧案内を含む調査報告 認証情報の交換には装置の管理者パスワード以外も関係する。LDAPやADとの連携、VPNなど、設定に含まれる秘密情報を確認する。外部へ持ち出された可能性のある設定が、別のシステムへ入る材料を含んでいないか、という見方だ。 解説図:サービス提供側の対応後も、利用側では装置の設定とそれに含まれる認証情報を確認する。 FortiOSの7.4系列を例にすると、12月のCVE-2025-59718への修正指定は7.4.9以上、1月のCVE-2026-24858への修正指定は7.4.11以上になる。これはそれぞれの問題に対する修正境界で、今から選ぶ全製品共通の最新バージョン一覧ではない。12月のCVEレコード、1月のCVEレコード このような対応では「更新済み」という一語だけを記録すると、どの問題まで対応したかが後で分からなくなる。対象CVE、製品と系列、適用したバージョン、追加告知の確認時点を一緒に残すと、次の知らせが来たときに確認しやすい。 Trivyと検査ツールの配布経路 脆弱性を調べるためにCIで動かしたスキャナーが秘密情報を持ち出す側になっていた。安全を確かめるための道具まで、どこから届き、何の権限で動くかをたどる必要がある。 バージョン番号だけでは対象が決まらない Trivyでは2026年3月19日、悪意あるコードを含むv0.69.4が配布された。Actionの既存タグも書き換えられ、その後にはDocker Hubの配布物への影響も公表された。CVE-2026-33634が割り当てられているが、通常の機能の不備にパッチを当てるだけで整理できる事例ではない。Trivy公式アドバイザリ 利用側が確認するのは、Trivyの数字だけではない。本体をどこから取得したか、どのActionを参照したか、タグかコミットSHAか、いつ実行したか。さらに、その処理にどんな秘密情報が渡されていたかを調べる。 同じタグを使っていても、取得した時点で中身が違うことがある。また、参照しているActionをSHAで固定していても、そのActionが内部で別の可変タグを呼ぶ構成なら、そこで参照先が変わり得る。固定した一行だけを見て安心せず、実際に呼ばれる配布物をたどる必要がある。 解説図:信用して実行する道具も取得元・参照先・実行時刻と渡している権限を確認する対象になる。 トークンを変える順番 3月30日の当事者の振り返りでは既存のGitHub・レジストリーのトークンを失効させてから、新しいアクセスを発行する対応が説明されている。初期の認証情報の更新が複雑で、別アカウントを通じたアクセスが残った経緯も報告された。危険なワークフローの実行条件の除去、SHA固定、リリースの不変化なども対策に含まれている。Trivyの事後報告、Aquaの調査・対応の更新 ここは利用側でも順序を考えたい。侵害された可能性のあるジョブが動いたまま、新しいキーだけを渡せば、それも読める状態にしてしまう。危険な実行を止め、影響する成果物と実行履歴を調べ、露出した秘密を失効・再発行し、安全な状態を確認した環境で再開する。この順序は今回の事例から運用へ引きつけた考察だ。 利用者側の判断も公表されている。Apache Software Foundationは3月20日の初報で、調査の間、それまで利用を許可していた関連Actionを無効化し、ビルドが失敗する可能性も明示した。調査中の段階で利用を止める範囲と、その業務への影響を伝えた例として読める。これは同組織の秘密情報流出がすべて確認された、という意味ではない。Apacheの初報 解説図:悪意ある配布物を除くだけでは、実行中に利用できた秘密情報までは失効しない。 当時の退避先として示された本体v0.69.2やv0.69.3と、情報基準日時点の公式最新リリースv0.76.1も区別する。古い対応記事から安全な退避先を見つけても、そのバージョンを使い続ける指示と読み替えない。Trivyのリリース一覧 検査ツールを入れたこと自体が、検査の完了を保証するわけではない。どこから入手し、何の権限で動かし、その結果を誰が読むのかまでが、CIで検査を運用する仕事になる。 LiteLLMで二度起きた配布物の侵害 Trivyの影響はそのツールを直接動かした組織だけにとどまらなかった。複数のLLMへの接続をまとめるLiteLLMは、3月のPyPI侵害とTrivyの関係を10月30日の振り返りで説明している。さらに10月には、別の経路から正規のリリース工程を使われた。同じ製品の事件でも、前回の「対象外」という説明を使い回せない。 3月と10月では、影響する配布物が違う 出来事 公式に示された主な対象と境界 2026-03-24 PyPIのlitellm==1.82.7、1.82.8。当時の公式Proxy Dockerイメージは非対象 2026-10-21 PyPIのlitellm==1.107.0と、公式が列挙したDocker Hub・GHCRのコンテナ。非公開のEnterprise配布は非対象 3月の公式初報はCIのセキュリティ検査に使ったTrivyの依存を侵害の起点とみていた。悪性版には認証情報を集めて外部へ送るコードが含まれた。公式Proxy Dockerが影響しなかったのは、その配布経路が当該PyPIパッケージに依存していなかったためだ。「LiteLLM全体が同じ状態」だったのではない。3月の公式報告 10月はコンテナも対象になった。v1.107.0だけでなく、latest、main-latest、main-stableも悪性イメージを指した時間帯がある。今のタグが正常な参照先へ戻っていても、以前に取得したイメージまで変わらない。取得・実行時刻とイメージの内容を識別するdigestを、10月の公式な対象一覧へ照らす必要がある。 解説図:3月の「公式Dockerは非対象」を、別事件である10月へ広げない。タグ名だけでなく、取得した内容を確かめる。 正規の工程を通っていても、入力と権限を調べる 10月23日の原因分析では、5月に公開リポジトリーへ誤って入った内部サービスの管理キーが、削除後も失効されずに残ったと説明された。攻撃者はそのキーからGitHub組織の所有者のトークンを取得し、リリース工程が読むソースを別のリポジトリーへ変更した。さらに別の盗難トークンで承認し、正規の工程から悪性版をビルド・配布させた。10月の公式の原因分析 ファイルから秘密を消すことと、その秘密を無効にすることは違う。10月30日の説明でも、悪性版に有効な署名とTrusted Publishingの来歴があったと報告された。署名や公開元の確認に加え、リリースに使うソースと承認権限まで調べる理由が見える。再発防止の説明 影響する環境で、確認のためのpipを動かさない 10月の公式案内は、影響環境でPythonを起動するとcook.pthやsitecustomize.pyが再実行され、pipも例外ではないと警告した。慌ててpip installから始めない。まず疑わしい実行を止めて記録を保全し、正常な別環境から対象の配布物と実行履歴を調べる。痕跡のファイルが見つからないことも、未侵害の証明にはならない。利用者向けの復旧案内 侵害が疑われる環境では、LLM提供元のAPIキーだけでなく、DBのパスワード、クラウドの実行権限、そのホストから利用できた秘密情報まで確認する。正常な別端末から失効・交換し、既知の正常な状態から実行環境を再構築する。古い配布物を削除する作業だけでは、既に利用できた認証情報を無効にできない。 解説図:確認コマンド自体が不正コードを再実行する場合がある。対象の特定、秘密情報の処置、環境の復旧を順序立てて進める。 Axiosとnpmの配布経路 アプリからHTTPリクエストを送る前に、ライブラリの導入だけで端末の侵害につながり得る。Axiosのサプライチェーン攻撃で押さえたいのは、攻撃が始まる場所だ。公開したWebアプリだけを点検しても、開発端末やCIで起きたことを見落としてしまう。 2026年3月31日、乗っ取られたメンテナーのnpmアカウントから、悪意ある依存を含むAxiosが公開された。Aikidoが整理した対象はaxios@1.14.1とaxios@0.30.4。普段使っているパッケージの正規の名前で配られ、二つの系列が影響を受けた。Aikidoの調査記事 importしていない依存が、導入時に動く 追加されたのはplain-crypto-js@4.2.1という依存パッケージだった。解析では、Axios本体のソースからこのパッケージをimportするコードは見つからなかった。攻撃者が使ったのは、インストール時に実行されるpostinstallスクリプトだ。そこから、端末を遠隔操作するための不正プログラムであるRATを、macOS、Windows、Linuxへ配備する仕組みになっていた。StepSecurityの解析 つまり、アプリのHTTP通信に不審な処理がないかを探すだけでは足りない。導入時スクリプトを許す環境では、自分のアプリを起動する前にも、依存パッケージのコードが実行される。テストを始めるより前に、ビルド用の認証情報へ届く可能性がある。調べる対象にはインストールを実行した端末とCIランナーも入る。 npmで配られたもの 今回確認された役割 axios@1.14.1 1.x系列に悪意ある依存を追加したバージョン axios@0.30.4 0.x系列に同じ依存を追加したバージョン plain-crypto-js@4.2.1 導入時のスクリプトでRATを配備するための依存 StepSecurityの時系列では、悪性のAxiosが出たのは3月31日00:21 UTCと01:00 UTC。二つの系列への公開は39分の間に続いた。解析では、対応するコミットやタグはAxiosのGitHubリポジトリーに存在しなかったとも報告された。GitHubでソースを読んだことと、npmから取得した配布物を確かめたことは、同じ確認にならない。 解説図:Axiosのソースからimportされない依存でも、導入時スクリプトを許すとコードが動く。取得した事実と実行結果は分けて調べる。 消えているファイルを、安全の根拠にしない この事件では、実行後にドロッパーが自分のファイルを削除し、package.jsonを無害に見える内容へ差し替える動きも解析されている。あとからnode_modulesを見て不審なスクリプトがなかったとしても、それだけで過去の実行を否定できない。Aikidoが示した痕跡と対応 反対に、plain-crypto-jsのフォルダーを見つけただけで、RATの配備まで成功したと断定するのも早い。パッケージを取得・展開したこと、導入時スクリプトが実行されたこと、その後の不正プログラムが動いたことには段階がある。スクリプトを無効にしていた構成も含めて調べる必要がある。 確認するのは現在の依存ツリーとlockfileに加え、該当時期のインストール・ビルドの履歴、ネットワーク通信、使ったキャッシュだ。悪性バージョンを解決していたジョブが、どの環境で、どの秘密情報を使えたかを対応させる。npmから削除された後でも、以前に取得した配布物やキャッシュを調査から外さない。 当事者は端末と認証情報まで対応した メンテナーは4月2日に公開した事後報告で、端末を狙ったソーシャルエンジニアリングとRATによってnpmアカウントの認証情報へアクセスされたと説明した。実施した対応として挙げたのは、メンテナーの端末の初期化と、認証情報のリセットだった。また、OIDCを使う公開手順や、公開後に変更できないリリースの整備などを、進行中の改善として挙げた。メンテナーの事後報告 Vercelも3月31日、自社のビルド基盤から攻撃者の通信先への接続を遮断したと公表した。同社は自社システムへの影響はなかったと説明する一方、悪性バージョンをビルド環境で使った利用者には、クリーンな依存での再デプロイと、その環境に置かれていたAPIキー、DB認証情報、トークンなどの交換を案内した。Vercelの対応と利用者向けの案内 ここでAxiosだけを前のバージョンへ戻しても、既に読まれた可能性のある秘密情報は元に戻らない。利用者側でも、影響する実行を止めて記録を保全する。侵害が疑われる場合は、その環境から利用できた認証情報を、正常な別の端末から早急に失効・更新する。端末やランナーは既知の正常な状態から再構築し、新しい秘密は安全を確かめた復旧先だけへ渡す。GitHubのマルウェア情報も別の端末から秘密情報を更新し、パッケージ削除だけで関連する不正プログラムを除去できたとは考えないよう案内している。Axiosのマルウェア情報 依存の入れ直し、環境の復旧、秘密情報の処置を、それぞれの完了として確認したい。 解説図:悪性バージョンを除去する作業と、実行した環境・そこで利用できた秘密情報への対処を分ける。 lockfileとnpm auditにも、担当する範囲がある 当時の退避先は1.xが1.14.0、0.xが0.30.3だった。これは今回の悪性バージョンを避けるために案内された番号で、今後もそのまま固定し続ける推奨ではない。現在のサポートと後続の修正を確認して選ぶ。 再発を考えるとき、lockfileは必要な記録になる。ただし、悪性バージョンを含むlockfileを固定すれば、その内容を再現することにもなる。npm ciがlockfileどおりに導入することと、そこにある配布物が安全であることは別だ。依存名、解決されたバージョン、取得元と時点も確かめたい。npm ciの公式仕様 導入時スクリプトを抑える--ignore-scriptsも対策の一つになるが、既に実行されたコードを取り消す機能ではない。使っているnpmのバージョンと設定、必要な生成処理やビルドへの影響を確認して採用し、通常のアプリ実行時に動くコードへの対策まで済んだとは考えない。npm auditも既知の脆弱性情報との照合が中心で、「結果がゼロだから未知の改ざんもない」という保証にはならない。何を実行させるかという設定と、何を検知したかという結果を分けて使う。npm CLI v11のスクリプト抑止設定、npm auditの公式仕様 この事件で問われたのは、HTTPクライアントをどのバージョンへ直すかだけではなかった。いつもの依存を導入する作業で、どのコードが、どの端末の、どの権限を使うのか。Webサービスの公開前にも守る場所があることを、具体的に突きつける事例だった。 Chromeと運用者の端末 公開サーバーやCIを点検していると、作業に使うブラウザーは背景に回りやすい。でも、そのブラウザーから管理画面を開き、クラウドへログインし、リポジトリーの設定も変えている。サーバーの更新と並べて、普段使う端末側にも確認項目を持っておきたい。 悪用確認と分かっていない範囲 Googleは2026年6月8日のChrome Stable更新で、V8の範囲外メモリーアクセスに関するCVE-2026-11645を修正した。同じ告知の中で、実環境にexploitが存在することを認識していると説明している。修正前から悪用の情報がある問題として、手元の適用状況も確認したい事例だ。Googleの6月8日の告知 ただ、この告知だけでは、誰がどのページへ誘導され、どこまで侵害されたかという攻撃全体の詳細は分からない。V8の一つの不備を、そのまま端末のOS全体の権限取得と説明したり、利用者全員の被害へ広げたりはできない。ブラウザーの処理、ほかの防御、攻撃の組み合わせは区別する。 解説図:管理対象には公開サーバーへアクセスする人のブラウザーも含まれる。特定の攻撃連鎖を再現した図ではない。 ダウンロードと適用を確かめる このとき案内されたバージョンはWindows・Mac向け149.0.7827.102/.103、Linux向け149.0.7827.102だった。これは6月の修正境界で、10月末に149を導入する推奨ではない。10月29日の公式告知では、Stable/Extended Stableの配布バージョンは既に別の156系列になっている。番号だけを抜き出すと、古い安全情報を新しい推奨として伝えてしまう。10月29日の公式告知 Chromeの公式ヘルプでは、通常はバックグラウンドで更新が進み、適用には再起動が必要になると説明されている。「自動更新だから大丈夫」と記憶していても、ブラウザーを長く開きっぱなしにしていれば、再起動待ちの更新が残る場合がある。Google Chromeを更新する WindowsのChromeなら、メニューの「ヘルプ」から「Google Chromeについて」を開き、更新状況を確認する。再起動が案内されていれば作業中の内容を確認して再起動し、もう一度その画面で稼働しているバージョンを見る。企業の管理下にある端末では、配信設定や利用チャネルを管理する担当とも確認する。 解説図:更新が提供されたこと、取得されたこと、手元で適用されたことを分けて確認する。 サーバー担当だけで対応表を閉じると、この作業の持ち主が抜けることがある。開発端末、運用用端末、共有端末を誰が更新するのか。ブラウザーの更新履歴を、製品ニュースと自分の端末をつなぐ小さな確認として扱いたい。 関連事例を同じ箱に入れない 本文の12事例以外にも候補を比較した。ここでは対応の違いを読み取れる例を短く挙げる。同じ期間の話でも、CVEのあるソフトウェアの不備と、アカウントや配布物そのものの侵害は区別しておく。 公開する機能によって変わる条件 Cisco AsyncOSのCVE-2025-20393では、脆弱なバージョンに加えて、Spam Quarantineが有効で、インターネットから到達できることが攻撃キャンペーンの対象条件だった。初報の2025年12月17日と、2026年1月15日の修正版追加も区別する。初報の「修正なし」を、いつまでも現在の状態として読むと対応を誤る。Ciscoの公式告知 FortiWebのCVE-2025-64446では、防御製品自身の管理画面が問題になり、CISAは更新後にも不正管理者や設定変更を確認するよう案内した。Ivanti EPMMでは当初のRPMによる暫定修正について、製品を更新したあとに再適用が必要となる条件が示された。対策を入れた瞬間だけでなく、その後の変更で何が残るかも確認する話だ。CISAのFortiWeb告知、CERT-EUのIvanti EPMM勧告 期間の終わりに近い例では、Ivanti Connect Secureに関する10月30日の政府機関の追報がある。CVE-2026-104935、CVE-2026-111357、CVE-2026-118527の連鎖による悪用を伝え、対応先を更新している。10月13日の最初の告知だけを読んで作った表では、後から説明された連鎖全体を扱えない。単独の権限昇格の問題と、組み合わせた侵入の話も分けて読む必要がある。NHS Englandの追報、カナダ政府の更新 依存パッケージの侵害 2025年11月のShai-Hulud 2.0では、Postmanが依存パッケージから自動ビルドと公開権限へ影響が広がった経緯を公表している。トークンの失効、公開権限の絞り込み、認証方法の見直しなど、当事者が何を変えたかを読むと、依存の更新とビルドの権限がつながって見える。Postmanの原因分析 同社の説明では17の公開npmパッケージに波及した一方、PostmanのWeb・クライアントアプリや本番システムは感染しなかった。公開パッケージの侵害を、そのままPostman本体の侵害と読み替えない。 Shai-HuludやAxios、LiteLLMをCVE件数へ足して「脆弱性が何件起きた」と数えると、何を比較したか分からなくなる。共通して考えられるのは、信頼して実行したものが何へ届くかと、そこで利用できた秘密情報をどう扱うかだ。事件の分類は残したまま、運用上の問いを共有する方が理解しやすい。 npmの依存とは別に、Node.js本体も更新する 10月21日のNode.jsの修正には、Brotli展開処理のCVE-2026-111497もあった。攻撃者が渡した圧縮データをアプリがnode:zlibで展開する場合、メモリー破壊からクラッシュ、情報露出、コード実行につながる可能性がある。影響は22・24・26系列で、当該修正は22.23.4、24.22.1、26.12.1に入った。Node.jsを使うすべてのアプリが、この経路から攻撃を受けるという意味ではない。Node.jsの公式リリース Notionは同日、コア製品の修正と本番への展開を完了したと公表した。npmパッケージの更新だけで対応を閉じず、コンテナや実行基盤に含まれるNode.jsも確認する理由になる。これらの資料から実悪用までは確認できない。Notionの対応記録 優先順位を決めるための確認 12事例を並べると、深刻度の数字だけでは作業順を決められないことが見えてくる。認証前に届く処理なのか、特定の機能を有効にしたときなのか、編集権限が必要なのか。どこから届き、どの権限まで使えるかで、自分の環境への影響は変わる。 最初の確認では製品とバージョンを見つける。次に、該当する機能と設定、インターネットからの到達性を確かめる。そこへ実際の悪用の情報と、届く先のデータや認証情報を合わせると、先に扱う環境を絞りやすくなる。 解説図:同じCVEでも、自分の環境の機能・公開範囲・権限によって、対応を急ぐ理由が変わる。 まず担当と対象をつなぐ たとえば、外部公開したAIフローと、閉じた検証環境の同じ製品を一つの行にまとめると、どちらを先に直すかが見えにくい。公開していないから一切のリスクがない、という意味ではない。前提を分けて記録し、正規の利用者や、既に内部へ届いている相手からの経路も検討する。 確認結果を残すなら、「更新済み」だけで終わらせず、次のような項目を一緒に持ちたい。 残す内容 後で判断しやすくなること 製品名、系列、稼働しているバージョン 別系列や別製品の告知を取り違えない 該当機能、設定、公開範囲 成立条件と実環境を対応させる 対象CVE・事件名と参照した告知の更新日 追加修正や訂正が来たときに追い直せる 担当者と管理サービスの範囲 提供側と利用側の残作業を分ける 取得したログと保存先 更新や再構築のあとも調査へ戻れる 認証情報の交換範囲と完了確認 アプリの更新だけで作業を閉じない この一覧を作るために、最初から大きな専用システムが必要なわけではない。手元の運用記録へ、何を見て判断したかを残すところから始められる。次の担当者が同じ環境を見つけ、追加告知を照合できる記録にする。 修正版の数字を読む この記事でも、複数のバージョン番号が並んだ。数字には「初回の修正が入ったバージョン」「当時の一時的な退避先」「今、保守されている系列のバージョン」という違いがある。Trivyのような配布物の侵害では、配布元や取得時刻も加わる。数字が一つ一致しただけで、同じ状態とは判断しにくい。 たとえば、古い対応記事のバージョンへ更新して、その問題の修正境界を越えたとしても、あとで公表された別の問題が残っていることがある。逆に、メジャーバージョンを無条件に上げると、アプリや連携先との互換性の確認が必要になる。利用中の系列、サポート期間、後続の告知、推奨されるアップグレード経路を見て選ぶ部分だ。 だから運用記録には数字だけでなく参照した告知を残す。次に見直す人が「このバージョンにしたのは、この問題に対応するためだった」と読めれば、後続の修正との関係を調べ直せる。次の担当者にも更新の理由と残った確認を伝えられる。 止める機能と残す機能を決める 暫定策として「公開フォームを止める」「統計機能を無効にする」「SSOを止める」といった選択が出てくる。ただ、同じ停止でも影響は違う。問い合わせが受け付けられなくなるのか、分析を見られなくなるのか、管理者が装置へ入れなくなるのか。脆弱性の条件だけでなく、使っている機能から考えたい。 前回のWordPress対応で連絡窓口を残すことを優先したのも、止めるだけでは利用者の用事が消えないからだった。今回の事例でも、一時的に閉じるなら、代わりの連絡方法や管理経路、復旧後の確認方法までつなげて考えられる。 委託先やサービス提供元へ問い合わせる場合も、「影響はありますか」から一歩具体的にできる。対象の製品・バージョン・機能を示し、修正は提供側で適用するのか、利用者が更新するのかを聞く。侵害の可能性があるなら、ログをどこまで取得でき、どの認証情報が確認対象になるかも合わせて確かめる。 対応を終えるための確認 パッチを入れたことは、対応の中で確かに大きな結果だ。ただ、ここまでの事例では、パッチが直すものと、別に処置するものが具体的に分かれていた。既に作られた管理者、使える状態のセッション、持ち出された可能性のある認証情報は、アプリのバージョンが変わっただけでは消えない。 解説図:何を確認して対応を終えたのかを分けて残す。一つの成功を、すべての作業の完了へ広げない。 修正したことと動いていること Axiosのように導入時のコードが問題になった環境では、クリーンな依存へ戻したことに加え、実行した端末・CIと秘密情報への対処を確認する。Reactのようなデプロイを伴う環境なら、依存の変更が稼働中の成果物へ反映されたかを確認する。WordPressの自動更新なら、配信対象になっただけでなく、実際にバージョンが上がったかを見る。Chromeなら、取得した更新を再起動で適用し、表示されたバージョンを確かめる。 更新後に通常の機能が動くことも、利用者のために必要な確認だ。ログイン画面が開くだけでなく、公開フォームの送信、管理画面での更新、必要なバッチや連携まで、対策のために止めた機能と、その影響を受ける範囲が動くかを確かめる。変更した場所と使い続ける機能を結びつけておくと、確認する理由も説明しやすい。 侵害が疑われるときの扱い 悪用が確認された脆弱性を使っていたからといって、すべての環境が侵害されたと決めつけることはできない。同時に、不審なログをすぐ見つけられなかっただけで、未侵害と断定することもできない。保存期間やログの種類によって、調べられる範囲は変わる。 SiteGuardのように更新で痕跡が失われる可能性が示される場合は、その案内に沿って先に保存する。FortiCloud SSOで侵害の兆候があるなら、装置の設定と関連する秘密情報まで扱う。Metabaseなら接続先DBにも調査が続く。対象の環境に合わせ、製品ごとの公式な調査・復旧案内に沿って進める。 バックアップも取得した事実と復旧に使えることを分けて扱う。戻す時点が既に侵害後だったり、ファイルとDBの世代が合っていなかったりすれば、戻したあとに別の問題が残る。必要なデータが戻り、修正も適用され、正常な利用へ戻れる状態を確認して初めて、復旧について具体的に話せる。 読み続けるための小さな運用 一次資料は初報で終わらない。Metabaseのように侵害後の点検が案内されることもあれば、FortiCloud SSOのように対象範囲が訂正されることもある。Reactのように、当初のRCEとは別の追加修正が続くこともある。初報の数字を一度転記して終わると、こうした変更を拾いにくい。 使っている製品の公式セキュリティ情報を決めておき、緊急の連絡先と通常の見直しの機会を持つ。告知を読む人、稼働バージョンを調べる人、業務への影響を判断する人が別なら、誰が誰へ結果を伝えるかを決める。ひとりで運用していても、どの知らせを見たら何を確認するかを残しておくと、次のニュースから作業へ入りやすくなる。 PHP2Shellのときに感じた、設定を一つずつ調べ直すことで理解と意識が深まる感覚は、今回の12事例にもつながっている。製品名を増やして読むと、同じ「更新」という言葉の後ろに、違う仕事があることが見えてきた。 次に脆弱性のニュースを見たときは、自分が使っているかを調べ、その構成で問題が成立する条件を読み、修正を適用したあとに残る確認へ進む。すべてを暗記するより、この順で資料を読めるようになりたい。定期的に見直しながら、何を守り、何を使い続けるための設定なのかを、もう少し具体的に説明できるようにしていく。
actions.lock を試した。タグの force push は防げたが、別リポジトリの再利用ワークフローと --relock は素通りした 2026 年 10 月 28 日に GA になった GitHub Actions のロックファイル(actions.lock)を、自分で用意したアクションのタグを実際に force push して試しました。 - ロックしたワークフローは、タグを別のコミットに動かされても、ロックしたコミットのまま動きました。アクションの中で使っているアクションも同じです。 - タグを、ロックしたコミットから辿れない履歴に付け替えられると、ワークフローはエラーで止まり、どちらのコードも動きませんでした。 - 一方で、次の 3 つは素通りしました。 - ロックファイルに載っていない新しいワークフローは、警告なしで動かされた先のコミットを実行した - 別のリポジトリの再利用ワークフロー(jobs.<id>.uses)は、ロック済みのワークフローから呼んでも、動かされた先を実行した - gh actions-lock --relock は「タグが動いた」と警告を出したうえで、動かされた先のコミットにロックを進め、終了コード 0 で終わった actions.lock を入れようとしている人と、入れたあとの更新や CI での確認の手順を決めたい人向けです。試した手順、結果、入れるときにやることをまとめました。 actions.lock の紹介と導入手順は GitHub Actions で checkout@v7 のようなバージョン指定を書くのはやめよう!時代は actions.lock! に分かりやすくまとまっています。この記事は、その先の「実際に攻撃されたときに何が起きるか」を確かめたものです。 対象と前提 - gh actions-lock 拡張 v0.4.0、ロックファイルの形式 v0.0.3 - 2026 年 10 月 31 日、GitHub.com の Free プラン、ubuntu-24.04 のランナー - gh actions-lock は手元では動かさず、使い捨てのランナーの上で動かしました - 試したリポジトリは公開しているので、実行のログもそのまま見られます - アクションの側(タグを force push する): OTA-EITA/actions-lock-lab-action - 使う側(ロックファイルを置く): OTA-EITA/actions-lock-lab 背景には、2026 年 3 月 19 日に aquasecurity/trivy-action の 77 個のバージョンのタグのうち 76 個が、認証情報を盗むコードに force push で書き換えられた事件があります(GHSA-69fq-xp46-6x23)。タグで指定していたワークフローは、約 12 時間、書き換えられたコードを動かしていました。 試したこと アクションの側は、OUTER_MARKER を出す composite アクションです。中で同じリポジトリの inner@v1 を呼び、こちらは INNER_MARKER を出します。どのコミットが動いたかは、この 2 つの値で分かります。 runs: using: composite steps: - shell: bash run: echo "OUTER_MARKER=A" - uses: OTA-EITA/actions-lock-lab-action/inner@v1 使う側は、ただこのアクションを呼ぶだけのワークフローです。 on: workflow_dispatch permissions: {} jobs: run: runs-on: ubuntu-24.04 steps: - uses: OTA-EITA/actions-lock-lab-action@v1 手順は次のとおりです。 1. 版 A(289fb71)に v1 のタグを付ける 2. 使う側で gh actions-lock を実行し、できたロックファイルを commit する 3. 版 B(875663c、A の上に積んだコミット)を作り、v1 を force push で B に動かす 4. ワークフローを動かし、どのコミットが動いたかを見る ロックファイルはこうなりました(一部)。 version: 'v0.0.3' workflows: '.github/workflows/use-action.yml': - 'ota-eita/actions-lock-lab-action@v1' dependencies: 'ota-eita/actions-lock-lab-action@v1': ref: 'v1' commit: 'sha1-289fb71b6ad20503e1a75480232a9ea0277cd855' 結果 | # | 試したこと | 動いたコミット | 結果 | |---|---|---|---| | E2 | v1 を B に動かして、ロック済みのワークフローを実行 | A | 守られた。中の inner@v1 も A | | E2 | 同じリポジトリの再利用ワークフロー(./.github/workflows/…)経由 | A | 守られた | | E9 | v1 を A から辿れない別の履歴 C に動かして実行 | なし | エラーで止まった | | E3 | ロックファイルにない新しいワークフローで @v1 を使う | B | 素通り。警告もなし | | E8 | 別リポジトリの再利用ワークフロー …/remote.yml@v2 を、v2 を動かしてから呼ぶ | B | 素通り。ロックファイルにも載らない | | E4 | ロック済みのワークフローに、ロックしていないアクションを足す | なし | 起動できない(422) | | E5 | gh actions-lock --verify | なし | E3 と E4 を検出して終了コード 1。タグが動いたことは報告しない | | E6 | gh actions-lock(通常の再生成) | なし | 既存の行は A のまま。タグが動いたことは報告しない | | E7 | gh actions-lock --relock | なし | 「タグが動いた」と警告したうえで B に進め、終了コード 0 | タグを動かされても、ロックしたコミットが動いた(E2) v1 が B を指している状態でロック済みのワークフローを動かすと、ログの最初にこう出て、A がダウンロードされました。 Using locked action versions from the workflow's lockfile Download action repository 'OTA-EITA/actions-lock-lab-action@v1' (SHA:289fb71b6ad20503e1a75480232a9ea0277cd855) OUTER_MARKER=A INNER_MARKER=A 中で呼んでいる inner@v1 は、ロックファイルに別の行として載っていませんが、A のコミットのものが動きました。同じリポジトリの再利用ワークフローを経由しても同じです。 辿れない履歴に付け替えられると、止まった(E9) 攻撃者は、元のコミットの上に積むとは限りません。v1 を A と無関係の履歴 C に付け替えると、ロック済みのワークフローは次のエラーで止まりました。 ##[error]Lockfile pin 289fb71b6ad20503e1a75480232a9ea0277cd855 for `ota-eita/actions-lock-lab-action` does not match ref `v1`. Ensure the pinned commit exists on the expected branch or tag. ドキュメントには、ロックしたコミットは記録したタグやブランチから辿れる必要があり、辿れなくなったものは実行時に拒否されると書かれています。そのとおりの動きで、攻撃者のコードも元のコードも動かずに止まります。安全側ですが、上流がまっとうな理由で履歴を書き換えたときも、同じように止まります。 新しいワークフローは、ロックし直すまで守られない(E3) ロックファイルを作ったあとに、同じ @v1 を使うワークフローを 1 本足しました。ロックし直さずに動かすと、警告もなく B が動きました。 Download action repository 'OTA-EITA/actions-lock-lab-action@v1' (SHA:875663c7885e676528d7f888745932fec4d63a0e) OUTER_MARKER=B ロックファイルは「載っているワークフロー」にだけ効きます。ドキュメントにも、ワークフローは 1 本ずつでもまとめてでも onboard できる、とあります。載っていないワークフローは、これまでどおりタグで解決されます。 別リポジトリの再利用ワークフローは、ロックの外(E8) 別リポジトリの再利用ワークフローを jobs.<id>.uses で呼ぶワークフローをロックすると、そのワークフローの行は空([])になり、警告も出ませんでした。v2 を動かしてから実行すると、動かされた先がそのまま動きました。 Uses: OTA-EITA/actions-lock-lab-action/.github/workflows/remote.yml@refs/tags/v2 (6e74cf98180d2b9bba393e9fc1fb4493c5f9f040) REMOTE_MARKER=B ドキュメントにも、ロックファイルは再利用ワークフローの呼び出しを固定しないので、フルのコミット SHA で固定する、と書かれています。 --relock は、動いたタグに警告を出しつつ進める(E7) ロックを更新するための --relock を、v1 が B に動いた状態で実行しました。 warning ref-moved .github/workflows/use-action.yml ref v1 now resolves to 875663c7885e, lockfile pins 289fb71b6ad2 警告は出ますが、ロックファイルの v1 は B(875663c)に書き換わり、終了コードは 0 でした。README には「動いたピンを受け入れる」ための --accept-moved というフラグがありますが、付けなくても進みました。 更新を CI で定期的に回していると、タグを書き換えられた直後の --relock で、攻撃者のコミットをそのまま取り込むことになります。通常の再生成(E6)と --verify(E5)は、タグが動いたことを報告しませんでした。 ロックしていないアクションを足すと、起動できない(E4) ロック済みのワークフローに actions/setup-node@v6 を足して、ロックし直さずに動かそうとすると、起動の時点で弾かれました。 HTTP 422: Invalid Argument - failed to parse workflow: Invalid dependency lockfile ".github/workflows/actions.lock": workflow ".github/workflows/use-action.yml" references actions not present in the lockfile: actions/setup-node@v6 ほかのロック済みのワークフローは、この間も普通に動きました。 そのほかに分かったこと - gh actions-lock は、ロックファイルを作るだけでなく、ワークフローも書き換えます。actions/checkout@v7 は @v7.0.1 に、actions/upload-artifact@v7 は @v7.0.2 に変わりました。v1 のように semver でないタグは書き換えず、「完全な semver の参照を使う」よう info で勧めてきます。 - container: のイメージ(alpine:3)はロックされず、「digest で固定していない」と warning が出ます。 入れるときにやること - PR で gh actions-lock --verify を動かす。ロックファイルに載っていないワークフロー(E3)と、載っていないアクション(E4)を、マージ前に止められます - --relock の出力の ref-moved を確かめてから commit する。動いたタグは、上流のリリースノートやタグの履歴で、まっとうな更新か確かめます(E7) - 別リポジトリの再利用ワークフローはコミット SHA で固定する。ロックファイルでは守られません(E8) - コンテナのイメージは digest で固定する - ワークフローを足したらロックし直す。--verify を CI に入れておけば、忘れても止まります 確かめていないこと - Dependabot がアクションを更新するときに、タグが動いていたらどうなるか。Changelog には、Dependabot がロックファイルの行も同じ PR で作り直すとあります - Docker のアクション(docker:// や Dockerfile のアクション) - プライベートリポジトリのアクションと、GitHub Enterprise Server(gh actions-lock は対応していません) - ブランチ(@main など)で指定したアクションのロック 参考 - GitHub Actions workflow lockfiles are generally available(GitHub Changelog, 2026-10-28) - Workflow dependency lockfiles(GitHub Docs) - github/gh-actions-lock - GHSA-69fq-xp46-6x23: Trivy ecosystem supply chain was briefly compromised - GitHub Actions で checkout@v7 のようなバージョン指定を書くのはやめよう!時代は actions.lock!(Zenn) - OTA-EITA/actions-lock-lab、OTA-EITA/actions-lock-lab-action
GitHub Actions で checkout@v7 のようなバージョン指定を書くのはやめよう! 時代は actions.lock! Progate で業務委託エンジニアをしている横江( @yokoe24 )です! セキュリティの話が重要視される昨今ですが、 みなさんは GitHub Actions の Workflow ファイルで、こんな書き方をしていませんか? steps: - uses: actions/checkout@v7 ・・・私は書いています!!🙈 この記事を書きながら、過去の個人リポジトリ、直さなきゃなぁ……って思っています。 @v7 と書くリスク npm の件などで敏感になっている方はお気付きかもしれませんが、 @v7 のような書き方は、最新の v7.x.x が使えて便利な一方1、 攻撃者が公開した、悪意のあるバージョンをインストールしてしまう危険を孕みます。 仮に actions/checkout が攻撃されて、 会社の非公開コードを攻撃者へ送信する Action になったら大変ですよね! 調べたところ、過去の事件としては 2026年3月に脆弱性スキャナー「Trivy」の GitHub Action である aquasecurity/trivy-action への攻撃があり、 (詳細: github.com/aquasecurity/trivy/security/advisories… ) 攻撃者はほとんどのバージョンタグを force push で書き換え、 認証情報を盗むマルウェアへと変貌させたことがあったそうです。 バージョンタグの中身を書き換えられているので、 仮に @v7 でなく @v7.0.1 のようにパッチバージョンまで指定していたとしても意味がなかったということです。 参考:Immutable Releases @v7.0.1 のようにパッチバージョンまで指定していたとしても意味がなかった という話を今しましたが、パッチバージョン指定が意味のある場合もあります。 それは GitHub の Immutable Releases の機能を使っている場合です。 この機能を使って公開されたリリースと同じタグ名を使ってリリースすることはできません。 github.com/aquasecurity/trivy-action/releases/tag… 先述した trivy-action ではこの機能が使われ、上の画像のように Immutable の印が付いていますが、 残念ながら actions/checkout では、2026年10月29日現在、この機能は使われていません。 (GitHub がメンテナンスしていると思っていたので、機能を活用していないのは意外) じゃあどうするか? 解決方法は2つあります! コミットのハッシュ値(SHA)を指定する方法と、lockfile を使う方法です。 GitHub の公式ドキュメントでも、 ロックファイルを使って固定するか、導入できない場合はコミット SHA を固定するかを案内しています。 解決策1:コミット SHA で固定する 完全な長さのコミット SHA を指定する方法から書きます。 こうなります。 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 長いですね!!! 「完全な長さの」と言われているように、 3d3c42 などと省略せずハッシュ値全体を書くのがポイントです。 たまたま別のものと前方一致するといけませんからね。 これであれば、あとから v7.0.1 のタグが別のコミットへ付け替えられても、その変更には追従しません。 解決策2:ロックファイル ( actions.lock ) を使う これは2026年10月14日にパブリックプレビューが発表され、 10月28日に一般提供(GA)となったばかりのアツアツな(?)機能です!🔥 公式ドキュメントで『最善の対策』とも言っています。 全体の作りとしては、 Workflow ファイルにバージョン番号を書き、 コミット SHA はロックファイル側に書かれる仕組みです。 ロックファイルは .github/workflows/actions.lock に置かれます。 Workflow 側 steps: - uses: actions/checkout@v7.0.1 actions.lock 側 dependencies: 'actions/checkout@v7.0.1': ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' ただ、これを見ると、 結局ハッシュが書かれるなら、 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 の書き方でも良くない?! ファイルが2つに分かれてむしろ読みにくくなってない?! ……って思いますよね? そう思ったのでAIに聞いてみたところ、 Action の内部で、さらに別の Action が使われている場合に、その依存先まで固定できることが肝要と知りました! 先述した trivy-action の件、実は私たち使用者が - uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # v0.28.0 のような書き方をしていたとしても、事件当時、trivy-action の内部では uses: aquasecurity/setup-trivy@v0.2.1 と書かれていたため、 setup-trivy が改ざんされるだけで被害に巻き込まれる脆弱さがありました。 github.com/aquasecurity/trivy-action/blob/v0.28.0… lockfile の場合、内部の Action への参照もたどり、それぞれのコミットハッシュを記録してくれるため、 このような問題にも対処できるのだそうです! つよい!💪💪 実際にロックファイルを作って見てみましょう。 actions.lock を作ってみる! gh コマンドのインストールがまだなら GitHub CLI をインストールし、 gh auth login でログインしておきましょう。 その後、公式の拡張機能をインストールします。 gh extension install github/gh-actions-lock そして、リポジトリのルートで以下のコマンドを実行すれば完了です! gh actions-lock .github/workflows/ 内の Workflow が読み込まれ、 .github/workflows/actions.lock が生成されます。 また、Workflow ファイル側も以下のように具体的なバージョンへと書き換えられます。 - uses: actions/checkout@v7 - uses: actions/checkout@v7.0.1 便利ですね! @v7 のような曖昧なバージョンは許されません。 actions.lock があると GitHub Actions は "Using locked action versions from the workflow's lockfile" とメッセージを出し、 指定されたバージョンをセットアップ時にダウンロードします。 actions.lock を見てみる 複数の Workflow を用意して、バージョン違いや依存関係の記録がどうなるのか見てみましょう。 Workflow 使う Action ci.yml checkout@v7.0.1、setup-node@v6.5.0 scan.yml checkout@v6.1.0、trivy-action@0.35.0 生成される actions.lock は以下のようになります。 This file is machine-generated by gh actions-lock. Do not edit by hand; run gh actions-lock to update. Docs: gh.io version: 'v0.0.3' workflows: '.github/workflows/ci.yml': - 'actions/checkout@v7.0.1' - 'actions/setup-node@v6.5.0' '.github/workflows/scan.yml': - 'actions/checkout@v6.1.0' - 'aquasecurity/trivy-action@0.35.0' dependencies: 'actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809': ref: 'v4.2.4' commit: 'sha1-0400d5f644dc74513175e3cd8d07132dd4860809' owner_id: 44036562 repo_id: 215566462 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8': ref: 'v5.0.0' commit: 'sha1-08c6903cd8c0fde910a37f88322edcfb5dd907a8' owner_id: 44036562 repo_id: 197814629 'actions/checkout@v6.1.0': ref: 'v6.1.0' commit: 'sha1-d23441a48e516b6c34aea4fa41551a30e30af803' owner_id: 44036562 repo_id: 197814629 'actions/checkout@v7.0.1': ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 'actions/setup-node@v6.5.0': ref: 'v6.5.0' commit: 'sha1-249970729cb0ef3589644e2896645e5dc5ba9c38' owner_id: 44036562 repo_id: 189476904 'aquasecurity/setup-trivy@e6c2c5e321ed9123bda567646e2f96565e34abe1': ref: 'main' commit: 'sha1-e6c2c5e321ed9123bda567646e2f96565e34abe1' owner_id: 12783832 repo_id: 869952525 uses: - 'actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809' - 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' 'aquasecurity/trivy-action@0.35.0': ref: '0.35.0' commit: 'sha1-57a97c7e7821a5776cebc9bb87c984fa69cba8f1' owner_id: 12783832 repo_id: 268833180 uses: - 'actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809' - 'aquasecurity/setup-trivy@e6c2c5e321ed9123bda567646e2f96565e34abe1' uses の項で、その Action が依存している Action の一覧がわかりますね! バージョンアップしたくなったら? Workflow 内で使っている外部 Action のバージョンを更新する場合は、 まずは通常通りファイル内のバージョンを書き換えます。 悪意のあるバージョンへ更新しないよう、更新先のリリースノートや、更新から日が浅くないかはチェックしておきましょうね! - uses: actions/checkout@v6.0.2 - uses: actions/checkout@v6.0.3 そのあとで actions-lock コマンドを実行します。 gh actions-lock これだけでOKです! actions.lock が更新されます。 なお、 gh actions-lock を忘れて Workflow ファイルだけを更新してコミット・pushした場合、 GitHub Actions は Invalid lockfile エラーで CI を失敗させます。 まとめ: gh actions-lock を使おう! ってなわけで、 actions/checkout@v7 のような書き方のリスクと、 その対策を話してきたわけですが、まとめとしては簡単ですね! 拡張機能のインストール gh extension install github/gh-actions-lock actions.lock の作成・更新コマンド gh actions-lock github/gh-actions-lock 拡張機能を gh コマンドにインストールしたあと、 リポジトリで gh actions-lock ! これだけ! 今から出来ますね!! これだけで、サードパーティーの GitHub Action を使っていることによる 予期せぬ攻撃から身を守ることにつながります。 みんなでインターネットを守っていきましょう!!🛡️🦾 ここまでお読みくださり、ありがとうございました!💝 脚注 厳密には、メンテナーが v7 タグを最新バージョンのコミットに付け直してくれている場合に限る。たいていはありがたいことにやってくれている…… ↩︎
Вызываем паранойю у ИИ-агентов: заменит ли MCP AppSec? Привет, Хабр! Я не писал на Хабре уже 15 лет, но всё это время хотел. Было много всяких разных проектов, продуктов, утилит и много чего ещё, о чём бы следовало написать. Но либо проекты устаревают быстрее, чем выдастся свободная неделька, либо NDA намекает не раскрывать деталей, без которых не получится забористо, либо просто лень. В общем “хорошие мысли преследовали меня, но я был быстрее”. И вот появилось время, чтобы… Во-первых, примерить на себя роль владельца продукта, не знаю уж почему, но всегда хотелось попробовать себя в роли повелителя бэклога. Во-вторых, опробовать всякие модные Агенты, LLM и прочую нечисть, поработав над интересной мне задачей. TLDR Раньше писали скрипты через vim прямо на проде, потом отлаживали через print i,z,b и кофейную гущу, а теперь запускаешь Cursor — и за вечер появляется веб-приложение, а CI/CD уже катит его на прод. Кода стало больше, он стал запутаннее, и хакеры уже не сидят в зелёном терминале — AI-эра тут. Старая добрая триада SAST/SCA/DAST с потоком кода ещё справляется, а вот AppSec становится узким местом. Поэтому я собрал себе ИИ-помощника с лёгкой степенью паранойи: локальный MCP-сервер AppSecGate, который запускает сканеры, ведёт базу файндингов и заставляет агента бояться уязвимостей. В статье: как агенты встраиваются в SSDLC, что происходит под капотом MCP, бенчмарк семи локальных моделей на одной RTX 4060 Ti — без явных победителей, но с явными выводами. И ответ на главный вопрос: заменит ли MCP классический AppSec? Спойлер: нет, но рутину снимет, оставив безопаснику архитектуру и логику. Итак, как было раньше или ещё сейчас SSDLC даёт довольно прозрачный, проверенный временем и понятный путь сделать разрабатываемое приложение безопасным. Хотя есть несколько стандартов, все они описывают один и тот же подход, но с разных сторон. Вот пример минимального жизненного цикла разработки. Рис. 1. Классический SSDLC: люди, инструменты, агрегаторы Легенда: зелёный — люди и роли, фиолетовый — инструменты безопасности, голубой — системы агрегации и учёта, оранжевый — агенты. Разработчик (на схеме — Developer) пишет код, используя свою любимую среду разработки (IDE). После написания код попадает в систему контроля версий (Git), где в рамках процесса сборки и тестирования (CI/CD) запускаются сканеры безопасности (SAST/SCA/DAST). Отчёты от сканеров загружаются в агрегатор файндингов (ASOC/ASPM), где обрабатываются, дедуплицируются, обогащаются и вот их наконец-то видит безопасник (AppSec). Он триажит файндинги, тестирует руками и, если ему что-то не понравилось, пишет задачу в систему управления заявками (TaskTracker). Задача доходит до разработчика, он её исправляет и процесс уходит на следующий круг, и так, пока всем не надоест. За всем этим безобразием естественно наблюдают руководители (Leads), они приоритизируют задачи, налаживают коммуникации, а иногда даже могут ходить в ASOC, чтобы посмотреть на красивые дашборды. Кстати, если в данной схеме заменить ASOC на TMS, а AppSec на QA, то схема не изменится совсем. У такого “наивного” подхода есть много минусов, но самый жирный — это непредсказуемая скорость исправления уязвимостей. Например, сломались сканеры, дырявую библиотеку нечем заменить или AppSec ушёл в отпуск, а надо релизиться. Вот и получается, что время от появления уязвимости в коде до её фикса может варьироваться от почти мгновенного до когда рак на горе свистнет. Но, несмотря на минусы, у него есть один перекрывающий всё “плюс” — чёткое и понятное разделение ответственности. Разработчик отвечает за код, безопасник за безопасность, руководители за создание ценности. А сопутствующие проблемы — это и не проблемы вовсе, поскольку решаются деньгами. Больше разработчиков, больше AppSec, дорогие сканеры, автоматизация процессов и так далее, пока деньги не закончатся. И я, несмотря на весь сарказм в описании процесса, против такой организации ничего не имею, потому что это работает! И, что самое главное, можно прозрачно и понятно донести до бизнеса, что если хочешь безопасности — плати. Надо сказать, что есть множество вариантов качественного улучшения процесса, но востребованы они чаще всего в специализирующихся на разработке компаниях, где критически важен TimeToMarket. И вот на сцену выходят LLM Несмотря на то, что я довольно давно в ИБ и вообще деревянный по пояс, но старался держать руку на пульсе и возможности LLM меня удивили не больше, чем их создателей. По факту я оказался абсолютно не готов к новой реальности, где агенты захватили умы и бюджеты, в том числе и мой. Статья написана руками, но дальше я опираюсь на данные, полученные от ИИ, и строю прогнозы и предположения, исходя из собственного разумения. Первым на появление LLM, конечно же, отреагировали разработчики и принялись радостно переходить на них. Сперва просто нажимали TAB, и функция писалась сама, потом просили реализовать такой-то метод в таком-то файле, и моделька сама всё писала, ну и по итогу разработчик просто скармливает агенту ТЗ, которое написала моделька по задаче, которую написала моделька, и следит, чтобы она сильно от него не отклонялась. И всё это привело к тому, что кода стало больше, а сам он стал сложнее и запутанней. Надо бы отметить, что хотя бы документация теперь есть, и она актуальная, и ей, наверное, можно даже верить. Увеличение количества кода увеличивает нагрузку на AppSec, не только увеличивая объём триажа, но и повышая сложность ручного тестирования. И внезапно AppSec становится узким местом, что усложняет и без того напряжённые отношения. Но всё-таки в индустрии безопасности тоже читают Хабр и не стоят на месте, когда другие интегрируют LLM. На помощь AppSec приходят замечательные ИИ-инструменты — ИИ-триаж, ИИ-пентест, ИИ-SAST, ИИ-SCA — всё это аккуратно упаковывается в продукты и продаётся и, конечно же, покупается и внедряется. Да и агента грех не использовать в работе, очень уж это сокращает время на поиск точек входа. Правда, есть вариант, что публичные LLM запрещены политикой компании, и тем, кто её пишет (это, кстати, мы) по-хорошему надо её соблюдать. В общем, если внедрить всё, что можно для триажа, то часть рутинной работы снимается. А вот сама структура работы перестраивается в сторону поиска логических уязвимостей и проработки безопасной архитектуры. Кстати, ИИ-бум породил совершенно новую категорию разработчиков — вайб-кодеров (Vibe-coding). Зачастую они реализуют проекты в одного, при этом могут быть далеки от программирования и безопасности, зато у них есть уникальные идеи, востребованные на рынке. Самое забавное, что идея может быть действительно востребованной и денежной. Менее забавно то, что им приходится верить, что LLM пишет безопасный код, не путает дев с продом и вообще не ошибается. Но, к сожалению, пока такие надежды легко разбиваются о реальность и с этим всем приходится мириться. По итогу получилась забавная ситуация, когда в процесс разработки вдруг вклиниваются агенты и захватывают его полностью. Рис. 2. Агенты встраиваются в каждую роль процесса Получается, что теперь процесс изменяется с “Я подумал и сделал” на “Я подумал, написал агенту, он сделал, я проконтролировал”. Давайте “прочитаем” процесс “по ролям”. Руководители генерят идею, агент скребёт по бэклогу и предлагает создать задачу на разработку Разработчик просит агента посмотреть бэклог и выбирает задачу. Если задача ок, то отдаётся команда — “фас” и код попадает в Git CI/CD запускает агента, который сканирует код на уязвимости и исправляет их или просто генерирует отчёт ASOC/ASPM получает все отчёты, нормализует их и отправляет агенту, чтобы тот протриажил и выдал своё заключение AppSec просит агента посмотреть файндинги на ревью, и всё, что ему не понравится, отправлять разработчику в виде задач Руководители в процессе работы просят агента поскрести по бэклогу, находят задачу от AppSec и перекидывают её на ответственного специалиста И вот петля обратной связи затянулась, план по токенам выполнен, задача закрыта, все довольны и идут пить кофе. Понятно с первого взгляда, что процесс не самый оптимальный. Но при классическом разделении зон ответственности, к которым прилагается бюджет, такой вариант может сложиться “сам собой”. Взаимодействие Agent-to-Agent И тут самое время вспомнить про Lean и DevOps (не все знают, но DevOps — это практически адаптация Lean к пайплайну разработки) и прочие прекрасные инструменты из недалёкого прошлого. А они нам предлагают искать максимально короткий путь обратной связи и маленькими шажками двигаться по нему. И такой путь, если взглянуть на схему, становится очевиден — дать агентам возможность работать напрямую друг с другом. Но тут есть нюансы… Если дать агентам: Разработчик, Руководитель и Безопасник, обсуждать приоритизацию бэклога, то кто из них быстрее поймёт, что с помощью промпт-инъекции договориться проще и быстрее. Но самое интересное — не закончится ли бюджет на токены раньше, чем они придут к этой мысли. Звучит как начало классического анекдота “Заходит в бар…”, кстати такие анекдоты последнее время публикуют в новостях, правда почему-то не в разделе “Юмор”. Как бы ни хотелось полной автоматизации, пока что она, скорее всего, заведёт нас в тупик. Так что возвращаемся к старому доброму системному подходу и начинаем думать. Собственно, немного подумав, я нарисовал следующую схему. Рис. 3. Системный вариант: меньше агентов, структурированный обмен, ASOC как система для агентов Что мы тут видим в отличие от прошлого варианта: TaskTracker — исключён из процесса обработки файндингов. Потому что раньше он передавал неструктурированную информацию от одного человека к другому, а для агентов лучше сразу обмениваться структурированной информацией. Но при этом трекер может остаться в схеме, например для учёта времени, списания токенов и т.п. В процессе CI/CD остался только один агент, но он в процессе сканирования получает контекст по файндингам из ASOC. И это единственный автономный агент, и он не обязателен, с его работой может справиться агент, запускаемый Безопасником. Нужен он или нет — зависит от того, готовы ли вы к автономным агентам или нет. Система ASOC/ASPM трансформируется из системы для людей в систему для агентов. Сохранится ли у неё старое название или будет называться по-другому, пока не ясно. А вот чего не видно, — это то, что можно лёгким движением руки сделать shift-left и сдвинуть этап сканирования на этап написания кода и заставить агента сканировать проект самостоятельно. В пользу этой гипотезы можно привести следующие тезисы. Разработчику не нужно париться, так как агент сам будет выполнять сканирование, проверит результаты и попробует исправить. Короче, ничем это не будет отличаться от форматера или юнит-тестов, которые агенты уже научились запускать самостоятельно. Нет смысла блокировать push и останавливать разработку, если вдруг сканер чего-то там нашёл. Проблема автоматически эскалируется на AppSec/Lead. Но при всём при том разработчику проще устранить проблему сейчас, нежели ждать, пока эта проблема прилетит сверху. При сканировании diff можно сильно экономить токены за счёт того, что Агент и так работает с этим файлом. Весь локальный триаж фиксируется на центральном сервере, так что мы можем контролировать процесс и, что самое главное, доказать эффективность подхода руководству Побочным продуктом такого сдвига может получиться неплохой инструмент для SOLO-VIBE-CODER’ов, которые, с одной стороны, читают новости и не хотели бы в них попадать в качестве иллюстрации ИИ-провалов, а с другой гонятся за TimeToMarket и не могут потратить вечность на изучение вопросов безопасности. А будет ли это работать? Для проверки подхода я решил не размениваться по мелочам и реализовать MVP локального MCP-сервера. Он будет запускать сканеры, возвращать результаты по запросу и хранить базу сканирования и триажа. Получилась вот такая вот архитектура. Рис. 4. Архитектура MVP AppSecGate MCP: изолированный Docker, сканеры, SQLite, задел на центральный сервер В процессе реализации хотелось ответить на несколько важных для меня вопросов. Сможет ли агент самостоятельно разобраться в инструментарии, если я попрошу “А проверь-ка, голубчик, проект наш на безопасность, да AppSecGate используя непременно”? Сколько времени/денег обойдётся разработка первой версии? Как это всё будет работать? Справятся ли с ролью агента локальные модели, доступные на моём железе “NVIDIA RTX 4060 Ti 16GB”? Словлю ли я “вайб”? Что дальше? А проверь-ка голубчик… И вот спустя 7 дней разработки, ~210 миллионов токенов, ~3000 рублей и ~8 литров кофе у меня на руках оказался собранный образ MCP-сервера. К тому моменту я уже переключился с Continue на Cline, написал AGENTS.md и вообще преисполнился, но об этом позже. Клонирую OWASP Juice Shop, без которого не обходится ни одно тестирование ASOC, подключаю AppSecGate MCP в Cline и в режиме планирования пишу “Проверить безопасность проекта” и наслаждаюсь магией. По результатам нескольких прогонов получилось выявить следующий путь рассуждений агента. Агент проверяет конфигурацию через get_config Запускает сканирование проекта scan_project и получает агрегированный результат сканирования Проводит анализ находок, используя SQL-доступ к базе файндингов query_findings_db. Не знаю почему, но Агенту нравится пользоваться именно таким вариантом, хотя есть альтернативы. Результат анализа содержит перечисление файндингов и предложение ничего не исправлять, потому что это OWASP Juice Shop и всё так и задумано. Приходится доказывать агенту, что я тут инструменты проверяю и придётся фиксить всё. Получив-таки рабочий план, перевожу его в режим Act и агент принимается за работу Дальше идёт обычный триаж. Получение контекста по срабатываниям — get_fix_context_batch; если требуется, исправляет и, используя verify_fix_batch, проверяет исправление или подавляет файндинг, если это FALSE_POSITIVE. Периодически агент проверяет закрытие файндингов через query_findings_db и продолжает их устранять. По итогу он формирует отчёт о проделанной работе с основными исправлениями Последний прогон занял ~50 минут с использованием deepseek-v4-flash и стоил ~15 рублей. За это время обработано 200 находок: 56 исправлено (из них 8 critical, 45 high, 3 medium), 27 признано WONT_FIX (в основном учебные codefix-данные и публичный ALB), 117 отмечено как FALSE_POSITIVE (тестовые секреты Gitleaks). Изменено 29 файлов, diff +333 / −115 строк. Расход токенов: 16 212 728 input (из них 15 173 760 — cache reads) и 56 411 output. Действия агента более чем предсказуемые, а рассуждения оправданны. Отдельно поразило, как ловко модели научились оперировать результатами сканеров и исправлять файндинги. Не могу утверждать точно, но, кажется, модели специально натаскивали под подобные задачи, что в целом неудивительно. Сценарий с мягким shift-left я тоже пробовал отработать. Изначально была идея использовать хуки, но как они работают понять не смог и нашёл вариант добавить требование в AGENTS.md, можно и через rules, но, кажется, оба варианта по итогу обрабатываются одинаково. И вариант с AGENTS.md в теории универсален для разных типов агентов. Перед тем, как добавлять правила, рекомендую выполнить один прогон вручную, чтобы агент набрал контекста по использованию MCP. Важно помнить, что бы вы ни написали в правилах, модель может это проигнорировать или воспринять неправильно. Всё хорошо, но проект явно сыроват и требует времени на допил. Во время прогонов я заметил несколько проблем (уверен, что их больше). Один раз модель потеряла 20 файндингов и нашла, когда я её попросил повторно просканировать проект Cline не сжимает контекст, пока задача полностью не закончится, почему — непонятно. Это точно неправильное поведение, которое может влиять на расход токенов и скорость сканирования. Иногда MCP обманывает агента, например, посчитав два дубля файндинга за один, что приводит к дополнительным рассуждениям Иногда возникает задумчивость при работе с query_findings_db, например, агенту требуется узнать состав полей Когда тестировал pre-commit, намеренно изменил файл и попросил закоммитить изменения. Агент запустил скан, но пометил находки как WONT_FIX, типа, человек же сам исправил, значит, так надо… Но самое главное — оно работает. Да, нужно поправить баги, да, нужна центральная база хранения — но это уже дело техники. Прежде чем превращать MVP (а если смотреть на полный функционал — будем честны, скорее PoC) в полноценный продукт, я решил глубже погрузиться в магию MCP и собрать побольше грабель. Как рождается магия MCP При запуске IDE агент считывает конфигурацию. Ниже пример конфигурации Cline (.cline/data/settings/cline_mcp_settings.json) для запуска локального MCP-сервера: { "mcpServers": { "appsecgate-jshop": { "type": "stdio", "command": "/usr/bin/docker", "args": [ "run", "--rm", "-i", "--network=none", "--read-only", "--tmpfs=/tmp:size=100M,mode=1777", "--tmpfs=/home/appsecgate/.cache:size=2G,mode=1777", "--tmpfs=/home/appsecgate/.appsecgate-mcp:size=100M,mode=1777", "--cap-drop=ALL", "--security-opt=no-new-privileges:true", "-v", "./projects/juice-shop/:/workspace:ro", "moondoom/appsecgate-mcp:latest", "--transport", "stdio", "--project", "/workspace" ], "disabled": false, "timeout": 600 } } } sourcecraft.dev В примере используется локальный запуск сервера “type”:“stdio”, что указывает агенту на необходимость самостоятельно запустить сервер, выполнив “command” с “args”. Есть ещё вариант удалённого сервера “type”: “http” и “url” вместо “command”. В варианте “stdio” есть как плюсы, так и минусы: Плюсы: Используется образ Docker — это позволяет легко распространять приложение, то есть конфигурация универсальная и не требует установки дополнительного софта, кроме Docker, конечно же. Для stdio не требуется наличие сети в контейнере, так что его можно запускать в режиме максимальной изоляции. Это в целом снижает риски, связанные с запуском “чужого” кода, что не может не радовать такого параноика, как я. Можно использовать volume для хранения постоянных данных, а можно и tmpfs прописать, и тогда они сами будут чиститься при каждом запуске, что удобно для отладки. В примере отладочный вариант представлен, для прода с README.md ознакомится ты должен сперва. Минусы: Требуется в явном виде пробрасывать исходники проекта в контейнер. Для связки VS Code + Continue — можно использовать переменные окружения, а вот связка VS Code + Cline у меня так и не завелась и пришлось хардкодить путь проекта (судя по всему из-за того, что у меня VS Code установлен через snap, а так-то вроде можно) Изоляция приводит к тому, что базы правил для SAST/SCA нельзя обновлять в режиме онлайн. Для MVP это не критично, для Enterprise вообще огонь, но вот для массовой аудитории может создать трудности с обновлением библиотек. Пока вижу вариант передавать аргумент “–pull=always”. Постоянные volume могут вызывать проблемы при обновлении сервера из-за разных версий баз данных, но это проблемы будущих итераций :) После запуска контейнера Агент начинает своё общение с сервером. Проводит инициализацию (initialize) и получает список доступных на сервере тулов (tools/list). Далее список тулов косвенно встраивается в SYSTEM_PROMPT и передаётся при каждом запросе к модели. Таким образом, модель понимает, что ей доступны tool-ы и сразу получает исчерпывающие инструкции. Очень удобно для пользователя и в равной степени для злоумышленников. Ведь для успешной атаки на агента через MCP достаточно просто подключить его и отправить хотя бы один запрос, а дальше модель, повинуясь внутреннему голосу, начнёт исполнять волю “Хаоса” и творить “добро” направо и налево. Это известная проблема и современные модели должны изолировать тулы и не воспринимать их как прямые указания к действию, но это не точно и зависит от модели. Так что будьте осторожны при подключении любых MCP, даже если вы запускаете его удалённо или в изолированной среде. Как вариант можно вручную проверить список тулов полученных от сервера в интерфейсе расширения, до первого запроса к агенту. Соответственно, при получении запроса “Проверить безопасность проекта” модель уже “знает” об инструментарии, доступном ей, и принимает единственно верное решение — использовать инструмент с подходящим описанием. Дальше поведение агента уже зависит от системного промпта и результатов которые вернул MCP. То есть у нас фактически есть три “крутилки”: Системный промпт. Определяет общее поведение агента, например — “митигировать, нельзя пофиксить” или “митигировать нельзя, пофиксить”, а третьего не дано. Описание тулов. Указывает какие инструменты использовать и как обрабатывать их результаты Результаты сканирования. Это один из самых вариативных инструментов, меняя описание правил и критичность различных категорий, можно регулировать поведение агента в зависимости от типа уязвимости. Про важность этих “крутилок” я узнал, пока тестировал локальные модели, но эту историю нужно рассказать подробнее. Тестирование open-source моделей Мне, как безопаснику, долгое время боровшемуся с утечками корпоративных тайн, горько смотреть, с какой лёгкостью сейчас люди переходят на публичные модели. Поэтому при работе с исходниками компании я всегда использовал только локальные LLM — благо ещё до повышения цен собрал себе неплохой сетап. Но железо ≠ магия, так что без подводных камней не обошлось. Железо Всё крутится на одном хосте под Proxmox VE, с включённым IOMMU и обеими GPU, проброшенными в гостей: Хост (PVE) VM “Workstation” VM “LLM” CPU i7-13700K, 16C/24T (8P+8E), до 5.4 GHz 8P 8P RAM 64 GB 24 GB 24 GB GPU — Intel UHD 770 NVIDIA RTX 4060 Ti 16GB Монитор только на старте да (подключён к матери) headless Диск Samsung 980 PRO NVMe ZFS ZFS Почему именно так: Обе GPU проброшены в гостей — хост “голый”, ни i915, ни nvidia в нём не грузятся. Workstation на Intel UHD 770 — там монитор, VS Code + Cline, браузер и Docker с MCP-сервером AppSecGate. Модели не крутятся, GPU нужна только чтобы рисовать интерфейс. LLM на 4060 Ti 16GB — headless, только под вычисление. Плюс такой конфигурации в том, что видеокарта полностью отдана под вычисления: ollama, hashcat или, например, jupyterlab + pytorch. Причём вся эта радость в докере, так что можно менять конфигурации как перчатки. Ну а если надо поиграться с настоящими ИИ можно загрузить Win и погонять ИИ-скинов в Cyberpunk через Moonlight. Есть и проблемы, но я к ним уже привык. Изначально хотел тестировать модели руками в Cline и по ходу дела делиться впечатлениями. Полагаясь на свой опыт работы с deepseek-r1:14b, думал, что слабые модельки попросту не смогут нормально работать с инструментами и будут лажать. Но, запустив более-менее современную модель, понял, что всё не так плохо. Пришлось пересматривать свои планы и быстренько собирать стенд для тестирования. Быстренько, как водится, не получилось, а получилось вот так вот. Цель для сканирования — микропроект на Flask (app.py) — 8 уязвимостей для Semgrep и Docker+requirements.txt для Trivy Тестовый Агент — с задачей сканировать, фиксить SAST и эскалировать находки Trivy AppSecGate MCP — сканирует и предоставляет агенту результаты Coding Tools MCP — в качестве замены стандартным тулам Cline Обёртка, которая всё это дело запускает и сохраняет логи и трейсы. Важный дисклеймер! Цель тестирования — проверить, что MCP вообще заработает! Тесты не претендуют на объективность, а результаты могут не отражать реальную производительность и возможности тестируемых моделей! Далее попросил DeepSeek выбрать несколько моделей, которые по его разумению сдюжат агентский режим и прогнал по ним предварительные тесты. Контекст выбрал 32768 токенов, это вроде как минимум для агентов. Размер и раскладку CPU/GPU брал из ollama ps. Никакого дополнительного тюнинга не применял, просто загрузил, используя ollama pull. Передварительно решил оценить скорость моделей. Модель Параметры Tools Thinking Размер CPU / GPU t/s gemma4:26b 25.2B да да ~16 GB 21% / 79% 84.0 gpt-oss:20b 20.9B да да 12 GB 0% / 100% 67.8 qwen3-coder:30b 30.5B да нет 15 GB 1% / 99% 58.5 glm-4.7-flash 29.9B да да 15 GB 1% / 99% 52.0 qwen2.5-coder:14b 14.8B да нет 15 GB 0% / 100% 31.0 qwen3.8:27b 27.3B да да 17 GB 35% / 65% 12.1 devstral:24b 23.6B да нет 20 GB 23% / 77% 9.5 Судя по токенам в секунду, думал, что лидерами будут qwen3-coder:30b или glm-4.7-flash, потому как большие и быстрые, хотя и не полностью на GPU. Первые прогоны выявили лидера — glm-4.7-flash, на нём я прогонял все предварительные тесты и тюнил промпт. Про то, что у меня есть ещё и gemma4:26b я вообще забыл и в первой табличке долго не замечал её отсутствие, а выплыла она уже когда заканчивал статью… Потому не рассматривал её как серьёзного конкурента. Писал агента не сам, но пока корректировал действиями Cline, выделил те самые “крутилки” из предыдущего параграфа. А ещё с удивлением для себя обнаружил, что в моём MCP описание инструментов написано на русском, что вроде как не круто, поэтому оперативно поменял. Так что, более-менее приведя агента в рабочее состояние, я запустил тестирование, скормил логи агенту и принялся изучать результаты. Модель Recall (mean) Fixed (med) Токены (mean) Время (mean) Pass qwen3.8:27b 100% 8 314k 650s 3/3 gemma4:26b 87% 6 879k 419s 3/3 qwen3-coder:30b 73% 5 877k 451s 2/3 glm-4.7-flash 73% 7 1.17M 772s 2/3 gpt-oss:20b 47% 0 254k 89s 1/3 devstral:24b 3% 0 4.7k 25s 0/3 qwen2.5-coder:14b 0% 0 4.2k 5s 0/3 А результаты меня удивили! Во-первых, мой фаворит glm-4.7-flash — залажал в самом конце и ушёл в петлю, а qwen3.8:27b взял и победил, что называется чисто на скиле. Для серьёзных выводов трёх прогонов, конечно, маловато, но у меня и не стояло такой задачи. Агент распределил места следующим образом. Модель Вердикт Обоснование qwen3.8:27b 🏆 “Победитель” 100% recall × 3 прогона, 8/8 fixed, минимум токенов среди топа, 0 ошибок gemma4:26b 🥈 Второй Стабилен (3/3, 80–90%), самый быстрый среди прошедших (419s), но в 2.8× дороже по токенам и медианно 6/8, а не 8/8 qwen3-coder:30b ⚠️ Средний Recall нестабилен (40–100%), patch_succ 0.57 — часто бьёт мимо после сдвига строк glm-4.7-flash ⚠️ Нестабилен Один прогон — 100%/376k, другой — 20%/1.8M с 3 ошибками. Высокий ceiling, но не гарантирует gpt-oss:20b ⚠️ Слабый 1/3 pass, 6 tool exceptions, два прогона из трёх — 20–30% recall devstral:24b ❌ Не оценить 0 tool-calls, JSON в тексте вместо вызова qwen2.5-coder:14b ❌ Не оценить То же — JSON-описание вместо tool call Главные выводы прогона. Небольшие модели могут стабильно работать в роли агента, пусть и с ограничениями. Скорость по токенам для агентов важна, но может ничего не стоить, если модель плохо ориентируется в инструментах или уходит в петлю. Лично для меня это означает, что можно автоматически сканировать и размечать большие проекты по цене электричества. При этом я, скорее всего, буду использовать glm-4.7-flash для большинства задач, возможно под контролем более тяжёлого, но стабильного qwen3.8:27b или всё же внимательнее присмотрюсь к gemma4:26b. Ещё думаю поиграться с настройками ollama, квантованием и пошерстить Hugging Face. Нужно больше тестов, больше прогонов, красивых табличек и ещё более красивых графиков. А пока я вдоволь наигрался в тестировщика и готов поделиться своими незабываемыми впечатлениями и далеко идущими выводами. Лёгкий оффтоп про “вайб” Если говорить честно, то всё, что написано выше, — это рассуждения, к которым я приходил по ходу реализации первой фазы проекта. А изначально я просто напрягал DeepSeek своими рассуждениями о переработке резюме и предложил оценить меня как Продукта. Потом была мысль, а не сделать ли свой пет-продукт. Слово за слово и вот “architecture.md” уже сохраняется в пустой каталог “docs”, а агенту ставится задача реализовать первую фазу проекта. А дальше всё как в тумане… И за три недели блужданий из тумана я так и не выбрался, лошадок не видел, ёжиков тоже, но зато вместе с ИИ поиграл в ролевые игры. Я-Продукт — хочу сделать продукт, чтобы и самому пользоваться и другим не стыдно было поставить. В общем исследуем рынок, смотрим кто там конкуренты… о, кажется, пора уже и запускать. ИИ-Программист — а чего ждать, сперва одну фичу, потом другую, потом баги, баги и ещё раз баги… Я-Безопасник — надо накрутить изоляцию в докере, проверить лицензионную чистоту и вот ещё бы изолировать Cline, а то один “rm -rf /” и всё… Я-Инженер — надо оптимизировать сборку докера, потом подумать, как будем релизиться. А ещё безопасник что-то там писал про изоляцию, но это потом. Я-Архитектор — так что-то продукт плохо работает, надо подумать о дедупликации файндингов, слышал недавно про какие-то векторные базы, надо прикрутить. DeepSeek говорит, нормальная штука, вот тебе ADR-0001 Я-Продукт — Так, версия 0.1.0 уже, считай, релиз, начинаю писать статью! Так, сперва процессы, потом технины добавить, но тут пусть Инженер с Программистом думают. А ещё надо рекламой заняться, ё-моё!!! оказывается 90% рынка MCP англоязычный… Надо срочно перевести всю документацию на английский… И так целыми днями, пока Агент фиксит очередной баг или делает рефакторинг, ты уже переключился на другую роль, исследуешь очередную проблему. Я бы не назвал это вайбом, но процесс затягивает и балует мозг изрядным количеством дофамина. Особенно цепляет скорость, с которой можно воплощать свои идеи в жизнь. Причём скорость тут зависит не от привычных всем переменных — сколько программистов, какая у них квалификация и какая атмосфера в команде, а от знаний конкретного человека и от того, сколько у него денег на токены. И скорость — это то, что бросается в глаза, а за скоростью прячется ещё и куча возможностей. Например, в разрезе моего проекта я не вижу смысла засовывать под капот множество сканеров или делать гибкую настройку workflow, это легко может сделать любой желающий одним промптом. Ведь для агента не важно, кто его запустил, изначальный разработчик или просто рандомный чувак, которому нужна какая-то фича, контекст будет одинаковый, а вот результат скорее всего будет ближе к ожиданиям заказчика. И что дальше У проекта был изначальный роадмап, который исходил из кратчайшего пути к готовому продукту. Но в процессе работы над проектом и статьёй я получил новые вводные, о части которых даже не догадывался на этапе планирования. Народ уже строит целые операционные системы из агентов, в которых нет IDE и минимум людей. Я пока не понимаю, насколько такой подход захватит мир, но надо разбираться. Потенциально, это новые клиенты, для которых проект может стать “санитарным минимумом” по внедрению SSDLC. Тут уже главное показать, что продукт готов к автономной работе и не требует больших усилий по настройке, а то и может кастомизироваться агентами на лету. Локальные LLM могут работать в Агентских сценариях и надо учитывать, что продукт может заинтересовать тех, для кого важна локальность — корпоративного заказчика. Причём, скорее всего, его средняя часть, у кого есть деньги на собственное железо, но нет Директора по ИИ-трансформации. Таким клиентам бывает сложно продать “чистую безопасность”, потому, возможно, стоит добавить больше правил на code-smell и compliance. Весь проект — это одна неделя разработки, его легко повторить. Конкурентов в данной нише много, от простеньких “обёрток для сканера”, например “Semgrep MCP”, до классических ASPM, уже реализовавших MCP-интерфейс, — например, “Checkmarx One” и “BoostSecurity”, и модных ИИ-native по типу “ZeroPath”. Так что общая ценность проекта стремится к нулю и надо как-то отстраиваться от конкурентов. Например, делать упор на открытость, лёгкость и возможность быстрой доработки под конкретные флоу. Подумав немного о вводных, решил, что дальше у меня будет три основных направления развития продукта. Разработка Фазы 2, то есть центрального сервера с возможностью агрегации событий от нескольких агентов, работающих независимо. И чутка захватить Фазу 4, чтобы получить возможность анализировать данные и строить красивые дашборды. Так можно реализовать самый главный функционал и оценить стоит ли развивать проект дальше. Сделать лёгкий реверанс в сторону корпоративного заказчика и поддержать популярный в корпоративной среде сканер — SonarQube. При этом можно попробовать показать, что с доработкой правил и центральным сервером, продукт работает не хуже SonarQube, но при этом легче и быстрее. Провести боевое слаживание в сценарии с несколькими агентами, а-ля ИИ-Мастер — ИИ-Слейвы. Это самый интересный кейс, лично для меня. Хочу провести анализ 50-100 открытых проектов и посмотреть, что там по находкам. Тут можно и надёжность самой тулы проверить, и правила потюнить и самому проверить как там с безопасностью OpenSource написанного Vibe-Coder-ами. Ну и по мелочи ещё куча всяких проблем, которые надо пофиксить. В общем, планов громадьё, кофе и печеньки закуплены. Есть огромное желание поделиться результатами с сообществом — так что если хотите следить за развитием проекта и не пропустить следующую статью (спойлер: она будет не про MCP, а про моё видение безопасности LLM в целом), подписывайтесь на мой канал ArchiSecOps. А пока я готовлю продукт и план для следующей статьи, приглашаю всех желающих попробовать и оценить AppSecGate MCP.
76 of 77 trivy-action Tags Were Repointed in One Night. Some SHA Pins Didn't Help Either. On March 19, 2026, someone with stolen credentials force-pushed 76 of the 77 version tags on aquasecurity/trivy-action to point at a credential stealer. All 7 tags on aquasecurity/setup-trivy too. The only clean trivy-action tag was 0.35.0, because it was an immutable release. If your workflow said uses: aquasecurity/trivy-action@0.34.2, you didn't change a line. Your repo looked exactly the same. The next run just executed different code, for about 12 hours (roughly 17:43 to 05:40 UTC per the advisory), and that code read secrets out of the runner's memory and shipped them off. The part I keep coming back to is in Aqua's own advisory, under "You are affected if you used": > SHA pinning to a commit prior to 2025-04-09. So some people who did the "right" thing and pinned trivy-action to a full SHA were still exposed, if their job ran during the ~4 hours setup-trivy was poisoned. Not because the SHA moved. Because the action they pinned called another action by tag. A lot of small SaaS repos have a CI file that was copied from a README in an afternoon and never opened again. Often a handful of uses: lines, some on @v3 or @main, and a deploy job with a cloud key or an npm token in it. This post is the one-hour version of fixing that. Three incidents, same shape These are all public, with advisories you can read: | When | Action | What moved | Window | |---|---|---|---| | Mar 14-15, 2025 | tj-actions/changed-files | multiple version tags repointed to one malicious commit; secrets dumped into build logs | ~1 day; GitHub's advisory says it impacted "over 23,000 repositories" | | Mar 3-10, 2026 | xygeni/xygeni-action | the v5 tag moved to a commit from an unmerged pull request | ~6 days | | Mar 19-20, 2026 | aquasecurity/trivy-action + setup-trivy | 76 of 77 tags, all 7 tags | ~12 h and ~4 h | In none of these did the victim's repository change. That's the whole problem with a tag. @v5 is not a version, it's a pointer that gets looked up every time the job starts. The xygeni one is worth a second look. Branch protection worked, the bad PRs were never merged. But the commit still existed in the repo's git objects, and moving a tag to it was enough. Merged or not doesn't matter to a tag. Three ways a pin fails People argue "tags vs SHAs" like it's one decision. It's really three separate failure modes: 1. The tag moves. @v4, @v4.2.2, @main. All mutable unless the publisher uses immutable releases. This is tj-actions, xygeni and trivy-action. 2. Your pin isn't transitive. You pin owner/action@<sha>. That action's own action.yml says uses: owner/other-thing@v1. You get whatever v1 is today. This is the trivy-action pre-April-2025 case: until a commit on 2025-04-09 ("Pin aquasecuriy/setup-trivy to hash instead of tag"), trivy-action's action.yaml called aquasecurity/setup-trivy@v0.2.2. It's right there in the repo history. GitHub's own docs say it plainly now: lockfiles cover transitive dependencies, "which SHA pinning does not." 3. The SHA is real but it's not from where you think. A full SHA can point at a commit that isn't on any branch or tag of the real repo. In the trivy binary part of the same incident, the attacker swapped an actions/checkout reference to what the advisory calls an "imposter commit". GitHub's docs have a one-liner on this: when you pick a SHA, "verify it is from the action's repository and not a repository fork." It's very easy to copy a SHA from a blog post or a bot PR and never check where it lives. Here's the order I'd fix things in. Step 1: list what you actually run (10 min) From the repo root: grep -rnE '^\s*-?\s*uses:\s*[^ ./$]' .github/workflows \ | grep -vE '@[0-9a-f]{40}([^0-9a-f]|$)' This prints every uses: that points outside your repo and isn't pinned to a full 40-character SHA. It skips local ./ actions. It will also show docker:// steps and reusable workflows (jobs.<id>.uses: org/repo/.github/workflows/x.yml@main), which is good, you want to see those too. On a test workflow it gave me lines like: .github/workflows/ci.yml:10: - uses: actions/setup-node@v4 .github/workflows/ci.yml:12: - uses: docker/setup-buildx-action@v3 .github/workflows/ci.yml:14: - uses: github/codeql-action/init@v3 .github/workflows/ci.yml:16: uses: docker://alpine:3.20 .github/workflows/ci.yml:18: uses: octo-org/shared/.github/workflows/deploy.yml@main Same idea as grepping your server for hardcoded renewal days before the Let's Encrypt change. You can't fix what you haven't listed. Write the list down in three groups: - GitHub's own (actions/*, github/*) - Big, well-known publishers (docker, aws, google, cloudflare, vercel...) - Everything else. Small repos, one maintainer, something you found in a Marketplace search at 1am. Step 2: delete before you pin (10 min) The cheapest action to secure is the one you remove. A lot of third-party actions in small repos do something like "post a comment", "get changed files", "set up a tool", "send a Slack message". Some of those are 3 to 10 lines of shell plus gh or curl, which runs with the same token anyway but without someone else's code in the middle. For example, a lot of what people use tj-actions/changed-files for is a git diff --name-only between two commits. For the ones you keep, look at them once before you trust them. Open the repo: how many maintainers, how old, does it pin its own dependencies, does the action.yml call other actions by tag (that's failure mode 2 again). It's basically the same spelling, age and real-usage check as checking a package an AI coding agent wants to install. A typo in an action name runs someone else's code with your secrets, same as a typo in npm install. Step 3: pin, and check the pin is honest (15 min) To get the commit a tag points to right now: git ls-remote https://github.com/actions/checkout \ 'refs/tags/v4.2.2' 'refs/tags/v4.2.2^{}' | tail -n1 | cut -f1 The ^{} line matters. Some repos use annotated tags, and then the first SHA you see is the tag object, not the commit. tail -n1 takes the peeled commit when it exists. (Before you pin, check that the tag is the release you meant and not already compromised. Pinning a bad commit just makes it permanent.) Then write it with the version as a comment so humans can read it: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 Now the check most people skip. This script reads every @<sha> # <tag> line and asks GitHub what that tag points to today: #!/usr/bin/env bash # For every "uses: owner/repo@<full sha> # <tag>" line, ask GitHub # what that tag points to right now and compare it with the pin. grep -rhoE 'uses:\s*[^ ./$][^ @]*@[0-9a-f]{40}\s*#\s*\S+' .github/workflows | while read -r _ ref _ tag; do sha=${ref##*@} repo=$(echo "${ref%@*}" | cut -d/ -f1-2) now=$(git ls-remote "https://github.com/$repo" \ "refs/tags/$tag" "refs/tags/$tag^{}" | tail -n1 | cut -f1) if [ "$now" = "$sha" ]; then echo "ok $repo $tag" else echo "CHECK $repo $tag pinned=$sha tag-now=${now:-missing}" fi done On my test file, with one deliberately wrong pin: ok actions/checkout v4.2.2 CHECK actions/checkout v4.2.2 pinned=d632683dd7b4114ad314bca15554477dd762a938 tag-now=11bd71901bbe5b1630ceea73d27597364c9af683 ok pnpm/action-setup v3.0.0 ok github/codeql-action v3.29.0 A CHECK means one of two things, and both are worth five minutes: either your SHA was never that tag (copied wrong, or an imposter commit), or the tag moved after you pinned it. The second one is exactly the event you want to hear about. Your pin protected you, but it's also a signal that upstream had a bad day and you should go read their issues. It only checks the format @sha # tag, it handles owner/repo/subdir actions like github/codeql-action/init, and it needs nothing but git and grep. You could run it weekly in a scheduled job. It does not look inside the actions for failure mode 2, that's what the next step is for. One tradeoff that's easy to miss: GitHub's docs say Dependabot "will not create alerts for actions pinned to SHA values", alerts are only for actions using semantic versioning. So if you pin by SHA, turn on Dependabot version updates for github-actions so the pins don't rot. Since there's a default cooldown now (Dependabot waits 3 days after a release before proposing it, on github.com), a freshly poisoned release has some time to get caught before a bot PR shows up. That helps with bad new releases. It does nothing for a tag that gets repointed under you, only the pin does. Step 4: or try the new lockfile (15 min) On October 14, 2026, GitHub put workflow dependency lockfiles into public preview, including on the Free plan. It's basically package-lock.json for Actions. A CLI extension writes .github/workflows/actions.lock with the commit for every action your workflows use, including the transitive ones, plus the numeric owner and repo IDs. Once a workflow is onboarded, Actions runs the locked commit even if the tag moves, and every dependency of that workflow has to be in the lockfile. gh extension install github/gh-actions-lock gh actions-lock # scan .github/workflows, write actions.lock gh actions-lock --verify # read-only check, good for a CI job Your workflow keeps readable refs (it may narrow @v4 to @v4.2.2). GitHub's docs now say that for repos with a lockfile, SHA pinning in the workflow file is "no longer necessary". There's also a "Require lockfile" policy at repo/org level, and Dependabot regenerates the lockfile entry in the same PR when it bumps an onboarded workflow. Things to know before you rely on it, straight from the README and release notes: - It's pre-1.0 and in preview. v0.3.0 (Oct 22) changed behavior enough that some lockfiles that used to pass --verify now fail it. - Reusable workflows called from another repo (jobs.<id>.uses) are not locked by your lockfile. - Container images are not locked. Anything without an @sha256: digest is only a warning. - A lockfile change that moves a floating ref like v4 back to an older commit in its history isn't detected. So review lockfile diffs, or pin exact tags. - Since v0.3.0 it rejects full-SHA pins to fork-only (imposter) commits, every locked commit must be in the history of its recorded tag or branch. That covers failure mode 3. If you have one repo and a few workflows, I'd honestly try this over hand-pinning. If you're not ready for a preview tool, steps 1 to 3 still get you most of the way. Step 5: make a stolen run worth less (10 min) Pinning is about which code runs. This step is about what that code can grab if something slips through anyway. The tj-actions and trivy payloads both went for secrets in the runner's memory. The xygeni one gave the attacker a remote shell on the runner for up to 180 seconds per run, which reaches the same things. At the top of every workflow: permissions: contents: read Then give extra scopes only to the job that needs them: jobs: comment: runs-on: ubuntu-latest permissions: contents: read pull-requests: write # steps: ... Once you set any permission, every one you didn't list becomes none. permissions: {} turns them all off. Why bother if new repos default to read-only? Because that default only applies to things created after February 2, 2023. GitHub's changelog was explicit: the change "will not impact any existing enterprises, organizations or repositories." If your repo or org is older than that, go to Settings → Actions → General → Workflow permissions and look. An older repo can still say "Read and write" there. Two more: - Keep prod secrets out of the PR workflow. Your test job doesn't need the production database URL or the live payment key. Put deploy secrets in an environment with required reviewers. Then only jobs that use that environment can read them, and only after someone clicks approve. - Prefer short-lived cloud creds. If you deploy to AWS/GCP/Azure, OIDC gives the job a token that expires, instead of a long-lived key sitting in repo secrets forever. On GitHub-hosted runners the GITHUB_TOKEN itself already dies with the job, max 6 hours. Step 6: if you ran a bad version All three advisories say some version of the same thing: treat every secret the pipeline could read as exposed, and rotate it. Not "probably fine". The trivy payload read the runner's memory, so masked secrets in the logs didn't matter. The order I'd go in: 1. Find the runs inside the advisory's window. gh run list --created 2026-03-19..2026-03-20 works, or just scroll the Actions tab. 2. Rotate whatever those jobs could reach. Cloud keys, deploy keys, package registry tokens, any API key in repo or org secrets. 3. For trivy specifically, look for a repo named tpcp-docs in your account or org. Per the advisory, the stealer creates it as a fallback upload spot when its normal exfiltration fails. 4. Check what got published from those runs (releases, packages, images) in case something was tampered. Rotation is the slow part if you don't know where each key is used. Writing that list down before you need it is worth doing just for this. The rules I'd actually adopt At under 50 customers, with one or two people touching CI: - Third-party actions: always locked (lockfile) or SHA-pinned, with the version comment. No exceptions for "it's popular". - GitHub's own actions/*: I'd pin them too. GitHub's "Require actions to be pinned to a full-length commit SHA" policy (added August 2025) treats them the same, and it makes the rule simple enough to enforce. - Fewer actions every quarter, not more. - permissions: contents: read at the top of every workflow, widen per job. - The pin check, weekly. Total time is about an hour for a typical small repo. The trivy-action window was about twelve hours, starting on a Thursday evening UTC, and the real scan still ran after the stealer, so the job looked normal. Nobody watching their CI casually would have caught it. I'm curious how other small teams handle this. Are you pinning SHAs by hand, trying the new gh actions-lock preview, or still on @v4? And if you already pin by SHA, have you ever opened the action.yml of what you pinned to see if it calls other actions by tag?
イメージスキャン結果をDevinに投げて脆弱性対応してもらう aquasecurity/trivy Trivyは、コンテナイメージやファイルシステムの脆弱性を検出できるOSSのスキャナーです。 最近は、不正アクセスや情報漏洩のニュースを目にすることが多くなってきました。 既知の脆弱性を放置しないことは、地味ですが基本的な対策の一つです。 弊社ではECSで動かしているコンテナイメージを、Trivyで定期的にスキャンする仕組みをGitHub Actionsで運用しています。 見つかったHIGH/CRITICALの脆弱性はGitHub Issueになり、DevinがIssueを元に対応します。 また、最近スキャンのワークフローを見直していたところ、2026年3月にTrivy自体が侵害されていたことに気づきました。 弊社では、たまたま侵害されていないバージョンに固定して使っていたため、被害はありませんでした。 本当に運が良かっただけで、もし被害に遭っていたらと思うとゾッとします。 そこで、スキャナーを信用しすぎない構成に見直したので、あわせて紹介します。 ! 侵害の詳細はページの最後の参考資料にまとめました。 気になる方は先に確認してみてください。 ECRの基本スキャンを使えばいいのでは? ECRには、もともとイメージスキャンの機能があります。 なので「それを使えばいいのでは?」と思われるかもしれません。 ただ、ECRの基本スキャンでは、結果が当てにならない場面がありました。 脆弱性が残っているのに、結果に出てこないことがあり、修正したあとに「本当に消えたのか」を確かめる手段もなく、確認が難しい状態でした。 改めて仕様を確認してみると、基本スキャンには次のような性質があります。 スキャンが走るのはpush時か、手動で実行したときだけ scan on pushのフィルタに一致しないリポジトリは、手動スキャンの扱いになります 同じイメージをスキャンできるのは24時間に1回まで push時のスキャンも手動スキャンも、この回数に含まれます 対象はOSパッケージの脆弱性 追加料金なしで使える つまり、一度pushしたイメージは、あとから新しいCVEが出ても自動では再スキャンされません。 デプロイしてから同じイメージで長く動き続けるほど、結果が古いままになりがちです。 結果に出てこなかったのも、このあたりが一因だったのかなと想像しています。 本格的に使うならAmazon Inspector 継続的に再スキャンしたい場合は、拡張スキャン(Amazon Inspector)を使うことになります。 拡張スキャンでは、イメージのpushに加えて、Inspectorの脆弱性データベースに影響のあるCVEが追加されたタイミングでも再スキャンが走ります。 OSパッケージだけでなく、言語パッケージも対象です。 ただし、こちらは有料です。 拡張スキャンはInspector側で課金され、push時の初回スキャンと、その後の再スキャンの回数で料金が決まります。 課金対象 単価(us-east-1) push時の初回スキャン $0.09 / イメージ 継続スキャンでの再スキャン $0.01 / 回 公式の料金例では、1,500イメージがそれぞれ月15回再スキャンされるケースで、再スキャンだけで$225、初回スキャンと合わせて月$315になっていました。 デプロイのたびにpushする運用だと、初回スキャンの回数も積み上がっていきます。 再スキャンの対象期間は設定で絞れますが、保持しているイメージが多いほど費用は増えていきます。 Trivyで自分たちで調べることにした 今回やりたかったのは「本番で動いているイメージを、定期的に、確実に調べたい」ということでした。 そのためにInspectorを入れて従量課金を払うよりは、OSSのTrivyで手順と結果を自分たちで握るほうが合っていると判断しました。 ECRのスキャンAPIは使わず、イメージをpullして手元で調べる方式です。 全体像 スキャンは定期実行しています。 ざっくり言うと、イメージを取得して、スキャンして、結果をIssueにする、という流れです。 どこかで失敗したときは、Slackに通知します。 ポイントは、AWSの認証情報を持つジョブと、Trivyを動かすジョブを分けたことです。 両者をつなぐのはartifactだけにしています。 各ジョブを覗いてみる 全体像の流れに沿って、ジョブごとに役割と権限を見ていきます。 イメージを取得(pull-images) 本番のAWSロールをOIDCで引き受けて、ECRからイメージをpullします。 docker saveでtarにして、artifactに入れるところまでがこのジョブの仕事です。 docker tag "$IMAGE" "${repo}:latest" docker rmi "$IMAGE" docker save --output "images/${repo}.tar" "${repo}:latest" tarにはECRのレジストリ名(アカウントID)が残ってしまいます。 そこで、ローカルの名前に付け替えてから保存しています。 artifactの保持期間は1日にしています。 Trivyでスキャン(trivy-scan) 権限もsecretsも持たず、permissions: {}で動きます。 artifactからtarを受け取り、Trivyを入れてスキャンし、結果をfindings.jsonにまとめます。 Issueを作成(create-issue) 持っている権限はissues: writeだけです。 findings.jsonを受け取り、重大度別の件数と、イメージごとのCVE一覧をIssueにします。 HIGH/CRITICALが0件なら、Issueは作りません。 わざわざIssueにしているのには理由があります。 findings.jsonをそのままDevinに渡すと、情報量が多く、トークンの消費が激しくなってしまうからです。 対応に必要な情報だけをIssueに絞り込んでから渡すようにしています。 Devinが対応 作成されたIssueを元に、Devinが対応します。 必要であれば、修正PRまで作成してくれます。 図には載せていない後片付け 全体像の図には載せていませんが、裏では次の2つも動いています。 artifactの削除(cleanup-artifacts) スキャンが終わると、GitHub Actionsのartifactに保存したイメージのtarを削除します。 ECR上のイメージには触れません。 always()で動くので、途中のジョブが失敗しても消えるようになっています。 権限はactions: writeのみです。 失敗時のSlack通知 いずれかのジョブが失敗したときは、Slackに通知が飛びます。 Trivyのオプション 実際に実行しているコマンドはこちらです。 trivy image \ --scanners vuln \ --pkg-types os \ --list-all-pkgs=false \ --severity HIGH,CRITICAL \ --format json \ --exit-code 0 \ --input "images/${repo}.tar" オプションを紹介します。 --scanners vuln 脆弱性だけを調べます。 シークレットや設定ミスの検出は対象外にしています。 --pkg-types os OSパッケージに絞っています。 言語パッケージの脆弱性は、Dependabotで拾っているためです。 --severity HIGH,CRITICAL 対応が必要な重大度だけを拾います。 MEDIUMやLOWまで拾うと、検出される件数が膨大になってしまいます。 そうなると、本当に対応すべきものが埋もれてしまいます。 Issueを読む側の負担を抑えるためにも、HIGHとCRITICALに絞っています。 スキャナー自体を疑う セキュリティツールは、強い権限で動くことが多い道具です。 侵害されれば、守るはずの環境に入り込む足がかりになってしまいます。 これが2026年3月に実際に起きました。 概要は次のとおりです。 3/19: 悪意あるv0.69.4が公開され、全配布経路に約3時間出回った 同日: trivy-actionとsetup-trivyのタグが改ざんされた 3/22: Docker Hubで0.69.5と0.69.6が出回った 約3時間という数字は短く見えます。 とはいえ、毎回最新を取りにいくCIや、タグでバージョンを指定しているCIなら、その間に走った実行は影響を受けかねません。 ちなみに侵害された版はすでに取り下げられており、Trivy側で再発防止も進められているようです。 乗り換えも選択肢ですが、どのスキャナーでも同じことは起こりえます。 そこで、ツールを替えるのではなく、「スキャナーが侵害されても被害を小さくできる構成」にすることを選びました。 具体的には、次の4つを入れました。 1. ハッシュを直書きして照合する 以前は0.69.3を指定して、公式のinstall.shをcurl | shで実行していました。 実はinstall.shも、ダウンロードしたファイルのチェックサムを照合してくれます。 ただ、照合に使うchecksums.txtも同じリリースから取ってくるので、リリースごと差し替えられてしまうと見抜けません。 そこで、リリースのtarballを直接取得し、ワークフローに書いたSHA256と照合してから入れるようにしました。 env: TRIVY_VERSION: "0.76.1" TRIVY_SHA256: "<リリースの checksums.txt にある Linux-64bit.tar.gz の値>" ARCHIVE="trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" curl -sfL -o "$ARCHIVE" "github.com/aquasecurity/trivy/releases/download/v…" echo "${TRIVY_SHA256} ${ARCHIVE}" | sha256sum -c - tar -xzf "$ARCHIVE" trivy 照合に使う値は、配布元ではなく自分たちのリポジトリにあります。 なので、リリースとchecksums.txtが丸ごと差し替えられても検知できます。 ただし、照合しているのはTrivy本体のバイナリだけです。 実行時に取得する脆弱性DBまでは照合していません。 2. 公開から7日待つ ハッシュを固定しても、最初から悪い版を固定してしまうリスクは残ります。 これを補うために、RenovateのminimumReleaseAgeを7日にしました。 { "matchDepNames": ["aquasecurity/trivy"], "matchDatasources": ["github-releases"], "minimumReleaseAge": "7 days", "automerge": false } 3月の侵害版が出回ったのは約3時間です。 7日あれば、悪意ある版は発覚して取り下げられている可能性が高くなります。 地味にハマりやすいのが、このルールを置く位置です。 RenovateのpackageRulesは、条件に合うルールが上から順に適用され、後ろのルールが前の設定を上書きします。 ほかにautomerge: trueのルールがあると、Trivyの更新まで自動マージされかねません。 そのため、Trivy用のルールはpackageRulesの末尾に置いています。 なお、TRIVY_SHA256はRenovateが更新できません。 そのため、更新PRの本文に手動更新の手順を表示するようにしています。 3. ハッシュの更新漏れをPRで検知する バージョンだけ上げてハッシュを忘れると、定期実行の当日に失敗して気づくことになります。 そこで、PRの時点で検証するワークフローを置きました。 on: pull_request: paths: - .github/workflows/ecr-vulnerability-scan.yml - .github/workflows/verify-trivy-checksum.yml スキャン用ワークフローからTRIVY_VERSIONとTRIVY_SHA256を読み取り、実際のtarballと照合しています。 一致しなければチェックが赤くなるので、レビューの段階で気づけます。 注意点として、このワークフローはpathsで対象のファイルを絞っているため、ブランチ保護の必須チェックにはできません。 関係のないPRでは動かず、必須にすると永遠に待ち状態になってしまうからです。 赤いままマージしないよう、レビューする側が気をつける必要があります。 4. 認証情報をTrivyから切り離す ここまでの対策をすり抜けて悪いTrivyが動いたとしても、盗めるものがなければ被害は小さく済みます。 Trivyを動かすtrivy-scanジョブには、AWSの認証情報も、OIDCトークンも、secretsも渡していません。 イメージの受け渡しはartifactです。 スキャン後に削除するので、イメージが残る時間も短くなります。 運用 HIGH/CRITICALが見つかると、ecr-vulnerabilityラベル付きのIssueができます。 本文には重大度別の件数、イメージごとのCVE、パッケージ名、現在の版と修正版が載ります。 Issueに修正版まで載っているので、Devinはそれを手がかりに対応を進められます。 ジョブ自体は脆弱性で失敗しないので、普段は静かです。 ワークフローが壊れたときだけ、Slackに失敗通知が届きます。 Devinへの指示で工夫していること Devinには、Playbookという形で対応の手順を渡しています。 Devinに任せるからこそ、判断の基準と「やってはいけないこと」をかなり細かく決めています。 主な工夫を紹介します。 最新のIssueだけを見る スキャンは定期的に動くので、対応が終わらないうちに次のIssueができることがあります。 そこで、参照するのは最新のIssueだけにして、それより古いIssueはcloseするようにしています。 古い結果を元に対応してしまうのを防ぐためです。 Dependabotと突き合わせる TrivyのスキャンはOSパッケージに絞っていて、言語パッケージの脆弱性はDependabotで拾っています。 PlaybookではIssueとDependabot alertsの両方を必ず参照させ、同じCVEは情報をまとめて扱います。 DependabotのPRがすでにあるパッケージには、新しくPRを作りません。 同じ脆弱性に対して、PRが二重にできるのを防いでいます。 実際の環境での危険度で優先度を付ける 重大度がHIGHやCRITICALでも、実際に動いている環境で悪用できるとは限りません。 そこで、攻撃ベクターや認証の要否を見て、優先度を付け直させています。 ネットワーク経由で、認証なしに攻撃できるもの → 優先して対応 ローカルからしか攻撃できないもの → 優先度を下げる 修正版がまだないもの → 記録だけ残してPRは作らない 逆に、CISA KEVに載っているなど、実際に悪用されているものは緊急扱いにしています。 一度に作るPRの数に上限を設ける Devinはその気になれば、PRをいくらでも作れてしまいます。 ただ、レビューするのは人間なので、一度に大量に来ても捌けません。 そこで、対応中のPRと合わせて5件までという上限を設けています。 上限を超えた分は「次回以降」として記録に残します。 緊急扱いのものだけは、上限の例外にしています。 直し方を決めておく OSパッケージの脆弱性は、原則としてベースイメージ(FROMのタグ)の更新で直させています。 Dockerfileの中でapt-get upgradeなどを使い、パッケージを個別に上げることは禁止です。 その場しのぎの修正が積み重なって、Dockerfileが読みづらくなるのを避けるためです。 ベースイメージが追いつくのを待てない緊急時だけ、例外として個別の更新を認めています。 その場合も、PRに理由を書くことと、あとでベースイメージの更新に戻すことを条件にしています。 中途半端なPRを出させない ランタイムのバージョンを上げるときは、Dockerfileだけ直して終わり、になりがちです。 実際には、バージョン指定のファイルやロックファイルなど、一緒に直すべきものがいくつもあります。 そこで、PRを出す前に満たすべき条件(Definition of Done)を決めています。 たとえば「ロックファイルの更新は『マージ前に人が実行』と書いて押し戻さず、PRに含める」といったものです。 最後の判断は人間に残す mainへの直接pushは禁止で、必ずPRを作らせます。 レビュワーもDevinには指定させず、チームの運用に従って人間が後から設定します。 結果を必ず報告させる 処理が終わったら、対応したもの、見送ったもの、その理由をIssueにまとめさせています。 同じ内容をSlackにも投稿させていて、こちらは対応がなかった場合でも省略させません。 「何もしなかった」のか「動かなかった」のかを、人間が区別できるようにするためです。 自分の現場に取り入れるなら ここまでの内容を、チェックリストにまとめてみました。 全部を一度に入れなくても、上から順に見直すだけで効果があると思います。 スキャナーをcurl | shで入れていないか バージョンとSHA256をリポジトリに直書きして、照合してから入れる 依存の更新に待機期間を設けているか RenovateのminimumReleaseAgeで、公開直後の版を取り込まないようにする ハッシュの更新漏れにPRの時点で気づけるか バージョンとハッシュの組み合わせを検証するワークフローを、PRで動かす スキャナーを動かすジョブに認証情報を渡していないか permissions: {}にして、secretsもOIDCトークンも渡さない イメージの取得とスキャンはジョブを分け、artifactだけでつなぐ ジョブごとの権限を最小にしているか Issueを作るジョブはissues: write、削除するジョブはactions: writeのように絞る 「脆弱性の検出」と「仕組みの故障」の通知を分けているか --exit-code 0で検出ではジョブを落とさず、失敗通知は故障のときだけにする スキャンに使ったイメージを残しっぱなしにしていないか always()で動く削除ジョブを置き、保持期間も短くする AIエージェントに任せる範囲と上限を決めているか 優先度の基準、直し方、PRの上限、やってはいけないことを手順書に書いておく まとめ スキャナーを入れて終わり、ではなく、スキャナー自体を疑うところまで考えることになりました。 セキュリティツールが攻撃の入口になるというのは、言われてみれば当たり前なのですが、実際に起きてみるとなかなか身につまされます。 ハッシュの固定、公開から7日待つ、権限の分離と、一つひとつは地味な対策です。 ただ、重ねてみると「どこか一つが破られても被害を小さくできる」構成になったかなと思っています。 見つかった脆弱性への対応はDevinに任せられるようになり、人間は最後の判断に集中できるようになりました。 最近の攻撃の多さには本当に頭を悩まされます。 私たちにできることには限りがありますが、今できることを地道にやっていくしかないですね。 なんとか頑張っていきましょう。 日々セキュリティ対応をしているみなさん、本当にお疲れ様です。 参考資料 ECRのスキャンとAmazon Inspectorについて ECRの基本スキャン docs.aws.amazon.com Amazon InspectorによるECRイメージのスキャン docs.aws.amazon.com Amazon Inspectorの料金 aws.amazon.com 基本スキャンと拡張スキャンの比較(AWS Containers Blog) aws.amazon.com Trivyの侵害について GitHub Security Advisory github.com/aquasecurity/trivy/security/advisories… Discussion trivy#10462
脆弱性だけでなく、IaC設定やsecretも同じscannerで検査する。 Trivyはcontainer image、filesystem、Git repository、VM image、Kubernetesをscan。CVE、dependency、IaC misconfiguration、sensitive information、licenseなどを検出対象にします。 aquasecurity/trivy
We cut our scanner binary by 68% — and removing packages one by one wasn't how --- title: "We cut our scanner binary by 68% — and removing packages one by one wasn't how" published: true description: "Two weeks of replacing dependencies one by one barely changed our dependency list. Removing one integration layer cut the scanner binary from 106.6 MB to 34.1 MB. Here's how, and how we tested it." tags: security, opensource, go, devops cover_image: --- Finding unmaintained dependencies is the easy part. Removing them is where it gets hard. This is the story of how we slimmed down vuls, the open-source vulnerability scanner I created — what didn't work, what did, and the tooling we built after doing it all by hand. The easy win: 15 minutes mitchellh/go-homedir has been archived for years. Vuls used it in a couple of lines in two source files: - import "github.com/mitchellh/go-homedir" - dir, err := homedir.Dir() + dir, err := os.UserHomeDir() Fifteen minutes, start to finish. The standard library has done this since Go 1.12. Trivy used the same package, so we sent them a similar fix. The Trivy maintainers reviewed and merged it within three weeks. We felt good. Let's keep going. The part where almost nothing happened We kept going. Over two weeks we replaced 9 direct dependencies with standard-library code or a few lines of our own. go.mod went from 60 direct dependencies to 51. Then we looked at what had actually left the build. go.sum went from 1,220 lines to about 1,210. Only 5 modules disappeared completely. Most of the packages we had stopped calling were still required by other modules, so they stayed in the dependency list anyway. Adding a dependency takes one line. Removing one can take real work and barely change what you ship. Just like a real diet. What worked: removing a whole layer So we stopped picking off packages one by one and looked at which layer they came in through. Vuls uses Trivy's parsers to read lockfiles (package-lock.json, go.mod, Cargo.lock, and so on). We called them through fanal, Trivy's analysis framework. fanal is built for everything Trivy scans — container images, disk images, filesystems, infrastructure-as-code. That breadth is exactly what Trivy needs. Our scanner only needed the part that reads lockfiles, but the way we had wired fanal in registered 25+ analyzers through blank imports, and with them the infrastructure-as-code and misconfiguration packages that Trivy needs and we never called. We changed Vuls to call Trivy's lockfile parsers directly. Much less code compiled in, and — as we checked below — the same results on almost every project we tested. (Vuls still depends on Trivy — we use its parsers, just not through fanal.) The result (vuls PR #2476): | Metric | Before | After | Change | |---|---|---|---| | Scanner binary | 106.6 MB | 34.1 MB | -68% | | Scanner build time | 7.8 s | 3.2 s | -59% | How we measured: scanner-only builds (-tags scanner) with our release build settings, CGO_ENABLED=0 GOEXPERIMENT=jsonv2 go build -a -trimpath -ldflags "-s -w". The build times are with a warm build cache. To be precise about what shrank: this is the scanner binary. The full vuls binary, which also includes the vulnerability detection and reporting code, went from 139.1 MB to 138.5 MB after this change and a follow-up one. That code still uses Trivy's vulnerability database layer, so most of the weight stays. !Left: after vuls removed its direct import of go-homedir, nine other modules, including trivy and go-getter, still required it, so go.sum stayed at 1,220 lines. Right: when vuls stopped going through fanal and called Trivy's lockfile parsers directly, what only fanal had pulled in left the build: 2,345 packages became 737, and the scanner binary went from 106.6 MB to 34.1 MB. Packages in the scanner build counted with go list -tags scanner -deps ./cmd/scanner before and after PR #2476. Don't pick leaves one by one. Find the branch they hang from. How we tested it The change rewrote how the whole scanner package reads lockfiles. Here's how we checked it. Layer 1: Golden tests. JSON output for 35 lockfile types, compared for an exact match. Layer 2: A/B comparison on real projects. We collected real lockfiles from 129 open-source projects across 13 ecosystems. Then: old code + lockfile → result A new code + lockfile → result B diff(A, B) No mocks. No hand-written expected values. Just: "does the new code produce the same results as the old code?" 127 of 129 were identical. The other 2 differed only in the pnpm dev flag, which our old wrapper code had been losing along the way. Calling Trivy's pnpm parser directly gave us the parser's correct output. Building the comparison set taught us something too. On the first pass, many fixtures produced zero packages — requirements.txt files with no pinned versions, a nearly empty yarn.lock, a root pom.xml with only <modules>. "Identical" on an empty result proves nothing, so we replaced those fixtures with ones that actually exercised the parsers. This lives in vuls as make compare-lockfile. It didn't catch everything: code review and testing on Windows still found a case the comparison had missed (.exe files weren't detected as executables), which we fixed before merging. The tool we built afterwards: uzomuzo diet We did all of the above by hand, in March 2026. Looking back, the expensive part wasn't writing code. It was deciding where to start — and our first choice was wrong. So in April we added a diet command to uzomuzo, our dependency lifecycle scanner. It ranks your direct dependencies as candidates for removal: 1. Dependency graph — from a CycloneDX SBOM: what would disappear along with each direct dependency? 2. Source coupling — tree-sitter reads your code: how many files import it, how many call sites? 3. Health — maintenance status (Active, Stalled, Archived, ...), from the same signals uzomuzo scan uses 4. Score — high impact, high risk, and easy to remove rises to the top scan and diet are rule-based. No LLM is involved. Their results reflect the dependency and maintenance data available when you run them. One caveat up front: diet is experimental. An audit against hand-checked expected values found cases where a dependency that is in use is reported as unused or trivial — for example when it's imported from a skipped directory or in an import style the analyzer doesn't recognize yet. The CLI prints a notice saying so after every run, and the Accuracy section of the docs lists what to check. Treat its output as a list of places to look, not a list of things to delete. Here's an excerpt from a run on Terraform (commit 9ec49e0, October 13, 2026; SBOM generated with cyclonedx-gomod; run on October 26, 2026): $ uzomuzo diet --sbom terraform-bom.json --source ./terraform RANK SCORE EFFORT PURL REMOVES IMPORTS CALLS STATUS 3 0.13 trivial github.com/mitchellh/gox 1 1 1 Archived 5 0.10 hard github.com/mitchellh/colorstring 0 22 79 Archived 6 0.10 moderate github.com/mitchellh/go-homedir 0 9 11 Archived ... 72 0.00 hard github.com/hashicorp/hcl/v2 0 352 6764 Active (Edited for width: four of the 77 direct dependencies, PURLs shortened to module paths, and the REMAINS column omitted. Ranks 1, 2 and 4 are not shown: 1 and 4 are Terraform's own nested internal/backend/remote-state/* modules, which the SBOM lists as direct dependencies, and 2 is an active OpenTelemetry package. The full run also reported zero "quick wins".) The ranking is a starting point for investigation, not a verdict: - gox is archived and has one import and one call site, so it gets a trivial effort label. That's exactly the kind of label to verify: check that one call site, what it does, and what replaces it. - go-homedir is the same package we removed from vuls in 15 minutes. Here it's imported in 9 files, so it's rated moderate. Removing the direct use wouldn't drop it from Terraform's dependency list either — another module still requires it. - hcl/v2 is at rank 72: 352 importing files, 6,764 call sites, and actively maintained. Don't start here. It works for Go, Python, JavaScript/TypeScript, and Java. The last mile: Claude Code skills Ranking is automated. Actually removing a dependency — tracing what data flows through it, writing the replacement, getting the change accepted — still takes judgment. uzomuzo ships three Claude Code skills for that part: /diet-assess-risk (how risky is it to keep?), /diet-evaluate-removal (is it worth the effort?), and /diet-remove (draft the change or the upstream report). They use an LLM, so review what they propose before you use it. The rules in /diet-remove come from our own mistakes. When we found that Next.js still depended on @vercel/kv, which Vercel has deprecated on npm in favor of Upstash Redis, we tried three channels by hand: - PR → we closed it ourselves after CI failed. It's a large pnpm-workspaces monorepo, and we couldn't reproduce its CI locally. - Issue → the repository's bot closed it within seconds, pointing to the bug report template. The issue tracker is for bugs with a reproduction, and ours wasn't one. - Discussion → the channel the repository's templates point to. It's still open with no reply yet. So the skill now defaults to drafting an issue rather than a PR for projects you don't own, checks whether the repository accepts blank issues, and falls back to a Discussion when it doesn't. Those rules are written from that manual experience; we haven't yet published an end-to-end run of the skill itself. And getting the channel right doesn't guarantee a response. Of our four upstream reports in April, two have been fixed and two are still waiting. Whether to fix something is the maintainers' call. Start your diet go install github.com/future-architect/uzomuzo-oss/cmd/uzomuzo@latest CGO_ENABLED=1 go install github.com/future-architect/uzomuzo-oss/cmd/uzomuzo-diet@latest # needs a C compiler export GITHUB_TOKEN=... # read-only is enough; gives uzomuzo repository signals such as archived status The output in this post was produced with v0.10.0. 1. Run uzomuzo scan --file go.mod (or pipe in an SBOM for other ecosystems). See what's unmaintained. 2. Generate a CycloneDX SBOM — for example trivy fs . --format cyclonedx -o bom.json, or for Go, cyclonedx-gomod mod -json -output bom.json — and run uzomuzo diet --sbom bom.json --source .. 3. Pick a dependency whose use and replacement you understand. Remove it, then measure: binary size, dependency list, build time. If nothing moved, look for the layer it came in through. And don't give up on your diet. --- Earlier posts: "Your dependencies are 48% unmaintained — and SCA tools can't see it" and "Your CI runs other people's code. Is anyone still maintaining it?" Source and docs: future-architect/uzomuzo-oss (Apache 2.0) Kota Kanbe — Creator of vuls, an open-source vulnerability scanner.
個人開発のGitHubリポジトリで見直したい設定リストと解説 こんにちは、uta8aです。 最近かなりセキュリティ的に物騒な世の中になってきましたね。GitHubも対策として色々機能を出していますが、個人でpublicにしているリポジトリの設定って結局どうするのがいいんでしょうか。 そこで今回は、一通り個人開発のGitHubリポジトリで設定を見直していきたいと思います。なお、書かれている内容は、2026/10/26現在の情報になります。 前提 対象プラン: GitHub Free for personal accounts Administrator相当の権限を持っていること Settings(github.com<owner>/<repo>/settings)を変更できる 🛡️ 個別の設定について 実際設定していく時はページごとに行うので、Settingsの上から順に行きます。 GitHubの設定画面のサイドバー(Private Repo) また、チェックをつけることをON, チェックを外すことをOFFと表現します。 General ページ Releases > Enable release immutability 推奨: ON 2025/10/28にGAになった機能。何が嬉しいかというと、公開したRelease assetsを後から変更不可能にできます。 Releaseにimmutable表示が出ている様子 ソフトウェアサプライチェーン攻撃の一つに、「リポジトリのRelease経路を奪取した後に、既存のReleaseをバージョンそのままに上書きする」という手法があります(Trivy 2026/03)。過去にダウンロード済みでchecksumで検証している場合は影響を受けませんが、新規にダウンロードする場合は影響を受けてしまいます。 ここでReleaseがimmutableであれば、既存のreleaseについては改変される心配がないので安心というわけです。ただ、注意すべき点として、設定時から過去に遡ってimmutableになるわけではないこと、攻撃を受けた後に新規にimmutableなReleaseがされた場合は防げないことがあります。 個人開発文脈だと、もし自分のリポジトリが乗っ取られても過去のReleaseについてはimmutableな状態を保てる点に良さがあります。 設定することで困ることはほぼないので、有効化してしまいましょう。 Features > Issues > Issue permissions 推奨: Collaborators only(※ただし、Issue起点でGitHub Actions等の操作をしていないなら、All usersでもOK) Issue permissionsの設定画面 2026/06/29頃に追加された機能。 何が嬉しいかというと、Issueの作成者をCollaboratorsのみに制限できます。 Issueを作ったら何か処理をGitHub Actionsを実行させる、とか外部のシステムと連携している場合はCollaborators onlyにしておくのがおすすめです。 トレードオフとして、これを設定すると後述するPull Requestの設定と合わせて、外部からの連絡ができない状態になります。外部からの連絡手段を残したい場合は、issue/pull requestのどちらか、または両方をAll usersにしておくと良いでしょう。 また、タスク管理をIssueでやってないなどの理由でissueを使わない場合は、issue自体を無効化するのも良いと思います。基本的に使わない機能はガンガンoffにしていくのがセキュリティ的には良いと思います。 Features > Pull requests > Pull request permissions 推奨: Collaborators only 2026/02/13に追加された機能。issueと同様に、外部からのPull Requestをcollaboratorsに制限できるのが嬉しいです。 基本的に個人開発の文脈だと外部からPRを受け付けるモチベーションより攻撃の起点にされるリスクが大きいと思うので、collaborators onlyにすることをおすすめします。 Rulesets ページ (GitHub Free for personal accountsだとpublic repositoryのみ) 最低限2つは設定しておきましょう。 デフォルトブランチの保護 Enforcement status: Active Target branches: Default Branch rules Restrict deletions Require signed commits Require a pull request before merging Block force pushes ここでは、デフォルトブランチが消されたり、force pushされたりすることを防ぎ、Repository writeの権限が取られても、Administrator権限がなければデフォルトブランチを保護できる状態にしています。 signed commits, pull request before mergingでPull Requestを必須にしておいて、署名がないPRはマージできないようにします。(ただし、どの署名かはチェックしていないので攻撃者が署名した場合はマージ可能になってしまう。アカウント権限を一部を乗っ取ってPRは出せるけど、署名のsecret keyまでは取れてない&&署名の設定追加ができない攻撃者からのPRは防げる) リリース経路の保護 例えばmainブランチでtag v2026.10.26.0 を打ってpushするとリリースが走る場合、immutable ReleaseでRelease保護をしているとはいえ、tagの保護もしておいた方が良いでしょう。 Enforcement status: Active Target tags: v* Tag rules Restrict creations Restrict updates Restrict deletions Restrict creationsはGit tagの新規作成を制限するものです。リリースのworkflowの実行主体の権限がRulesetのBypassリストにない場合はリリースできません。 ここはBotリリースとの相性もありますが、なんらかリリース経路はRulesetsで保護しておくと良いと思います。 Actions ページ Actionsを使っていない場合はOFFにするのがおすすめです。Actions起点でtoken奪取やリリースフローの乗っ取りが行われるので、使ってないならOFFにするのが賢明です。OFFにするのは "Actions permissions > Disable actions" でできます。 General > Actions permissions > Require actions to be pinned to a full-length commit SHA 推奨: ON 2025/08/15に追加された機能。 GitHub Actionsをタグ指定だけにしていると、タグの指す先が変わった時に次回のjobからそれに追従して更新されてしまいます。もしタグの指す先が悪意ある変更になっていたら影響を受けるので、full-length commit SHAを強制しておきましょう。 General > Approval for running fork pull request workflows from contributors 推奨: Require approval for all external contributors (public repositoryのみ) forkからのPull Requestに対して、GitHub Actionsのworkflowを実行するかどうかのポリシーを決められます。個人的には(個人開発の規模にもよりますが)まず外部からのcontributorのPRは承認付きにした方がいいと思います。 特に、一度無害なPRを送って、しばらくしてから有害なPRを送る、といった手法が知られている以上first-timeはあんまり効果がないんじゃないかと思っています。 General > Workflow permissions 推奨: Read repository contents and packages permissions デフォルトでcontents, packages, metadataのreadになっていると思いますが、古いリポジトリだとRead and writeになっていることがあります。最小権限の方が良いので、古いリポジトリは注意してください。 Advanced Security ページ 機能によっては課金が発生するので注意。 Dependency Scanning > Dependency graph 推奨: ON 依存関係を記載したロックファイルなどから、自動で依存関係を検出してくれます。これがあるとDependabot alertsを受け取ることができるようになるので、ONにしておくのがいいでしょう(デフォルトでONなので多くのリポジトリでは気にしなくて良い) Dependabot > Dependabot alerts 推奨: ON 依存関係(直接の依存、推移的依存)に脆弱性があった場合に通知してくれる機能。この機能だけではDependabotによるPRは作られません。 ➕ セキュリティ以外のおすすめ設定や、個人の好みの設定 General ページ Pull Requests > Allow merge commits 私はmerge commitsが好きなので、Allow merge commitsだけONにして他squashとrebaseはoffにしています。各自の好きなmerge styleに制限しよう! Pull Requests > Always suggest updating pull request branches ここは個人開発だとデフォルトブランチの更新に気づいて各トピックブランチで取り込みたいので、ONにしています Pull Requests > Automatically delete head branches 余計なブランチは残さない方が見た目的にも良いのでONにしています。 昔はデフォルトブランチ以外に pull_request_target が残っているとそこを起点に攻撃できたので、余計なブランチを残さないことにセキュリティ上の利点があったけど、おそらく pull_request_target の仕様変更で封じられたはず。 Advanced Security ページ Dependabot > Dependabot security updates 個人的にここはOFFにしています。DependabotがPR作っても対応しないことが多いし、即座に反映させたいようなプロダクトを持っていないのが理由です。 どちらかというと自動updateに対してソフトウェアサプライチェーンセキュリティ事案が発生してCI上でやばいことが起きるリスクの方が怖いので、OFFにしています。 本当は依存関係の更新をegressをL7レイヤで制限したrunnerで行いたい... 🧰 これらの設定を一括で行う これら個別の設定を、リポジトリを作るたびに毎回ポチポチするのは面倒...!というわけで、ツールを作りました。 uta8a/github-policy-cli issue permissionsはAPIがないのでデフォルトのAll usersになってしまいますが、それ以外は大抵対応しています。 ただ、デフォルトに個人の好みの設定も含まれている点と、リポジトリ設定に関わるためfine-grained Personal Access TokenのAdministrator writeを必要とする点から、このコードを参考にお手元で自分向けのCLIをClaude/Codexに書かせるのが良いかと思います。 (参考になったらstarください!) 使い方はREADME.mdのQuick startにあるように、 export GH_TOKEN='fine-grained PAT (Administrator:write)' repo-policy plan owner/repo # 現在の設定と、これから設定する項目の差分 repo-policy apply owner/repo # 実際に設定を適用 という形です。デフォルト以外はTOMLファイルで設定項目を指定できて、すべてのオプションはexampleから確認できます。 技術的には以下を意識したくらいで、後はCodexの書いたコードをちょっと読むくらいで作りました。 GitHubのgh CLIで使われるcli/go-gh/v2を用いた これが一番APIが充実してるのではないかと判断したため 設定ファイルはhuman-readableなものが良いのでTOMLを採用 VersioningはCalVerを使用 patchを入れて、 v2026.10.26.0 のようなバージョニングにした 結構使ってみた感じ便利なので、これからリポジトリを作ったり、過去のリポジトリの見直しをするときは使っていくつもりです。 あと、fine-grained PATは権限が強いので使ったらすぐに消すこと!使う時だけ発行! まとめ GitHubの個人開発向け設定を一覧して、推奨設定とその理由を紹介しました これらの設定を一括で行うツールを作りました 強い権限を与えるので、自分の手元でAI Agentを用いて再生成するのをおすすめします!