Перейти к основному содержимому
Перевод от 2026-10-15; оригинал с тех пор изменился.

Цепочка поставок CI

Каждый job этого репозитория запускает сторонний код на машине, где есть checkout исходного кода, а в workflow релиза — ещё и учётные данные registry GHCR, registry npm и самих git-ссылок репозитория. Этот код приходит двумя путями: GitHub Actions (uses:) и бинарники, скачанные через curl (сканеры). Ни то, ни другое не пишется здесь, и ни то, ни другое не проходит ревью в PR.

Эта страница посвящена той части, которую мы контролируем. Она существует потому, что механизм жил только в комментариях workflow — там, где правило можно прочитать, но нельзя проверить, и где две половины разошлись именно по этой причине.

Угроза без обиняков​

Тег — это указатель. actions/checkout@v4 — не версия, а имя, которое владелец action может в любой момент удалить и создать заново, указав на другой коммит, без какого-либо сигнала в этом репозитории. Тот, кто перемещает этот тег, запускает свой код на нашем runner'е, в нашем checkout, с секретами, которые получил этот workflow.

То же самое верно для curl ассета GitHub Release: скомпрометированный релиз или MITM между runner'ом и CDN подсовывает другой бинарник, и никто этого не замечает — пока сканер не перестанет запускаться или, что хуже, пока он «работает».

Ни то, ни другое не является гипотезой именно о нашем репозитории. Это два задокументированных способа компрометации CI в реальном мире, и защита от обоих — одна и та же идея: ссылаться на содержимое, а не на имя.

Два механизма​

Бинарники: контрольная сумма после каждой загрузки​

За каждым curl ассета релиза в ci.yml и в обоих Dockerfile движка следует sha256sum -c против хеша, записанного в самом workflow:

GITLEAKS_SHA256: '551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb'

Хеши берутся из checksums.txt, публикуемого с каждым релизом, отдельной загрузкой. Расхождение проваливает job ещё до того, как бинарник будет хотя бы запущен.

propriedades.yml (плановые доказательства свойств, BRB-009) следует тому же правилу для трёх бинарников, нужных ему для подъёма кластера, — k3d, helm и kubectl, у каждого свой хеш в env: workflow. Он устанавливает их до запуска deploy/k8s/bootstrap.sh, и в этом порядке вся суть: ensure_k3d самого bootstrap скачивает k3d без контрольной суммы, и именно наличие бинарника в PATH не даёт ему скачивать. Отдельный шаг проваливает job, если закреплённые там версии расходятся с теми, что установил бы bootstrap. Что этот workflow по-прежнему принимает на веру, заявлено: чарты Helm приходят из своих репозиториев по версии, а не по digest (deploy/k8s/helm/charts.env), вместе с образами, на которые эти чарты ссылаются; а k3d скачивает образ узла rancher/k3s и образ своего балансировщика нагрузки по тегу, зашитому в бинарник k3d, — то есть они закреплены лишь настолько, насколько их закрепляет контрольная сумма k3d.

У версий сканеров есть второе ограничение: они должны совпадать с docker/engine/Dockerfile.prod, потому что тестировать против сканера, отличного от работающего в production, — ложный зелёный. Комментарий, обещавший это, был там со времён Фазы 5; шаг, который это проверяет (Versões dos scanners batem com o Dockerfile.prod do engine, в job lint), появился только с #408.

Actions: SHA коммита с версией рядом​

Каждый uses: закреплён на 40-символьном SHA коммита, а тег, из которого он взят, сохранён в комментарии в конце строки:

- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

Комментарий обязателен, и не для красоты. Это единственное, что сообщает человеку — и Dependabot, который читает именно этот комментарий, — какой версии соответствует хеш. SHA без версии рядом — это закрепление, которое никто не может проверить и никто не может обновить.

Чтобы разрешить тег в SHA для записи:

gh api repos/actions/checkout/commits/v4 --jq .sha

Почему есть проверка, а не только правило​

#408 закрепил actions в ci.yml и только в нём. Остальные пятнадцать workflow месяцами оставались на изменяемых тегах — включая release.yml (пушит образы в GHCR), publish-runner.yml (публикует в npm), tag-release.yml (создаёт теги) и docs-deploy.yml (пушит в gh-pages). Незакреплённой осталась именно та половина, у которой есть учётные данные.

Это не небрежность, а предсказуемая форма правила без механизма: новый workflow пишется копированием соседа, а у соседа был тег. Поэтому теперь у правила есть механизм — scripts/ci/actions-pinadas.ts, запускаемый в job lint, который проваливает любой uses:, не являющийся SHA коммита, и любой SHA без комментария с версией. Это то же рассуждение, что и в подсчётах RN и ADR: число, поддерживаемое вручную, устаревает в тот момент, когда кто-то забывает, поэтому артефакт читается, а не принимается на доверие.

Ссылка на action внутри этого репозитория (./.github/...) проходит: это наш код, проверяемый PR, который его меняет, без третьей стороны, способной что-либо переместить.

Браузер Playwright​

Браузерный E2E (ADR 0120) скачивает chromium во время CI из CDN Playwright — третья загрузка, которая не является ни action, ни ассетом GitHub Release, поэтому ни один из двух механизмов выше её не покрывает.

Закрепляет её точная версия @playwright/test в e2e/pnpm-lock.yaml: каждый релиз пакета привязан к сборке браузера, и playwright install загружает именно её. Значит, закреплением служит lockfile, а --frozen-lockfile в job превращает его в закрепление, а не в рекомендацию.

sha256sum -c здесь нет, и это честное описание: инструмент скачивает и проверяет сам, а воспроизводить его таблицу контрольных сумм вручную означало бы держать устаревающую копию. Слабее, чем бинарники сканеров, — сказано прямо, а не подразумевается.

Что job образов хранит в кеше и что кеш не может изменить​

Начиная с AT-304 job images в ci.yml хранит между запусками три вещи с помощью actions/cache (закреплённого по SHA, как и любой action), и каждая выбрана так, чтобы кеш экономил время, не сдвигая вердикт:

кешключчто определяет содержимое
браузер Playwright (~/.cache/ms-playwright)точная версия @playwright/test + хеш e2e/pnpm-lock.yamlзакрепление lockfile выше — новый Playwright означает новый ключ, никогда не старое попадание
store pnpm для e2e/ (setup-node, cache-dependency-path: e2e/pnpm-lock.yaml)хеш e2e/pnpm-lock.yamlpnpm install --frozen-lockfile, который по-прежнему сверяет целостность каждого tarball с lockfile
база уязвимостей Trivy (/tmp/trivy-cache/seed/db)версия Trivy + день UTC, с восстановлением самого свежего предыдущего днясам Trivy: --download-db-only по-прежнему запускается, читает NextUpdate самой базы и скачивает заново, когда срок прошёл

Последняя строка — та, что могла бы ослабить gate, но не ослабляет: кеш лишь засевает каталог, а свежесть оценивается тем же правилом, которое Trivy применяет на ноутбуке. Кеш убирает загрузку ~110 МБ из registry при каждом запуске — она же была источником эпизодического сбоя TOOMANYREQUESTS. release.yml этот кеш не использует: тег сканируется с базой, которую он скачивает сам.

Кеш браузера хранит только браузер. Системные библиотеки, которые ставит --with-deps, уходят в apt runner'а, который никакой кеш каталога сохранить не может, поэтому тёплый запуск всё равно их устанавливает (playwright install-deps).

Область видимости определяет GitHub, а не мы: ci.yml запускается только на pull_request, поэтому эти кеши записываются в области самого PR и читаются последующими push этого PR (и областью базовой ветки, в которую этот workflow никогда не пишет). Один PR не может засеять кеш другого PR.

Образы контейнеров: digest с тегом внутри ссылки​

Сторонние образы закрепляются по digest, а тег, из которого они взяты, пишется внутри ссылки, перед digest — image:tag@sha256:<index> (ADR 0178):

image: neo4j:5.26-community@sha256:d9cfe82983d27f5a75b3aaae8f316d04f9a698a3b7f6103a508f7caf8362f255
FROM node:26.12.1-alpine3.24@sha256:40cd011b55415bc894e45b02fdd5a5dfde056921b7d23a64c74f80497af48797 AS deps

Когда присутствуют оба, Docker скачивает по digest: тег — информация для читающего строку, а байты определяет digest.

До ADR 0178 тег жил в комментарии (neo4j@sha256:… # 5.26-community, а в Dockerfile — на строке выше), по образцу actions. Он сменил место из-за измерения (AT-139, 2026-09-27): Dependabot не читает этот комментарий ни в каком формате, а закрепление только по digest «обновляется» до digest latest — что увело бы pgvector, neo4j и node на другой major, при этом комментарий по-прежнему утверждал бы старый тег, а lint был бы зелёным. С тегом inline Docker, Dependabot и человек читают один и тот же тег. imageName у CloudNativePG уже имел такую форму по другой причине: его webhook отвергает ссылку только с digest, потому что читает версию Postgres из тега.

Комментарий по-прежнему разрешён, но только если он говорит то же самое: комментарий, утверждающий тег (один токен с цифрой), отличный от inline, проваливает проверку — это именно тот остаток, который бросила бы смена версии. В Dockerfile комментарий в конце строки FROM по-прежнему проваливает проверку: парсер Docker распознаёт # только в начале строки, поэтому FROM alpine:3.20@sha256:… # 3.20 — это FROM с тремя аргументами, и сборка падает с "FROM requires either one or three arguments". Это было обнаружено правильным образом при ADR 0159 — шаг bake job images упал при первом push за 27 секунд, — а hadolint пропустил тот же файл: согласие линтера — не согласие сборки.

Эта страница раньше утверждала обратное — закрепление по тегу было «осознанной остановкой, а не небрежностью», покупающей повседневную воспроизводимость «без затрат на сопровождение digest для образов, которые мы не публикуем». Это рассуждение отменено, и причина — измерение: все 37 сторонних ссылок в репозитории были на тегах, и три места, где они выполняются, хуже, чем устаревший docker compose pull.

  • Строки FROM — основа образов, которые мы публикуем в GHCR, — их пять начиная с ADR 0162. Перемещённый тег превращается в байты внутри образа, который мы подписываем и отдаём другим людям.
  • docker/docker-compose.install.yml работает на машине того, кто установил продукт, рядом с его Postgres. В этом же файле наши собственные образы уже приходят по digest, через переменную, которую заполняет установщик, — а сторонние приходили по тегу, рядом с ними.
  • ci.yml, golden-set-rag.yml и golden-set-qa.yml запускают сторонние образы как services: job. Это буквально тот runner, ради защиты которого существует правило для actions, достигнутый через другую дверь. Начиная с ADR 0197 они не хранят никаких литералов: см. Workflow читают compose ниже.

Чтобы разрешить тег в digest для записи — digest индекса, чтобы закрепление продолжало работать на linux/arm64, помимо linux/amd64:

docker manifest inspect neo4j:5.26-community | head -3 # confirms it is an index
docker buildx imagetools inspect neo4j:5.26-community --format '{{.Manifest.Digest}}'

Тег обязателен по той же причине, по которой обязателен комментарий с версией у actions: sha256:22ec5cd0… никому не говорит, что это Neo4j 5.26. И, как с actions, у правила есть механизм, а не добрая воля, — scripts/ci/imagens-pinadas.ts, запускаемый в job lint, который проваливает любой image:/imageName:/FROM, не являющийся digest, любой digest без тега inline (включая старую форму только с комментарием), комментарий с тегом, расходящийся с тегом inline, комментарий в конце FROM и один и тот же тег inline с двумя разными digest в двух файлах. Он родственник actions-pinadas.ts, а не его расширение: uses: живёт в YAML workflow с одним синтаксисом, образы живут в compose, манифестах kustomize и Dockerfile с тремя другими, и функция, отвечающая на оба вопроса, ответила бы плохо на оба.

Что проверка намеренно не покрывает:

  • Образы, которые мы собираем сами (brabo-api, brabo-engine, brabo-web, brabo-backup и brabo-broker — последний публикуется начиная с ADR 0162). Нет третьей стороны, которая могла бы что-то переместить, а brabo-api:prod — локальный тег, digest которого до сборки не существует. Там, где они проходят через registry, они уже идут по digest, через механизм, который ими владеет: .release/images.json, записываемый release.yml и применяемый make imagens-do-release (ADR 0119). Overlay репозитория хранит маркер, а не замороженный релиз — требование литерального digest там противоречило бы этому механизму, вместо того чтобы его усиливать.
  • Интерполированные ссылки (${BRABO_API_IMAGE:?…}) — тот же случай, увиденный с другой стороны.
  • Стадии multi-stage сборки (FROM deps AS build, FROM scratch), которые не являются образами registry.

Список исключений задаётся по имени и закрыт при сбое: новый сторонний образ никогда не совпадёт с brabo-, поэтому он рождается под правилом.

Workflow читают compose​

До ADR 0197 pgvector и ollama жили в двух местах: в compose и литералами в services: у ci.yml и двух workflow golden-set. Ни одна экосистема Dependabot не читает services: workflow, поэтому каждый PR бота, поднимающий один из них в compose, рождался бы красным из-за правила «один тег, два digest», а шаг, который выровнял бы workflow в PR бота, не может запушить с GITHUB_TOKEN, у которого никогда нет права менять .github/workflows/.

Литерал ушёл. Переиспользуемый workflow .github/workflows/imagens-do-compose.yml запускает scripts/ci/imagens-do-compose.ts, который читает image: каждого сервиса из docker/docker-compose.yml (и отвергает тот, что не имеет вида image:tag@sha256:<index>), а вызывающие пишут image: ${{ needs.imagens.outputs.pgvector }}. Несуществующий дубликат не может разойтись: PR бота трогает только compose, и CI того же PR уже работает против нового образа. То, что golden-set-rag.yml обещал в комментарии, — тот же Ollama, что и в dev, потому что нижняя граница golden-set привязана к модели, а не к окружению, — теперь верно по построению.

imagens-pinadas.ts выучил новую форму и закрывает дверь, которую она открывает. В workflow он проваливает любой сторонний литерал (закреплённый или нет); любое выражение, не являющееся в точности ${{ needs.<job>.outputs.<name> }}, — литерал внутри выражения, значение по умолчанию через ||, env., vars., format() — всё это места, где могла бы спрятаться изменяемая ссылка, а старый шаблон даже не совпадал со строкой с пробелами; и needs.<job>, чей job не вызывает переиспользуемый workflow, или выход, который он не объявляет. Каждое правило доказано мутацией в своём spec.

Цена реальна, и часть её теперь платит Dependabot. Образ, закреплённый по digest, не получает обновлений безопасности, пока кто-то не сменит digest, — тот же долг, что несут SHA у actions. .github/dependabot.yml включает github-actions по этой причине и, начиная с ADR 0197, docker-compose (/docker) и docker (/docker/* и /deploy/k8s/**), каждый сгруппирован в еженедельный PR в dev. Временный ignore MAJOR для node (AT-360) ушёл с переходом на Node 26 (решение владельца, 2026-10-02): обе экосистемы образов теперь предлагают major-версии node, как любое другое обновление. По-прежнему вручную, и это заявлено в ADR 0197: neo4j и ollama живут также в deploy/k8s/base/, две экосистемы никогда не делят один PR, а перевыпуск digest с тем же тегом приводит к падению обоих PR, пока человек их не объединит; imageName у CloudNativePG и IMAGEM_DO_GOLDEN_SET_QA не читаются ни одной экосистемой. Они следуют процедуре runbook.

Digest гарантирует неизменяемость, а не доступность. Закрепление защищает от того, что владелец тега его переместит; оно не защищает от того, что издатель удалит репозиторий. Так случилось с MinIO: S3-сервер локального overlay был закреплён по digest индекса в quay.io/minio/minio, MinIO перестал публиковать community-образ, и тот же digest стал отвечать 401 (Docker Hub: 404). Bootstrap падал на rollout status, и ни одно доказательство свойств не запускалось, пока сервер не заменили на SeaweedFS (ADR 0169). Симптом, если это повторится с любым закреплённым образом, — ImagePullBackOff на digest, который не менялся; исправление — новый образ от издателя, который его ещё раздаёт, но никогда не снятие закрепления.

Кеш сборки не может скрыть устаревший пакет​

Финальная стадия (runtime) каждого Dockerfile.prod выполняет apk upgrade. BuildKit строит ключ слоя по инструкции и родительскому слою, а никогда не по времени, поэтому при включённом cache-from сборка PR переиспользовала слой apk upgrade, замороженный при первой сборке этого PR (измерено: CACHED в 5 из 5 образов), и Trivy сканировал старый openssl, которого тег, собранный с нуля, не содержал бы. Поэтому docker-bake.hcl задаёт no-cache-filter = ["runtime"] в общей базовой цели: финальная стадия и всё, что после неё, пересобираются каждый раз, стадии сборки сохраняют свой кеш (именно там время), а сканирование видит то, что опубликовал бы тег. Это не allowlist Trivy. Стоимость — около 7–9 с на образ, параллельно.

Это исправило то, что сканирует PR. Это не могло исправить то, что публикует тег: тег собирается с нуля, в другом запуске, и до ADR 0172 его ничто не сканировало.

Релиз сканирует то, что публикует, до подписи​

release.yml запускает Trivy для каждого образа по digest, прямо из registry (--image-src remote), после того как bake запушил и записал .release/images.json, и до cosign sign. Порядок и есть механизм: образ, не прошедший проверку, никогда не подписывается, а install.sh не устанавливает неподписанный образ. Push к этому моменту уже произошёл (именно он создаёт digest), поэтому проваленный тег оставляет свои теги в GHCR без подписи и без Release — правильное состояние, ведь ничто их не устанавливает.

Gate — то же правило, что и job в ci.yml, флаг за флагом: --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 и тот же .trivyignore.yaml, где у каждой записи есть expired_at. HIGH или CRITICAL с доступным исправлением проваливает релиз. То, у чего исправления нет, сообщается без блокировки: второе сканирование без --ignore-unfixed и с --exit-code 0 питает scripts/ci/trivy-do-release.ts, который пишет сводку job и ассет Release trivy-sem-correcao.md. Этот скрипт никогда не выносит вердикт — его выносит код выхода Trivy, — потому что второе правило для «есть исправление» разошлось бы с правилом самого Trivy при первом же добавлении им нового статуса. Нового allowlist нет: шаг не принимает файл игнорирования, кроме того, что уже использует PR.

Бинарник — тот же v0.70.0 с тем же sha256, что и в ci.yml, объявленный дважды, потому что env: одного workflow нельзя импортировать. За дублированием следят: scripts/ci/trivy-do-release.spec.ts проваливается, когда версия, хеш или флаги gate расходятся между двумя workflow, когда gate переносится после подписи или когда появляется второй файл игнорирования. Как и install-e2e.yml, релиз запускается только на финальном теге, поэтому этот spec — то доказательство, которое может дать PR.

Измерено 2026-09-27 против digest последнего релиза (v6.1.0, четыре образа, опубликован 2026-09-14), с теми же флагами: api и web не прошли бы — CVE-2026-45447 (HIGH, libcrypto3/libssl3 3.3.7-r0, исправлено в 3.3.7-r1), CVE, опубликованная после того тега, — engine и backup проходят, и ничего HIGH/CRITICAL без исправления не появляется после применения .trivyignore.yaml. Без него engine нёс бы 56 исправимых находок в сторонних бинарниках сканеров, которые этот файл документирует. Четыре сканирования плюс загрузка базы заняли около 30 с.

Зависимость, закреплённая на git-коммите: bandit, временно​

Каждая зависимость движка приходит из hex.pm, с одним ВРЕМЕННЫМ исключением. Advisory EEF-CVE-2026-105659 (GHSA-69m6-wcv4-rwcf, HIGH, HTTP/2 в Bandit) затрагивает любой Bandit >= 1.4.0, включая 1.12.5, последний релиз в hex, и mix hex.audit в CI проваливает из-за него каждый PR. Исправление существует только как коммит d88b21953c65061f21136ea76a89a4f95a2ac7a1 в mtrudel/bandit (смержен 2026-10-12), без релиза. С 2026-10-14 apps/engine/mix.exs указывает bandit на этот коммит, по полному SHA и никогда по ветке, с override: true, потому что phoenix и opentelemetry_bandit требуют пакет из hex (измерено: без этого mix deps.get останавливается с "Dependencies have diverged"). SHA играет ту же роль, что контрольная сумма для пакета из hex: lock его записывает, и никто не может его переместить. Оба Dockerfile движка уже устанавливают git на стадии сборки.

Путь назад: вернуться к {:bandit, "~> 1.12"} из hex, как только будет опубликован релиз, содержащий этот коммит, и удалить этот раздел вместе с этим.

Что до сих пор принимается на веру​

Заявлено, не исправлено:

  • Без Dependabot. Половина закрыта, а другую половину закрывать не нам. Утверждение также перестало быть верным: обновления безопасности включаются в интерфейсе репозитория, ни в каком файле, и выполнялись ежедневно против npm_and_yarn in /., /website — и падали каждый день с Dependabot::DependabotError. Измеренная причина: пакеты, за которыми он гоняется (multer, esbuild и список рядом с ними), — те, что этот репозиторий исправляет через overrides в pnpm-workspace.yaml, механизм, с которым Dependabot работать не умеет. Alert, за которым он гоняется, уже закрыт; остаётся ошибка.

    .github/dependabot.yml теперь существует и делает две вещи, которые может сделать файл: включает github-actions — долг, названный прямо здесь, поскольку SHA перемещаются вручную, а закрепление, которое никто не обновляет, — это закрепление, стареющее до заведомо уязвимой версии, — и объявляет два дерева npm с open-pull-requests-limit: 0, что отключает рутинные обновления версий, не затрагивая обновления безопасности. Отключение обновлений безопасности — переключатель интерфейса, доступный только владельцу репозитория; пока кто-то не решит, этот job продолжает падать на alert'ах, которые overrides уже закрыли.

    Каждый PR Dependabot входит через dev (target-branch: dev во всех пяти записях — включая обе экосистемы образов, начиная с ADR 0197). Обновления безопасности игнорируют этот ключ и открываются против ветки по умолчанию, поэтому dependabot-para-dev.yml закрывает те, что dev уже исправляет, и перенаправляет остальные — см. Dependabot enters through dev в branching-policy.md.

  • Зависимости npm/pnpm не аттестуются. Lockfile закрепляет версии и хеши целостности, и это реально, но проверки происхождения (npm audit signatures или аналог) нет ни в одном job.

  • Нет подписи и аттестации наших собственных артефактов. Закрыто ADR 0149 (BRB-005). release.yml подписывает публикуемые образы по digest с помощью keyless cosign — OIDC-идентичность workflow, без ключа на хранении где-либо, — а build-runner-binaries.yml получил консолидирующий job, публикующий один подписанный checksums.txt, покрывающий бинарники runner (четыре цели начиная с ADR 0174, убравшего darwin-x64), вместо подписи для каждого бинарника. Оба workflow проверяют то, что только что подписали, в том же запуске: подпись, которую никто не пытается проверить, — просто ещё один файл в релизе, а сбой иначе проявился бы на машине устанавливающего — худшем месте для его обнаружения.

    Начиная с ADR 0160 этот манифест покрывает больше, чем бинарники: install.sh и четыре файла, нужные ему для подъёма установки, — compose установки и три файла, которые он использует по относительному пути, публикуемые как ассеты brabo-install-* из checkout тега, — являются строками того же checksums.txt, и job выполняет над ним sha256sum -c --strict перед тем, как что-либо прикрепить. Установщик сверяет каждый из четырёх с этим манифестом, прежде чем что-либо спрашивать или записывать (RN-570). До этого compose был единственным, что использовал установщик и что ничто не проверяло — он даже не скачивался. Список живёт в scripts/ci/assets-do-instalador.ts, и его spec проваливается, когда копия в самом установщике расходится или когда compose получает относительный bind-mount, которого в нём нет.

    Образ broker — пятый, и он важнее всех (ADR 0162). До этого его никто не собирал: Dockerfile.prod существовал и проходил hadolint, но ни одна цель bake его не собирала, поэтому он никогда не сканировался и не публиковался, и установке нечем было предложить этот сервис. Теперь он проходит каждый gate, который проходят остальные четыре, — ci.yml собирает его в каждом PR, отвергает запуск от root, запускает на нём Trivy и поднимает его здоровым с rootfs только для чтения и без сети; release.yml публикует, записывает, подписывает и проверяет его по digest без лишней строки, потому что оба цикла читают .release/images.json. Причина, по которой ему это нужно больше, чем любому другому: в установке, давшей согласие, это единственный сервис, получающий socket Docker хоста, поэтому уязвимость в нём — путь ко всей машине. Его публикация делает его публичной целью для изучения; эта цена заявлена в ADR, а не скрыта. Overlay Kubernetes о нём не знает (argumentosDeSetImage выдаёт только четыре образа, которые объявляет база kustomize), — Deployment для broker нет, по решению.

    Чего это не покрывает, и это отдельный пункт: подписи кода бинарников runner для операционной системы (нотаризация macOS, Authenticode Windows), которая требует платной идентичности подписи и остаётся в backlog.

  • Сторонние образы закреплены по тегу, а не по digest. Закрыто (выше): все 33 сторонние ссылки — compose, манифесты kustomize и строки FROM в Dockerfile — закреплены по digest с тегом внутри ссылки (ADR 0178), services: у workflow читают свои из compose (ADR 0197), а scripts/ci/imagens-pinadas.ts провалит job lint при следующей регрессии. Что не закрыто, и это цена закрепления, а не остаток: digest не получает обновлений безопасности, пока кто-то его не поднимет. Экосистемы docker/docker-compose в Dependabot предлагают эти поднятия начиная с ADR 0197; то, до чего они не дотягиваются, перечислено там.

  • Разрешения самих workflow здесь не покрываются; это блок permissions: каждого workflow, и это отдельный аудит.

  • Повторный таймаут pnpm audit — ПРИНЯТЫЙ РИСК, по решению. Когда endpoint advisories npm перестаёт отвечать три раза подряд, job остаётся зелёным с громким предупреждением, а не красным — см. ADR 0140. Чего это стоит, говорится прямо: в таких запусках дерево зависимостей не проверяется, и запуск говорит об этом, а не намекает на чистый результат. Чего это не касается: реально сообщённая уязвимость по-прежнему проваливает с первой попытки, без retry, а нераспознанный сбой тоже проваливает (закрыто при сбое). Альтернатива — оставаться красным, пока третья сторона недоступна, — была измерена в день, когда это случилось (четыре PR заблокированы одним и тем же 503), и отвергнута, потому что gate, который кричит «волки», учит людей перезапускать до зелёного, а эта привычка не отличает настоящий красный от ложного.