# План: доменный go-live аудит (ревизия H) — отчёт ВЫВОДИТСЯ из greps, не пред-зафиксирован

## Цель

Записать **промежуточный неавторизующий** отчёт доменного аудита по спеке
`2026-06-15-go-live-security-audit-evidence-3.md` (раздел D5). Порядок строго прямой:
**greps дают фактический вывод → правило агрегации → Write выводит отчёт**. Доказательная
база отчёта формируется ИЗ выводов greps НА Write-шаге, а не до него. Показанная ниже
«проектная форма отчёта» — это **шаблон + гипотезы под подтверждение**, а НЕ финальный
текст: Write подставляет фактические значения статуса и доказательной базы из реальных
выводов greps; при расхождении гипотезы с выводом статус становится `requires-manual-check`
(гипотеза НЕ выдаётся за факт). Каждая находка имеет шесть самостоятельных полей, включая
**рекомендация**. Блокеры = все открытые P1 = **{F-T2, F-P1}**. Форма — **не авторизация
go-live**. Открытые вопросы (Б-1/SEC/CTO/Биз/Диз/DO) не закрываются.

## Правило агрегации (управляет Write-шагом)

Для каждой находки Write читает фактический вывод её grep-шага и присваивает статус:

- grep вернул ожидаемый **дискриминирующий вывод** → `подтверждён`, факт. вывод → в доказательную базу;
- grep вернул **иное** (опроверг гипотезу) → `requires-manual-check`, гипотеза не выдаётся за факт;
- grep по **отсутствию** (шаги 5, 10 — ожидается пустой вывод): пустой → `observation` с явной
  пометкой «вывод отсутствует» (отсутствие НЕ заякоривается как положительный факт); непустой →
  `requires-manual-check`;
- **контр-гипотеза «чисто»** (шаги 6, 11 — grep подтверждает корректность): подтверждено → строка
  в раздел отчёта «Чисто», НЕ находка; не подтверждено → поднимается как находка
  `requires-manual-check`.

## Роль скилов: интерпретирующий метод (не шаги steps-json)

`steps-json` по контракту допускает op ∈ {Write, Edit, Bash, MultiEdit} — **Skill в этот enum
не входит**, поэтому скилы не могут быть шагами steps-json. Это документирующая задача: скилы —
**линза интерпретации** grep-evidence (как `test-driven-development` ведёт код, не будучи файловым
шагом). Порядок вызова **строго фиксирован**: после шага 12 и **до** шага 13 (Write),
последовательно `threat-model` → `pdn-152fz-audit` → `billing-audit` → `security-go-live` (не
параллельно, не до greps). Их выводы материализуются Write-шагом в соответствующих строках отчёта:
threat-model→F-T2/F-T1/F-T3, pdn-152fz-audit→F-P1/F-P2, billing-audit→F-B1+«Чисто»,
security-go-live→итоговый вердикт (NO-GO, блокеры {F-T2,F-P1}; контракт GO/NO-GO соблюдён,
«промежуточный» = не финально-авторизующий).

## Сопоставление шагов и находок

| Шаг | grep | Тип | Находка | Дискриминирует |
|----|------|-----|---------|----------------|
| 1 | `EnsureSaasAdmin` в app/app·app/bootstrap·app/routes | discovery | F-T2 | где применён гейт; найден в роут-биндинге → в цепочке; иначе binding → requires-manual-check |
| 2 | `next` в EnsureSaasAdmin | факт | F-T2 | тело `handle()` = `return $next($request);` (anchor) |
| 3 | `admin_user_id` в ResolvesAdminUserId | факт | F-T2 | id из `$request->input` (anchor) |
| 4 | `Http::timeout` в WebhookSettingsController | факт | F-T1 | исходящий вызов `Http::timeout(10)` (anchor) |
| 5 | `withoutRedirecting` там же | absence | F-T1 | пусто → флаг не выставлен; непусто → requires-manual-check |
| 6 | `blockReason` в WebhookUrlGuard | чисто | (Чисто) | guard добротен `blockReason` (anchor) |
| 7 | `verifyIpAllowlist` в SupplierWebhookController | факт | F-T3 | приёмник `verifyIpAllowlist` (anchor) |
| 8 | `contact_name` в PdErasureService | факт | F-P1 | erasure без phones[] (anchor) |
| 9 | `phones` в schema.sql | факт | F-P1 | колонка `phones JSONB` (anchor) |
| 10 | `sentry` в app/config | absence | F-P2 | пусто → нет конфига sentry (observation); непусто → requires-manual-check |
| 11 | `bcsub` в LedgerService | чисто | (Чисто) | деньги bcmath `bcsub` (anchor) |
| 12 | `lead_charges_deal_id` в schema.sql | факт | F-B1 | `CREATE INDEX` (не `UNIQUE`) (anchor) |
| 13 | Write отчёта | — | все | выводит отчёт по правилу агрегации из выводов 1–12 |

ref шагов = якоря секций спеки {#D1}..{#D5} (контракт steps-json), не verified-context-id.
Файл-артефакт пишется в `docs/superpowers/audits/` — директория существует (там лежит
промежуточный отчёт сессии), Write не упадёт по отсутствию пути.

## Карточки навыков (верифицированные — дословно из `docs/registry/contracts/`)

```json
[
  {"skill":"threat-model","kind":"own","needs":["портал для моделирования угроз перед публикацией"],"produces":["STRIDE-модель: attack surface + приоритеты защиты"],"constraints":["self-authored; STRIDE going-public","ADR-014 IS6 НЕ Trail of Bits #39"],"preview-form":"outline","defaults":["карта точек входа → приоритизация по STRIDE"],"key-decisions":["что защищать первым перед публикацией"],"acceptance-criteria":["attack surface картирован, защита приоритизирована"]},
  {"skill":"pdn-152fz-audit","kind":"own","needs":["задача аудита ПДн / 152-ФЗ"],"produces":["аудит ПДн: инвентаризация, согласия, маскирование, логи доступа, pd_subject_request"],"constraints":["self-authored; режим техника + закон","ADR-014: IS4 НЕ pg_anonymizer #29, IS5 НЕ D2"],"preview-form":"outline","defaults":["инвентаризация ПДн в схеме/коде → проверка соответствия"],"key-decisions":["режим: техника или закон"],"acceptance-criteria":["ПДн инвентаризированы, соответствие 152-ФЗ проверено"]},
  {"skill":"billing-audit","kind":"own","needs":["код/диф биллинга для аудита денежной корректности"],"produces":["отчёт о money-инвариантах биллинга"],"constraints":["self-authored; аудит кода биллинга","ADR-012: НЕ налоги, НЕ процесс, НЕ security"],"preview-form":"outline","defaults":["проверять потерю копеек, двойное списание, tier-резолюцию, дрейф reconcile"],"key-decisions":["какие денежные инварианты в scope"],"acceptance-criteria":["каждый денежный путь проверен на потерю копеек и двойное списание"]},
  {"skill":"security-go-live","kind":"own","needs":["портал перед выходом в интернет (go/no-go)"],"produces":["вердикт GO/NO-GO + сводка проверок безопасности"],"constraints":["self-authored оркестратор #68-72 + D3","ADR-014 IS7 НЕ audit-portal"],"preview-form":"outline","defaults":["оркеструет ZAP/Nuclei/Ward/pdn-152fz/threat-model + Semgrep"],"key-decisions":["go/no-go порог"],"acceptance-criteria":["вердикт GO/NO-GO обоснован результатами проверок"]}
]
```

## Проектная форма отчёта (ШАБЛОН + гипотезы; финал Write выводит из greps по правилу)

Ниже — структура и гипотезы. Поля «Статус» и «Доказательная база» Write подставляет из
ФАКТИЧЕСКИХ выводов greps (правило агрегации выше). Значения статуса показаны как **проектные
при гипотезе подтверждения**; реальный вывод grep их подтверждает или понижает до
`requires-manual-check`.

```markdown
# Промежуточный доменный go-live аудит безопасности Лидерры (статический, неавторизующий)

Дата 15.06.2026. Статус: промежуточный, НЕ авторизация go-live (финал — владелец после
live-DAST ZAP/Nuclei/Ward + поднятый сервер). Открытые вопросы не закрыты. Метод:
threat-model (STRIDE) + pdn-152fz-audit + billing-audit, сведено security-go-live.

| ID   | Инвариант | Файл:строка                       | Приоритет | Статус (из grep) | Рекомендация                          |
|------|-----------|-----------------------------------|-----------|------------------|---------------------------------------|
| F-T2 | D1        | EnsureSaasAdmin.php:34             | P1        | ←шаги 1-3        | вернуть app-гейт + admin id из сессии  |
| F-P1 | D3        | PdErasureService.php:167          | P1        | ←шаги 8-9        | чистить deals.phones[] JSONB при erasure |
| F-T1 | D2        | WebhookSettingsController.php:150 | P2        | ←шаги 4-5        | добавить ->withoutRedirecting()        |
| F-T3 | D2        | SupplierWebhookController.php      | P3        | ←шаг 7           | body-HMAC + fail-closed allowlist      |
| F-P2 | D3        | config/sentry.php (наличие?)      | P3        | ←шаг 10 (observation) | send_default_pii=false + скраббер  |
| F-B1 | D4        | schema.sql lead_charges index     | P3        | ←шаг 12          | рассмотреть UNIQUE на lead_charges     |

Доказательная база: под каждой находкой — фактический вывод её grep-шага (что вернул grep),
вписывается Write. Чисто (контр-гипотезы): шаг 6 (WebhookUrlGuard.blockReason добротен),
шаг 11 (деньги bcmath), + идемпотентность lockForUpdate+PK supplier_lead_deliveries, логи
без ПДн, майские /api/managers и /api/webhooks/test под auth+гард; фича lead-region в копии
отсутствует. Вердикт (security-go-live): промежуточный NO-GO, блокеры {F-T2, F-P1} — НЕ
авторизация go-live; остаточные риски F-T1/F-T3/F-P2/F-B1 к устранению до публикации.
```

## Порядок исполнения

Шаги 1–12 (Bash grep) дают фактический вывод. Затем `threat-model` → `pdn-152fz-audit` →
`billing-audit` → `security-go-live` интерпретируют evidence. Шаг 13 (Write) выводит отчёт из
фактических выводов 1–12 по правилу агрегации и пишет его в файл-артефакт.

```skills-json
["threat-model", "pdn-152fz-audit", "billing-audit", "security-go-live"]
```

```steps-json
[
  {"op":"Bash","object":"grep -rn EnsureSaasAdmin app/app app/bootstrap app/routes","ref":"D1"},
  {"op":"Bash","object":"grep -n next app/app/Http/Middleware/EnsureSaasAdmin.php","ref":"D1"},
  {"op":"Bash","object":"grep -n admin_user_id app/app/Http/Controllers/Concerns/ResolvesAdminUserId.php","ref":"D1"},
  {"op":"Bash","object":"grep -n Http::timeout app/app/Http/Controllers/Api/WebhookSettingsController.php","ref":"D2"},
  {"op":"Bash","object":"grep -n withoutRedirecting app/app/Http/Controllers/Api/WebhookSettingsController.php","ref":"D2"},
  {"op":"Bash","object":"grep -n blockReason app/app/Support/WebhookUrlGuard.php","ref":"D2"},
  {"op":"Bash","object":"grep -n verifyIpAllowlist app/app/Http/Controllers/Api/SupplierWebhookController.php","ref":"D2"},
  {"op":"Bash","object":"grep -n contact_name app/app/Services/Pd/PdErasureService.php","ref":"D3"},
  {"op":"Bash","object":"grep -n phones db/schema.sql","ref":"D3"},
  {"op":"Bash","object":"grep -rn sentry app/config","ref":"D3"},
  {"op":"Bash","object":"grep -n bcsub app/app/Services/Billing/LedgerService.php","ref":"D4"},
  {"op":"Bash","object":"grep -n lead_charges_deal_id db/schema.sql","ref":"D4"},
  {"op":"Write","object":"docs/superpowers/audits/2026-06-15-portal-domain-audit-report.md","ref":"D5"}
]
```

```verified-context-json
[
  {"id":"D1a","kind":"EXTRACTED","ref":"app/app/Http/Middleware/EnsureSaasAdmin.php","anchor":"return $next($request);"},
  {"id":"D1b","kind":"EXTRACTED","ref":"app/app/Http/Controllers/Concerns/ResolvesAdminUserId.php","anchor":"$request->input('admin_user_id')"},
  {"id":"D2a","kind":"EXTRACTED","ref":"app/app/Http/Controllers/Api/WebhookSettingsController.php","anchor":"Http::timeout(10)"},
  {"id":"D2b","kind":"EXTRACTED","ref":"app/app/Http/Controllers/Api/WebhookSettingsController.php","anchor":"->post($sub->target_url, $testPayload)"},
  {"id":"D2c","kind":"EXTRACTED","ref":"app/app/Support/WebhookUrlGuard.php","anchor":"blockReason"},
  {"id":"D3a","kind":"EXTRACTED","ref":"app/app/Services/Pd/PdErasureService.php","anchor":"'contact_name' => 'Удалено'"},
  {"id":"D3b","kind":"EXTRACTED","ref":"db/schema.sql","anchor":"phones          JSONB"},
  {"id":"D3f","kind":"EXTRACTED","ref":"app/app/Http/Controllers/Api/SupplierWebhookController.php","anchor":"verifyIpAllowlist"},
  {"id":"D4a","kind":"EXTRACTED","ref":"app/app/Services/Billing/LedgerService.php","anchor":"bcsub((string) $lockedTenant->balance_rub"},
  {"id":"D4b","kind":"EXTRACTED","ref":"db/schema.sql","anchor":"lead_charges_deal_id_deal_received_at_index"}
]
```
