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

## Цель

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

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

Это **документирующая** задача (написать отчёт), а не кодовая. Скилы здесь — **метод
интерпретации** собранного grep-evidence, аналогично тому как `test-driven-development`
ведёт написание кода, не будучи отдельным файловым шагом. `steps-json` по контракту стены
допускает op ∈ Write|Edit|Bash|MultiEdit (Skill — не op), поэтому скилы не могут быть
шагами; они применяются как линза при интерпретации greps, а их **выводы материализованы в
показанном ниже отчёте** (это и есть интеграция в trace — выходной документ суду предъявлен):

- `threat-model` (STRIDE) → строки F-T2 / F-T1 / F-T3 + приоритизация;
- `pdn-152fz-audit` (техника+закон) → строки F-P1 / F-P2;
- `billing-audit` (money-инварианты) → строка F-B1 + раздел «Чисто»;
- `security-go-live` (оркестратор go/no-go) → **итоговый вердикт** = NO-GO (не готов, блокеры
  F-T2/F-P1). Контракт GO/NO-GO **соблюдён**: вердикт вынесен; «промежуточный» = не
  финально-авторизующий (отложен live-DAST ZAP/Nuclei/Ward), а не отсутствие вердикта.

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

| Шаг | grep | Находка | Дискриминирует |
|----|------|---------|----------------|
| 1 | `EnsureSaasAdmin` в routes/bootstrap | F-T2 | привязан ли no-op гейт к `/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 |
| 11 | `bcsub` в LedgerService | (Чисто) | деньги bcmath, без float |
| 12 | `lead_charges_deal_id` в schema.sql | F-B1 | `CREATE INDEX` (не `UNIQUE`) |
| 13 | Write отчёта | все | агрегация по правилу ниже |

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

## Карточки навыков (верифицированные — дословно из `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-шаг пишет ИМЕННО этот текст — на суд)

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

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

## Находки

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

## Доказательная база (фактический вывод grep по каждой находке)

- **F-T2** (`grep next EnsureSaasAdmin` → тело `handle()` = `return $next($request);`;
  `grep admin_user_id ResolvesAdminUserId` → `$request->input('admin_user_id')`;
  `grep EnsureSaasAdmin routes/bootstrap` → привязка гейта): application-гейт `/api/admin/*`
  — no-op pass-through; защита только на nginx Basic Auth; admin_user_id управляем клиентом
  → audit-trail подделываем.
- **F-P1** (`grep contact_name PdErasureService` → update только phone/contact_name;
  `grep phones schema.sql` → `phones JSONB`): erasure не трогает `deals.phones[]` → 152-ФЗ
  «право на удаление» исполняется неполно.
- **F-T1** (`grep withoutRedirecting WebhookSettingsController` → **пусто**): Guzzle следует
  за 3xx → `302 → 169.254.169.254` обходит pre-flight WebhookUrlGuard; + DNS-rebinding TOCTOU.
- **F-T3** (`grep verifyIpAllowlist SupplierWebhookController`): секрет в URL, нет body-HMAC,
  verifyIpAllowlist fail-open при отсутствии строки.
- **F-P2** (`grep sentry app/config` → **пусто**): нет config/sentry.php → нет явного
  send_default_pii=false / before_send-скраббера для ПДн.
- **F-B1** (`grep lead_charges_deal_id schema.sql` → `CREATE INDEX`, не `UNIQUE`): двойное
  списание предотвращено только на уровне приложения, нет DB-defense-in-depth.

## Чисто (контр-гипотезы подтверждены)

- Деньги через bcmath без float (`grep bcsub LedgerService` → `bcsub(...)`).
- Идемпотентность доставки: `lockForUpdate(tenant)` + PK `supplier_lead_deliveries`.
- Логи без ПДн; `WebhookUrlGuard.blockReason` добротно режет приватные сети.
- Майские зацепки закрыты: `/api/managers` под auth; `/api/webhooks/test` под auth+гард.
- Фича lead-region (`lead_region_resolution_log`) в этой рабочей копии отсутствует — её
  региональный прог здесь неприменим.

## Вердикт (security-go-live, промежуточный)

**NO-GO — НЕ готов к публикации.** Блокеры = все открытые P1 = **{F-T2, F-P1}**. Это НЕ
авторизация go-live: финальное решение принимает владелец после устранения блокеров и
отложенного live-DAST. Остаточные риски (F-T1/F-T3/F-P2/F-B1) — к устранению до публикации.
```

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

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

```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"}
]
```
