Перейти к содержимому

28. Границы соответствия требованиям

Большинство архитектурных вопросов целиком живут внутри команды — какие сервисы существуют, что они вызывают, кто ими владеет. Соответствие требованиям — это другое. Соответствие — это про границы: какие данные пересекают какие линии, какие сервисы попадают в область действия PCI, какие касаются PII, какие могут дотянуться до публичного интернета. Эти границы существуют независимо от того, моделируете вы их или нет. Вопрос в том, знает ли о них модель.

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

Сценарий

Небольшая система, охватывающая три уровня безопасности: сервисы с публичным выходом (DMZ), внутренние сервисы (Internal) и сервисы в области действия PCI (PCI). Плюс отдельная ось классификации данных: сервисы, касающиеся PII, должны быть помечены.

От модели мы хотим трёх вещей:

  1. Автоматические проекции соответствия — «покажи всё, что в области PCI», «покажи всё, что касается PII».
  2. Контроль границ — никакой сервис в Internal не должен напрямую вызываться сервисом из DMZ; PCI-сервисы никогда не должны вызываться извне PCI.
  3. Ясность при онбординге — новый инженер, читающий модель, должен понимать, какие ограничения соответствия применяются к каждому сервису.

Метки

Две оси меток, обе каскадирующие:

МеткаЗначенияНазначение
security.zonePublic, DMZ, Internal, PCI, ExternalСетевой/безопасный уровень
data.classificationpublic, internal, pii, pci, secretЧувствительность данных

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

Опционально, третья ось:

МеткаЗначенияНазначение
compliance.regimegdpr, ccpa, pci-dss, sox, hipaaКакой нормативный фреймворк применяется

Многозначные метки не поддерживаются — модуль, который одновременно в области GDPR и PCI, получает по метке на каждый режим, объявляя две: compliance.regime.gdpr: true, compliance.regime.pci: true. Форма ключа поля через точку покрывает это чисто.

Модули

platform.arch (выборочно, чтобы показать разметку метками):

frontend StoreFront {
team: Frontend
labels {
domain: Commerce
security.zone: Public
data.classification: public
}
"Витрина для клиентов. Доступна из публичного интернета."
}
service WebGateway {
team: Platform
labels {
domain: Platform
security.zone: DMZ
data.classification: internal
}
"Шлюз на периметре. Терминирует TLS, применяет правила WAF, пробрасывает во Internal."
}
service Orders {
team: Commerce
labels {
domain: Commerce
security.zone: Internal
data.classification: pii
}
"Обработка заказов. Хранит имя/адрес/email клиента — PII."
}
service Payments {
team: Payments
labels {
domain: Payments
security.zone: PCI
data.classification: pci
compliance.regime.pci: true
}
"Обработка карточных платежей. Карточные данные НИКОГДА не покидают этот модуль в открытом виде."
}
database PaymentVault {
team: Payments
labels {
domain: Payments
security.zone: PCI
data.classification: pci
data.encryption: aes-256
}
"Зашифрованное хранилище для токенизированных карточных данных."
}

Обратите внимание на compliance.regime.pci: true — логическое поле хорошо работает для «этот модуль попадает в область этого режима». Несколько режимов — несколько ключей.

Сдвиг мышления. Метки соответствия — это не опциональные метаданные. Они задают поведенческие ограничения. Метка security.zone: PCI — не ярлык, это контракт, что данные и вызовы этого модуля будут трактоваться по правилам PCI. Если вы навешиваете метки только на те модули, которые случайно подпадают под соответствие, вы получите ложноотрицательные результаты (модуль Internal протащит PCI-вызов через валидацию). Расставьте метки везде, включая сервисы Public. Модель либо полна, либо бесполезна.

Проекции

views.arch:

view PCIScope {
focus security.zone: PCI
group by team
layout dagre
"Каждый модуль в области PCI. Используется при квартальном ревью соответствия."
}
view PIIScope {
focus data.classification: pii
group by team
layout dagre
"Каждый модуль, касающийся PII. Используется в упражнениях по картированию данных."
}
view PublicSurface {
focus security.zone: Public
focus security.zone: DMZ
layout dagre
"Поверхность, открытая в интернет. Полезно для определения области пен-теста."
}
view CrossZoneFlow {
layout dagre
"Все процессы; подсвечивает рёбра, пересекающие зоны безопасности."
}

Первые три тривиальны, как только проставлены метки. CrossZoneFlow — та же модель, но с раскладкой, которая даёт ревьюерам самое удобное чтение границ зон (обычно для этого лучше всего работает dagre или иерархическая раскладка).

Правила валидации

Здесь метамодель отрабатывает свой хлеб. Валидатор стандартной библиотеки выполняет небольшой набор встроенных проверок (обязательные пустые слоты, разрешение вызываемых). Кастомные проверки живут в типах вашего проекта — required слоты, специально нацеленные на факты соответствия.

kinds.arch:

// Каждый сервис в области PCI должен задекларировать runbook и контакт.
export type service pci_service {
required cascade team
required ext.runbook_url
required ext.compliance.contact
required labels.data.classification // экземпляр должен выбрать значение (pii, pci, secret, ...)
labels {
security.zone: PCI
compliance.regime.pci: true
}
}
// Каждый сервис, касающийся PII, должен задекларировать политику хранения.
export type service pii_service {
required cascade team
required data.retention_days
required data.deletion_endpoint
labels {
data.classification: pii
}
}

Теперь вместо service Payments пишем:

pci_service Payments {
team: Payments
ext.runbook_url: "https://wiki.acme.com/pci-runbook"
ext.compliance.contact: "compliance@acme.com"
labels {
data.classification: pci // закрывает обязательный пустой слот
}
command Authorize
command Capture
command Refund
}

Если разработчик добавляет pci_service без одного из обязательных полей, валидация падает. Факты соответствия больше не «надо не забыть это указать» — они принудительно проверяются на этапе разбора.

Ограничения на уровне процессов

Встроенная валидация процессов ловит структурные ошибки (вызывающий — модуль, вызываемый — интерфейс). Ограничения уровня соответствия — «никакой DMZ-сервис не должен напрямую вызывать сервис в Internal» — пока не первоклассные в валидаторе. Паттерн, который работает сегодня:

Используйте метки и проекции, чтобы выявлять нарушения визуально, и небольшой скрипт-проверку (поверх @archlang/core, глава 24) для принудительной проверки в CI:

scripts/check-zones.ts
import { loadPackage } from "@archlang/lsp";
import { NodeIO } from "@archlang/lsp/node";
import { resolvePackage, buildGraph } from "@archlang/core";
const io = new NodeIO();
const loaded = await loadPackage(io, ".");
const model = resolvePackage(loaded.input, loaded.deps).model;
const graph = buildGraph(model);
function zone(moduleId: string | undefined): string | undefined {
if (!moduleId) return undefined;
const m = graph.modulesById.get(moduleId);
return m?.labels.get("security.zone");
}
const violations: string[] = [];
for (const edge of graph.edges) {
const fromZone = zone(edge.fromModuleId);
const toZone = zone(edge.toModuleId);
if (fromZone === "DMZ" && toZone === "Internal") {
violations.push(`${edge.fromName}${edge.toName} (DMZ→Internal)`);
}
if (fromZone !== "PCI" && toZone === "PCI") {
violations.push(`${edge.fromName}${edge.toName} (outside→PCI)`);
}
}
if (violations.length > 0) {
console.error("Zone violations:\n" + violations.join("\n"));
process.exit(1);
}

Запускайте в CI. Нарушения роняют сборку. Соответствие теперь обеспечивается машиной.

Что видят команды управления

После того как метки и типы расставлены:

  • Квартальное ревью PCI — откройте проекцию PCIScope, сделайте снимок экрана, подшейте. Проекция точная и актуальная, потому что является функцией от исходника.
  • Карта данных PII — откройте PIIScope. Каждый сервис, касающийся PII, в списке; по каждому видны хранение и удаление.
  • Инвентаризация DPIA — скрипт по разрешённой модели: перечислите все модули с compliance.regime.gdpr: true.
  • Нарушение границы — разработчик добавляет service без меток соответствия и шаг процесса в PCI-сервис. CI падает. Разработчик либо обосновывает вызов (и тогда тот проходит ревью), либо убирает его.

Ничто из этого не требует поддерживать параллельный документ соответствия. Документ — это и есть модель.

Решения, с которыми вы столкнётесь

Покрытие меток. Либо каждый модуль несёт security.zone и data.classification, либо у вас есть невидимые пробелы. Достигайте полноты, делая метки required в типах своих видов. Цена: разовый проход с обновлением существующих экземпляров. Выгода: железобетонное покрытие.

Каскад против явного указания. Контейнер system PaymentsDomain { ... } с labels { security.zone: PCI } каскадирует на каждый вложенный микросервис. Чище, чем повторять метку пять раз. Используйте каскад для общих зон; используйте явные метки, когда отдельный модуль является исключением.

Где живут правила. Локальные для проекта типы в вашей стандартной библиотеке (pci_service, pii_service) кодируют словарь соответствия. Переиспользуйте их между пакетами, экспортируя из общего пакета и импортируя.

Что НЕ моделировать. Реализацию хранения данных (cron-задача, выполняющая удаление). Конкретные алгоритмы шифрования (они операционные, иногда индивидуальны для экземпляра). Места назначения журналов аудита. Всё, что является выбором реализации, должно отражаться в полях («какой именно»), а не в структуре модели («существует ли»).

Итоги

  • Метки (security.zone, data.classification, compliance.regime.*) — это то, как модель несёт факты соответствия.
  • Каскадируйте эти метки — задайте один раз, охватите всё, что внутри.
  • Виды-типы (pci_service, pii_service) делают обязательные поля соответствия проверяемыми на этапе разбора.
  • Проекции соответствия (focus security.zone: PCI) сами вытекают из меток — никакого ручного сопровождения.
  • Проверки границ на уровне процессов живут в небольшом скрипте CI, построенном поверх @archlang/core.
  • Модель становится документом соответствия.

Что дальше

Глава 29: Проектируем метамодель → — практический разбор для авторов видов. Постройте предметный словарь поверх стандартной библиотеки.