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('') + '
node web/scripts/upgrade-2023-04.mjs --verify-fixture. PASS подтверждает исключительно assertions synthetic контракта и должен оставить vulnerabilityStatus неопределённым.',
'Решение. Если факты не связываются, оставьте статус «требует project review», а не «ложное срабатывание». Отсутствие доказательства достижимости не становится доказательством отсутствия риска.',
]),
h2('Как сделать запись пригодной для следующего человека'),
p('Минимальная запись удобнее огромного отчёта, если в ней можно отделить наблюдаемое от предполагаемого. В поле source кладут immutable advisory URL или идентификатор базы. В поле resolved component — package, version и путь в lockfile именно того commit, который проверяется. В provenance SBOM — build ID или другой неизменяемый указатель на артефакт. В reachability — не слово «используется», а точка входа, конфигурация и выбранный метод проверки. В outcome — один из честных статусов: подтверждено в границах метода, не найдено в артефакте или ещё не проверено.'),
p('NIST SP 800-218 v1.1 говорит о безопасных практиках разработки и о необходимости работать с рисками в жизненном цикле. Для этой заметки важна не попытка выдать NIST за scanner, а порядок ответственности: входной сигнал надо связать с конкретным компонентом, решением и evidence. Если пакет обновляется, новая запись получает свой lockfile diff, новый inventory и отдельный test plan. Старый alert нельзя закрывать лишь ссылкой на pull request с изменённым диапазоном versions.'),
h2('Ограничение модели и следующий шаг'),
p('Этот fixture не знает реальную advisory database, schema конкретного SBOM, package manager, private registry, исходный код, feature flags, зависимости ОС или runtime. Он не вычисляет vulnerable range, не строит call graph, не запускает приложение и не выносит вердикт «безопасно». Его полезная граница уже видна в output: всё названо synthetic, достижимость не assessed, а решение всегда требует review в настоящем проекте.'),
p('Следующий рабочий шаг — выбрать один новый advisory и заполнить описанную запись без чувствительных данных. Если SBOM отсутствует, не рисуйте его задним числом: сначала назовите артефакт, который надо инвентаризировать, и владельца генерации. Если lockfile и SBOM расходятся, не угадывайте, какой прав. Сначала найдите commit и build, из которых каждый получен. После этого уже можно обсуждать обновление как контролируемое изменение, а не как реакцию на страшное слово.'),
], [references.advisoryDatabase, references.ntiaSbom, references.ssdf]);
const mechanism = revision({
slug: 'editorial-2023-04-mechanism-dependency-security',
title: 'Lockfile и SBOM: какие факты о зависимости они фиксируют',
categories: ['Безопасность', 'Архитектура'],
cover: '/assets/editorial/2023/dependency-security-2023-lockfile-sbom.svg',
excerpt: 'Граница между manifest, resolved tree и SBOM: почему три похожих списка не дают один и тот же ответ о поставленном компоненте.',
readingMinutes: 13,
}, [
p('После advisory команда часто открывает package.json, меняет видимую версию и считает работу законченной. Цена такой правки проявляется позже: lockfile разрешил другое транзитивное дерево, SBOM относится к старому build, а reviewer не может восстановить, что именно было поставлено. Ошибка не в существовании трёх файлов. Ошибка в ожидании, что manifest, lockfile и инвентарь отвечают на один вопрос.'),
p('Полезнее разделить их роли до изменения. Manifest выражает намерение автора: direct dependency и допустимый range. Lockfile фиксирует resolved tree, которое выбрал конкретный installer при известных ему правилах. SBOM описывает компоненты и supply-chain relationships выбранного software artifact. Runtime отвечает ещё на четвёртый вопрос: что реально было загружено в конкретном процессе. Эти слои связаны, но не взаимозаменяемы; у каждого есть свой владелец и свой способ проверки.'),
h2('Один package — четыре разные границы'),
p('Документация npm 7 описывает package-lock.json как автоматически созданное представление dependency tree, которое помогает последующим installs получить то же дерево. Это сильнее, чем range в manifest, но не превращает файл в журнал уже запущенного сервиса. Installer flags, platform, optional dependencies, lifecycle scripts и выбор workspace могут менять условия, в которых дерево превращается в конкретный build. Поэтому проверить lockfile — обязательный шаг, но назвать его production inventory можно только при явной связи с артефактом.'),
p('NTIA называет SBOM формальной записью о деталях и отношениях компонентов. В отличие от lockfile, SBOM может жить рядом с поставкой и быть полезен вне конкретного package manager. Но полезность требует provenance: кто и когда сгенерировал документ, из какого commit или image, в каком format, где хранится неизменяемая ссылка. SPDX 2.3 описывает спецификацию формата и versioned fields; он не создаёт честную связь с binary автоматически. Файл без указанного artifact identity остаётся списком, происхождение которого невозможно проверить.'),
table('Граница ответственности артефактов', ['Артефакт', 'Главный вопрос', 'Владелец состояния', 'Проверяемый сигнал', 'Чего не обещает'], [
['package.json', 'какую direct dependency просит проект?', 'автор change', 'manifest diff и review range', 'точно resolved tree'],
['package-lock.json', 'какое дерево выбрал installer?', 'resolver и committed lockfile', 'lockfile diff, clean install с теми же правилами', 'что модуль загрузился в process'],
['SBOM', 'какие компоненты заявлены для named artifact?', 'build pipeline и владелец inventory', 'artifact identity, format, component records', 'что путь вызова достижим'],
['Runtime evidence', 'что произошло в named scenario?', 'запуск и наблюдение среды', 'test, trace или controlled smoke', 'все возможные configuration paths'],
['Advisory', 'какой внешний risk signal надо разобрать?', 'источник advisory и triage owner', 'ID, URL, package/version scope', 'факт воздействия на этот сервис'],
]),
h2('Минимальный graph, который можно прочитать'),
p('Учебный fixture строит три lockfile rows: demo-service зависит от demo-shell, а demo-shell зависит от demo-parser. SBOM намеренно перечисляет только два component records, потому что пример показывает не полноту инвентаря, а необходимость сравнения. buildSyntheticInventory выдаёт строку для каждого package и отдельно помечает, есть ли synthetic запись в SBOM. Статус synthetic-not-listed означает пропуск в специально заданном массиве, не проблему стороннего генератора и не evidence о настоящей поставке.'),
code(`import {
buildSyntheticInventory,
syntheticDependencyInput,
} from './web/scripts/upgrade-2023-04.mjs';
const inventory = buildSyntheticInventory(syntheticDependencyInput);
console.log(inventory.rows);
// demo-parser: lockfile = synthetic-listed
// demo-parser: sbom = synthetic-listed
// runtime = not-observed-by-fixture
console.log(inventory.deploymentInventory);
// not-proven-by-fixture`),
p('Этот пример воспроизводим, потому что все строки определены рядом с функцией. Он честно неполон: не читает package-lock.json, не формирует SPDX или CycloneDX, не строит container image и не знает, какая команда release взяла артефакт. Важно не расширять его обещание. Строка sbom = synthetic-listed подтверждает лишь пересечение двух arrays в памяти. Она не даёт право написать в ticket, что production SBOM обновлён или что transitive dependency больше не существует.'),
figure('/assets/editorial/2023/dependency-security-2023-lockfile-sbom.svg', 'Схема границ: manifest выражает намерение, lockfile фиксирует resolved tree, SBOM связывается с named build artifact, а runtime evidence требует отдельного запуска; стрелки показывают необходимые проверки, а не автоматические гарантии.', 'Схема объясняет владение данными о synthetic dependency. Она не является SBOM, lockfile реального проекта, контейнерной манифестацией или записью выполненного запуска.'),
h2('Почему совпадение списков не заканчивает работу'),
p('Совпадение name и version между lockfile и SBOM — хороший consistency check, но оно оставляет важные вопросы. Какой graph branch выбрал resolver? Не попал ли optional component в другой platform? Совпадает ли current commit с build, для которого получен SBOM? Был ли SBOM regenerated после overrides? Есть ли bundle, native addon или generated source, чей состав не виден в простом списке npm packages? Ответы зависят от build pipeline, а не от красивого JSON-формата.'),
p('Особенно опасно обходить это distinction после patch update. Direct dependency может выглядеть как единственная изменённая строка, но новый resolution меняет transitive children. Поэтому в review полезно смотреть не только manifest, но и lockfile diff: added, removed, version-shifted, integrity или source changes. Затем SBOM generation запускают для candidate artifact и сопоставляют его с тем же commit. Если процесс не умеет такого связывания, результат не «SBOM не нужен», а явный пробел в цепочке evidence, который надо закрыть до уверенного выпуска.'),
h2('Маршрут: симптом → причина → проверка → действие'),
ol([
'Симптом. В pull request изменился package.json, но lockfile не изменился, изменился неожиданно широко или рядом лежит SBOM без указания build.',
'Причина. Намерение автора, resolved tree и inventory artifact смешаны в один этап. Поэтому нельзя понять, какой файл устарел и кто должен его обновить.',
'Проверка. Назовите commit, installer command и relevant flags для lockfile; затем назовите artifact identity, generator и format для SBOM. Сверяйте exact name/version, но сохраняйте различие уровней.',
'Действие. Сделайте таблицу соответствий для candidate release: manifest request → lockfile resolution → SBOM component → build artifact. Отсутствующее звено пишите как unknown, не как совпадение.',
'Проверка контракта. Запустите fixture. Он должен оставить deploymentInventory равным not-proven-by-fixture и не превратить synthetic arrays в настоящий inventory.',
'Фиксация. В release record храните ссылки на lockfile diff, generated SBOM и artifact ID. Это даёт следующему reviewer точку сравнения для следующего advisory.',
]),
h2('Как выбирать минимально достаточный SBOM'),
p('Не надо начинать с обещания «соберём идеальный список всего». Для одного pipeline достаточно установить contract: какой artifact считается release unit, кто запускает generator, какой format принимается, где лежит document, как он связывается с immutable build identity и кто проверяет failure. Минимальные элементы NTIA помогают не забыть о data fields, automation support и practices/processes. Но они не диктуют команду package manager и не решают, какие части monorepo являются отдельными поставками.'),
p('Внутри команды полезно хранить не только generated file, но и criterion freshness. Например, SBOM считается актуальным, если он был generated из того же source revision и относится к тому же artifact digest, что release candidate. Это criterion, а не факт в этой статье: его надо реализовать и проверить своим pipeline. Если такой связи нет, честнее назвать документ inventory candidate и не закрывать им вопрос о deployed package. Слова «lockfile» и «SBOM» здесь не являются печатями качества.'),
h2('Ограничение модели и следующий шаг'),
p('Функция buildSyntheticInventory не выполняет install, не парсит реальный lockfile, не создаёт SPDX, не проверяет signature, digest, license или provenance. Она не видит environment variables, bundles, generated assets и runtime imports. Её единственное назначение — показать, что lockfile listing, SBOM listing и runtime observation имеют разные статусы даже у одного demo-parser. Из-за этого PASS fixture не может быть приложен как доказательство состава production image.'),
p('Следующий шаг — взять один существующий build pipeline и нарисовать его по таблице: где появляется manifest diff, где resolver создаёт lockfile, где generator получает artifact, где сохраняется SBOM и какой review видит оба указателя. Если ваш инструментарий не поддерживает часть цепочки, это нормальный результат диагностики. Сначала зафиксируйте precise gap и владельца, затем выберите инструмент. Так update перестаёт быть заменой строки и становится проверяемым изменением supply chain.'),
], [references.npmLock, references.ntiaSbom, references.spdx23, references.ssdf]);
const field = revision({
slug: 'editorial-2023-04-field-dependency-security',
title: 'Безопасное обновление зависимости: runtime-совместимость и доказательства выпуска',
categories: ['Безопасность', 'Практика'],
cover: '/assets/editorial/2023/dependency-security-2023-safe-update-gate.svg',
excerpt: 'Полевой маршрут обновления без ложного PASS: diff дерева, чистая установка, тесты, runtime smoke и честный rollback.',
readingMinutes: 13,
}, [
p('Проблема обновления зависимости в том, что одна version в diff часто выглядит безопасной без доказательств. Цена скрыта за этой простотой: новая версия может сменить transitive tree, peer range, optional branch, lifecycle behavior или предположение о Node runtime. Если merge делают только потому, что advisory страшный, риск переносится в следующий запуск: service не стартует, feature ведёт себя иначе, а откат не подготовлен. Значит, вопрос не «обновили ли пакет», а «какие доказательства нужны, чтобы выпустить именно это изменение».'),
p('Нельзя ответить на него фразой «engines подходят». Поле engines в package.json сообщает заявленный диапазон runtime; в документации npm 8 указано, что без engine-strict оно обычно advisory и даёт warning. Даже строгая проверка range не исполняет application path, не проверяет native addon, browser bundle, peer dependency или внешний protocol. Runtime compatibility — отдельная проверка в названном окружении. Версия в metadata может быть входом для gate, но не доказательством его результата.'),
h2('Соберите candidate change до запуска команд'),
p('Перед npm install надо назвать границы change. Какой package меняется: direct или transitive? Какая fromVersion зафиксирована в baseline lockfile? Какой toVersion предлагается? Что может измениться рядом: другие records, integrity, source URL, peer resolution, scripts, generated output? Какой runtime поддерживает сервис и где это проверяется? У этих вопросов нет одного универсального ответа, но без них невозможно отличить small patch от resolver rewrite. Задача update должна содержать не только target version, но и expected evidence.'),
p('Учебный input в этом sidecar задаёт demo-parser 1.0.0 → 1.0.1 и одинаковую метку node-18-demo у проекта и candidate. Функция planSyntheticSafeUpdate возвращает labelMatches: true, но сразу ставит compatibility и safety в not-proven-by-fixture. Это ключевое свойство примера. Совпадение двух строк проверяемо в памяти; совместимость настоящего процесса — нет. Если убрать это различие, fixture начнёт имитировать результат, которого не получал.'),
table('Gate обновления и его честный результат', ['Gate', 'Что проверяется', 'Артефакт PASS/FAIL', 'Чего недостаточно'], [
['Scope и baseline', 'package, from/to version, owner, advisory context', 'reviewable change record', 'что resolver выбрал ожидаемое дерево'],
['Lockfile diff', 'добавленные, удалённые и changed records', 'diff конкретного commit', 'что install и app tests прошли'],
['Clean install', 'воспроизводимость tree при зафиксированных правилах', 'CI log и exit status', 'что runtime path совместим'],
['Project tests', 'выбранные application contracts', 'test report с environment', 'все modes и integrations'],
['Runtime smoke', 'старт и named scenario в declared environment', 'наблюдаемый log/trace/result', 'отсутствие всех уязвимостей'],
['Rollback', 'как вернуться к baseline change', 'план и проверяемая процедура', 'что уже применённые внешние эффекты исчезли'],
]),
h2('Fixture называет gate, но не исполняет его'),
p('Ниже можно увидеть предел модели. Все четыре gates в synthetic input помечены как declared. Это значит только, что автор учебной записи не забыл назвать lockfile diff, clean install, project tests и runtime smoke. У каждого gate поле execution равно not-run-by-fixture. Даже когда все поля true, plan.safety остаётся not-proven-by-fixture. Такой контракт полезен для review: он запрещает переносить булево поле из task template в утверждение о реальной проверке.'),
code(`import {
planSyntheticSafeUpdate,
syntheticDependencyInput,
} from './web/scripts/upgrade-2023-04.mjs';
const plan = planSyntheticSafeUpdate(syntheticDependencyInput);
console.log(plan.runtime.labelMatches); // true
console.log(plan.runtime.compatibility); // not-proven-by-fixture
console.log(plan.gates[3].execution); // not-run-by-fixture
console.log(plan.safety); // not-proven-by-fixture
const rollback = plan.rollback;
console.log(rollback.version); // 1.0.0, только synthetic label`),
p('Запуск node web/scripts/upgrade-2023-04.mjs --verify-fixture проверяет двадцать две assertions о synthetic object. Среди них есть отрицательная ветка с incomplete runtime gate, candidate, который не начинается от advisory version, и отсутствующим project contract. Нет assertion, который запускает npm ci, downloader, test runner или Node process приложения. Это специально: иначе учебный файл зависел бы от сети, локального cache и чужой конфигурации, а его результат пришлось бы выдавать за evidence, которого он не способен собрать.'),
figure('/assets/editorial/2023/dependency-security-2023-safe-update-gate.svg', 'Схема безопасного обновления: scope и advisory ведут к lockfile diff, затем к clean install, project tests и runtime smoke; только доказательства конкретного проекта могут открыть release decision, а rollback возвращает baseline version как отдельный план.', 'Схема показывает последовательность gates и разделяет named checks от результата их выполнения. Она не сообщает, что какой-либо package обновлялся, runtime запускался или release признан безопасным.'),
h2('Runtime-совместимость проверяется в контексте'),
p('Первый контекст — version policy: declared Node range, package manager version, OS, CPU, libc и доступные build tools. Второй — dependency semantics: peer dependencies, optional dependencies, postinstall, native code, exports и module format. Третий — приложение: startup path, configuration, migration, network contract и feature, ради которой package подключён. У одного update могут быть разные владельцы этих частей. Хороший gate называет, кто даёт environment и какой scenario должен завершиться, а не прячет весь риск за названием команды.'),
p('Документация npm 8 для npm ci полезна здесь именно ограничением: команда требует существующий lockfile, не переписывает его и может завершиться ошибкой, если manifest и lockfile не соответствуют друг другу. Это помогает воспроизводимо проверить install contract для определённого репозитория. Но успех npm ci не доказывает startup сервиса или использование конкретной функции. Поэтому clean install идёт до project tests, а runtime smoke — после них. Если проект не умеет безопасно выполнить smoke, это не повод объявлять range совместимым; это повод уменьшить change, подготовить environment или добавить наблюдаемую проверку.'),
h2('Маршрут: симптом → причина → проверка → действие'),
ol([
'Симптом. В задаче есть только «обновить package до версии X» и ссылка на advisory. Нет baseline, diff дерева, runtime или rollback.',
'Причина. Версия воспринимается как локальная строка, хотя update меняет resolution и может пересечь границы installer, build и приложения.',
'Проверка. Зафиксируйте baseline commit, candidate manifest и lockfile diff. Отдельно назовите package manager, flags, supported runtime и scenario, который обязан пройти.',
'Действие. На CI выполните clean install с теми же правилами, затем targeted tests и controlled runtime smoke. Сохраните для каждого gate ссылку на log или report, а не итоговую фразу «всё зелёное».',
'Проверка модели. Запустите fixture: он должен показать, что named gate не равен execution, а matching runtime labels не равны compatibility. PASS не разрешает merge.',
'Откат. До выпуска запишите baseline lockfile и условия возврата. Если update меняет data format, migration или внешний protocol, одного version rollback недостаточно — остановите задачу и добавьте отдельный release plan.',
]),
h2('Выберите размер доказательства, а не размер страха'),
p('Не каждый advisory требует одинакового rollout. Но масштаб проверки выбирают по scope изменения и известной границе воздействия, а не по уверенности формулировки. Если update затрагивает только development tool и не попадает в release artifact, proof может ограничиться воспроизводимой установкой и checks pipeline. Если component участвует в server startup или обрабатывает внешние данные, нужен сценарий этой границы. Если затронуты native bindings или protocol, заранее договоритесь о compatible environments и способе убрать candidate. При недостатке информации корректный статус — pending evidence, а не «безопасный patch».'),
p('NIST SP 800-218 рекомендует встроить практики безопасной разработки в жизненный цикл, а не ждать отдельного большого аудита после каждой строки. В таком подходе dependency update оставляет след: source advisory, owner, diff, named checks, реальные результаты, decision и rollback. Форма может быть короткой, но ключевые переходы должны быть наблюдаемы. Нельзя заменять evidence чужим badge, statement package manager или результатом учебной функции. Эти вещи полезны только на своём уровне.'),
h2('Ограничение модели и следующий шаг'),
p('planSyntheticSafeUpdate сравнивает строки demo runtime и хранит четыре declared gates. Он не читает engines, не решает semver range, не вызывает npm ci, не собирает bundle, не устанавливает package, не запускает test и не открывает network. rollbackSyntheticSafeUpdate возвращает только version label внутри object; он не меняет deployment и честно ставит deploymentEffect в not-assessed-by-fixture. Поэтому модель не умеет признать update совместимым или безопасным даже при двадцати зелёных assertions.'),
p('Следующий шаг — применить тот же gate к одному настоящему update, не перенося synthetic values. В task создайте baseline, candidate, expected lockfile diff, environment, test scenario, owner и rollback condition. После каждого фактического запуска приложите реальный artifact результата и подпишите его границу: install, test или runtime. Тогда advisory закрывается не из-за смены строки, а потому что команда может показать, что именно проверила и чего пока не проверяла.'),
], [references.npmPackage, references.npmCi, references.ssdf, references.advisoryDatabase]);
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
function verifyFixture() {
const report = runDependencySecurityFixture();
const failed = Object.entries(report.assertions)
.filter(([, value]) => value !== true)
.map(([key]) => key);
if (failed.length > 0) {
process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
process.exitCode = 1;
return;
}
const count = Object.keys(report.assertions).length;
process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n');
}
if (process.argv.includes('--verify-fixture')) verifyFixture();
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');