# План: доменный go-live аудит (ревизия F) — закрытие замечаний судьи

## Цель

Записать **промежуточный неавторизующий** отчёт доменного аудита по спеке
`2026-06-15-go-live-security-audit-evidence-3.md` (раздел D5) как выходной артефакт
(финальный Write-шаг). Находки ниже — **кандидаты-гипотезы**; каждая привязана к
дискриминирующему grep-шагу (или явно помечена как observation), её фактический вывод
фиксируется в отчёте, финальный статус определяется правилом агрегации (ниже). Поля
находки — шесть **самостоятельных**: ID; инвариант (D1–D4); `файл:строка`; приоритет;
статус; рекомендация. **Блокер = любая открытая `P1`; блокеры этого прогона = {F-T2, F-P1}**
(перечислены явно). Форма — **не авторизация go-live**; финал go/no-go за владельцем.
Открытые вопросы (Б-1/SEC/CTO/Биз/Диз/DO) не закрываются.

## Интеграция скилов в trace (ответ на plan_soundness)

`steps-json` по контракту стены допускает только op ∈ Write|Edit|Bash|MultiEdit — **Skill
не является op шага** by design. Поэтому 4 объявленных скила исполняются как часть конвейера
**между** grep-шагами и финальным Write (по skill-discipline они обязаны быть вызваны до
мутирующего Write-шага), а их структурный вывод **интегрируется в trace через цитирование
в Write-артефакте**:

- `threat-model` → строки F-T2 / F-T1 / F-T3 (attack surface + приоритеты);
- `pdn-152fz-audit` → строки F-P1 / F-P2 (полнота erasure, ПДн в Sentry);
- `billing-audit` → строка F-B1 + раздел «Чисто» (деньги bcmath, идемпотентность);
- `security-go-live` → **итоговый вердикт прогона**.

Контракт `security-go-live` (GO/NO-GO) **соблюдён, не переопределён**: скил выносит вердикт
= **NO-GO (не готов, блокеры F-T2/F-P1)**. Квалификатор «промежуточный» означает лишь, что
вердикт **не финально-авторизующий** (отложен live-DAST: ZAP/Nuclei/Ward + полный review
владельца), а не то, что вердикт не выносится. ADR не требуется — это сужение по цели D5
самой задачи, а не изменение контракта скила.

## Структура выходного отчёта (что пишет Write-шаг, ответ на spec_fidelity)

Write-шаг производит документ с тремя блоками:

1. **Таблица находок** — по строке на находку, ВСЕ шесть полей самостоятельны:

```
| ID   | Инвариант | Файл:строка                       | Приоритет | Статус          | Рекомендация            |
|------|-----------|-----------------------------------|-----------|-----------------|-------------------------|
| F-T2 | D1        | EnsureSaasAdmin.php:34             | P1        | подтверждён     | app-гейт + id из сессии |
| F-P1 | D3        | PdErasureService.php:167          | P1        | подтверждён     | чистить deals.phones[]  |
| F-T1 | D2        | WebhookSettingsController.php:150 | P2        | подтверждён     | withoutRedirecting()    |
| F-T3 | D2        | SupplierWebhookController.php      | P3        | подтверждён     | body-HMAC + fail-closed |
| F-P2 | D3        | config/sentry.php отсутствует     | P3        | observation     | send_default_pii=false  |
| F-B1 | D4        | schema.sql lead_charges index     | P3        | подтверждён     | UNIQUE на lead_charges  |
```

2. **Доказательная база** — под каждой находкой фактический вывод соответствующего grep-шага
   (что вернул `grep`), как evidence.
3. **Вердикт** — `security-go-live` итог: **промежуточный NO-GO, блокеры {F-T2, F-P1}**, явная
   пометка «НЕ авторизация go-live; финал — владелец после live-DAST».

**Правило агрегации статуса** (ответ на step_clarity/verifiability):
- grep дал ожидаемый дискриминирующий вывод → статус `подтверждён`;
- grep НЕ совпал с ожиданием → статус `requires-manual-check` (находка НЕ удаляется молча, НЕ
  выдаётся за факт);
- находка без grep (только F-P2, отсутствие файла) → статус `observation`.

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

| Шаг | grep | Находка | Что дискриминирует |
|----|------|---------|--------------------|
| 1 | `EnsureSaasAdmin` в routes/bootstrap | F-T2 | привязка гейта к `/api/admin/*` (обходит ли роут стек) |
| 2 | `next` в EnsureSaasAdmin | F-T2 | тело = no-op `return $next` |
| 3 | `admin_user_id` в ResolvesAdminUserId | F-T2 | id из `$request->input`, не из сессии |
| 4 | `Http::timeout` в WebhookSettingsController | F-T1 | исходящий вызов существует |
| 5 | `withoutRedirecting` там же | F-T1 | **пусто** → редирект не отключён |
| 6 | `blockReason` в WebhookUrlGuard | F-T1 | guard добротен (контр-доказательство глубины) |
| 7 | `verifyIpAllowlist` в SupplierWebhookController | F-T3 | приёмник + fail-open при пустом allowlist |
| 8 | `contact_name` в PdErasureService | F-P1 | erasure deals без phones[] |
| 9 | `phones` в schema.sql | F-P1 | колонка `phones JSONB` существует |
| 10 | `sentry` в app/config | F-P2 | **пусто** → нет config/sentry.php (нет явного PII-скраббера) |
| 11 | `bcsub` в LedgerService | (Чисто) | деньги через bcmath, без float |
| 12 | `lead_charges_deal_id` в schema.sql | F-B1 | `CREATE INDEX` (не `UNIQUE`) |
| 13 | 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 обоснован результатами проверок"]}
]
```

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

12 grep-шагов производят дискриминирующий вывод по каждой находке (фиксируется как evidence).
Между шагом 12 и финальным Write вызываются 4 объявленных скила (threat-model → pdn-152fz-audit
→ billing-audit → security-go-live), их вердикты цитируются в отчёте. Write (шаг 13) агрегирует
по правилу статуса в отчёт-артефакт по структуре D5.

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

```steps-json
[
  {"op":"Bash","object":"grep -rn EnsureSaasAdmin app/routes app/bootstrap","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":"D2b","kind":"EXTRACTED","ref":"app/app/Http/Controllers/Api/WebhookSettingsController.php","anchor":"->post($sub->target_url, $testPayload)"},
  {"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"}
]
```
