diff --git a/editorial/production/README.md b/editorial/production/README.md index 9412ba5..5c4f3dc 100644 --- a/editorial/production/README.md +++ b/editorial/production/README.md @@ -1,6 +1,6 @@ # Производство редакционных партий -На 31 июля 2026 года строгий аудит проходит 193 из 358 созданных материалов. Остальные 165 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. +На 31 июля 2026 года строгий аудит проходит 196 из 358 созданных материалов. Остальные 162 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. ## Одна партия diff --git a/editorial/reviews/2023-06-draft.md b/editorial/reviews/2023-06-draft.md new file mode 100644 index 0000000..f314533 --- /dev/null +++ b/editorial/reviews/2023-06-draft.md @@ -0,0 +1,37 @@ +# П64 · 2023-06 · Секреты и цепочка поставки — три прохода саморевью + +## Рамка пакета + +- Slug: `editorial-2023-06-practice-secrets-supply-chain`, `editorial-2023-06-mechanism-secrets-supply-chain`, `editorial-2023-06-field-secrets-supply-chain`. +- Голос: М6, июнь 2023 года. Системный практик идёт от наблюдаемого deploy к раздельным фактам source, builder, secret boundary, artifact digest, attestation declaration и release. Формула текста: «симптом → причина → проверка → действие»; без обещаний, будто учебная модель наблюдала CI. +- Главная граница: пакет строит только детерминированные `synthetic-*` objects в памяти. Он не читает CI, сеть, registry, secret store, файлы или журналы; не создаёт secret, certificate, signature, attestation, SBOM и deploy; не проверяет provenance, identity, claims, образ или среду. +- Sidecar содержит ровно пять новых файлов: этот review, один upgrade-скрипт и три локальных SVG. Registry, README, `articles.json`, очередь, документация, Git, чужие файлы и релизные данные не менялись. + +## Проход 1 — источники, факты и границы + +- Историческая граница перепроверена 31.07.2026 по первичным и официальным источникам. К июню 2023 уже были доступны: [SLSA v1.0 final, 19.04.2023](https://slsa.dev/blog/2023/04/slsa-v1-final), [версионная SLSA specification v1.0](https://slsa.dev/spec/v1.0/), [Cosign v2.0.0, 24.02.2023](https://github.com/sigstore/cosign/releases/tag/v2.0.0) и [NIST SP 800-218 SSDF v1.1 Final, 03.02.2022](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-218.pdf). У SLSA v1.0 сейчас retired-статус, но ссылка фиксирует именно сохранённую историческую версию; в тексте не используется текущая v1.2 как материал автора 2023 года. +- Фактическая формулировка ограничена источниками: SLSA v1.0 задаёт язык уровней, provenance и рекомендуемых attestation formats; Cosign v2.0.0 существовал как официальный release; SSDF даёт общий язык безопасной разработки. Ни один источник не используется как доказательство состояния данного проекта или конкретного pipeline. +- `assembleSyntheticSupplyChain()` принимает только `synthetic: true` и явно помеченные labels. Она отклоняет отсутствие builder, не-synthetic input, другой subject digest и value-like label секрета. Valid branch намеренно оставляет `signature: not-created`, `verification: not-run`, `provenance: not-verified` и `release: manual-review-required`. +- `runSupplyChainFixture()` содержит 16 assertions о памяти: структура links, reference-only secret label, совпадение subject и digest, закрытый набор входных полей, отрицательные ветки, отсутствие создания crypto material, граница модели и предел rollback. Нет assertions о настоящем secret, certificate, signature, attestation, scanner, CI, network, registry, deploy или provenance. +- Во всех трёх статьях отдельно сказано: declaration signing или attestation не доказывает provenance и не доказывает, что deployed artifact является тем же артефактом, пока отдельная проверка не сопоставит subject, доверенную identity, условия builder, digest и deploy input. + +## Проход 2 — голос, полнота и объём + +- Первые два абзаца каждой статьи содержат проблему и цену: аудит видит только deploy, а секрет или неподписанный output может пройти ранние звенья без связанного evidence. Это не объявляется инцидентом, утечкой или свойством локального CI. +- Три статьи различают задачи: practice строит карту trust boundary, mechanism разделяет data/declaration/verification/provenance, field ведёт диагностику неполной цепочки и решение о stop/rollback. В каждой есть отдельная таблица, содержательная SVG с alt/caption, исполнимый fixture/code, упорядоченный маршрут, ограничения, rollback и следующий шаг. +- Внутренний revision builder считает основной текст без списка источников и удерживает диапазон 5 000–15 000 знаков. Фактический объём: practice — 9 084, mechanism — 10 055, field — 10 282 знака. Тон соответствует М6: короткие технические утверждения, отдельные статусы неизвестности и отсутствие выдуманных метрик, incident story или опыта эксплуатации. +- В учебных примерах нет реальных secrets, tokens, URLs рабочей системы, policies, CVE, CI logs или scanner data. External URLs находятся только в списке проверяемых официальных источников; фикстура использует synthetic labels и относительный путь к своему модулю. + +## Проход 3 — визуалы, безопасность и выпуск + +- SVG разделяют вопросы: trust-boundaries показывает шесть звеньев и их неполное доказательное значение; attestation-contract отделяет input fields от declaration и verification; release-decision показывает ручной статус для любой отсутствующей связи. На первом рендере 375 px были исправлены пересечение подписи с boundary и наложение веток дерева; повторный визуальный проход подтвердил читаемость без обрезки текста. +- В SVG нет `script`, `foreignObject`, внешних URL, `data:image`, event handlers, пользовательского ввода или интерактивности. Подписи фигур прямо отделяют схему от реальной сети, CI, certificate, registry, deploy и policy. +- До интеграции выполнены: `node --check web/scripts/upgrade-2023-06.mjs` — PASS; `node web/scripts/upgrade-2023-06.mjs --verify-fixture` — PASS 16/16 после независимого model review; `cd web && npm run audit:draft -- scripts/upgrade-2023-06.mjs` — PASS для трёх slug; отдельная import-safe проверка — PASS, 3 revisions без `date`/`author`; `xmllint --noout` для трёх SVG — PASS; SVG safety scan — clean; Sharp-review всех SVG на ширине 375 px — PASS после корректировки компоновки. +- Пакет сознательно не интегрирован: registry, README, `articles.json`, строгий аудит поверх registry, production build, staging, commit и push оставлены главному агенту для отдельного ревью и приёмки. + +## Приёмка главного редактора + +- Первичные источники сверены отдельно от рукописи: SLSA подтверждает выпуск v1.0 19.04.2023 и сохранённая v1.0 specification прямо описывает уровни и рекомендуемые attestation formats, включая provenance; GitHub release Cosign v2.0.0 показывает публикацию 24.02.2023; NIST CSRC фиксирует SSDF v1.1 Final от 03.02.2022. Это исторические опорные точки, не evidence данного pipeline. +- Независимый model review обнаружил, что accepted fixture игнорировал дополнительные входные поля: реальное значение могло оказаться в `secretValue`, хотя returned record его не копировал. Вход сделан plain-record и закрытым whitelist; неизвестное поле отклоняется до сборки. Одновременно rollback начал сохранять subject и declaration вместе с четырьмя остальными labels, чтобы не восстанавливать неполный контракт. Fixture проходит 16/16 и включает отрицательный case для `secretValue`. +- Все три SVG прошли XML и safety scan; просмотр на 375 px подтвердил порядок стрелок, читаемость критичных подписей и отсутствие обрезки. Строгий audit после подключения revision: 9 132 / 10 261 / 10 350 знаков, по одной фигуре, таблице и примеру. Registry содержит 187 уникальных revision; production build прошёл с 374 статическими страницами. +- `articles.json` и чужие рабочие изменения не редактировались. Следующий этап — staging и отдельный коммит только этой июньской партии. diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs index 244fbe7..f856970 100644 --- a/web/data/editorial-revisions.mjs +++ b/web/data/editorial-revisions.mjs @@ -60,6 +60,7 @@ import { revisions as february2023Revisions } from '../scripts/upgrade-2023-02.m import { revisions as march2023Revisions } from '../scripts/upgrade-2023-03.mjs'; import { revisions as april2023Revisions } from '../scripts/upgrade-2023-04.mjs'; import { revisions as may2023Revisions } from '../scripts/upgrade-2023-05.mjs'; +import { revisions as june2023Revisions } from '../scripts/upgrade-2023-06.mjs'; // This layer replaces archived source entries without losing their stable slug and date. export const editorialRevisions = [ @@ -125,4 +126,5 @@ export const editorialRevisions = [ ...march2023Revisions, ...april2023Revisions, ...may2023Revisions, + ...june2023Revisions, ]; diff --git a/web/public/assets/editorial/2023/secrets-supply-chain-2023-attestation-contract.svg b/web/public/assets/editorial/2023/secrets-supply-chain-2023-attestation-contract.svg new file mode 100644 index 0000000..6886e9a --- /dev/null +++ b/web/public/assets/editorial/2023/secrets-supply-chain-2023-attestation-contract.svg @@ -0,0 +1,71 @@ + diff --git a/web/public/assets/editorial/2023/secrets-supply-chain-2023-release-decision.svg b/web/public/assets/editorial/2023/secrets-supply-chain-2023-release-decision.svg new file mode 100644 index 0000000..fcbd2ba --- /dev/null +++ b/web/public/assets/editorial/2023/secrets-supply-chain-2023-release-decision.svg @@ -0,0 +1,69 @@ + diff --git a/web/public/assets/editorial/2023/secrets-supply-chain-2023-trust-boundaries.svg b/web/public/assets/editorial/2023/secrets-supply-chain-2023-trust-boundaries.svg new file mode 100644 index 0000000..ff2688f --- /dev/null +++ b/web/public/assets/editorial/2023/secrets-supply-chain-2023-trust-boundaries.svg @@ -0,0 +1,63 @@ + diff --git a/web/scripts/upgrade-2023-06.mjs b/web/scripts/upgrade-2023-06.mjs new file mode 100644 index 0000000..700eae6 --- /dev/null +++ b/web/scripts/upgrade-2023-06.mjs @@ -0,0 +1,402 @@ +function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +const p = (text) => '
' + text + '
'; +const h2 = (text) => '' + escapeHtml(text) + '';
+const ol = (items) => '| ' + item + ' | ').join('') + '
|---|
| ' + item + ' | ').join('') + '
secretValue, возвращает отказ до сборки записи. Такой тест полезен для ревью структуры: он не может прочитать секрет, запустить runner, создать certificate или увидеть развёрнутый образ.'),
+ code(syntheticExample + '\n\nnode web/scripts/upgrade-2023-06.mjs --verify-fixture\n\n# PASS означает только: учебный договор непротиворечив.\n# PASS не означает: secret защищён, statement подписан или deploy проверен.'),
+ p('Важная ветка fixture — совпадение artifactDigest и attestationSubjectDigest. Это минимальная защита от ошибки, когда красивый statement относится к другому output. Но совпадение двух строк не проверяет криптографию, доверие к builder или содержимое образа. В реальной цепочке такой результат становится лишь входом для следующей проверки. Если проект не знает, кто её выполняет и где хранится результат, лучше оставлять статус «не подтверждено», а не превращать fixture в формальный допуск.'),
+ h2('Маршрут: симптом → причина → проверка → действие'),
+ ol([
+ 'Симптом. В release виден deploy, но нельзя назвать его source revision, digest или границу работы с секретом.',
+ 'Причина. Последний шаг хранит событие выкладки, а evidence промежуточных звеньев не имеют общего ключа и владельца.',
+ 'Проверка. Для одного учебного выпуска заполните строку на каждое звено таблицы. Прогоните fixture: он обязан отклонить отсутствие builder, непомеченный input, иной subject и value-like label секрета.',
+ 'Действие. Выберите один общий ключ сопоставления: digest артефакта. Рядом определите, какой реальный источник позднее подтвердит source, build, secret boundary, attestation и deploy input.',
+ 'Проверка границы. Отдельно спросите владельца CI, кто будет проверять подпись и identity. Ответ «в declaration написано» не является шагом проверки.',
+ 'Следующий шаг. Добавьте контракт evidence в release design и договоритесь о минимальном безопасном запуске проверки вне этого fixture.',
+ ]),
+ h2('Что откатывать, когда цепочка неполна'),
+ p('Rollback здесь не означает удалить секрет, отозвать сертификат или повернуть production-трафик. Учебная функция умеет только вернуть snapshot своей записи и помечает provenance как not-verified, release как not-attempted. В настоящем выпуске сначала решают операционный вопрос: какой deploy input надо остановить, как сохранить рабочее доказательство без чувствительных данных и какой другой артефакт разрешён как известный. Это отдельный план с владельцем и средой; его нельзя вывести из одной attestation.'),
+ p('Опасная ошибка — признать неполную цепочку «безопасной», потому что текущий deploy уже прошёл. Правильный статус иной: выпуск недостаточно объяснён, поэтому решение о продолжении требует явного владельца и дополнительного evidence. Такой статус не обвиняет CI и не объявляет инцидент. Он не позволяет подменить неизвестность уверенным текстом, а значит уменьшает риск сделать необратимый шаг на ложной связи.'),
+ h2('Границы модели и следующий рабочий шаг'),
+ p('Эта статья не содержит настоящего секрета, токена, адреса, командной политики, CVE, scanner data, CI log, registry или deploy. Она не создаёт и не проверяет certificate, signature или attestation и не заявляет, что какой-либо артефакт имеет provenance. Отдельно важно: даже подписанная декларация может говорить только о том, что конкретный statement был подписан. До проверки subject, доверенной identity, условий builder и связи с deploy она не доказывает ни происхождение, ни факт использования артефакта.'),
+ p('Следующий шаг для команды — выбрать один выпуск, построить карту из шести звеньев и обозначить у каждого владельца evidence. Если на любой стрелке нельзя назвать проверяемый факт, не достраивайте его предположением. Зафиксируйте пробел и сузьте задачу до одного реального вопроса: например, как сопоставить deploy input с digest, не публикуя чувствительные данные. Так системная практика растёт из наблюдаемой связи, а не из набора терминов.'),
+ h2('Историческая граница июня 2023'),
+ p('К июню 2023 уже были доступны стабильная SLSA v1.0, опубликованная 19.04.2023, и Cosign v2.0.0, опубликованный 24.02.2023; SSDF v1.1 был финальным документом NIST с февраля 2022. В статье используются только их версии и общий понятийный аппарат того периода. Поздние возможности платформ доставки, современные форматы bundle и текущие интеграции не приписываются автору 2023 года.'),
+]);
+
+const mechanism = revision({
+ slug: 'editorial-2023-06-mechanism-secrets-supply-chain',
+ title: 'Attestation без самообмана: что именно связывает provenance с артефактом',
+ categories: ['Безопасность', 'DevOps'],
+ cover: '/assets/editorial/2023/secrets-supply-chain-2023-attestation-contract.svg',
+ excerpt: 'Механическая модель для различения artifact digest, statement, подписи, проверки и deploy: declaration не становится provenance без самостоятельной цепочки evidence.',
+ readingMinutes: 12,
+}, [
+ p('Проблема выглядит убедительно на словах: рядом с образом есть attestation, поэтому кажется, что его происхождение установлено. Цена ошибки появляется при первом спорном deploy. Если statement относится к другому digest, не проверена identity builder или никто не сравнивает вход выкладки с тем же артефактом, команда не знает, что именно защищает её решение. Наличие файла создаёт ощущение завершённости, хотя главное сопоставление не сделано.'),
+ p('Вторая потеря менее заметна: секрет, доступный build, начинает считаться частью provenance. Это разные вопросы. Provenance должно описывать происхождение и свойства output в заданной модели; обращение с секретом — отдельная trust boundary с собственным evidence. Если их склеить в одну декларацию «CI безопасен», нельзя проверить ни то, что secret не попал в output, ни то, что артефакт получен ожидаемым способом. Вместо проверяемого контракта остаётся набор обещаний.'),
+ h2('Разделите пять уровней утверждения'),
+ p('Начните с того, что разные слова отвечают на разные вопросы. Source revision обозначает выбранный вход. Builder identity обозначает, кого ожидают видеть исполнителем. Artifact digest обозначает конкретный результат. Attestation statement заявляет связь между полями. Verification — отдельная операция, которая проверяет допустимые свойства statement в известном контексте. Deploy input — ещё один факт: его нужно сопоставить с артефактом, а не вывести из названия release.'),
+ p('SLSA v1.0 полезна как рамка именно для такой дисциплины. Спецификация описывает уровни, recommended attestation formats и provenance; она не включает в себя память о каждом вашем запуске CI. Документация Sigstore объясняет, что подпись и attestation проверяются с параметрами доверия. Из этого следует практический вывод: имя формата или наличие подписи нельзя выдавать за результат реальной verification. Модель сначала фиксирует условия, которые должны быть проверены, и только затем допускает отдельную процедуру.'),
+ table('Что можно утверждать на каждом уровне', ['Уровень', 'Допустимая запись', 'Недопустимый вывод', 'Что добавить для следующего уровня'], [
+ ['Source', 'выбрана synthetic source revision', 'этот input действительно собрался', 'факт запуска build в заданном контексте'],
+ ['Builder', 'ожидается synthetic builder identity', 'identity доверена и исполнила build', 'правило доверия и результат проверки'],
+ ['Artifact', 'назван synthetic digest output', 'именно он поступил в deploy', 'сопоставление с deploy input'],
+ ['Attestation', 'statement ссылается на тот же synthetic digest', 'statement подписан и правдив', 'проверка подписи, identity и claims'],
+ ['Secret boundary', 'указана synthetic ссылка без значения', 'значение не утекло в output', 'отдельная проверка пути передачи'],
+ ['Release', 'нужен ручной review', 'артефакт применён в среде', 'наблюдаемый deploy input и его связь с digest'],
+ ]),
+ h2('Контракт attestation должен быть маленьким и точным'),
+ p('У учебной записи есть три строгие границы. Первая: `artifactDigest` равен `attestationSubjectDigest`. Вторая: secret представлен как label `synthetic-secret-reference-no-value`, а не как значение. Третья: вход принимает только заранее перечисленные поля, поэтому `secretValue` или произвольный extra field не может тихо пройти в accepted record. Первая связь защищает от бессмысленного statement про чужой output. Вторая и третья защищают от более простой ошибки в документации: не превращать разбор цепочки в новый канал распространения чувствительных данных. Ни одна из границ не утверждает, что криптографический материал существует.'),
+ p('Вместо булева поля `trusted: true` модель возвращает ограничения явными значениями: signature not-created, verification not-run, provenance not-verified и release manual-review-required. Это не излишняя осторожность, а полезный интерфейс. Читатель сразу видит, чего модель не делает. Если функции присвоить статус «verified» без внешней проверки, она создаст ложное evidence, потому что локальная строка будет выглядеть как результат работы реального verifier.'),
+ figure('/assets/editorial/2023/secrets-supply-chain-2023-attestation-contract.svg', 'Контракт учебной attestation: source revision и builder identity дают входные утверждения, artifact digest обязан совпасть с subject declaration, но справа отдельно отмечены не выполненные verification, provenance и deploy; секрет обозначен только ссылкой без значения.', 'Рисунок разделяет данные, declaration и самостоятельную verification. Он не показывает настоящий certificate, подпись, registry, CI configuration или факт использования образа.'),
+ h2('Исполнимый fixture проверяет форму, не поставку'),
+ p('Функция `assembleSyntheticSupplyChain()` принимает только явно synthetic input и закрытый набор полей. Она отклоняет пустой builder, непомеченную source revision, другой subject digest, label, похожий на попытку передать значение секрета, и отдельное поле secretValue. Затем `runSupplyChainFixture()` проверяет, что valid record остаётся ручным кандидатом на review, а не получает разрешение на deploy. Это удобно для изменения модели: при добавлении нового поля становится видно, какую отрицательную ветку надо добавить.'),
+ code(syntheticExample + '\n\nnode web/scripts/upgrade-2023-06.mjs --verify-fixture\n\n# PASS проверяет детерминированные synthetic objects в памяти.\n# Он не запускает CI, не обращается к secret store и не вызывает verifier.'),
+ p('Fixture намеренно не использует hash-функцию. Здесь `synthetic-digest-artifact-A` — label, а не digest реального файла. Иначе пример стал бы выглядеть как проверка артефакта, хотя не знает его байтов, registry, execution context и доверенной цепочки. Для production проверка должна работать с известным форматом, актуальным ключом или identity и точным input. В учебном материале полезнее сохранить честную границу, чем имитировать криптографию красивым именем поля.'),
+ h2('Почему declaration не доказывает provenance'),
+ p('Даже если declaration подписан, из этого ещё не следует, что output произошёл из заявленного source. Надо определить, кому доверяют как signer или builder, какая версия формата приемлема, какие claims обязаны совпасть, какой subject проверяется и как проверка связана с deploy. Каждый пункт имеет вероятность оказаться не выполненным. Поэтому фраза «есть signed attestation» описывает только часть состояния. Она не должна становиться короткой формой для «provenance доказано».'),
+ p('То же относится к секрету. Указание, что build вправе получить секрет, не доказывает отсутствие следа в layer, cache, metadata, журнале или другом output. Нельзя проверить это, перечислив места утечки из головы. Нужно выбрать конкретный поток передачи, владельца и безопасный метод наблюдения в закрытом рабочем контуре. Если он пока не определён, контракт должен сказать `not-observed`. Это точнее, чем обещание «секретов в образе нет» без реального исследования.'),
+ h2('Маршрут: симптом → причина → проверка → действие'),
+ ol([
+ 'Симптом. Attestation существует, но reviewer не может ответить, относится ли она к deploy input и какой факт о builder был проверен.',
+ 'Причина. В одной строке смешаны subject, подпись, provenance, работа с секретом и выпуск; у них нет отдельных условий и статусов.',
+ 'Проверка. Сравните subject statement с artifact digest. Затем перечислите отдельными полями signature, verification, provenance и release. Прогоните fixture и убедитесь, что он отвергает несовпадающий subject.',
+ 'Действие. Оставьте declaration только declaration. В release design запишите реальную процедуру verification, её владельца, доверенные входы и ожидаемый результат без публикации чувствительных данных.',
+ 'Проверка границы секрета. Уберите значение из документа и спланируйте отдельное evidence о маршруте передачи, а не о содержимом секрета.',
+ 'Следующий шаг. Включите сопоставление deploy input с digest как самостоятельный acceptance criterion, когда для него согласованы среда и владелец.',
+ ]),
+ h2('Rollback не отменяет уже сделанные заявления'),
+ p('У этой учебной модели rollback восстанавливает только исходные labels. Он не отзывает ключ, не удаляет artefact, не исправляет реальную запись или deployment. В production откат должен сначала ответить на два вопроса: какой факт оказался недостоверным и что уже зависит от спорного output. Затем выбирают действие, которое не увеличивает неопределённость: остановить следующий шаг, сохранить минимальное evidence и вернуться к известному кандидату. Механизм работает только при заранее определённых границах ответственности.'),
+ p('Нельзя считать reset документа доказательством, что риск исчез. Если statement уже был использован в release decision, надо отдельно пересмотреть это решение. Если вопрос касается секрета, его жизненный цикл и возможная смена доступа живут в операционном плане, а не в функции, которая сравнивает labels. Простая модель полезна тем, что не маскирует эту разницу: возврат записи — это возврат записи, не исправление среды.'),
+ h2('Ограничения и следующий шаг'),
+ p('Пакет не содержит реального секрета, token, URL рабочей системы, policy, CVE, CI log, scanner data или сведений о конкретном образе. Он не генерирует certificate, signature, attestation, SBOM или release. Источники объясняют версию SLSA, доступный инструмент и практику SSDF, но не предоставляют проекту автоматического соответствия. В частности, declaration signing или attestation не доказывает provenance и не доказывает, что артефакт был использован в deploy.'),
+ p('Следующий шаг — не внедрять все уровни сразу. Возьмите один существующий release design и выпишите пять разных утверждений из таблицы. Там, где обещание не имеет метода проверки, оставьте `not-verified` и назначьте владельца следующего маленького исследования. Это делает обсуждение короче: команда спорит не о красивом слове provenance, а о конкретном отсутствующем сопоставлении.'),
+ h2('Историческая граница июня 2023'),
+ p('Версии в этой статье не выходят за июнь 2023: SLSA v1.0 стала финальной 19.04.2023, Cosign v2.0.0 опубликован 24.02.2023, SSDF v1.1 — 03.02.2022. Поэтому нет ссылок на будущие функции платформ и нет предположения, что современный формат attestation был доступен автору тогда. Термины применены как язык для проектного контракта, а не как обещание уровня зрелости.'),
+]);
+
+const field = revision({
+ slug: 'editorial-2023-06-field-secrets-supply-chain',
+ title: 'Deploy виден, происхождение нет: диагностика разрыва в цепочке поставки',
+ categories: ['Безопасность', 'DevOps'],
+ cover: '/assets/editorial/2023/secrets-supply-chain-2023-release-decision.svg',
+ excerpt: 'Практический маршрут для случая, когда виден только deploy: как остановить выводы, восстановить цепочку source → build → digest → declaration → release и не перепутать ее с доказательством provenance.',
+ readingMinutes: 12,
+}, [
+ p('Проблема: после deploy есть только финальная запись о выпуске, а вопрос «какой именно output был использован и откуда он взялся?» остаётся без ответа. В такой ситуации легко открыть больше журналов и собрать много деталей, но не получить связанный факт. Цена — неверное решение под давлением: можно откатить не тот output, продолжить спорный выпуск или опубликовать чувствительные данные в попытке доказать, что secret не участвовал в сборке.'),
+ p('Второй риск — принять declaration за расследование. Рядом с deploy может лежать документ с полями source и builder, но без проверки subject, доверенного контекста и сравнения с deploy input он не меняет статус выпуска. Если сразу написать «provenance есть», команда лишает себя права задавать самый важный вопрос: какое звено подтверждено, какое неизвестно и что безопасно делать до завершения проверки.'),
+ h2('Начните с узкого диагностического сигнала'),
+ p('Не пытайтесь восстановить всю историю CI. Зафиксируйте один наблюдаемый пробел: deploy input не связан с digest, или digest не связан с declaration, или у declaration нет определённого метода verification. Каждый вариант ведёт к разной следующей проверке. Наблюдение должно быть нейтральным: «связь не найдена» не равно «артефакт вредоносный», а «secret boundary не описана» не равно «секрет утёк». Такая точность сохраняет возможность безопасно остановиться до ложного вывода.'),
+ p('Дальше отделите данные, которые можно назвать, от данных, которые нельзя публиковать. Допустимы synthetic labels, ожидаемый формат digest, идентификатор рассматриваемого звена и владелец проверки. Не нужны секреты, адреса сред, персональные identity, реальные policy, CI log и результаты scanner. В production эти сведения живут в контролируемом контуре и доступны тем, кому это необходимо. Редакционная схема должна помогать поставить вопрос, а не становиться копией чувствительного материала.'),
+ table('Диагностика разрыва перед решением о выпуске', ['Симптом', 'Вероятная причина', 'Проверяемый вопрос', 'Безопасное действие'], [
+ ['Deploy есть, digest неизвестен', 'вход release не сохранён как ключ сопоставления', 'какой output был передан в deploy?', 'приостановить следующий шаг и запросить owner evidence'],
+ ['Digest есть, declaration указывает иной subject', 'statement собран для другого output или перепутан', 'равны ли subject и рассматриваемый digest?', 'не использовать declaration как evidence'],
+ ['Builder назван, verification не определена', 'identity смешана с её проверкой', 'кто и по каким условиям проверяет identity?', 'завести отдельный verification design'],
+ ['Secret boundary описана общо', 'нет владельца пути передачи', 'какой этап получает ссылку, а какой не должен видеть значение?', 'сузить вопрос до одного перехода'],
+ ['Все поля есть только в документе', 'декларация принята за факт среды', 'какой источник независимо подтвердит каждую связь?', 'оставить статус not-verified'],
+ ['Нужен срочный rollback', 'не отделены спорный output и известный кандидат', 'что уже зависит от deploy input?', 'выбрать отдельный операционный plan'],
+ ]),
+ h2('Постройте цепочку в прямом, а не обратном порядке'),
+ p('Даже если расследование началось с deploy, карту лучше читать слева направо: source revision → builder identity → secret boundary → artifact digest → attestation declaration → deploy input. Так видно, что каждое звено имеет свой тип evidence. Source и builder отвечают на вопрос о контексте build. Secret boundary отвечает на вопрос о допустимом переходе, но не о содержимом. Digest связывает результат. Declaration описывает claims. Deploy input показывает, какой результат пытались применить. Нельзя заменить одно звено соседним.'),
+ p('Эта последовательность нужна не для бюрократии. Она защищает от диагностической ловушки: когда найден digest, возникает желание считать всё до него и после него понятным. Но digest сам по себе не наблюдал CI; declaration сама по себе не наблюдала deploy. Если все шесть звеньев не имеют общего контракта, их надо пометить как отдельные неизвестности. Такой результат может быть полезнее длинного отчёта, потому что владельцу выпуска ясно, какую проверку заказывать дальше.'),
+ figure('/assets/editorial/2023/secrets-supply-chain-2023-release-decision.svg', 'Дерево решения для учебного выпуска: сначала сопоставить deploy input и artifact digest, затем subject declaration, потом определить отдельную verification и границу секрета; любой отсутствующий факт переводит выпуск в ручной review, а не в подтверждённое provenance.', 'Рисунок показывает порядок диагностического решения и безопасный статус неизвестности. Он не является policy, автоматическим gate, историей CI или разрешением на deployment.'),
+ h2('Проверьте форму рассуждения локально'),
+ p('Fixture в пакете не заменяет исследование среды, но помогает не перепутать статусы. Он собирает только `synthetic-*` labels, требует один и тот же synthetic digest в artifact и subject declaration, запрещает непомеченный input и не создаёт секрет. В valid branch release всё равно остаётся `manual-review-required`. Это важно: даже идеально собранная учебная карточка не знает, что произошло в CI, и не должна выдавать разрешение на deploy.'),
+ code(`node web/scripts/upgrade-2023-06.mjs --verify-fixture
+
+# Проверяем: missing builder, другой subject digest,
+# непомеченный input, value-like label и отдельное поле secretValue отклоняются.
+# Не проверяем: CI, secret store, подпись, attestation, provenance или deploy.
+
+${syntheticExample}`),
+ p('Особенно полезна отрицательная ветка с другим subject digest. Она не утверждает, что настоящий statement поддельный: она показывает, почему несопоставленный statement нельзя использовать в решении. Для реального случая потребуется точный формат данных, метод verification и доказательство, что проверяется тот же предмет. Пока этого нет, release status остаётся не «fail» и не «pass», а «нужен ручной review». Это снижает риск сделать техническое решение на основании неполной семантики.'),
+ h2('Маршрут: симптом → причина → проверка → действие'),
+ ol([
+ 'Симптом. Запишите ровно одну отсутствующую связь: deploy → digest, digest → subject, builder → verification или secret boundary → owner.',
+ 'Причина. Проверьте, не заменил ли соседний факт отсутствующее evidence. Например, название CI не доказывает execution, а declaration не доказывает deploy input.',
+ 'Проверка. Пройдите таблицу и составьте карточку только с данными, которые допустимо назвать. Локально прогоните fixture, чтобы увидеть все отрицательные ветки контракта.',
+ 'Действие. Переведите выпуск в понятный ручной статус, назначьте владельца одного отсутствующего доказательства и выберите наименее рискованный следующий шаг.',
+ 'Проверка release. Когда появится real evidence в разрешённом контуре, отдельно сопоставьте его subject с digest и digest с deploy input. Не подменяйте этот шаг текстом в ticket.',
+ 'Следующий шаг. После решения сохраните минимальную ссылку на evidence и обновите карту, не копируя секреты или журналы в описание change.',
+ ]),
+ h2('Остановка и rollback — разные действия'),
+ p('Если связь не найдена, первое действие не обязательно rollback. Часто безопаснее не продолжать следующий шаг, сохранить текущий статус и запросить evidence. Rollback нужен, когда уже определён спорный output и есть известный кандидат, к которому можно вернуться без нового скрытого риска. Эти условия нельзя угадывать по наличию attestation. Они зависят от конкретной среды, совместимости и владельцев, поэтому живут в операционном плане, а не в универсальной статье.'),
+ p('Учебный rollback в этом пакете делает гораздо меньше: возвращает шесть synthetic полей исходной карточки, включая subject и declaration, и помечает release not-attempted. Он специально не делает вид, что изменил CI, удалил образ, отозвал credential или исправил production. Это полезная граница для ревью. Она напоминает, что «мы можем откатить» — проверяемое техническое утверждение, а не обязательная строка в конце security документа.'),
+ h2('Не выдавайте декларацию за evidence'),
+ p('Подписанная declaration может быть сильным входом, если проверены её подпись, identity, claims, subject и связь с тем output, который идёт в deploy. Но само наличие declaration не доказывает ни provenance, ни использование артефакта, ни отсутствие секрета в output. Это не недостаток формата: так устроена граница утверждения и проверки. Чем точнее команда называет её, тем легче построить контролируемую процедуру без универсальных обещаний.'),
+ p('SSDF помогает обсуждать такие процедуры общим языком, а SLSA v1.0 описывает, какие assurance-утверждения в принципе различаются. Они не заменяют принятие решения в конкретном delivery-контуре. В этой статье нет автоматического gate: есть порядок вопросов, который уменьшает шанс подменить незнание уверенностью. Если реальное evidence невозможно получить безопасно, это тоже результат диагностики: выпуск требует другого решения, а не более громкого statement.'),
+ h2('Ограничения и следующий шаг'),
+ p('Здесь нет реальных secrets, tokens, URLs рабочей системы, policies, CVE, CI logs, scanner data, registry или deployment результатов. Fixture строит детерминированный synthetic flow в памяти и не выполняет криптографию. Статья не заявляет, что certificate, signature или attestation создан, проверен или используется. В частности, declaration signing или attestation не доказывает provenance и deployed artifact до самостоятельного сопоставления и проверки в реальной среде.'),
+ p('Следующий шаг — провести короткое review одного release design с владельцем CI и владельцем deploy. Выберите одну missing link из таблицы, договоритесь о границе допустимого evidence и запишите, какой результат переведёт статус из `not-verified` в следующий технически определённый статус. Не требуйте от первой встречи готового соответствия SLSA. Достаточно, чтобы появилась одна проверяемая связь, которую нельзя спутать с декларацией.'),
+ h2('Историческая граница июня 2023'),
+ p('Все ссылки относятся к материалам, доступным к июню 2023: SLSA v1.0 стала стабильной 19 апреля 2023, Cosign v2.0.0 опубликован 24 февраля 2023, NIST SSDF v1.1 — в феврале 2022. Поэтому рассуждение не опирается на будущие CI-интеграции, поздние форматы bundle или современные dashboards. Автор 2023 года пользуется уже доступным словарём supply chain, но не приписывает себе несуществующие проверки.'),
+]);
+
+export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
+
+function verifyFixture() {
+ const report = runSupplyChainFixture();
+ const failed = Object.entries(report.assertions).filter(([, value]) => value !== true).map(([key]) => key);
+ if (failed.length) {
+ process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
+ process.exitCode = 1;
+ return;
+ }
+ process.stdout.write('PASS fixture: ' + Object.keys(report.assertions).length + '/' + Object.keys(report.assertions).length + ' assertions\n');
+}
+
+if (process.argv.includes('--verify-fixture')) verifyFixture();
+if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');