revise December 2023 security audit articles
Build and deploy / deploy (push) Successful in 15s

This commit is contained in:
2026-07-31 15:24:14 +03:00
parent 9610ad68c1
commit 201dc7bd54
7 changed files with 756 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Производство редакционных партий
На 31 июля 2026 года строгий аудит проходит 211 из 358 созданных материалов. Остальные 147 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 214 из 358 созданных материалов. Остальные 144 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия
+55
View File
@@ -0,0 +1,55 @@
# П70 · 2023-12 · Практический аудит веб-проекта — три прохода саморевью
## Рамка sidecar-партии
- Slug: `editorial-2023-12-practice-security-audit`, `editorial-2023-12-mechanism-security-audit`, `editorial-2023-12-field-security-audit`.
- Голос: М6, декабрь 2023. Автор действует как системный практик: начинает с конкретного пробела в audit-работе и цены неверного решения, называет границу, evidence и owner, затем оставляет обратимый следующий шаг. Речь короткая и предметная: «симптом → причина → проверка → действие». Нет вымышленного инцидента, скана, риска, coverage, compliance или результата в production.
- Три независимых угла темы: practice собирает scope и карту активов до первого теста; mechanism разделяет evidence, scope и authorization; field строит порядок исправлений из evidence, owner и reversibility, а не из самой громкой severity-метки.
- Граница изменений выдержана: созданы только этот review, import-safe script и три локальных SVG. Не менялись `articles.json`, registry, README, очередь, Git, staging, commit, push и любые чужие пути.
## Проход 1 — факты, модель и безопасные границы
- Исторический срез проверен 31.07.2026 по первичным или официальным документам, существовавшим до конца декабря 2023: [NIST SP 800-115, сентябрь 2008](https://csrc.nist.gov/pubs/sp/800/115/final), [OWASP Web Security Testing Guide v4.2, 03.12.2020](https://owasp.org/www-project-web-security-testing-guide/v42/), [OWASP ASVS 4.0.3, 28.10.2021](https://owasp.org/www-project-application-security-verification-standard/), [RFC 9116, апрель 2022](https://www.rfc-editor.org/rfc/rfc9116.html) и [FIRST CVSS v3.1, июнь 2019](https://www.first.org/cvss/v3-1/specification-document). Использованы versioned или dated формы, а не последующие правила как будто они уже были известны в 2023 году.
- Формулировки ограничены источниками. NIST описывает planning, test/examination, analysis и mitigation, но это обзор 2008 года, а не готовый современный audit-процесс. WSTG v4.2 даёт версионированные сценарии и mapping архитектуры, но не даёт permission и не доказывает coverage. ASVS 4.0.3 задаёт verification requirements, но не является сертификатом. RFC 9116 определяет disclosure channel и прямо оговаривает отсутствие implied permission to test. CVSS v3.1 описывает Base, Temporal и Environmental groups, но не заменяет контекст, owner, evidence, обратимость или решение конкретной команды.
- `createSyntheticSecurityAuditPacket()` создаёт только fixed marked synthetic audit records в памяти Node. В valid branch ровно один declared boundary record, четыре asset records, четыре evidence cards и три triage cards. Все имена фиксированы и имеют `synthetic-` marker. Любой extra top-level field, заявленное real permission, новый asset field, перестановка asset map, raw evidence payload, claimed external confirmation или automatic-change triage отклоняются.
- Fixture не сканирует и не брутфорсит, не обращается к URL или сети, не читает проект, код, логи, secrets, environment, Git, CI или часы, не запускает browser и реальные security tests. Ответ явно фиксирует `vulnerability=not-claimed`, `coverage=not-claimed`, `compliance=not-claimed`, `scan=not-performed`, `bruteForce=not-performed`, `securityTest=not-run`, `realPermission=not-granted` и `productionEffect=not-attempted`.
- `runSecurityAuditFixture()` содержит 23 assertions: valid path проверяет scope, owners, four boundaries, unverified evidence, manual gates, отсутствие реальных claims и отсутствие обращений к данным проекта; отрицательные ветки проверяют не-synthetic input, extra field, claimed permission, asset mutation/reorder/sparse map, raw evidence, claimed confirmation и automatic change; две ветки проверяют узкий rollback. PASS не подтверждает аудит, finding, vulnerability, permission, coverage, compliance или безопасность реального веб-проекта.
- Rollback намеренно возвращает только snapshot in-memory draft. Он не удаляет внешние записи, не отменяет deploy, не меняет доступы и не трогает network. В статьях отдельно сказано, что реальный rollback требует владельца, версии изменения, validation criterion и операционного плана в той среде, где действие действительно разрешено.
- В первом проходе найдена модельная недосказанность: третий triage gate требовал validation/rollback plan, но не содержал literal ограничение `before-change`. Gate был исправлен на `synthetic-write-validation-and-rollback-plan-before-change`; assertion теперь проверяет одинаковое правило для всех трёх маршрутов.
## Проход 2 — редактура, голос и объём
- Все три статьи начинают с проблемы и цены. Practice показывает пропущенную boundary и риск затронуть чужую инфраструктуру; mechanism — неподтверждённое утверждение, ошибочно названное finding; field — backlog, который сортируют по громкости score и превращают в неуправляемую правку. Тексты не используют вымышленные метрики, инциденты, пользователей или production-результаты.
- Содержательная разница сохранена. Practice отвечает на «как сузить объект аудита» через asset card, owner и scope; mechanism — «что делает вывод проверяемым и где заканчивается разрешение» через evidence matrix; field — «почему порядок исправлений требует gates» через evidence, exposure, owner и reversibility. Общий fixture — сознательно общий учебный предмет, но каждый текст делает из него другой инженерный вывод.
- В каждой статье есть минимум пять смысловых разделов, доступная таблица, собственный SVG с осмысленными `alt` и caption, исполнимый fixture, упорядоченный маршрут с буквальной формулой «симптом → причина → проверка → действие», explicit limits, rollback, next step и раздел официальных источников. Запрещённые шаблонные обороты и нераскрытые общие оценки отсутствуют.
- Объём основного текста, посчитанный без списка источников: practice — **9 031** знак, mechanism — **8 586** знаков, field — **8 780** знаков. Все тексты находятся в диапазоне 5 000–15 000 и в целевой плотности 8–10 тыс. `readingMinutes`: 11, 12 и 12.
- Тон соответствует траектории 2022–2024: термин получает соседний объект проверки. Scope привязан к asset/boundary/owner; evidence — к источнику, статусу и limit; CVSS — к характеристике, не к автоматической команде; rollback — к выбранному изменению и валидации. Автор не выдаётся за руководителя платформы и не заявляет, что его модель универсальна.
- Во втором проходе автоматический аудит показал, что у вступления field-статьи в первых 800 знаках не было буквально слова «проблема», хотя цена и предмет были названы. Первое предложение переписано: теперь оно прямо задаёт проблему backlog-а; после исправления audit видит постановку задачи и текст не потерял краткость.
## Проход 3 — визуал, автоматические проверки и выпуск
- `security-audit-2023-scope-map.svg` показывает только четыре synthetic типа активов и четыре границы; внизу крупно указан запрет трактовать схему как карту сети, перечень URL или permission. `security-audit-2023-evidence-matrix.svg` отделяет scope, owner/status и next step от permission/finding/compliance. `security-audit-2023-remediation-route.svg` завершает каждый путь gate-ом до изменения и отдельно перечёркивает automatic change, scan, brute force, URL/network access и реальные claims.
- Во всех SVG есть `title`, `desc`, `role="img"`, `aria-labelledby`, контрастные цвета, крупные подписи и локальные векторные элементы. Нет `<script>`, `foreignObject`, внешних URL, `data:image`, JavaScript, event handlers, пользовательского ввода или интерактивности. XML валиден; safety scan чистый.
- После правок выполнены: `node --check web/scripts/upgrade-2023-12.mjs` — PASS; `node web/scripts/upgrade-2023-12.mjs --verify-fixture` — `PASS fixture: 23/23 assertions` после независимого model review; `cd web && npm run audit:draft -- scripts/upgrade-2023-12.mjs` — PASS для всех трёх slug и всех требований quality gate; import-safe comparison CLI/export — PASS, 3 revisions без `date`/`author`, все slug существуют в базовом archive.
- Визуалы проверены `xmllint --noout`, SVG safety scan и Sharp-render на ширине 375 px. Ручной просмотр подтвердил читаемость заголовков, стрелок, таблицы, legend и ограничений на узком экране. Локальные assets существуют по путям, на которые ссылаются соответствующие revisions.
- Партия намеренно не интегрирована: registry, README, overlay, строгий архивный audit, production build, staging и Git остаются обязанностью главного агента на отдельном приёмочном проходе. Этот sidecar не меняет базовые metadata и не делает заявления об опубликованном результате.
## Интеграционное ревью главного агента
Независимо сверены первоисточники. NIST SP 800-115 действительно задаёт
planning, testing/examination, analysis и mitigation как обзор технической
оценки; WSTG v4.2 содержит Testing Framework и Map Application Architecture;
RFC 9116 прямо называет отсутствие implied permission to test; CVSS v3.1
разделяет Base, Temporal и Environmental metrics и остаётся входом в
management process, а не готовым решением. Эти границы сохранены в тексте.
Model review обнаружил, что `Array.prototype.every()` пропускает пустой индекс
в разреженном asset array. `matchesExpectedRecords()` теперь требует все
собственные индексы и ровно ожидаемое число ключей; добавлена отрицательная
ветка `sparseAssetMap`. Fixture проходит `23/23` assertions, не принимая
неполную карту как валидный scope.
Три SVG прошли XML и safety scan, затем вручную проверены после Sharp-render
на 375 px. Повторный draft audit: 9 031 / 8 586 / 8 780 знаков. Строгий archive
audit прошёл для трёх slug; registry содержит 205 уникальных ревизий. `npm run
build` успешно сгенерировал 374 статические страницы.
+2
View File
@@ -66,6 +66,7 @@ import { revisions as august2023Revisions } from '../scripts/upgrade-2023-08.mjs
import { revisions as september2023Revisions } from '../scripts/upgrade-2023-09.mjs';
import { revisions as october2023Revisions } from '../scripts/upgrade-2023-10.mjs';
import { revisions as november2023Revisions } from '../scripts/upgrade-2023-11.mjs';
import { revisions as december2023Revisions } from '../scripts/upgrade-2023-12.mjs';
// This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [
@@ -137,4 +138,5 @@ export const editorialRevisions = [
...september2023Revisions,
...october2023Revisions,
...november2023Revisions,
...december2023Revisions,
];
@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="760" viewBox="0 0 1200 760" role="img" aria-labelledby="title desc">
<title id="title">Матрица evidence для учебного аудита</title>
<desc id="desc">Четыре synthetic карточки evidence: scope map, owner route, permission limit и remediation gate. Каждая имеет статус не внешне подтверждено. Нижняя полоса разделяет evidence, разрешение, finding и compliance.</desc>
<rect width="1200" height="760" fill="#101827"/>
<text x="58" y="66" fill="#f8fafc" font-family="Arial, sans-serif" font-size="34" font-weight="700">Evidence matrix: что известно и чего ещё нет</text>
<text x="58" y="98" fill="#bae6fd" font-family="Arial, sans-serif" font-size="20">Одна карточка связывает scope, owner, статус и следующий gate. Она не выдаёт permission или finding.</text>
<rect x="58" y="138" width="1084" height="70" rx="12" fill="#223247"/>
<text x="87" y="183" fill="#e0f2fe" font-family="Arial, sans-serif" font-size="22" font-weight="700">Карточка</text>
<text x="430" y="183" fill="#e0f2fe" font-family="Arial, sans-serif" font-size="22" font-weight="700">Связь со scope</text>
<text x="700" y="183" fill="#e0f2fe" font-family="Arial, sans-serif" font-size="22" font-weight="700">Owner / status</text>
<text x="950" y="183" fill="#e0f2fe" font-family="Arial, sans-serif" font-size="22" font-weight="700">Следующий шаг</text>
<g font-family="Arial, sans-serif" font-size="19">
<rect x="58" y="218" width="1084" height="88" fill="#16334b" stroke="#5eead4" stroke-width="1"/>
<text x="87" y="255" fill="#f8fafc" font-weight="700">scope map</text><text x="87" y="281" fill="#bae6fd">synthetic card</text>
<text x="430" y="269" fill="#ccfbf1">asset boundaries</text>
<text x="700" y="255" fill="#ccfbf1">web owner</text><text x="700" y="281" fill="#fcd34d">not externally verified</text>
<text x="950" y="269" fill="#ccfbf1">confirm map</text>
<rect x="58" y="306" width="1084" height="88" fill="#1e3150" stroke="#93c5fd" stroke-width="1"/>
<text x="87" y="343" fill="#f8fafc" font-weight="700">owner route</text><text x="87" y="369" fill="#bfdbfe">synthetic card</text>
<text x="430" y="357" fill="#dbeafe">identity boundary</text>
<text x="700" y="343" fill="#dbeafe">identity owner</text><text x="700" y="369" fill="#fcd34d">not externally verified</text>
<text x="950" y="357" fill="#dbeafe">ask owner</text>
<rect x="58" y="394" width="1084" height="88" fill="#3b304f" stroke="#c4b5fd" stroke-width="1"/>
<text x="87" y="431" fill="#f8fafc" font-weight="700">permission limit</text><text x="87" y="457" fill="#ddd6fe">synthetic card</text>
<text x="430" y="445" fill="#ede9fe">declared boundary</text>
<text x="700" y="431" fill="#ede9fe">real permission: no</text><text x="700" y="457" fill="#fcd34d">not externally verified</text>
<text x="950" y="445" fill="#ede9fe">get approval</text>
<rect x="58" y="482" width="1084" height="88" fill="#46311e" stroke="#fbbf24" stroke-width="1"/>
<text x="87" y="519" fill="#fef3c7" font-weight="700">remediation gate</text><text x="87" y="545" fill="#fde68a">synthetic card</text>
<text x="430" y="533" fill="#fef3c7">change boundary</text>
<text x="700" y="519" fill="#fef3c7">web owner</text><text x="700" y="545" fill="#fcd34d">not externally verified</text>
<text x="950" y="533" fill="#fef3c7">review before change</text>
</g>
<rect x="58" y="608" width="1084" height="104" rx="14" fill="#3a1720" stroke="#fb7185" stroke-width="2"/>
<text x="87" y="649" fill="#fecdd3" font-family="Arial, sans-serif" font-size="22" font-weight="700">Evidence ≠ permission ≠ finding ≠ coverage/compliance</text>
<text x="87" y="685" fill="#fecdd3" font-family="Arial, sans-serif" font-size="19">Synthetic records не читают код, логи и secrets; они не выполняют тест и не подтверждают реальный продукт.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="760" viewBox="0 0 1200 760" role="img" aria-labelledby="title desc">
<title id="title">Безопасный маршрут triage для учебного аудита</title>
<desc id="desc">Три synthetic карточки идут по порядку к проверочным gate: authz boundary к подтверждению evidence и owner, identity contract к подтверждению scope, upload delivery к плану валидации и rollback. Ни один путь не ведёт к автоматическому исправлению.</desc>
<rect width="1200" height="760" fill="#0e1726"/>
<text x="54" y="66" fill="#f8fafc" font-family="Arial, sans-serif" font-size="34" font-weight="700">Triage: сначала gate, потом изменение</text>
<text x="54" y="99" fill="#bae6fd" font-family="Arial, sans-serif" font-size="20">Порядок объясняется evidence, boundary, owner и обратимостью. Это не риск-оценка и не remediation.</text>
<defs>
<marker id="routeArrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#67e8f9"/></marker>
</defs>
<g font-family="Arial, sans-serif">
<rect x="58" y="156" width="302" height="154" rx="16" fill="#3d2031" stroke="#fb7185" stroke-width="2"/>
<text x="82" y="197" fill="#fff1f2" font-size="24" font-weight="700">1. authz boundary</text>
<text x="82" y="231" fill="#fecdd3" font-size="18">evidence gap</text>
<text x="82" y="260" fill="#fecdd3" font-size="18">public entry</text>
<text x="82" y="289" fill="#fecdd3" font-size="18">reversible draft</text>
<rect x="450" y="156" width="302" height="154" rx="16" fill="#193d4a" stroke="#5eead4" stroke-width="2"/>
<text x="474" y="197" fill="#ecfeff" font-size="24" font-weight="700">Gate 1</text>
<text x="474" y="232" fill="#ccfbf1" font-size="18">confirm evidence</text>
<text x="474" y="261" fill="#ccfbf1" font-size="18">and owner</text>
<text x="474" y="290" fill="#fcd34d" font-size="18">before change</text>
<path d="M360 233 L450 233" fill="none" stroke="#67e8f9" stroke-width="5" marker-end="url(#routeArrow)"/>
<rect x="58" y="354" width="302" height="154" rx="16" fill="#30244f" stroke="#c4b5fd" stroke-width="2"/>
<text x="82" y="395" fill="#f5f3ff" font-size="24" font-weight="700">2. identity contract</text>
<text x="82" y="429" fill="#ddd6fe" font-size="18">owner confirmation</text>
<text x="82" y="458" fill="#ddd6fe" font-size="18">third-party boundary</text>
<text x="82" y="487" fill="#ddd6fe" font-size="18">scope unknown</text>
<rect x="450" y="354" width="302" height="154" rx="16" fill="#193d4a" stroke="#5eead4" stroke-width="2"/>
<text x="474" y="395" fill="#ecfeff" font-size="24" font-weight="700">Gate 2</text>
<text x="474" y="430" fill="#ccfbf1" font-size="18">confirm scope</text>
<text x="474" y="459" fill="#ccfbf1" font-size="18">with owner</text>
<text x="474" y="488" fill="#fcd34d" font-size="18">before change</text>
<path d="M360 431 L450 431" fill="none" stroke="#67e8f9" stroke-width="5" marker-end="url(#routeArrow)"/>
<rect x="58" y="552" width="302" height="154" rx="16" fill="#4a351d" stroke="#fbbf24" stroke-width="2"/>
<text x="82" y="593" fill="#fffbeb" font-size="24" font-weight="700">3. upload delivery</text>
<text x="82" y="627" fill="#fde68a" font-size="18">test plan absent</text>
<text x="82" y="656" fill="#fde68a" font-size="18">user-content boundary</text>
<text x="82" y="685" fill="#fde68a" font-size="18">rollback required</text>
<rect x="450" y="552" width="302" height="154" rx="16" fill="#193d4a" stroke="#5eead4" stroke-width="2"/>
<text x="474" y="593" fill="#ecfeff" font-size="24" font-weight="700">Gate 3</text>
<text x="474" y="628" fill="#ccfbf1" font-size="18">write validation</text>
<text x="474" y="657" fill="#ccfbf1" font-size="18">and rollback plan</text>
<text x="474" y="686" fill="#fcd34d" font-size="18">before change</text>
<path d="M360 629 L450 629" fill="none" stroke="#67e8f9" stroke-width="5" marker-end="url(#routeArrow)"/>
</g>
<rect x="810" y="156" width="332" height="550" rx="18" fill="#1b2738" stroke="#94a3b8" stroke-width="2"/>
<text x="842" y="203" fill="#f8fafc" font-family="Arial, sans-serif" font-size="25" font-weight="700">Не происходит</text>
<text x="842" y="250" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="20">× automatic change</text>
<text x="842" y="291" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="20">× scan / brute force</text>
<text x="842" y="332" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="20">× URL / network access</text>
<text x="842" y="373" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="20">× finding claim</text>
<text x="842" y="414" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="20">× coverage claim</text>
<text x="842" y="455" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="20">× compliance claim</text>
<text x="842" y="535" fill="#a5f3fc" font-family="Arial, sans-serif" font-size="20" font-weight="700">Synthetic fixture</text>
<text x="842" y="570" fill="#bae6fd" font-family="Arial, sans-serif" font-size="18">фиксирует только порядок</text>
<text x="842" y="598" fill="#bae6fd" font-family="Arial, sans-serif" font-size="18">вопросов и rollback draft.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

@@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="760" viewBox="0 0 1200 760" role="img" aria-labelledby="title desc">
<title id="title">Карта scope для учебного аудита веб-проекта</title>
<desc id="desc">Четыре synthetic актива: browser UI, web API, identity boundary и upload delivery. На стрелках указаны граница и владелец; внизу крупно отмечено, что карта не является картой реальной сети и не даёт разрешения на тестирование.</desc>
<rect width="1200" height="760" fill="#0b1220"/>
<rect x="42" y="34" width="1116" height="96" rx="18" fill="#10253d" stroke="#5eead4" stroke-width="2"/>
<text x="78" y="77" fill="#f8fafc" font-family="Arial, sans-serif" font-size="33" font-weight="700">Scope до первого теста: активы, границы, владельцы</text>
<text x="78" y="109" fill="#a5f3fc" font-family="Arial, sans-serif" font-size="20">Учебная карта фиксирует вопросы для review, а не адреса, доступы или результат проверки.</text>
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#67e8f9"/></marker>
</defs>
<path d="M294 276 C360 276 388 276 452 276" fill="none" stroke="#67e8f9" stroke-width="5" marker-end="url(#arrow)"/>
<path d="M748 276 C816 276 842 276 902 276" fill="none" stroke="#67e8f9" stroke-width="5" marker-end="url(#arrow)"/>
<path d="M600 374 L600 485" fill="none" stroke="#67e8f9" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="78" y="198" width="216" height="156" rx="16" fill="#19324d" stroke="#5eead4" stroke-width="2"/>
<text x="102" y="240" fill="#f8fafc" font-family="Arial, sans-serif" font-size="25" font-weight="700">Browser UI</text>
<text x="102" y="274" fill="#bae6fd" font-family="Arial, sans-serif" font-size="17">synthetic-public</text>
<text x="102" y="301" fill="#bae6fd" font-family="Arial, sans-serif" font-size="17">owner: web</text>
<text x="102" y="328" fill="#bae6fd" font-family="Arial, sans-serif" font-size="17">вопрос: entry</text>
<rect x="452" y="198" width="296" height="156" rx="16" fill="#153f4a" stroke="#5eead4" stroke-width="2"/>
<text x="476" y="240" fill="#f8fafc" font-family="Arial, sans-serif" font-size="25" font-weight="700">Web API</text>
<text x="476" y="274" fill="#ccfbf1" font-family="Arial, sans-serif" font-size="17">browser → API boundary</text>
<text x="476" y="301" fill="#ccfbf1" font-family="Arial, sans-serif" font-size="17">owner: web</text>
<text x="476" y="328" fill="#ccfbf1" font-family="Arial, sans-serif" font-size="17">вопрос: authz contract</text>
<rect x="902" y="198" width="220" height="156" rx="16" fill="#382c56" stroke="#c4b5fd" stroke-width="2"/>
<text x="926" y="240" fill="#f8fafc" font-family="Arial, sans-serif" font-size="24" font-weight="700">Identity</text>
<text x="926" y="274" fill="#ddd6fe" font-family="Arial, sans-serif" font-size="17">third-party</text>
<text x="926" y="301" fill="#ddd6fe" font-family="Arial, sans-serif" font-size="17">owner: identity</text>
<text x="926" y="328" fill="#ddd6fe" font-family="Arial, sans-serif" font-size="17">вопрос: scope</text>
<rect x="420" y="485" width="360" height="152" rx="16" fill="#4a2f1b" stroke="#fbbf24" stroke-width="2"/>
<text x="446" y="526" fill="#fef3c7" font-family="Arial, sans-serif" font-size="25" font-weight="700">Upload delivery</text>
<text x="446" y="560" fill="#fde68a" font-family="Arial, sans-serif" font-size="17">upload → delivery boundary</text>
<text x="446" y="587" fill="#fde68a" font-family="Arial, sans-serif" font-size="17">owner: web</text>
<text x="446" y="614" fill="#fde68a" font-family="Arial, sans-serif" font-size="17">вопрос: serve contract</text>
<rect x="42" y="677" width="1116" height="51" rx="12" fill="#3a1720" stroke="#fb7185" stroke-width="2"/>
<text x="78" y="711" fill="#fecdd3" font-family="Arial, sans-serif" font-size="21" font-weight="700">Synthetic only: это не карта сети, не перечень URL и не permission to test.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

+569
View File
@@ -0,0 +1,569 @@
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
const p = (text) => '<p>' + text + '</p>';
const h2 = (text) => '<h2>' + text + '</h2>';
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function plainText(content) {
return content
.replace(/<[^>]+>/g, ' ')
.replaceAll('&nbsp;', ' ')
.replaceAll('&quot;', '"')
.replaceAll('&#039;', "'")
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&amp;', '&')
.replace(/\s+/g, ' ')
.trim();
}
function bodyText(content) {
return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
}
const sources = [
{
title: 'NIST SP 800-115: Technical Guide to Information Security Testing and Assessment, сентябрь 2008',
url: 'https://csrc.nist.gov/pubs/sp/800/115/final',
note: 'Первичный документ NIST о планировании, проведении, анализе и mitigation технических проверок. Это обзор 2008 года: он не заменяет актуальный договор об авторизации, не задаёт scope конкретного веб-проекта и не подтверждает результаты этого учебного fixture.',
},
{
title: 'OWASP Web Security Testing Guide v4.2, 3 декабря 2020',
url: 'https://owasp.org/www-project-web-security-testing-guide/v42/',
note: 'Версионированный официальный guide описывает testing framework, mapping архитектуры и WSTG identifiers. Он не даёт права выполнять тест, не является перечнем всех активов и не доказывает coverage или finding в чужой системе.',
},
{
title: 'OWASP Application Security Verification Standard 4.0.3, 28 октября 2021',
url: 'https://owasp.org/www-project-application-security-verification-standard/',
note: 'Официальный ASVS задаёт требования к верификации приложения. Версия 4.0.3 существовала к концу 2023 года, но стандарт не выдает сертификат, не подменяет evidence и не делает учебную запись compliance-результатом.',
},
{
title: 'RFC 9116: security.txt, апрель 2022',
url: 'https://www.rfc-editor.org/rfc/rfc9116.html',
note: 'Информационный RFC описывает канал disclosure и прямо оговаривает отсутствие подразумеваемого разрешения на testing. security.txt относится к домену или IP, из которого он получен; сам по себе он не расширяет scope и не создаёт authorization.',
},
{
title: 'FIRST: Common Vulnerability Scoring System v3.1, июнь 2019',
url: 'https://www.first.org/cvss/v3-1/specification-document',
note: 'Первичная спецификация различает Base, Temporal и Environmental metrics и называет CVSS входом для management process. Она не учитывает автоматически владельца, обратимость изменения, доказательство, разрешённые границы или бизнес-контекст конкретной команды.',
},
];
function sourceList() {
return '<ul>' + sources.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
}
function revision(meta, parts) {
const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + '\n' + sourceList();
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) {
throw new Error(meta.slug + ': основной текст вне диапазона 5 000–15 000 знаков: ' + proseLength);
}
return Object.freeze({ ...meta, contentHtml, proseLength });
}
const MODEL_LIMIT = 'in-memory-fixed-marked-synthetic-audit-records-no-scan-no-brute-force-no-url-access-no-network-no-code-read-no-log-read-no-secret-read-no-security-test-no-vulnerability-no-coverage-no-compliance';
const SYNTHETIC_SCOPE_ID = 'synthetic-web-audit-scope-2023-12-A';
const SYNTHETIC_WEB_OWNER = 'synthetic-web-owner';
const SYNTHETIC_IDENTITY_OWNER = 'synthetic-identity-owner';
const EXPECTED_AUTHORIZATION = Object.freeze({
id: 'synthetic-declared-boundary-record-2023-12-A',
kind: 'synthetic-declared-boundary-v1',
scopeId: SYNTHETIC_SCOPE_ID,
mode: 'synthetic-inventory-and-record-review-only',
realPermission: 'not-granted',
ownerStep: 'synthetic-owner-approval-required-outside-fixture',
});
const EXPECTED_ASSETS = Object.freeze([
Object.freeze({
id: 'synthetic-asset-browser-ui',
kind: 'synthetic-browser-ui',
boundary: 'synthetic-public-browser-entry',
owner: SYNTHETIC_WEB_OWNER,
dataClass: 'synthetic-session-metadata',
reviewQuestion: 'synthetic-map-before-review',
}),
Object.freeze({
id: 'synthetic-asset-web-api',
kind: 'synthetic-web-api',
boundary: 'synthetic-browser-to-api',
owner: SYNTHETIC_WEB_OWNER,
dataClass: 'synthetic-account-operation-data',
reviewQuestion: 'synthetic-confirm-authz-boundary',
}),
Object.freeze({
id: 'synthetic-asset-identity-broker',
kind: 'synthetic-identity-broker',
boundary: 'synthetic-third-party-identity-boundary',
owner: SYNTHETIC_IDENTITY_OWNER,
dataClass: 'synthetic-identity-claims',
reviewQuestion: 'synthetic-confirm-owner-and-contract',
}),
Object.freeze({
id: 'synthetic-asset-upload-delivery',
kind: 'synthetic-upload-delivery',
boundary: 'synthetic-upload-to-delivery-boundary',
owner: SYNTHETIC_WEB_OWNER,
dataClass: 'synthetic-user-upload-metadata',
reviewQuestion: 'synthetic-confirm-storage-and-serving-boundary',
}),
]);
const EXPECTED_EVIDENCE = Object.freeze([
Object.freeze({
id: 'synthetic-evidence-scope-map',
kind: 'synthetic-scope-card',
statement: 'synthetic-asset-map-recorded',
status: 'synthetic-not-externally-verified',
owner: SYNTHETIC_WEB_OWNER,
}),
Object.freeze({
id: 'synthetic-evidence-owner-route',
kind: 'synthetic-owner-route-card',
statement: 'synthetic-identity-owner-recorded',
status: 'synthetic-not-externally-verified',
owner: SYNTHETIC_IDENTITY_OWNER,
}),
Object.freeze({
id: 'synthetic-evidence-permission-limit',
kind: 'synthetic-permission-limit-card',
statement: 'synthetic-record-is-not-test-permission',
status: 'synthetic-not-externally-verified',
owner: SYNTHETIC_WEB_OWNER,
}),
Object.freeze({
id: 'synthetic-evidence-remediation-gate',
kind: 'synthetic-remediation-gate-card',
statement: 'synthetic-owner-review-before-change',
status: 'synthetic-not-externally-verified',
owner: SYNTHETIC_WEB_OWNER,
}),
]);
const EXPECTED_TRIAGE = Object.freeze([
Object.freeze({
id: 'synthetic-triage-authz-boundary',
evidence: 'synthetic-evidence-gap',
exposure: 'synthetic-public-entry',
reversibility: 'synthetic-reversible-mitigation-draft',
action: 'synthetic-owner-review-before-change',
}),
Object.freeze({
id: 'synthetic-triage-identity-contract',
evidence: 'synthetic-owner-confirmation-needed',
exposure: 'synthetic-third-party-boundary',
reversibility: 'synthetic-contract-review-before-change',
action: 'synthetic-confirm-scope-and-owner',
}),
Object.freeze({
id: 'synthetic-triage-upload-serving',
evidence: 'synthetic-test-plan-not-written',
exposure: 'synthetic-user-content-boundary',
reversibility: 'synthetic-rollback-plan-required',
action: 'synthetic-write-reversible-plan',
}),
]);
function hasExactKeys(value, keys) {
return value && typeof value === 'object' && !Array.isArray(value)
&& Object.keys(value).length === keys.length
&& keys.every((key) => Object.hasOwn(value, key));
}
function matchesExpectedRecord(value, expected) {
return hasExactKeys(value, Object.keys(expected))
&& Object.entries(expected).every(([key, expectedValue]) => value[key] === expectedValue);
}
function matchesExpectedRecords(values, expected) {
return Array.isArray(values)
&& values.length === expected.length
&& Object.keys(values).length === expected.length
&& expected.every((record, index) => Object.hasOwn(values, index) && matchesExpectedRecord(values[index], record));
}
function rejectSyntheticAuditPacket(reason) {
return Object.freeze({
kind: 'synthetic-security-audit-fixture-v1',
syntheticOnly: true,
accepted: false,
reason,
modelLimit: MODEL_LIMIT,
});
}
/**
* Учебная функция создаёт и проверяет только заранее заданные marked synthetic
* audit records в памяти Node. Она не сканирует и не перебирает ничего, не
* обращается к URL или сети, не читает код, логи, secrets, environment, часы,
* проект, Git или CI. Она не запускает security test и не утверждает finding,
* vulnerability, coverage, compliance, permission либо результат для реального
* веб-проекта. Название authorization ниже — лишь форма учебной записи; поле
* realPermission намеренно равно not-granted.
*/
export function createSyntheticSecurityAuditPacket(input) {
if (!input || input.synthetic !== true || input.kind !== 'synthetic-security-audit-input-v1') {
return rejectSyntheticAuditPacket('synthetic-input-required');
}
const allowedTopLevel = ['synthetic', 'kind', 'authorization', 'assets', 'evidence', 'triage'];
if (!hasExactKeys(input, allowedTopLevel)) return rejectSyntheticAuditPacket('unexpected-input-field');
if (!matchesExpectedRecord(input.authorization, EXPECTED_AUTHORIZATION)) {
return rejectSyntheticAuditPacket('synthetic-declared-boundary-rejected');
}
if (!matchesExpectedRecords(input.assets, EXPECTED_ASSETS)) {
return rejectSyntheticAuditPacket('synthetic-asset-map-rejected');
}
if (!matchesExpectedRecords(input.evidence, EXPECTED_EVIDENCE)) {
return rejectSyntheticAuditPacket('synthetic-evidence-matrix-rejected');
}
if (!matchesExpectedRecords(input.triage, EXPECTED_TRIAGE)) {
return rejectSyntheticAuditPacket('synthetic-triage-route-rejected');
}
const snapshot = Object.freeze({
scopeId: SYNTHETIC_SCOPE_ID,
declaredBoundaryId: EXPECTED_AUTHORIZATION.id,
assetIds: Object.freeze(input.assets.map((asset) => asset.id)),
evidenceIds: Object.freeze(input.evidence.map((evidence) => evidence.id)),
triageIds: Object.freeze(input.triage.map((card) => card.id)),
});
const remediationRoute = Object.freeze([
Object.freeze({
position: 1,
cardId: 'synthetic-triage-authz-boundary',
gate: 'synthetic-confirm-evidence-and-owner-before-change',
operation: 'not-performed',
}),
Object.freeze({
position: 2,
cardId: 'synthetic-triage-identity-contract',
gate: 'synthetic-confirm-third-party-scope-before-change',
operation: 'not-performed',
}),
Object.freeze({
position: 3,
cardId: 'synthetic-triage-upload-serving',
gate: 'synthetic-write-validation-and-rollback-plan-before-change',
operation: 'not-performed',
}),
]);
return Object.freeze({
kind: 'synthetic-security-audit-fixture-v1',
syntheticOnly: true,
accepted: true,
reason: 'synthetic-audit-packet-assembled',
modelLimit: MODEL_LIMIT,
declaredBoundary: Object.freeze({
id: input.authorization.id,
scopeId: input.authorization.scopeId,
mode: input.authorization.mode,
realPermission: 'not-granted',
testing: 'not-started',
}),
assetMap: Object.freeze(input.assets.map((asset) => Object.freeze({ ...asset }))),
evidenceMatrix: Object.freeze(input.evidence.map((evidence) => Object.freeze({ ...evidence }))),
remediationRoute,
claims: Object.freeze({
vulnerability: 'not-claimed',
coverage: 'not-claimed',
compliance: 'not-claimed',
scan: 'not-performed',
bruteForce: 'not-performed',
securityTest: 'not-run',
}),
limits: Object.freeze({
url: 'not-accessed',
network: 'not-used',
code: 'not-read',
logs: 'not-read',
secrets: 'not-read',
project: 'not-read',
productionEffect: 'not-attempted',
}),
rollback: Object.freeze({ action: 'restore-synthetic-audit-draft', snapshot }),
});
}
export function rollbackSyntheticSecurityAuditPacket(packet) {
if (!packet || packet.accepted !== true || !packet.rollback?.snapshot) {
return Object.freeze({
restored: false,
syntheticOnly: true,
reason: 'no-accepted-synthetic-audit-packet',
});
}
return Object.freeze({
restored: true,
syntheticOnly: true,
reason: 'synthetic-audit-draft-restored',
snapshot: packet.rollback.snapshot,
records: 'not-created-or-deleted-outside-memory',
permission: 'not-granted',
securityTest: 'not-run',
network: 'not-used',
productionEffect: 'not-attempted',
});
}
const validSyntheticInput = Object.freeze({
synthetic: true,
kind: 'synthetic-security-audit-input-v1',
authorization: EXPECTED_AUTHORIZATION,
assets: EXPECTED_ASSETS,
evidence: EXPECTED_EVIDENCE,
triage: EXPECTED_TRIAGE,
});
export function runSecurityAuditFixture() {
const valid = createSyntheticSecurityAuditPacket(validSyntheticInput);
const nonSynthetic = createSyntheticSecurityAuditPacket({ ...validSyntheticInput, synthetic: false });
const extraTopLevel = createSyntheticSecurityAuditPacket({ ...validSyntheticInput, targetHint: 'synthetic-unapproved-target-hint' });
const changedAuthorization = createSyntheticSecurityAuditPacket({
...validSyntheticInput,
authorization: { ...EXPECTED_AUTHORIZATION, realPermission: 'synthetic-granted' },
});
const changedAsset = createSyntheticSecurityAuditPacket({
...validSyntheticInput,
assets: [
{ ...EXPECTED_ASSETS[0], host: 'synthetic-not-allowed' },
...EXPECTED_ASSETS.slice(1),
],
});
const reorderedAsset = createSyntheticSecurityAuditPacket({
...validSyntheticInput,
assets: [EXPECTED_ASSETS[1], EXPECTED_ASSETS[0], ...EXPECTED_ASSETS.slice(2)],
});
const sparseAssets = new Array(EXPECTED_ASSETS.length);
sparseAssets[1] = EXPECTED_ASSETS[1];
sparseAssets[2] = EXPECTED_ASSETS[2];
sparseAssets[3] = EXPECTED_ASSETS[3];
const sparseAssetMap = createSyntheticSecurityAuditPacket({
...validSyntheticInput,
assets: sparseAssets,
});
const rawEvidence = createSyntheticSecurityAuditPacket({
...validSyntheticInput,
evidence: [
{ ...EXPECTED_EVIDENCE[0], rawSecret: 'synthetic-not-allowed' },
...EXPECTED_EVIDENCE.slice(1),
],
});
const changedEvidenceStatus = createSyntheticSecurityAuditPacket({
...validSyntheticInput,
evidence: [
{ ...EXPECTED_EVIDENCE[0], status: 'synthetic-externally-confirmed' },
...EXPECTED_EVIDENCE.slice(1),
],
});
const unsafeTriage = createSyntheticSecurityAuditPacket({
...validSyntheticInput,
triage: [
{ ...EXPECTED_TRIAGE[0], action: 'synthetic-automatic-change' },
...EXPECTED_TRIAGE.slice(1),
],
});
const restored = rollbackSyntheticSecurityAuditPacket(valid);
const rejectedRollback = rollbackSyntheticSecurityAuditPacket(unsafeTriage);
return Object.freeze({
assertions: Object.freeze({
acceptsOnlyFixedSyntheticPacket: valid.accepted === true && valid.reason === 'synthetic-audit-packet-assembled',
keepsDeclaredScopeAndFourAssets: valid.declaredBoundary.scopeId === SYNTHETIC_SCOPE_ID && valid.assetMap.length === 4,
keepsAssetOwnersExplicit: valid.assetMap[0].owner === SYNTHETIC_WEB_OWNER && valid.assetMap[2].owner === SYNTHETIC_IDENTITY_OWNER,
mapsFourDifferentBoundaries: new Set(valid.assetMap.map((asset) => asset.boundary)).size === 4,
makesPermissionBoundaryExplicit: valid.declaredBoundary.realPermission === 'not-granted' && valid.declaredBoundary.testing === 'not-started',
keepsEvidenceAsUnverifiedSyntheticRecords: valid.evidenceMatrix.length === 4 && valid.evidenceMatrix.every((evidence) => evidence.status === 'synthetic-not-externally-verified'),
keepsSafeTriageOrder: valid.remediationRoute.map((item) => item.cardId).join(',') === 'synthetic-triage-authz-boundary,synthetic-triage-identity-contract,synthetic-triage-upload-serving',
keepsEveryTriageOperationManual: valid.remediationRoute.every((item) => item.operation === 'not-performed' && item.gate.includes('before-change')),
doesNotClaimFindingOrCoverage: valid.claims.vulnerability === 'not-claimed' && valid.claims.coverage === 'not-claimed' && valid.claims.compliance === 'not-claimed',
doesNotRunARealSecurityAction: valid.claims.scan === 'not-performed' && valid.claims.bruteForce === 'not-performed' && valid.claims.securityTest === 'not-run',
doesNotAccessProjectData: valid.limits.url === 'not-accessed' && valid.limits.network === 'not-used' && valid.limits.code === 'not-read' && valid.limits.logs === 'not-read' && valid.limits.secrets === 'not-read',
rejectsNonSyntheticInput: nonSynthetic.accepted === false && nonSynthetic.reason === 'synthetic-input-required',
rejectsUnexpectedTopLevelInput: extraTopLevel.accepted === false && extraTopLevel.reason === 'unexpected-input-field',
rejectsAClaimedPermission: changedAuthorization.accepted === false && changedAuthorization.reason === 'synthetic-declared-boundary-rejected',
rejectsAssetDataOutsideFixedMap: changedAsset.accepted === false && changedAsset.reason === 'synthetic-asset-map-rejected',
rejectsAssetReorderingWithoutNewContract: reorderedAsset.accepted === false && reorderedAsset.reason === 'synthetic-asset-map-rejected',
rejectsSparseAssetMap: sparseAssetMap.accepted === false && sparseAssetMap.reason === 'synthetic-asset-map-rejected',
rejectsRawEvidencePayload: rawEvidence.accepted === false && rawEvidence.reason === 'synthetic-evidence-matrix-rejected',
rejectsExternallyConfirmedClaim: changedEvidenceStatus.accepted === false && changedEvidenceStatus.reason === 'synthetic-evidence-matrix-rejected',
rejectsAutomaticChangeTriage: unsafeTriage.accepted === false && unsafeTriage.reason === 'synthetic-triage-route-rejected',
rollsBackOnlyTheInMemoryDraft: restored.restored === true && restored.reason === 'synthetic-audit-draft-restored' && restored.securityTest === 'not-run',
neverRollsBackExternalState: restored.records === 'not-created-or-deleted-outside-memory' && restored.network === 'not-used' && restored.productionEffect === 'not-attempted',
rejectsRollbackOfRejectedPacket: rejectedRollback.restored === false && rejectedRollback.reason === 'no-accepted-synthetic-audit-packet',
}),
});
}
const fixtureCommand = 'node web/scripts/upgrade-2023-12.mjs --verify-fixture';
const practice = revision({
slug: 'editorial-2023-12-practice-security-audit',
title: 'Практический аудит веб-проекта: scope и карта активов до первого теста',
categories: ['Безопасность', 'Практика разработки'],
cover: '/assets/editorial/2023/security-audit-2023-scope-map.svg',
excerpt: 'Как начать аудит веб-проекта с понятного scope: собрать карту активов, назвать владельца и границу данных, отделить инвентаризацию от права на проверку и оставить обратимый следующий шаг.',
readingMinutes: 11,
}, [
p('Проблема обычно выглядит безобидно: в задаче написано «проверить веб-проект», а в руках команды только основной домен и длинный чек-лист. Через день один человек изучает форму входа, другой обсуждает CDN, а интеграция с identity provider вообще не попала в разговор. Цена такой экономии — не только пропущенный участок. Можно потратить время на второстепенный экран, принять чужую инфраструктуру за свою или затронуть внешний сервис, для которого команда не получила разрешения. До любого теста нужен не инструмент, а короткий scope: что считаем системой, где граница и кто подтверждает каждый участок.'),
p('Вторая ошибка — путать карту активов с перечнем URL. Адрес показывает точку входа, но не объясняет, где принимается решение об авторизации, кто владеет данными сессии, куда уходит загрузка файла и что считается внешней зависимостью. Для аудита это разные вопросы. Если ответ на них спрятан в голове одного разработчика, итоговый отчёт будет выглядеть аккуратно, но не позволит повторить проверку или безопасно выбрать первое исправление.'),
h2('Scope начинается с действия пользователя и заканчивается владельцем'),
p('В декабре 2023 автор уже не сводит безопасность к одному сканеру. После тем о сессиях, CSRF и threat model естественный следующий шаг — увидеть путь пользователя как несколько границ. Для обычного веб-проекта это минимум browser UI, web API, identity boundary и delivery пользовательского файла. Это не универсальная архитектура и не список обязательных сервисов. Это способ задать первый вопрос: где конкретно меняется состояние и кто может объяснить контракт этого перехода.'),
p('Начинать полезно с одного ценного сценария: вход, смена адреса доставки, оплата или загрузка документа. Затем отмечаем активы, по которым проходит сценарий. Для каждого нужны шесть полей: понятный идентификатор, тип, граница, владелец, класс данных и вопрос аудита. Поле «вопрос» особенно полезно: оно не позволяет записать «проверить API» вместо проверяемого условия вроде «подтвердить границу авторизации между browser и API». Карта не говорит, что на границе есть ошибка. Она делает незнание видимым и назначает, у кого уточнить его до следующего действия.'),
table('Минимальная карточка актива для первого scope', ['Поле', 'Пример учебной записи', 'Зачем нужно', 'Чего не доказывает'], [
['ID', '<code>synthetic-asset-web-api</code>', 'связывает scope, evidence и triage без адреса реальной системы', 'что актив существует в production'],
['Граница', 'browser → API', 'показывает момент смены ответственности', 'что запрос уже проверен на практике'],
['Владелец', 'web owner', 'даёт адрес для уточнения контракта и плана изменения', 'что владелец разрешил любой тест'],
['Класс данных', 'session metadata', 'помогает не смешать интерфейс, учетные данные и файл', 'что данные действительно хранятся или передаются'],
['Вопрос', 'подтвердить authz boundary', 'делает следующий шаг наблюдаемым', 'что найдено нарушение'],
]),
figure('/assets/editorial/2023/security-audit-2023-scope-map.svg', 'Карта четырёх synthetic активов: browser UI, web API, identity boundary и upload delivery. У каждой стрелки отмечены граница, владелец и вопрос для review; схема отдельно указывает, что это не карта реальной сети и не разрешение на тест.', 'Scope map нужен, чтобы договориться о границах до проверки. Схема не содержит адресов, сетевых маршрутов или результатов security testing.'),
h2('Не подменяйте инвентаризацию разрешением'),
p('Самая опасная формулировка в стартовой задаче — «раз доступно из браузера, можно проверять». RFC 9116 описывает формат <code>security.txt</code> для disclosure, а не разрешение на действия. В его security considerations отдельно сказано, что файла недостаточно для подразумеваемого permission to test. Даже документ с contact и policy относится к домену или IP, из которого он получен; он не переносится автоматически на поддомены, подрядчика или соседний продукт. Поэтому рядом с scope нужна отдельная запись: кто утверждает границы, допустимый метод, среду, время и способ остановки. Пока этой записи нет, честный режим — inventory и review, а не активная проверка.'),
p('Это различие не бюрократическое. Scope отвечает «какой объект обсуждаем». Authorization отвечает «что разрешено делать с объектом». Evidence отвечает «на чём основан вывод». Когда три слоя смешаны в одной таблице, любая ссылка на документацию превращается в мнимое разрешение, а любой скриншот — в мнимое доказательство. Разделённые карточки делают разговор короче: сначала владелец подтверждает границу, потом команда фиксирует допустимый способ проверки, затем в отчёт попадают только воспроизводимые сведения.'),
h2('Учебный fixture проверяет форму карты, а не проект'),
p('Ниже запускается маленькая модель этой статьи. В ней четыре заранее заданных объекта с префиксом <code>synthetic-</code>, одна запись declared boundary, четыре evidence cards и три triage cards. У входа нет поля URL, учётных данных, лога, исходного кода или произвольной цели. Любое лишнее поле, перестановка карты, попытка назвать permission granted или добавить raw secret отвергаются. Это полезно для редакторской проверки: короткий пример не должен незаметно стать сканером или источником ложного отчёта.'),
code(fixtureCommand),
p('PASS означает только одно: fixed in-memory contract собран, отрицательные ветки отклонены и rollback вернул учебный draft. Код не открывает browser, не читает репозиторий, не выполняет HTTP, не обращается к URL, не пробует пароль, не перебирает пути и не запускает security test. В нём нет реального актива, finding, coverage, compliance или permission. Такие явные ограничения важнее красивого примера: читатель видит, где заканчивается модель и начинается работа с владельцем системы.'),
h2('Карта активов помогает ставить точный вопрос'),
p('Симптом часто маскируется под техническую деталь: форма авторизует пользователя, а после загрузки файла команда спорит, кто обязан проверять доступ к выдаче. Причина — на карте был отмечен upload, но не был отмечен переход от хранения к delivery. Проверка — не отправить специальный файл и не искать обход, а попросить владельца назвать контракт: кто принимает файл, где его имя становится серверным, кто выдаёт его пользователю, какие роли участвуют и где хранится evidence этого решения. Действие — добавить отсутствующую boundary card, владельца и отдельный plan проверки. После этого можно обсуждать метод, не расширяя scope наугад.'),
p('NIST SP 800-115 полезен именно этой последовательностью: планирование, проведение, анализ findings и mitigation не должны быть одним импульсом. Документ старше современного web stack, поэтому не стоит искать в нём готовую схему OAuth, CDN или SPA. Но его практический принцип остаётся применим: сначала назначить цель, ограничения и evidence, затем выбрать технику. OWASP WSTG v4.2 дополняет это версионированными сценариями и рекомендацией фиксировать идентификатор версии. В отчёте лучше написать <code>WSTG-v42-…</code> с границей применимости, чем «проверили OWASP» без предмета и версии.'),
h2('Маршрут: симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> В задаче есть домен и тема аудита, но непонятно, входит ли в него identity provider, upload delivery или административный путь.',
'<strong>Причина.</strong> Команда начала с техники, не зафиксировав активы, владельцев, данные и переходы между ними.',
'<strong>Проверка scope.</strong> Выберите один пользовательский сценарий и заполните карточки активов. Для каждого перехода спросите: кто владелец, какие данные пересекают границу и какой вопрос мы хотим подтвердить.',
'<strong>Проверка permission.</strong> Отдельно получите и запишите допустимый метод, среду, период, точку остановки и контакт. <code>security.txt</code>, публичный экран и ссылка на стандарт не заменяют этот шаг.',
'<strong>Действие.</strong> Уберите из плана всё, у чего нет owner или разрешённой границы. Неизвестный участок пометьте как evidence gap и назначьте следующий разговор, а не как «низкий риск».',
'<strong>Повтор.</strong> Перед каждым новым классом проверки сравните план с той же картой: если появился новый актив или third-party boundary, обновите scope и согласование до действия.',
]),
h2('Rollback должен возвращать договор, а не обещание'),
p('У аудита тоже есть обратимое действие. Если карта оказалась неверной, безопасный rollback — не «продолжаем осторожнее», а возврат к предыдущей версии scope: убрать неподтверждённый актив из плана, отметить evidence gap, восстановить старую формулировку и снова запросить подтверждение владельца. Это не удаляет результаты и не отменяет инцидент: в учебной модели таких объектов вообще нет. В реальной работе нужно отдельно указать, что происходит с уже собранными материалами, доступами и задачами, потому что именно там возникает риск распространить лишние данные.'),
p('Следующий шаг занимает меньше часа: провести review одной карты с владельцем самого ценного пользовательского пути. В результате должна появиться не презентация, а versioned scope record: активы, boundary, owner, класс данных, вопрос проверки, разрешённый метод, запретные действия и способ остановки. Если хотя бы одно поле неизвестно, это и есть результат аудита на сегодня. Не следует имитировать полноту новым чек-листом; сначала надо закрыть пробел в договоре.'),
h2('Историческая граница декабря 2023'),
p('К концу декабря 2023 уже были доступны NIST SP 800-115 (сентябрь 2008), OWASP WSTG v4.2 (3 декабря 2020), ASVS 4.0.3 (28 октября 2021) и RFC 9116 (апрель 2022). В статье используются их определения planning, versioned testing guidance, verification requirements и disclosure boundary. Они не превращают synthetic fixture в аудит, не дают permission и не позволяют говорить о реальном покрытии проекта.'),
]);
const mechanism = revision({
slug: 'editorial-2023-12-mechanism-security-audit',
title: 'Практический аудит веб-проекта: evidence, разрешённые границы и проверяемый вывод',
categories: ['Безопасность', 'Инженерные процессы'],
cover: '/assets/editorial/2023/security-audit-2023-evidence-matrix.svg',
excerpt: 'Почему evidence, hypothesis и permission нельзя хранить в одной строке аудита; как собрать матрицу доказательств, назвать ограничения проверки и не выдать учебный факт за finding или compliance.',
readingMinutes: 12,
}, [
p('Проблема появляется после первого же аудиторского созвона: в документе уже есть «уязвимость», но внизу только ссылка на задачу, пересказ слов коллеги и скриншот без контекста. Никто не может ответить, кто наблюдал факт, в какой разрешённой среде, что именно проверялось и можно ли повторить вывод. Цена высока в обе стороны. Неподтверждённую гипотезу могут срочно отправить в релиз, а важное ограничение доступа — отложить, потому что evidence оформлен хуже, чем громкое предположение.'),
p('У этой ситуации одна частая причина: evidence, scope и authorization складывают в одну колонку «доказательство». Но это разные объекты. Evidence связывает конкретное утверждение с источником и ограничением. Scope определяет, к какому участку системы относится вопрос. Authorization ограничивает допустимые действия. Ни ссылка на стандарт, ни контакт из <code>security.txt</code>, ни положительный результат автоматического инструмента не могут заменить два остальных слоя. Пока они склеены, отчёт невозможно безопасно проверить или оспорить.'),
h2('Evidence — это цепочка, а не убедительная формулировка'),
p('Для каждого утверждения полезно завести короткую evidence card. В ней есть ID scope, объект, наблюдаемый факт или вопрос, происхождение материала, владелец, состояние верификации, ограничение и следующий шаг. Важно различать «видели», «подтверждено владельцем», «воспроизводимо в согласованной среде» и «не проверено». Это не шкала доверия к человеку. Это способ не потерять то, чего в записи пока нет. Если источник — только устный контекст, он может быть полезен для гипотезы, но не должен превращаться в finding при экспорте отчёта.'),
p('ASVS задаёт требования к верификации, а WSTG предоставляет каталог сценариев. Обе рамки помогают назвать предмет проверки, но не производят evidence сами. WSTG identifier без версии может измениться вместе с guide, а ASVS requirement без описания метода не показывает, что было сделано. Поэтому рядом с каждой карточкой нужно фиксировать точную версию источника, локальную границу и тот факт, который действительно подтверждён. Если этого нет, честный статус — <code>evidence gap</code>, а не «контроль пройден». '),
table('Четыре слоя записи, которые нельзя взаимозаменять', ['Слой', 'Вопрос', 'Минимальная запись', 'Опасная подмена'], [
['Scope', 'какая часть системы обсуждается?', 'asset ID, boundary, owner', 'домен выдают за карту всей системы'],
['Permission', 'что разрешено делать?', 'согласованный метод, среда, stop condition, контакт', '<code>security.txt</code> трактуют как carte blanche'],
['Evidence', 'что именно можно утверждать?', 'источник, статус, ограничение, связь со scope', 'скриншот или мнение называют воспроизводимым finding'],
['Decision', 'кто и что делает дальше?', 'owner, reversible step, criterion, rollback', 'оценку severity выдают за автоматическое исправление'],
]),
figure('/assets/editorial/2023/security-audit-2023-evidence-matrix.svg', 'Матрица synthetic evidence связывает scope card, owner route, permission limit и remediation gate. В каждой ячейке отмечено состояние «не внешне подтверждено»; отдельная полоса показывает, что evidence не является разрешением, finding или compliance.', 'Матрица показывает только форму учётной записи. Она не собирает материалы, не читает логи или секреты и не подтверждает безопасность реального продукта.'),
h2('Разрешённая проверка начинается там, где заканчивается догадка'),
p('Иногда команда ищет формальное слово, которое позволит «аккуратно проверить». Его нет. Authorization должна быть конкретной: объект, среда, временное окно, допустимые и запрещённые классы действий, контакт, условие остановки, обращение с данными и ожидаемый артефакт. Эта запись может быть краткой, но она должна существовать до того, как будет изменено состояние, отправлен необычный запрос или затронута внешняя зависимость. Если право не подтверждено, безопасное действие — задать вопрос владельцу, а не расширять эксперимент до фактической проверки.'),
p('RFC 9116 полезен именно как предохранитель от неверного вывода. Документ помогает организации опубликовать contact, policy и срок актуальности disclosure-информации. Он также напоминает, что файл относится к конкретному домену или IP и не создаёт implied permission for testing. Значит, <code>security.txt</code> можно положить в evidence как канал связи, но нельзя записать в поле authorization. Такой разделитель экономит время на споре и снижает риск, что исследование выйдет за неоговорённую границу.'),
h2('Fixture хранит только форму evidence, не доказательство'),
p('Исполнимый пример этой партии намеренно строгий. Он принимает только четыре fixed synthetic evidence cards: scope map, owner route, permission limit и remediation gate. Каждая помечена <code>synthetic-not-externally-verified</code>. Если добавить raw secret, произвольное поле, изменить status на «externally confirmed» или назвать declared boundary реальным permission, contract отклонит вход. Такой тест не заменяет review; он проверяет, что учебная модель не переобещает то, чего у неё нет.'),
code(fixtureCommand),
p('Проверка не читает сайт, URL, исходный код, CI, логи, переменные окружения или секреты. Она не производит сетевой запрос, не запускает browser и не выполняет security test. В ответе намеренно стоят <code>vulnerability=not-claimed</code>, <code>coverage=not-claimed</code> и <code>compliance=not-claimed</code>. PASS означает, что модель сохранила разделение слоёв, а не то, что команда имеет право проверять проект или что в нём нет слабых мест.'),
h2('Как вести отрицательное evidence без самообмана'),
p('Отсутствие материала тоже нужно записывать. Например, для identity provider не удалось получить владельца или договор интеграции. Это не вывод «интеграция небезопасна» и не повод проставить zero risk. Правильная карточка говорит: boundary известна, owner route отсутствует, метод не согласован, утверждение не сделано, следующий шаг — получить подтверждение. Такая запись помогает triage: она поднимает вопрос туда, где его можно решить, но не создаёт фиктивный finding и не заставляет разработчика исправлять неизвестную причину.'),
p('Не следует компенсировать недостаток evidence длинным списком технических терминов. Если неизвестно, как проверяется authz, полезнее написать «требуется подтверждение механизма и тестового пути в разрешённой среде», чем предположить OAuth flow, claim mapping или роль CDN. Автор 2023 года умеет назвать boundary и стоимость пробела, но не притворяется владельцем каждой интеграции. Техническая точность здесь проявляется в том, что вывод ровно соответствует доказательству.'),
h2('Маршрут: симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> В отчёте есть громкое утверждение, но нельзя показать scope, источник, статус верификации, владельца и разрешённую среду.',
'<strong>Причина.</strong> Evidence, permission и decision попали в одну строку; предположение получило статус finding только из-за убедительной формулировки.',
'<strong>Проверка записи.</strong> Для каждого утверждения спросите: к какому asset ID оно относится, кто владелец, откуда взят материал, что именно наблюдалось, какое ограничение у источника и что ещё не проверено.',
'<strong>Проверка границы.</strong> До действия подтвердите method, environment, stop condition и канал связи. Публичность ресурса, стандарт или disclosure contact не дают разрешения сами по себе.',
'<strong>Действие.</strong> Переведите неподтверждённый пункт в hypothesis или evidence gap; назначьте owner и короткий запрос на подтверждение. Finding публикуйте только вместе с воспроизводимым и разрешённым evidence.',
'<strong>Повтор.</strong> Перед triage проверьте, что значение severity не скрывает отсутствие доказательства. Нулевая уверенность не становится низким приоритетом без отдельного решения владельца.',
]),
h2('Rollback нужен и для документа'),
p('Если новая информация опровергла карточку, откат должен быть простым: вернуть статус к hypothesis, убрать неподтверждённый вывод из decision record, сохранить ссылку на причину пересмотра и повторно назначить владельца. Не надо стирать след изменения: иначе команда снова будет спорить, почему пункт исчез. Но и нельзя оставлять старую формулировку в списке findings с пометкой «уточняется» — на неё всё равно начнут опираться. В реальном процессе порядок хранения, доступа и удаления материалов должен быть определён отдельной политикой; fixture этого не делает.'),
p('Следующий шаг — взять один пункт из текущего security backlog и переписать его как evidence card. Добавьте scope ID, owner, literal observation или вопрос, источник, version, status, limit, разрешённый способ подтверждения, reversible next step и критерий закрытия. Если карточка не помещается в несколько строк, значит, в ней смешаны несколько границ или решений. Разделите её до того, как разработка начнёт исправление.'),
h2('Историческая граница декабря 2023'),
p('Эта статья опирается на NIST SP 800-115 (сентябрь 2008), OWASP WSTG v4.2 (3 декабря 2020), OWASP ASVS 4.0.3 (28 октября 2021) и RFC 9116 (апрель 2022), все доступные до конца 2023 года. Они объясняют planning, versioned testing guidance, verification requirements и disclosure channel. Они не подтверждают конкретный finding, не устанавливают authorization для этого репозитория и не превращают fixed synthetic records в compliance evidence.'),
]);
const field = revision({
slug: 'editorial-2023-12-field-security-audit',
title: 'Практический аудит веб-проекта: порядок исправлений и безопасный triage',
categories: ['Безопасность', 'Надёжность изменений'],
cover: '/assets/editorial/2023/security-audit-2023-remediation-route.svg',
excerpt: 'Как разбирать список security-гипотез без гонки за самым громким score: отделить evidence от severity, выбрать обратимый шаг, назначить владельца и не превратить triage в неуправляемое изменение.',
readingMinutes: 12,
}, [
p('Проблема после аудита в том, что команда часто получает не ответ, а очередь из разнородных пунктов: где-то есть версия зависимости, где-то вопрос к авторизации, где-то неполная карта загрузки файла. Самый громкий заголовок или высокий score легко забирает весь фокус. Цена — исправить то, что удобно показать в статусе, и оставить без владельца публичную boundary с неполным evidence. Вторая цена не менее реальна: поспешная «security-правка» ломает рабочий путь, а rollback никто не продумал, потому что triage уже назвал её очевидной.'),
p('Причина в том, что severity читают как готовый порядок действий. CVSS v3.1 полезен для описания технических характеристик и прямо допускает Base, Temporal и Environmental groups. Но сама спецификация говорит, что организация учитывает и другие факторы при remediation decision. Score не знает, есть ли разрешённое воспроизведение, кому принадлежит boundary, насколько широко она используется, что изменится при фиксе и можно ли вернуть состояние. Поэтому безопасный triage строится не вокруг одной цифры, а вокруг цепочки evidence → owner → reversible decision → проверка результата.'),
h2('Сначала отделяем гипотезу от решения'),
p('Пусть есть карточка «authz boundary требует review». Она не означает, что в системе найдена vulnerability; это вопрос, который получил высокий порядок потому, что относится к public entry и не имеет полного evidence. Следующая карточка про third-party identity contract может оказаться важнее зависимости с высоким Base score, если у неё нет согласованного owner и scope. Это не спор с CVSS. Это признание границ метода: он описывает свойства уязвимости, а не контекст конкретного изменения и не право действовать с чужими системами.'),
p('Для каждого пункта нужны четыре развилки. Первая — evidence: есть ли достаточно материала для утверждения или пока только gap. Вторая — exposure: где находится boundary относительно пользовательского пути и внешней зависимости. Третья — owner: кто может подтвердить контракт и принять решение. Четвёртая — reversibility: какой шаг можно отменить, если гипотеза не подтвердится. Пока хотя бы одна развилка неизвестна, правильное действие — не автоматическая правка, а уточнение. Это быстрее, чем создать новый риск из хорошего намерения.'),
table('Матрица безопасного triage перед исправлением', ['Поле', 'Вопрос', 'Если ответа нет', 'Безопасный следующий шаг'], [
['Evidence', 'что подтверждено и с каким ограничением?', 'severity опирается на гипотезу', 'перевести в evidence gap и запросить разрешённое подтверждение'],
['Exposure', 'какая boundary и пользовательский путь затронуты?', 'не виден возможный blast radius', 'связать пункт с asset map и ценным сценарием'],
['Owner', 'кто подтверждает контракт и change?', 'исправление становится бесхозным', 'назначить владельца до изменения'],
['Reversibility', 'что вернём и как проверим?', 'rollback звучит как обещание', 'написать validation и rollback plan'],
['Decision', 'почему именно этот пункт идёт первым?', 'очередь строится по громкости', 'зафиксировать evidence-based rationale, а не только score'],
]),
figure('/assets/editorial/2023/security-audit-2023-remediation-route.svg', 'Маршрут synthetic triage: evidence gap на authz boundary ведёт к owner review, third-party contract — к подтверждению scope, upload delivery — к плану validation и rollback. Стрелки оканчиваются gate «до изменения», а не автоматическим исправлением.', 'Схема не оценивает реальный риск, не находит уязвимости и не выполняет remediation. Она показывает порядок вопросов, который сохраняет возможность остановиться.'),
h2('Порядок — это объяснение, а не сортировка по колонке'),
p('Хороший порядок можно пересказать одним предложением. Например: «сначала подтверждаем evidence и владельца на public authz boundary, затем согласуем scope с identity provider, затем пишем validation и rollback для upload delivery». В такой формулировке видно, что пункт №1 не объявлен найденной уязвимостью и не отправлен на автоматический fix. Его подняли потому, что незакрытый вопрос касается ценного входа и есть обратимый шаг — получить доказательство и договориться о change. Если причина порядка не помещается в одно предложение, значит, в очереди смешались разные классы работы.'),
p('Нельзя скрывать decision в шкале «critical/high/medium». Эти labels полезны для сводки, но не отвечают на «кто сделает что утром». К моменту изменения нужны owner, exact boundary, критерий валидации, отказоустойчивый план остановки и правило возврата. Если операция требует неотменяемой миграции, влияния на совместимость или доступа к third-party, её нельзя выдавать за «быстрый security fix». Пункт должен перейти в отдельный review, даже если проблема кажется знакомой.'),
h2('Fixture моделирует только безопасные gates'),
p('В учебной модели три synthetic triage cards уже имеют порядок, но не получают право менять систему. Первая карта требует подтвердить evidence и owner до change. Вторая требует согласовать scope третьей стороны. Третья требует сначала написать validation и rollback plan. Любая подмена на <code>synthetic-automatic-change</code> отвергается. Тест также отказывается принимать claimed permission, externally confirmed evidence, лишние asset fields и raw secret. Это не защита реального проекта; это защита примера от ложного смысла.'),
code(fixtureCommand),
p('После запуска можно увидеть только PASS набора assert-выражений. Никакого score не вычисляется, CVSS vector не создаётся, URL не открывается, сеть не используется, код и логи не читаются. Fixture не запускает scanner, brute force или browser и не заявляет vulnerability, coverage, compliance либо production effect. В этом его ценность: модель показывает, что triage начинается с запрета на действие без evidence и owner, а не с имитации риск-оценки без системы.'),
h2('Исправление должно быть проверяемым и обратимым'),
p('Техническое решение можно назвать только после того, как сформулирован критерий проверки. «Добавим проверку доступа» ещё не план: нужно знать, на какой boundary, для какого действия пользователя, кто проверит ожидаемый и запрещённый исход, где записан результат и как вернуть предыдущую версию. Здесь автор продолжает линию статей о контрактных тестах и e2e stability: хороший gate не обещает, что баг исчез. Он фиксирует, какой наблюдаемый результат отличит рабочее изменение от неверной гипотезы.'),
p('Rollback не обязан быть сложным, но обязан быть реальным для выбранного изменения. Для конфигурации это может быть версия правила и явный owner восстановления. Для UI-flow — возврат к известной реализации и ограничение exposure. Для third-party contract — прекращение rollout до согласования. Нельзя писать «откатим базу» или «выключим интеграцию» без оценки последствий для данных и пользователей. Если безопасного обратного действия нет, triage должен повысить требование к review, а не ускорить выпуск из-за тревожного названия.'),
h2('Маршрут: симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> Backlog полон security-пунктов, но первые места заняты самым громким score или последним сообщением в чате.',
'<strong>Причина.</strong> Severity подменила evidence, scope, owner и reversibility; очередь стала списком тревог, а не планом изменения.',
'<strong>Проверка evidence.</strong> Для каждого пункта зафиксируйте literal claim, источник, status, границу, ограничение и то, что не подтверждено. При gap не называйте карточку finding.',
'<strong>Проверка change.</strong> Назовите owner, пользовательский путь, expected result, запрещённый результат, валидацию, stop condition и rollback. Если одного элемента нет, решение ещё не готово.',
'<strong>Действие.</strong> Поставьте первым не «самый страшный» пункт, а тот, для которого есть понятный evidence-based question и безопасный следующий gate. Неавторизованные или необратимые действия вынесите из очереди исправлений.',
'<strong>Повтор.</strong> После подтверждения или изменения обновите evidence и rationale. Если причина порядка изменилась, пересортируйте очередь открыто, а не оставляйте старый номер по инерции.',
]),
h2('Ограничения и следующий шаг'),
p('Эта статья не даёт универсальный SLA, пороги severity, срок исправления или готовый порядок для всех команд. CVSS v3.1 — исторически доступный стандарт к декабрю 2023, но он не определяет business impact, permission, owner или rollback конкретного продукта. NIST, OWASP и RFC дают язык для планирования, границ и верификации; они не выводят автоматически, что надо менять в данном коде. Любая реальная проверка требует согласованного scope и допустимого метода вне этого fixture.'),
p('Следующий шаг — выбрать одну карточку из backlog и провести пятнадцатиминутный triage без обсуждения реализации. Запишите: claim, evidence status, asset boundary, owner, exposure, reversible next step, validation, stop condition и rollback. Только после этого решайте, нужна ли разработка, review контракта, запрос к подрядчику или дополнительное доказательство. Такая карточка короче типичного отчёта, но даёт команде возможность действовать без вымышленных гарантий.'),
h2('Историческая граница декабря 2023'),
p('Для порядка исправлений использована версия CVSS 3.1 от июня 2019 года, доступная до декабря 2023, вместе с NIST SP 800-115, OWASP WSTG v4.2, ASVS 4.0.3 и RFC 9116. CVSS здесь назван входом в decision, а не автоматической командой. Учебные числа, findings, exploitation, coverage, compliance и production-результаты не создаются и не заявляются.'),
]);
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
function verifyFixture() {
const report = runSecurityAuditFixture();
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;
}
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');