integ: merge ORCH-022 security/secret-scanning

This commit is contained in:
stream
2026-06-08 06:32:13 +00:00
32 changed files with 2850 additions and 5 deletions

View File

@@ -36,7 +36,7 @@ created → analysis → architecture → development → review → testing →
| deploy | — | `check_deploy_status` | 14-deploy-log.md (`deploy_status:`) |
| done | — | — | — |
**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058).
**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058), check_security_gate (ORCH-022).
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`.
@@ -155,6 +155,38 @@ helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION`
образа, без миграций). Подробнее: [adr-0008](adr/adr-0008-staging-image-provenance.md),
детально — `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`.
### Security-гейт: secret-scanning + dependency audit перед мержем (ORCH-022 — реализовано)
Автономный конвейер вливал ветку в `main` без проверки на утёкший секрет (ключ/токен/пароль/
приватный ключ) и уязвимую зависимость (CVE); для self-hosting один секрет/CVE через одну
задачу уезжал в общий прод всех проектов (CLAUDE.md §8). ORCH-022 вводит детерминированный
(без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**, рядом с merge-gate
(ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди edge-под-гейтов
(ДО merge-gate). Паттерн соседей: leaf `src/security_gate.py` (never-raise) + тонкая обёртка
`check_security_gate` в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`.
`STAGE_TRANSITIONS` и схема БД — **без изменений**.
- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне
аллоулиста `.gitleaks.toml` → вклад в FAIL. Offline → гарантия «секрет всегда блокирует»
не зависит от сети (безусловна).
- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity`
(дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open +
громкий warning** (анти-петля ORCH-061; флаг `security_dep_audit_fail_closed` для строгого
режима). best-effort при доступности фида.
- **ПЕРВЫМ, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки ДО rebase
не «обвиняет» задачу в CVE из обновившегося `main`; до захвата merge-lease → при FAIL lease
освобождать не нужно.
- **Артефакт `17-security-report.md`** (YAML-frontmatter `security_status`/`secrets_found`/
`deps_blocking`/`deps_warning`/`deps_audit_degraded`); вердикт читается ТОЛЬКО из
frontmatter (гейт пишет → читает обратно через `parse_security_status` → возвращает: единый
источник истины), negative-токен авторитетен, битый/нет → fail-closed.
- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3,
затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046).
- **Условность как ORCH-35/43/58:** `security_gate_enabled` + `security_gate_repos` (пусто →
только self-hosting); never-raise; таймаут `security_scan_timeout_s`; гейт не деплоит/не
рестартит прод. v1 — Python-only; SAST/мульти-стек — follow-up (BR-14).
Подробнее: [adr-0012](adr/adr-0012-security-gate.md), детально —
`docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`.
### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано)
Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде,
нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча
@@ -306,4 +338,4 @@ ORCH-065 вводит фоновый watchdog, чтобы смерть проц
Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md).
---
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест).*
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-022 (security-гейт: secret-scanning gitleaks + dependency audit pip-audit как под-гейт ребра `deploy-staging → deploy` ПЕРВЫМ, adr-0012, `docs/work-items/ORCH-022/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-022-security-secret-scanning (leaf src/security_gate.py never-raise + check_security_gate в src/qg/checks.py `QG_CHECKS` + врезка _handle_security_gate в src/stage_engine.py блок `current_stage == "deploy-staging"` ПЕРВОЙ; флаги `security_*` в src/config.py; gitleaks (pinned) в Dockerfile, pip-audit в requirements.txt, `.gitleaks.toml` в корне; артефакт 17-security-report.md; обновлять также при изменении этих мест).*

View File

@@ -0,0 +1,63 @@
# adr-0012: Security-гейт — secret-scanning + dependency audit перед мержем
- **Статус:** proposed
- **Дата:** 2026-06-07
- **Задача:** ORCH-022
- **Детальный ADR:** `docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`
## Контекст
Оркестратор автономен: `developer` пишет код без человека-фильтра. Перед слиянием ветки в
`main` нет проверки на утёкший секрет (ключ/токен/пароль/приватный ключ) и уязвимую
зависимость (CVE). Для self-hosting один общий прод-инстанс обслуживает все проекты с общей
БД — секрет/CVE через одну задачу попадает в прод всех (CLAUDE.md §self-hosting, §8). Фактический
мерж PR в `main` делает `deployer` в начале стадии `deploy`.
## Решение
Детерминированный (без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**,
рядом с merge-gate (ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди
edge-под-гейтов (ДО merge-gate). `STAGE_TRANSITIONS` не меняется; в `QG_CHECKS` добавлен
`check_security_gate`. Паттерн — как у соседей: leaf-модуль `src/security_gate.py`
(never-raise) + тонкая обёртка в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`.
- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне
аллоулиста (`.gitleaks.toml`) → вклад в FAIL. Offline → гарантия «секрет всегда блокирует»
не зависит от сети.
- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity`
(дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open +
громкий warning** (анти-петля; флаг `security_dep_audit_fail_closed` для строгого режима).
- **ПЕРВЫМ на ребре, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки
ДО rebase не «обвиняет» задачу в CVE, притащенной обновившимся `main` (анти-петля
ORCH-061); до захвата merge-lease → при FAIL lease освобождать не нужно.
- **Артефакт `17-security-report.md`** с YAML-frontmatter (`security_status`,
`secrets_found`, `deps_blocking`, `deps_warning`, `deps_audit_degraded`); вердикт читается
ТОЛЬКО из frontmatter (канон), negative-токен авторитетен; битый/нет → fail-closed.
- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3,
затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046).
- **Условность (как ORCH-35/43/58):** `security_gate_enabled` + `security_gate_repos`; пусто
→ реально только self-hosting (`orchestrator`), прочие репо — no-op pass.
- **never-raise**, таймаут `security_scan_timeout_s`, гейт не деплоит/не рестартит прод.
## Альтернативы
- **Вариант R (review-стадия):** diff может разойтись с мержем в `main`; merge-edge — последняя
страховка. Отклонено.
- **Вариант C (CI-job через `check_ci_green`):** пороги/severity/аллоулист/артефакт плохо
выражаются статусом коммита; коуплинг с раннером. Отклонено для v1 (точка расширения).
- **Новая стадия `security`:** «пустая» стадия без агента не имеет триггера (как в ORCH-043).
Отклонено.
- **fail-closed dep-audit / аудит после rebase:** ложные откаты → петля. Отклонено.
- **Новая колонка retry в БД:** не нужна (переиспользуем `_developer_retry_count`).
## Последствия
- Класс «тихо влитый секрет/CVE» закрыт: секреты — безусловно (offline), CVE — best-effort при
доступности фида. Самоприменение CLAUDE.md §8 без человека.
- Плата: ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`); внешние инструменты
(gitleaks в образе, pip-audit в зависимостях); время скана на каждом прогоне (ограничено
таймаутом); v1 — Python-only (SAST/мульти-стек — follow-up WI).
- Сквозное изменение (новый QG + edge-под-гейт) → `arch:major-change`; прод-деплой ORCH-022 —
строго через staging-гейт (8501), без рестарта прод-контейнера.
## Связи
adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness —
условность/never-raise/fail-closed), adr-0003 (условный гейт / `is_self_hosting_repo`),
adr-0009 (анти-петля ложных FAIL, ORCH-061), ORCH-046 (дословный reason в `task_desc`),
ORCH-9/15 (мульти-стек — будущая зависимость), ORCH-2 (worktree-изоляция).

View File

@@ -0,0 +1,7 @@
# Business Request: [★ высокий] Security-гейт: secret-scanning + аудит зависимостей перед мержем
Work Item ID: ORCH-022
## Description
TBD

View File

@@ -0,0 +1,150 @@
# 01 — BRD: Security-гейт (secret-scanning + аудит зависимостей перед мержем)
Work Item: **ORCH-022**
Приоритет: **★ высокий**
Источник: предложение Стрим, одобрено Славой (2026-06-04).
Стадия: analysis.
---
## 1. Бизнес-проблема
Оркестратор — автономная мульти-агентная система: агенты (`developer`) пишут код
**без человека-фильтра по умолчанию**. Перед мержем в `main` сейчас нет проверки на:
- **утёкший секрет** — закоммиченный API-ключ / токен / пароль / приватный ключ;
- **дырявую зависимость** — пакет с известной CVE;
- (опционально) **базовую уязвимость кода** — типовой SAST-паттерн.
Для автономной системы это критично: ошибку, которую в обычной команде «выловили бы
глазами на ревью», здесь поймать некому. Утёкший в `git`-историю ключ или уязвимая
зависимость может уехать в прод и обслуживать **все** проекты (общий инстанс,
self-hosting).
### Прецеденты / связки
- **PR #18** (`check_ci_green`: красный CI → возврат на `development`) — задаёт целевой
паттерн поведения красного гейта. Security-гейт должен вести себя так же.
- **Управление секретами** (CLAUDE.md §8): секреты живут только в `.env`/`.env.staging`
на хосте, канон — `.env.example`. Гейт — это автоматический страж этого правила.
---
## 2. Цель
Ввести **security-гейт перед слиянием ветки задачи в `main`**, который детерминированно
(без LLM) проверяет diff/ветку на секреты и уязвимые зависимости и **блокирует
продвижение** при нарушении порогов: красный security-гейт → **возврат на `development`**
(developer-retry, как красный CI / merge-gate), задача **не уезжает в прод**.
### Бизнес-ценность
- Структурно невозможно «тихо» влить секрет или известную CVE в прод автономной системы.
- Самоприменение правила CLAUDE.md §8 (секреты не в гит) без участия человека.
- Расширяет уже выстроенную линию автономных страховок (CI-гейт, merge-gate ORCH-043,
staging-провенанс ORCH-058, post-deploy ORCH-021).
---
## 3. Объём (Scope)
### 3.1 В объёме (v1) — **предположение по умолчанию (A1)**
1. **Secret-scanning** — обязательный минимум гейта. Поиск закоммиченных секретов
в ветке задачи / её diff относительно `main`.
2. **Dependency audit** — аудит зависимостей проекта на известные CVE.
3. **Машиночитаемый артефакт-вердикт** security-гейта (YAML-frontmatter — канон гейтов).
4. **Поведение красного гейта** = откат на `development` + developer-retry (cap
`MAX_DEVELOPER_RETRIES = 3`), наблюдаемость (Telegram + Plane-коммент).
5. **Условный раскат** (kill-switch + scope репозиториев), **never-raise**,
self-hosting (`orchestrator`) — первым.
### 3.2 Вне объёма (v1) — **предположение (A2), отдельные WI**
- **SAST (semgrep)** — вынесен в follow-up WI: шумнее, требует policy-тюнинга правил;
гейт проектируется с точкой расширения под него, но в v1 не включается.
- **Полноценный мульти-стек** (JS/npm, Android) — см. A3 ниже; в v1 целевой стек —
Python (сам оркестратор). Связь с ORCH-9/15 фиксируется как зависимость на будущее.
- Ретроспективное сканирование уже существующей истории `main` (гейт смотрит вперёд —
ветку перед мержем, не чистит прошлое).
- Управление аллоулистом ложных срабатываний через UI/Plane (в v1 — файл в репозитории).
### 3.3 Зафиксированные предположения по умолчанию
> ⚠️ Интерактивный опрос Owner на стадии анализа не дал ответа; ниже —
> **дефолты по конвенциям проекта**. Любой из них Owner/архитектор может переопределить
> (для A4 предусмотрены конфиг-флаги порогов).
- **A1 (объём сканеров v1):** secret-scanning + dependency-audit. SAST отложен.
- **A2 (SAST):** отложен в отдельный WI; гейт оставляет точку расширения.
- **A3 (стек):** **Python-only сначала**, реально только для self-hosting
(`is_self_hosting_repo` / scope-CSV), как ORCH-35/43/58. Прочие репо — no-op pass.
Мульти-стек (детект стека по репо) — отдельный WI.
- **A4 (пороги):** **секреты — всегда блок**; **зависимости — блок на HIGH/CRITICAL,
warning на MEDIUM/LOW**. Пороги вынесены в конфиг (переопределяемы без редеплоя кода).
---
## 4. Заинтересованные стороны
| Роль | Интерес |
|------|---------|
| Owner (Слава) | Прод-безопасность автономного конвейера; контроль порогов и раската. |
| Стрим | Инициатор; снижение риска утечки/уязвимости в автономном режиме. |
| Агент `developer` | Получает понятную причину красного гейта → быстрый фикс. |
| Агент `reviewer` | Гейт снимает с него непосильную задачу «глазами ловить ключи». |
| Все проекты на инстансе | Общий прод не должен получить секрет/CVE через одну задачу. |
---
## 5. Бизнес-требования
| ID | Требование | Приоритет |
|----|-----------|-----------|
| BR-1 | Перед слиянием ветки задачи в `main` обязателен security-гейт (секреты + аудит зависимостей). | MUST |
| BR-2 | Найден секрет (порог A4) → гейт **красный** → откат на `development`, в прод не уходит. | MUST |
| BR-3 | Уязвимость зависимости уровня блокировки (порог A4) → гейт **красный** → откат на `development`. | MUST |
| BR-4 | Уязвимость ниже порога блокировки → **warning**, продвижение не блокируется, но фиксируется в артефакте. | MUST |
| BR-5 | Красный гейт ведёт себя как красный CI / merge-gate: откат на `development` + developer-retry (cap 3), затем эскалация (Telegram + Plane Blocked). | MUST |
| BR-6 | Вердикт гейта — **машиночитаемый** (YAML-frontmatter артефакта), читается гейтом ТОЛЬКО из frontmatter (канон проекта), не из прозы. | MUST |
| BR-7 | Гейт **детерминированный, без LLM** в критическом пути (как merge-gate / image-freshness). | MUST |
| BR-8 | Гейт **never-raise**: внутренняя ошибка не роняет `advance_stage` и не вешает конвейер всех проектов. | MUST |
| BR-9 | Условный раскат: глобальный kill-switch + scope-CSV репозиториев; пусто → реально только self-hosting (`orchestrator`), прочие репо — no-op pass. | MUST |
| BR-10 | Пороги блокировки конфигурируемы (env-флаги, без редеплоя кода). | SHOULD |
| BR-11 | Наблюдаемость: причина блокировки видна (Telegram + Plane-коммент + артефакт); проход — без шума. | MUST |
| BR-12 | Документация (CLAUDE.md «Артефакты задачи», `docs/architecture/README.md` таблица гейтов, CHANGELOG, ADR) обновлена в том же PR. | MUST |
| BR-13 | Аллоулист ложных срабатываний (заведомо-безопасные совпадения, напр. в `.env.example`, фикстуры тестов) поддерживается версионируемым файлом в репозитории. | SHOULD |
| BR-14 | Точка расширения под SAST и мульти-стек заложена, но в v1 не активна (A2/A3). | SHOULD |
---
## 6. Ограничения и риски (бизнес-уровень)
- **Self-hosting:** гейт исполняется внутри инстанса, который правит сам себя. Запрет на
рестарт/падение прод-контейнера в рамках задачи (CLAUDE.md §self-hosting) сохраняется —
гейт ничего не деплоит и не рестартит, только читает/сканирует.
- **Ложные срабатывания** (false positives) могут зациклить откат `→ development`
(прецедент ORCH-061 со staging-петлёй). Митигировано: cap retry=3 + аллоулист (BR-13)
+ конфигурируемые пороги (BR-10) + kill-switch (BR-9).
- **Внешние БД уязвимостей** (CVE-фиды) — сетевая зависимость; недоступность фида не
должна давать ложный красный (см. AC: degrade-поведение при недоступности фида —
решение порога «fail-open vs fail-closed для аудита» закрепляется в acceptance + ADR).
- **Стоимость/время** сканирования добавляется к каждому прогону задачи — должно быть
ограничено таймаутом (как merge-retest).
---
## 7. Критерий успеха (бизнес)
Ветка с подсаженным тестовым секретом и/или зависимостью с известной CRITICAL-CVE
**не может** дойти до `main`/прода: гейт краснеет, задача откатывается на `development`
с понятной причиной. Чистая ветка проходит гейт без задержек и без шума. Для не-self
репозиториев конвейер не меняется (no-op). Прод-контейнер не рестартится гейтом.
---
## 8. Открытые вопросы (для архитектора / Owner)
1. **Размещение гейта** (решение архитектора): (а) на стадии `review`, либо (б) отдельный
под-гейт перед мержем на ребре `deploy-staging → deploy` (где уже живёт merge-gate
ORCH-043 / image-freshness ORCH-058). Требование BRD — «перед слиянием в `main`»;
обе опции его удовлетворяют. См. 02-trz §4.
2. **Где запускается сканер**: новый job в `.gitea/workflows/ci.yml` (тогда вердикт может
течь через существующий `check_ci_green`) **или** отдельный QG-чек/под-гейт в `src/qg`.
Решение — архитектор (02-trz фиксирует требования к обоим путям).
3. **Аудит зависимостей при недоступном CVE-фиде:** fail-open (warning) или fail-closed
(блок)? Дефолт-предложение — **fail-open с громким warning** (не плодить ложные
завороты), закрепить в ADR.
4. **Выбор конкретных инструментов** (gitleaks vs trufflehog; pip-audit vs trivy) —
технологическое решение архитектора; BRD фиксирует только функцию.

View File

@@ -0,0 +1,175 @@
# 02 — ТЗ: Security-гейт (secret-scanning + dependency audit)
Work Item: **ORCH-022** · Стадия: analysis · См. `01-brd.md`, `03-acceptance-criteria.md`.
> **Граница ответственности аналитика.** Ниже — *функциональные требования и точки
> касания* кода. Выбор размещения гейта в пайплайне, конкретных инструментов и схемы
> модулей — **решение архитектора** (см. §4 и `01-brd.md` §8). ТЗ фиксирует требования к
> любому из допустимых вариантов и инварианты, которые нельзя нарушать.
---
## 1. Контекст кода (как есть)
- **Стадии:** `src/stages.py::STAGE_TRANSITIONS` — линейный конвейер
`… review → testing → deploy-staging → deploy → done`. Фактический merge ветки в
`main` делает агент `deployer` **в начале стадии `deploy`** (CLAUDE/README).
- **Quality Gates:** `src/qg/checks.py` — реестр `QG_CHECKS` (имя → функция), сигнатуры
диспетчеризуются в `src/stage_engine.py::_run_qg`.
- **Существующий паттерн «красный гейт → возврат developer»:**
`check_ci_green` (PR #18) и rollback-ветки в
`stage_engine._handle_qg_failure_rollbacks` (откат на `development`, developer-retry,
cap `MAX_DEVELOPER_RETRIES = 3`, затем `set_issue_blocked` + Telegram).
- **Эталонный паттерн детерминированного под-гейта на ребре** (без LLM, never-raise,
условный раскат, откат на `development`):
- merge-gate **ORCH-043**`src/merge_gate.py` + `check_branch_mergeable` +
`stage_engine._handle_merge_gate` (ребро `deploy-staging → deploy`);
- image-freshness **ORCH-058**`src/image_freshness.py` + `_check_staging_image_fresh`
+ `stage_engine._handle_image_freshness` (то же ребро).
Оба: leaf-модуль с чистой логикой (never-raise) + тонкая обёртка в `QG_CHECKS` +
врезка-обработчик в `advance_stage`, kill-switch `*_enabled` + scope `*_repos`,
реально только для self-hosting при пустом scope.
- **CI:** `.gitea/workflows/ci.yml` — один job `test` (pytest) на `self-hosted` раннере,
push в `feature/**` и PR в `main`. `check_ci_green` читает комбинированный статус
коммита из Gitea API.
- **Артефакты задачи** нумерованы до `16-post-deploy-log.md`.
- **Зависимости Python:** `requirements.txt` (корень репо).
---
## 2. Функциональные требования к реализации
### FR-1. Secret-scanning ветки перед мержем
- Сканировать ветку задачи / её diff относительно `origin/main` на секреты
(ключи, токены, пароли, приватные ключи).
- **Любой** подтверждённый секрет (не из аллоулиста) → вердикт **FAIL** (порог A4: секреты
всегда блокируют).
- Инструмент (gitleaks / trufflehog) — выбор архитектора. Должен запускаться offline-/
детерминированно (без LLM) и иметь конфиг правил/аллоулиста в репозитории.
### FR-2. Dependency audit
- Аудит зависимостей целевого стека на известные CVE. Для Python — манифест
`requirements.txt` (инструмент pip-audit / trivy — выбор архитектора).
- Классификация по severity. **Порог блокировки (A4, конфигурируемо BR-10):**
- `CRITICAL`, `HIGH` → вклад в **FAIL**;
- `MEDIUM`, `LOW`**warning** (фиксируется в артефакте, не блокирует).
- Недоступность CVE-фида: degrade-поведение по решению ADR (дефолт-предложение —
fail-open + громкий warning, чтобы не плодить ложные завороты). Поведение должно быть
детерминированным и протестированным.
### FR-3. Машиночитаемый артефакт-вердикт
- Гейт порождает артефакт security-отчёта с **YAML-frontmatter**, напр.:
```
---
security_status: PASS # PASS | FAIL
secrets_found: 0
deps_blocking: 0 # число уязвимостей уровня блокировки
deps_warning: 2
---
```
Имя артефакта — предложение: **`17-security-report.md`** (следующий свободный номер;
финализирует архитектор). Тело — человекочитаемый список находок.
- Вердикт читается гейтом **ТОЛЬКО из frontmatter** (канон проекта: «машинные вердикты —
строго YAML-frontmatter, никогда проза»), по образцу `_parse_deploy_status` /
`_parse_staging_status` / `check_reviewer_verdict`. Negative-токен (FAIL) авторитетен.
- Отсутствие/битый frontmatter → `(False, reason)` (fail-closed на чтении вердикта,
как у существующих парсеров).
### FR-4. Поведение красного гейта (откат)
- `security_status: FAIL` → откат на `development` + enqueue `developer`, по образцу
`_handle_qg_failure_rollbacks` (merge-gate-ветка — точный шаблон):
- cap `MAX_DEVELOPER_RETRIES` (3); при исчерпании — `set_issue_blocked` + Telegram-алерт;
- `task_desc` для developer несёт **дословную причину** (какие секреты/CVE), по образцу
ORCH-046 (встраивание must-fix в `task_desc`), а не только ссылку на артефакт;
- Plane-коммент + `notify_qg_failure` (наблюдаемость BR-11).
### FR-5. Условный раскат (как ORCH-35/43/58)
- Глобальный kill-switch `security_gate_enabled` (env `ORCH_SECURITY_GATE_ENABLED`,
дефолт по согласованию; рекомендуется `true` с safety-net, как у соседних фич).
- Scope `security_gate_repos` (CSV); пусто → реально только `is_self_hosting_repo(repo)`
(`orchestrator`). Прочие репо → `(True, "security-gate N/A for <repo>")` (мгновенный pass).
- Отдельные пороги-флаги (A4/BR-10): напр. `security_dep_block_severity`
(`HIGH` по умолчанию), при желании `security_secrets_block` (`true`).
### FR-6. never-raise
- Любая внутренняя ошибка гейта (сбой сканера, отсутствие бинаря, таймаут) →
`(False, "<reason>")` **без** проброса исключения в `advance_stage`. Контракт —
как у `check_branch_mergeable` (внешний + внутренний guard).
- Таймаут сканирования ограничен (по образцу `merge_retest_timeout_s`).
### FR-7. Наблюдаемость
- Блокировка → Telegram + Plane-коммент (BR-11). Проход → лог-строка, без шумных
нотификаций (по образцу merge-gate pass).
- Желательно: краткий снимок в `GET /queue` (опционально, по образцу блоков `reconcile`/
`reaper`/`post_deploy`) — на усмотрение архитектора.
---
## 3. Задействованные модули `src/` (точки касания)
| Модуль | Изменение |
|--------|-----------|
| `src/security_gate.py` (**новый leaf-модуль**) | Чистая логика гейта: запуск сканеров, классификация по severity, применение порогов/аллоулиста, формирование вердикта + парсер frontmatter. **never-raise.** По образцу `src/merge_gate.py` / `src/image_freshness.py` / `src/post_deploy.py`. |
| `src/qg/checks.py` | Новый чек `check_security_gate` (тонкая обёртка над `security_gate`, ленивый импорт во избежание циклов) + регистрация в `QG_CHECKS`. Условность (kill-switch/scope/self-hosting) — как `check_branch_mergeable` / `_check_staging_image_fresh`. |
| `src/stage_engine.py` | Врезка-обработчик `_handle_security_gate(...)` по образцу `_handle_merge_gate` / `_handle_image_freshness`: вызов в `advance_stage` на выбранном архитектором ребре; FAIL → откат на `development` (FR-4); never-raise. **`STAGE_TRANSITIONS` НЕ меняется**, если выбран вариант «под-гейт ребра». |
| `src/config.py` | Новые настройки: `security_gate_enabled`, `security_gate_repos`, `security_dep_block_severity`, `security_scan_timeout_s` (+ при необходимости пути к бинарям/конфигам сканеров). С docstring-комментариями по образцу ORCH-043/058. |
| `.gitea/workflows/ci.yml` | **Если** архитектор выберет CI-путь: новый job `security` (secret-scan + dep-audit), влияющий на комбинированный статус коммита (тогда срабатывает `check_ci_green`-паттерн PR #18). Иначе — не трогается. |
| `requirements.txt` / Dockerfile | Установка выбранных сканеров (если они Python-пакеты — в `requirements.txt`; если бинари — в Dockerfile/раннер). |
| Конфиг сканера + аллоулист | Версионируемые файлы в репозитории (напр. `.gitleaks.toml` / аллоулист) — BR-13. |
| `.openclaw/agents/developer.md` | (Если нужно) краткая инструкция developer'у про устранение security-находок при заворотах. |
> Если выбран вариант «гейт на стадии `review`» — врезка делается в соответствующую
> ветку `advance_stage`/обработчик ревью вместо ребра `deploy-staging → deploy`.
---
## 4. Размещение в пайплайне — варианты для архитектора
Требование BRD: **«перед слиянием ветки в `main`»**. Допустимы (выбор + обоснование — в ADR):
- **Вариант R (review):** security-проверка на стадии `review` (раньше отлов, дешевле
откат — задача ещё близко к development). Минус: дальше по конвейеру `main` может уйти
вперёд (но это закрывает merge-gate).
- **Вариант M (merge-edge, рекомендуемый к рассмотрению):** под-гейт на ребре
`deploy-staging → deploy`, рядом с merge-gate (ORCH-043) и image-freshness (ORCH-058) —
непосредственно перед фактическим мержем `deployer`'ом. Плюс: единое место «последней
страховки перед main», переиспользование готового паттерна врезки/отката/lease.
- **Вариант C (CI-job):** добавить job в `ci.yml`; вердикт течёт через `check_ci_green`.
Плюс: меньше нового кода в движке. Минус: пороги/severity-логика и артефакт-вердикт
сложнее выразить только статусом коммита.
ТЗ не предписывает вариант; реализация обязана сохранить инварианты §6.
---
## 5. Изменения API
- Новых HTTP-endpoint'ов **не требуется**.
- Допустимо (опционально, FR-7): расширить ответ `GET /queue` блоком `security`
(counts/last_run) — по образцу блоков `reconcile`/`reaper`/`post_deploy`. Не обязательно.
## 6. Изменения схемы БД
- **Не требуется.** Состояние гейта — артефакт-файл + (при необходимости) sentinel-файлы,
по образцу merge-lease / deploy-state / post-deploy-state. Миграций БД нет.
- Если архитектор сочтёт нужным считать security-retry отдельно от developer-retry —
предпочесть подсчёт по `jobs`/`agent_runs` (как `_developer_retry_count` /
`_merge_defer_count`), без новых колонок.
## 7. Инварианты (НЕ нарушать)
1. `STAGE_TRANSITIONS` и реестр `QG_CHECKS` остаются консистентными; при варианте
«под-гейт ребра» — `STAGE_TRANSITIONS` не меняется (триггер — то же событие стадии).
2. Машинный вердикт — только из YAML-frontmatter, не из прозы.
3. never-raise: гейт никогда не пробрасывает исключение в `advance_stage`.
4. Условность как ORCH-35/43/58: не-self репо при пустом scope не затрагиваются (no-op).
5. Гейт **не деплоит и не рестартит** прод-контейнер (self-hosting safety).
6. Откат и retry-счётчик developer не ломаются (cap=3, затем эскалация).
7. Документация (CLAUDE.md, README, CHANGELOG, ADR) обновлена в том же PR (BR-12).
## 8. Артефакты pipeline, создаваемые/обновляемые
- **Новый:** `docs/work-items/ORCH-022/17-security-report.md` (имя финализирует архитектор)
с `security_status:`-frontmatter (FR-3) — порождается гейтом per-task.
- **ADR:** `docs/work-items/ORCH-022/06-adr/ADR-001-<slug>.md` (решение: размещение,
инструменты, degrade-поведение фида, пороги). При сквозном влиянии — global ADR в
`docs/architecture/adr/`.
- **Обновить:** `CLAUDE.md` (раздел «Артефакты задачи» — добавить 17-…),
`docs/architecture/README.md` (таблица гейтов + реестр `QG_CHECKS` + новый раздел),
`CHANGELOG.md`, `.env.example` (новые `ORCH_SECURITY_*`).

View File

@@ -0,0 +1,140 @@
# 03 — Критерии приёмки: Security-гейт (ORCH-022)
Формат: каждый критерий имеет чёткое условие **PASS/FAIL**. Привязка к
`01-brd.md` (BR-*) и `02-trz.md` (FR-*).
---
## A. Secret-scanning (FR-1, BR-1/BR-2)
### AC-1 — Подсаженный секрет блокирует гейт
- **PASS:** ветка с тестовым секретом (напр. фиктивный AWS-ключ формата `AKIA…` вне
аллоулиста) → `security_status: FAIL`; гейт возвращает `(False, reason)`, причина
называет секрет/файл.
- **FAIL:** секрет не обнаружен ИЛИ гейт зелёный при наличии секрета.
### AC-2 — Чистая ветка проходит
- **PASS:** ветка без секретов → `security_status: PASS`; `secrets_found: 0`;
гейт возвращает `(True, …)`.
- **FAIL:** ложное срабатывание (FAIL на чистой ветке).
### AC-3 — Аллоулист подавляет заведомо-безопасное (BR-13)
- **PASS:** совпадение, явно занесённое в версионируемый аллоулист (напр. плейсхолдер в
`.env.example` / фикстура теста), **не** даёт FAIL.
- **FAIL:** аллоулист игнорируется и даёт ложный FAIL.
---
## B. Dependency audit (FR-2, BR-3/BR-4)
### AC-4 — CVE уровня блокировки краснит гейт
- **PASS:** зависимость с известной `CRITICAL`/`HIGH` CVE (при пороге по умолчанию
`HIGH`) → вклад в `security_status: FAIL`; `deps_blocking >= 1`.
- **FAIL:** блокирующая уязвимость не приводит к FAIL.
### AC-5 — Низкая severity = warning, не блок
- **PASS:** только `MEDIUM`/`LOW` уязвимости → `security_status: PASS`, при этом
`deps_warning >= 1` и находки перечислены в теле артефакта.
- **FAIL:** `MEDIUM`/`LOW` блокирует продвижение.
### AC-6 — Порог блокировки конфигурируем (BR-10)
- **PASS:** при `ORCH_SECURITY_DEP_BLOCK_SEVERITY=CRITICAL` та же `HIGH`-уязвимость
становится warning (не блок); при `=HIGH` — блок. Поведение детерминированно
определяется флагом.
- **FAIL:** флаг не влияет на классификацию.
### AC-7 — Degrade при недоступном CVE-фиде
- **PASS:** недоступность фида обрабатывается по решению ADR детерминированно и
протестированно (дефолт: fail-open + громкий warning, гейт не краснеет ложно).
- **FAIL:** недоступность фида даёт неконтролируемый красный/исключение.
---
## C. Вердикт и артефакт (FR-3, BR-6)
### AC-8 — Машинный вердикт только из frontmatter
- **PASS:** вердикт читается ТОЛЬКО из YAML-frontmatter `17-security-report.md`; проза с
«PASS»/«FAIL» в теле не влияет на решение. Negative-токен (FAIL) авторитетен.
- **FAIL:** вердикт извлекается из тела/прозы.
### AC-9 — Битый/отсутствующий frontmatter → fail-closed на чтении
- **PASS:** нет frontmatter / битый YAML / нет поля `security_status``(False, reason)`
(как `_parse_deploy_status`/`check_reviewer_verdict`).
- **FAIL:** битый артефакт трактуется как PASS.
### AC-10 — Артефакт создаётся с корректными полями
- **PASS:** после прогона существует `17-security-report.md` с валидным frontmatter
(`security_status`, `secrets_found`, `deps_blocking`, `deps_warning`) и телом-списком.
- **FAIL:** артефакт не создан/без машинных полей.
---
## D. Откат и retry (FR-4, BR-5)
### AC-11 — Красный гейт → откат на development + developer-retry
- **PASS:** `FAIL` → стадия задачи становится `development`, enqueue `developer`,
Plane-коммент + `notify_qg_failure`; счётчик developer-retry растёт.
- **FAIL:** при FAIL задача продвигается дальше / не откатывается.
### AC-12 — task_desc несёт дословную причину (ORCH-046-паттерн)
- **PASS:** `task_desc` для перезапущенного developer содержит конкретику находок
(какие секреты/CVE), а не только ссылку на артефакт.
- **FAIL:** developer получает только ссылку без сути.
### AC-13 — Cap retry и эскалация
- **PASS:** после `MAX_DEVELOPER_RETRIES` (3) безуспешных фиксов — `set_issue_blocked` +
Telegram-алерт; бесконечного отскока нет.
- **FAIL:** откат зацикливается без cap/эскалации.
---
## E. Условный раскат и устойчивость (FR-5/FR-6, BR-8/BR-9)
### AC-14 — Не-self репозиторий = no-op pass
- **PASS:** для repo, не входящего в scope и не self-hosting → гейт возвращает
`(True, "security-gate N/A for <repo>")` мгновенно, конвейер такого репо не меняется.
- **FAIL:** гейт реально запускается/блокирует чужой репо при пустом scope.
### AC-15 — Kill-switch отключает гейт
- **PASS:** `ORCH_SECURITY_GATE_ENABLED=false` → гейт — no-op pass (`(True, …)`),
поведение конвейера 1:1 как до ORCH-022.
- **FAIL:** при выключенном флаге гейт всё ещё блокирует.
### AC-16 — never-raise
- **PASS:** искусственный сбой (нет бинаря сканера / таймаут / исключение внутри) →
`(False, reason)` без проброса исключения; `advance_stage` не падает, конвейер других
задач/проектов не встаёт.
- **FAIL:** внутренняя ошибка пробрасывается/вешает движок.
### AC-17 — Таймаут ограничен
- **PASS:** сканирование, превысившее `ORCH_SECURITY_SCAN_TIMEOUT_S`, корректно
прерывается → детерминированный вердикт (по политике degrade), без зависания.
- **FAIL:** сканер висит без таймаута.
---
## F. Инварианты и интеграция (BR-7/BR-12, TRZ §7)
### AC-18 — STAGE_TRANSITIONS/QG_CHECKS консистентны
- **PASS:** при варианте «под-гейт ребра» `STAGE_TRANSITIONS` не изменён; новый чек
зарегистрирован в `QG_CHECKS`; `_run_qg` корректно его диспетчеризует. Все
существующие тесты гейтов/стадий зелёные.
- **FAIL:** сломан реестр/переходы/существующие тесты.
### AC-19 — Гейт не деплоит/не рестартит прод
- **PASS:** код гейта не вызывает деплой-хук/рестарт прод-контейнера; только
чтение/сканирование.
- **FAIL:** гейт инициирует рестарт/деплой.
### AC-20 — Документация обновлена в том же PR (BR-12)
- **PASS:** обновлены `CLAUDE.md` (артефакт 17-…), `docs/architecture/README.md`
(таблица гейтов + реестр QG + раздел ORCH-022), `CHANGELOG.md`, `.env.example`
(`ORCH_SECURITY_*`); заведён ADR `06-adr/ADR-001-*`.
- **FAIL:** функционал есть, документация/ADR не обновлены → reviewer обязан
REQUEST_CHANGES (CLAUDE.md §6).
### AC-21 — End-to-end на тестовой задаче
- **PASS:** прогон на self-hosting-репо: грязная ветка (секрет/CVE) → откат на
`development`; после фикса чистая ветка → гейт зелёный → конвейер идёт дальше; прод не
затронут в процессе.
- **FAIL:** любой шаг E2E не воспроизводится.

View File

@@ -0,0 +1,126 @@
work_item: ORCH-022
title: "Security-гейт: secret-scanning + dependency audit перед мержем"
notes: >
План тестов для security-гейта. Чистая логика выносится в leaf-модуль
src/security_gate.py (never-raise) — основной предмет unit-тестов (по образцу
tests для merge_gate / image_freshness / post_deploy / staging_verdict).
Интеграция врезки в advance_stage и условный раскат — integration-тесты.
Имена модулей тестов финализирует разработчик/архитектор по факту реализации.
tests:
# --- Secret-scanning (FR-1 / AC-1..AC-3) ---
- id: TC-01
type: unit
description: "Подсаженный тестовый секрет в diff -> вердикт FAIL, secrets_found>=1, причина называет находку."
module: tests/test_security_gate.py
expected: PASS
- id: TC-02
type: unit
description: "Чистая ветка без секретов -> вердикт PASS, secrets_found=0."
module: tests/test_security_gate.py
expected: PASS
- id: TC-03
type: unit
description: "Совпадение из аллоулиста (плейсхолдер .env.example / фикстура) НЕ даёт FAIL."
module: tests/test_security_gate.py
expected: PASS
# --- Dependency audit + пороги (FR-2 / AC-4..AC-7) ---
- id: TC-04
type: unit
description: "CVE уровня HIGH/CRITICAL при пороге HIGH -> вклад в FAIL, deps_blocking>=1."
module: tests/test_security_gate.py
expected: PASS
- id: TC-05
type: unit
description: "Только MEDIUM/LOW уязвимости -> PASS, deps_warning>=1, находки в теле артефакта."
module: tests/test_security_gate.py
expected: PASS
- id: TC-06
type: unit
description: "Конфиг порога: severity=CRITICAL делает HIGH-CVE warning; severity=HIGH делает её блоком."
module: tests/test_security_gate.py
expected: PASS
- id: TC-07
type: unit
description: "Недоступный CVE-фид -> детерминированный degrade по политике ADR (дефолт fail-open + warning), без исключения и без ложного FAIL."
module: tests/test_security_gate.py
expected: PASS
# --- Вердикт / парсер frontmatter (FR-3 / AC-8..AC-10) ---
- id: TC-08
type: unit
description: "Вердикт читается ТОЛЬКО из YAML-frontmatter; проза PASS/FAIL в теле не влияет; negative-токен авторитетен."
module: tests/test_security_gate.py
expected: PASS
- id: TC-09
type: unit
description: "Нет frontmatter / битый YAML / нет поля security_status -> (False, reason) (fail-closed на чтении)."
module: tests/test_security_gate.py
expected: PASS
- id: TC-10
type: unit
description: "Артефакт 17-security-report.md создаётся с валидным frontmatter (security_status, secrets_found, deps_blocking, deps_warning) и телом-списком."
module: tests/test_security_gate.py
expected: PASS
# --- never-raise / таймаут / условность (FR-5/FR-6 / AC-14..AC-17) ---
- id: TC-11
type: unit
description: "Отсутствие бинаря сканера / внутреннее исключение -> (False, reason), исключение не пробрасывается (never-raise)."
module: tests/test_security_gate.py
expected: PASS
- id: TC-12
type: unit
description: "Превышение ORCH_SECURITY_SCAN_TIMEOUT_S -> корректное прерывание и детерминированный вердикт, без зависания."
module: tests/test_security_gate.py
expected: PASS
- id: TC-13
type: unit
description: "check_security_gate: не-self репо при пустом scope -> (True, 'security-gate N/A for <repo>') мгновенно."
module: tests/test_qg_security.py
expected: PASS
- id: TC-14
type: unit
description: "check_security_gate: ORCH_SECURITY_GATE_ENABLED=false -> no-op pass (True)."
module: tests/test_qg_security.py
expected: PASS
- id: TC-15
type: unit
description: "Новый чек зарегистрирован в QG_CHECKS и корректно диспетчеризуется _run_qg."
module: tests/test_qg_security.py
expected: PASS
# --- Откат / retry в stage_engine (FR-4 / AC-11..AC-13) ---
- id: TC-16
type: integration
description: "security_status FAIL -> advance_stage откатывает на development, enqueue developer, Plane-коммент + notify_qg_failure."
module: tests/test_stage_engine_security_gate.py
expected: PASS
- id: TC-17
type: integration
description: "task_desc перезапущенного developer содержит дословную причину находок (ORCH-046-паттерн), не только ссылку."
module: tests/test_stage_engine_security_gate.py
expected: PASS
- id: TC-18
type: integration
description: "После MAX_DEVELOPER_RETRIES (3) -> set_issue_blocked + Telegram-алерт; бесконечного отскока нет."
module: tests/test_stage_engine_security_gate.py
expected: PASS
- id: TC-19
type: integration
description: "security_status PASS -> advance_stage продвигает конвейер штатно (без отката, без шумных нотификаций)."
module: tests/test_stage_engine_security_gate.py
expected: PASS
# --- Инварианты / интеграция (BR-7/BR-12 / AC-18..AC-19) ---
- id: TC-20
type: integration
description: "При варианте 'под-гейт ребра' STAGE_TRANSITIONS не изменён; существующие тесты стадий/гейтов остаются зелёными."
module: tests/test_stages.py
expected: PASS
- id: TC-21
type: integration
description: "Гейт не вызывает деплой-хук/рестарт прод-контейнера (self-hosting safety)."
module: tests/test_stage_engine_security_gate.py
expected: PASS

View File

@@ -0,0 +1,235 @@
# ADR-001: Security-гейт — secret-scanning + dependency audit перед мержем
- **Статус:** Accepted (proposed → принято архитектором ORCH-022)
- **Дата:** 2026-06-07
- **Задача:** ORCH-022
- **Связанный global ADR:** `docs/architecture/adr/adr-0012-security-gate.md`
- **Источники:** `01-brd.md` (BR-1..BR-14), `02-trz.md` (FR-1..FR-7, §4 варианты, §7 инварианты),
`03-acceptance-criteria.md` (AC-1..AC-21).
---
## Контекст
Оркестратор автономен: `developer`-агент пишет код без человека-фильтра. Перед слиянием
ветки задачи в `main` нет автоматической проверки на утёкший секрет (ключ/токен/пароль/
приватный ключ) и на уязвимую зависимость (известная CVE). Для self-hosting это особенно
опасно: один общий прод-инстанс обслуживает все проекты с общей БД — секрет или CVE,
просочившийся через одну задачу, попадает в прод всех проектов (CLAUDE.md §self-hosting, §8).
Конвейер уже содержит линию детерминированных страховок на ребре `deploy-staging → deploy`
(непосредственно перед фактическим мержем PR в `main`, который делает `deployer` в начале
стадии `deploy`):
- **merge-gate** (ORCH-043, `check_branch_mergeable`) — догон `main` + re-test + сериализация;
- **image-freshness** (ORCH-058, `check_staging_image_fresh`) — провенанс staging-образа.
Оба построены по одному паттерну: **leaf-модуль чистой логики (never-raise) + тонкая обёртка
в `QG_CHECKS` + врезка-обработчик `_handle_*` в `advance_stage`**, с условным раскатом
(`*_enabled` + `*_repos`, реально только для self-hosting при пустом scope) и откатом на
`development` с developer-retry (cap `MAX_DEVELOPER_RETRIES = 3`).
Открытые вопросы BRD §8 / TRZ §4, требующие решения архитектора:
1. Размещение гейта в пайплайне (review / merge-edge / CI-job).
2. Где запускается сканер (CI-job через `check_ci_green` / отдельный QG-чек).
3. Degrade при недоступном CVE-фиде (fail-open / fail-closed).
4. Выбор инструментов (gitleaks/trufflehog; pip-audit/trivy).
---
## Решение
### Р-1. Размещение — Вариант M (под-гейт ребра `deploy-staging → deploy`), ПЕРВЫМ среди edge-под-гейтов
Security-гейт реализуется как **детерминированный под-гейт того же ребра**
`deploy-staging → deploy`, что merge-gate и image-freshness, и исполняется **ПЕРВЫМ**
**ДО** merge-gate. `STAGE_TRANSITIONS` **не меняется** (триггер — то же событие «staging-
deployer завершился»; инвариант TRZ §7.1).
Порядок врезок в `advance_stage` (блок `current_stage == "deploy-staging"`):
```
check_staging_status (PASS, существующий QG стадии)
→ security-gate (НОВЫЙ, _handle_security_gate) ← первым
→ merge-gate (_handle_merge_gate)
→ image-freshness (_handle_image_freshness)
→ Phase A (self-deploy approve)
```
**Почему merge-edge, а не review (Вариант R):**
- BRD-требование «перед слиянием в `main`» удовлетворяют оба, но на review-стадии diff
может разойтись с тем, что реально вольётся в `main` (параллельная задача двигает `main`
вперёд между review и merge). Merge-edge — последняя точка перед фактическим мержем.
- Переиспользуется готовая машинерия отката/retry/нотификаций edge-под-гейтов
(минимальный blast-radius, инвариант TRZ §7).
**Почему ПЕРВЫМ (до merge-gate), а не после image-freshness:**
- **Дёшево фейлить.** merge-gate (rebase + re-test, минуты) и image-freshness (docker
rebuild, до 1200с) — дорогие. Нет смысла гонять их на ветке с секретом/CVE.
- **Корректность для секретов.** Секрет живёт в собственных коммитах ветки;
rebase онто `main` его не добавляет и не убирает → скан диапазона `origin/main..HEAD`
до rebase ловит ровно те коммиты, что попадут в `main`.
- **Анти-петля для зависимостей.** Аудит ветки **до** rebase оценивает то, что вносит
ИМЕННО эта задача (её `requirements.txt`/diff), а не уязвимость, которую притащил в
ветку обновившийся `main`. Аудит после rebase «обвинял» бы задачу в чужой (main'овой)
CVE → ложный откат `→ development` → петля (прецедент ORCH-061). Скан до rebase этого
избегает.
- **Проще, чем image-freshness.** Гейт исполняется ДО захвата merge-lease → при FAIL
**lease освобождать не нужно** (в отличие от `_handle_image_freshness`). Чистый откат.
**Почему не CI-job (Вариант C):** пороги severity, warning-vs-block, аллоулист и
машиночитаемый артефакт-вердикт плохо выражаются одним статусом коммита Gitea; путь
коуплится с CI-раннером. Отклонено для v1; оставлено как точка расширения (BR-14).
### Р-2. Инструменты
- **Secret-scanning — `gitleaks`.** Полностью **offline** (без сетевого фида → гарантия
«секрет всегда блокирует» не зависит от сети, BR-2), один статический бинарь,
детерминированный, конфиг + аллоулист в репо (`.gitleaks.toml`, BR-13), поддержка
`--log-opts="origin/main..HEAD"` (скан диапазона), JSON-отчёт, exit-code контракт
(0 = чисто, 1 = найдены секреты, ≥2 = ошибка инструмента). Бинарь устанавливается в
`Dockerfile` (Go-бинарь, не pip-пакет) — см. `07-infra-requirements.md`.
- **Dependency audit — `pip-audit`.** Python-native (v1-стек — сам оркестратор, Python),
читает `requirements.txt`, источник advisory — OSV/PyPI, JSON-выход, ставится через
`requirements.txt`. trivy/trufflehog отклонены как тяжелее/контейнер-ориентированные для
v1-цели «Python-only» (A3).
Конкретные инструменты — деталь реализации; контракт гейта (вход: repo/branch/wi,
выход: `(bool, reason)` + артефакт) от них не зависит, заменяемы за leaf-модулем.
### Р-3. Degrade при недоступном CVE-фиде — **fail-open + громкий warning** (дефолт)
`pip-audit` требует сети (OSV/PyPI advisory DB). Недоступность фида **по умолчанию**:
- **fail-open**: dep-audit не даёт FAIL по причине недоступности фида (иначе — ложные
откаты `→ development` → петля при сетевых проблемах прод-инстанса, прецедент ORCH-061);
- **громко**: в артефакте `deps_audit_degraded: true`, лог `logger.warning`, Telegram-алерт.
- **Секреты не деградируют:** gitleaks offline → гарантия BR-2 безусловна даже при
отсутствии сети. Деградирует ТОЛЬКО dep-audit.
- **Конфигурируемо:** флаг `security_dep_audit_fail_closed` (дефолт `false`) позволяет
Owner'у переключить на fail-closed (недоступность фида → FAIL) без редеплоя кода.
Это разделяет две гарантии: «нет секрета в прод» — **безусловная**; «нет известной CVE» —
**best-effort при доступности фида**. Закреплено в acceptance (AC-7).
### Р-4. Пороги классификации (A4, BR-10)
- **Секреты:** любой подтверждённый (не из аллоулиста) секрет → **вклад в FAIL** (всегда
блок; флаг `security_secrets_block`, дефолт `true`).
- **Зависимости:** severity ≥ `security_dep_block_severity` (дефолт `HIGH`) → **вклад в
FAIL** (`deps_blocking`); ниже порога (`MEDIUM`/`LOW`) → **warning** (`deps_warning`,
не блокирует, фиксируется в теле).
- **Severity = UNKNOWN** (OSV/advisory без CVSS — частый случай pip-audit): трактуется как
**ниже порога → warning**, никогда не авто-блок (анти-петля). Логируется.
### Р-5. Артефакт и вердикт (FR-3, BR-6, канон проекта)
- Новый артефакт **`17-security-report.md`** (следующий свободный номер; финализировано).
- YAML-frontmatter:
```
---
security_status: PASS # PASS | FAIL
secrets_found: 0
deps_blocking: 0
deps_warning: 2
deps_audit_degraded: false
---
```
Тело — человекочитаемый список находок (секреты: файл/правило/маскированное совпадение;
CVE: пакет/версия/идентификатор/severity).
- **Единый источник истины:** гейт вычисляет находки → пишет артефакт → **читает вердикт
обратно через `parse_security_status(content)`** (frontmatter-парсер по образцу
`_parse_deploy_status`/`_parse_staging_status`) → возвращает этот вердикт. Так возвращаемый
`(bool, reason)` гарантированно == frontmatter артефакта (канон «машинный вердикт — только
из YAML-frontmatter, никогда из прозы», AC-8). Negative-токен (`FAIL`) авторитетен.
- Битый/отсутствующий frontmatter / нет поля `security_status` → `(False, reason)` —
fail-closed на чтении вердикта (AC-9).
### Р-6. Поведение красного гейта (FR-4, BR-5)
`security_status: FAIL` → врезка `_handle_security_gate` (по образцу
`_handle_image_freshness`, но БЕЗ работы с lease — гейт до его захвата):
- `update_task_stage(development)` + `enqueue_job("developer", …)`;
- retry-счётчик — **существующий** `_developer_retry_count` (общий с merge/freshness;
без новой колонки, TRZ §6); cap `MAX_DEVELOPER_RETRIES = 3` → при исчерпании
`set_issue_blocked` + Telegram;
- `task_desc` несёт **дословную причину** (какие секреты/файлы, какие пакеты/CVE/severity)
по образцу ORCH-046 — не только ссылку на артефакт (AC-12);
- `notify_qg_failure` + Plane-коммент (наблюдаемость BR-11).
PASS → `return False` из обработчика → `advance_stage` идёт к merge-gate (тишина, без шума).
### Р-7. Условный раскат и устойчивость (FR-5/FR-6)
- `check_security_gate(repo, work_item_id, branch)` в `QG_CHECKS`; обёртка делегирует в
`src/security_gate.py` (ленивый импорт во избежание цикла — по образцу
`_check_staging_image_fresh`).
- Условность: `security_gate_enabled=False` → `(True, "security-gate disabled")`;
`security_gate_repos` (CSV) пусто → реально только `is_self_hosting_repo` → прочие репо
`(True, "security-gate N/A for <repo>")` (AC-14/AC-15).
- **never-raise** (двойной guard как `check_branch_mergeable`): любая ошибка (нет бинаря,
таймаут, исключение) → `(False, reason)`, исключение не уходит в `advance_stage` (AC-16).
- Таймаут сканирования `security_scan_timeout_s` (дефолт 300) на каждый внешний вызов
(`subprocess … timeout=`) — превышение → детерминированный degrade-вердикт (AC-17).
### Р-8. Self-hosting safety (инвариант TRZ §7.5, AC-19)
Гейт **только читает/сканирует** (git, gitleaks, pip-audit, запись артефакта). Не вызывает
деплой-хук, не рестартит и не трогает прод-контейнер (8500/8501).
---
## Точки касания (для developer; reviewer проверяет полноту — AC-20)
| Модуль | Изменение |
|--------|-----------|
| `src/security_gate.py` (**новый leaf**) | `security_gate_applies`, `scan_secrets`, `audit_dependencies`, `classify_severity`, `compute_verdict`, `write_security_report`, `parse_security_status`, `check_security_gate`. never-raise, fail-closed на чтении вердикта. По образцу `image_freshness.py`. |
| `src/qg/checks.py` | `check_security_gate` (тонкая обёртка, ленивый импорт) + регистрация в `QG_CHECKS`. |
| `src/stage_engine.py` | `_handle_security_gate(...)` + врезка ПЕРВОЙ в блоке `current_stage == "deploy-staging"` (до `_handle_merge_gate`). FAIL → откат на `development`. never-raise. **`STAGE_TRANSITIONS` НЕ меняется.** |
| `src/config.py` | `security_gate_enabled` (True), `security_gate_repos` (""), `security_dep_block_severity` ("HIGH"), `security_scan_timeout_s` (300), `security_dep_audit_fail_closed` (False), `security_secrets_block` (True) — с docstring по образцу ORCH-043/058. |
| `Dockerfile` | Установка `gitleaks` (release-бинарь). |
| `requirements.txt` | `pip-audit`. |
| `.gitleaks.toml` (**новый, корень репо**) | Конфиг правил + аллоулист (`.env.example`-плейсхолдеры, тест-фикстуры) — BR-13. |
| `.openclaw/agents/developer.md` | (Опц.) краткая инструкция про устранение security-находок при заворотах. |
| `tests/` | `test_security_gate.py`, `test_qg_security.py`, `test_stage_engine_security_gate.py` (см. `04-test-plan.yaml`). |
| **Документация** | `CLAUDE.md` (артефакт 17-…), `docs/architecture/README.md` (таблица гейтов + реестр QG + раздел), `CHANGELOG.md`, `.env.example` (`ORCH_SECURITY_*`), global `adr-0012`. |
---
## Альтернативы (отклонены)
- **Вариант R (review-стадия):** раньше/дешевле, но diff может разойтись с тем, что
вольётся в `main`; merge-edge уже закрывает «последнюю страховку».
- **Вариант C (CI-job через `check_ci_green`):** пороги/severity/аллоулист/артефакт плохо
выражаются статусом коммита; коуплинг с CI-раннером. → точка расширения BR-14.
- **fail-closed dep-audit по умолчанию:** ложные откаты при сетевых сбоях → петля. →
только опционально через флаг.
- **Аудит после rebase (как анкер image-freshness):** обвиняет задачу в CVE из `main` →
петля. → скан ветки ДО merge-gate.
- **Новая стадия `security`:** «пустая» стадия без агента не имеет триггера (как
отклонено в ORCH-043). → под-гейт ребра.
- **Новая колонка retry в БД:** не нужна — переиспользуем `_developer_retry_count`.
---
## Последствия
**Плюсы.** Структурно невозможно тихо влить секрет (безусловно) или известную CVE
(best-effort) в `main`/прод автономной системы. Самоприменение CLAUDE.md §8. Минимальный
blast-radius: `STAGE_TRANSITIONS`/схема БД не меняются, переиспользован готовый паттерн.
**Минусы / плата.** Ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`).
Добавлены внешние инструменты (gitleaks-бинарь в образ, pip-audit в зависимости). Время
сканирования добавляется к каждому прогону (ограничено таймаутом). Dep-audit best-effort
при сетевых сбоях (осознанный компромисс против петли). v1 — Python-only (A3); мульти-стек
и SAST — follow-up WI (BR-14).
**Раскат.** Сквозное изменение конвейера (новый QG + новый edge-под-гейт) → лейбл
`arch:major-change`. Прод-деплой ORCH-022 — строго через staging-гейт (8501), без рестарта
прод-контейнера в рамках задачи (self-hosting safety).
## Связи
adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness —
условность/never-raise/fail-closed), adr-0003 (`is_self_hosting_repo` — образец условности),
adr-0009/ORCH-061 (анти-петля ложных FAIL), ORCH-046 (дословный reason в `task_desc`),
ORCH-9/15 (мульти-стек — будущая зависимость).

View File

@@ -0,0 +1,56 @@
# 07 — Инфраструктурные требования: Security-гейт (ORCH-022)
См. `06-adr/ADR-001-security-gate.md` (Р-2, Р-3, Р-8). Топология не меняется (один сервер
mva154, Docker Compose). Новые требования — только инструменты сканирования и сетевой доступ
к CVE-фиду.
## I-1. Бинарь `gitleaks` в образе
- **Что:** статический Go-бинарь `gitleaks` (secret-scanning), устанавливается в `Dockerfile`
(НЕ pip-пакет). Зафиксировать версию (pinned release) для детерминизма.
- **Почему в образе, а не на хосте:** гейт исполняется внутри контейнера оркестратора
(`advance_stage`); сканируется per-task worktree, смонтированный в контейнер.
- **Оффлайн:** gitleaks не требует сети (правила локальны) → гарантия «секрет всегда
блокирует» (BR-2) не зависит от доступности интернета.
- **Контракт exit-кодов:** 0 = чисто, 1 = найдены секреты, ≥2 = ошибка инструмента
(≥2 → never-raise degrade-вердикт гейта).
## I-2. `pip-audit` в зависимостях
- **Что:** Python-пакет `pip-audit` (dependency audit), добавляется в `requirements.txt`
(pinned-версия).
- **Источник advisory:** OSV / PyPI advisory DB — **требует сетевого доступа** (исходящий
HTTPS к OSV/PyPI).
- **Цель v1:** аудит `requirements.txt` корня репо (Python-стек, A3). Мульти-стек — follow-up.
## I-3. Сетевой доступ к CVE-фиду (degrade-политика)
- **Требование:** исходящий HTTPS из прод-контейнера к OSV/PyPI advisory.
- **При недоступности (Р-3):** **fail-open + громкий warning** по умолчанию — dep-audit не
краснит гейт из-за сетевого сбоя (анти-петля ORCH-061); фиксируется
`deps_audit_degraded: true` + Telegram + лог. Флаг `security_dep_audit_fail_closed`
(дефолт `false`) — для перевода в строгий режим без редеплоя кода.
- **Секреты не зависят от сети** (I-1) — критическая гарантия безусловна.
## I-4. Конфиг-файлы в репозитории (версионируемые, BR-13)
- `.gitleaks.toml` (корень репо): правила + аллоулист заведомо-безопасных совпадений
(плейсхолдеры `.env.example`, тест-фикстуры). Версионируется, ревьюится как код.
## I-5. Env-флаги (`.env.example` + хост `.env`/`.env.staging`)
| Переменная | Дефолт | Назначение |
|------------|--------|-----------|
| `ORCH_SECURITY_GATE_ENABLED` | `true` | глобальный kill-switch |
| `ORCH_SECURITY_GATE_REPOS` | `` (пусто) | CSV scope; пусто → только self-hosting |
| `ORCH_SECURITY_DEP_BLOCK_SEVERITY` | `HIGH` | порог блокировки зависимостей |
| `ORCH_SECURITY_SCAN_TIMEOUT_S` | `300` | таймаут каждого внешнего вызова сканера |
| `ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED` | `false` | строгий режим при недоступном фиде |
| `ORCH_SECURITY_SECRETS_BLOCK` | `true` | секреты блокируют (всегда по дефолту) |
Секреты-значения в гит НЕ коммитятся (CLAUDE.md §8) — только дефолты в `.env.example`.
## I-6. Ресурсы и тайминги
- Время сканирования добавляется к каждому прогону задачи на ребре `deploy-staging → deploy`,
ограничено `ORCH_SECURITY_SCAN_TIMEOUT_S` (по образцу `merge_retest_timeout_s`).
- Гейт исполняется ДО merge-gate/image-freshness (дёшево фейлить до дорогих rebase/rebuild).
## I-7. Self-hosting safety (инвариант)
Гейт **только читает/сканирует** (git, gitleaks, pip-audit, запись артефакта). Не вызывает
деплой-хук, не рестартит/не трогает прод-контейнер (8500/8501). Прод-деплой ORCH-022 — строго
через staging-гейт (8501).

View File

@@ -0,0 +1,26 @@
# 08 — Требования к схеме БД: Security-гейт (ORCH-022)
## Решение: схема БД НЕ меняется
Миграций нет. Обоснование (соответствует TRZ §6 и паттерну edge-под-гейтов ORCH-043/058):
1. **Вердикт гейта — артефакт-файл** `17-security-report.md` (YAML-frontmatter), как
`14-deploy-log.md` / `15-staging-log.md`. Не хранится в БД.
2. **Состояние/идемпотентность** — детерминированная пересборка вердикта при каждом тике
(гейт чистый, без долгоживущего состояния между прогонами); sentinel-файлы НЕ требуются
(в отличие от deploy-state/post-deploy-state — там асинхронный self-restart).
3. **Retry-счётчик** — переиспользуется существующий `_developer_retry_count(task_id)`
(подсчёт по `jobs`/`agent_runs`), общий с merge-gate/image-freshness. **Новой колонки
`security_retry` НЕ вводим** (TRZ §6: предпочесть подсчёт по `jobs`/`agent_runs`). Это
корректно: security-FAIL, как merge/freshness-FAIL, откатывает на `development` и
запускает developer — он и есть единица retry; общий cap=3 защищает от петли.
## Используемые существующие таблицы (без изменений)
- `tasks` — стадия задачи (`update_task_stage` при откате на `development`).
- `jobs` — enqueue `developer` при FAIL; основа `_developer_retry_count`.
- `agent_runs` — usage/duration; основа подсчёта retry.
## Что НЕ делаем
- Не добавляем таблицу findings/CVE-журнала (история находок — в артефактах per-task; петля
уроков ORCH-8 читает артефакт).
- Не добавляем колонок в `tasks`/`jobs`.

View File

@@ -0,0 +1,16 @@
# 10 — Технические риски: Security-гейт (ORCH-022)
| ID | Риск | Вероятность / Влияние | Митигация (заложена в ADR-001) |
|----|------|----------------------|-------------------------------|
| R-1 | **Ложные срабатывания → петля отката** `→ development` (прецедент ORCH-061 staging-loop). | Средн. / Выс. | Аллоулист `.gitleaks.toml` (BR-13); cap `MAX_DEVELOPER_RETRIES=3` → эскалация (`set_issue_blocked`+Telegram); конфигурируемый порог severity; kill-switch; UNKNOWN-severity → warning, не блок. |
| R-2 | **Недоступность CVE-фида** даёт ложный красный/исключение. | Средн. / Выс. | fail-open + громкий warning по умолчанию (Р-3); `deps_audit_degraded:true`; флаг `security_dep_audit_fail_closed` для строгого режима. Секреты offline → не затронуты. |
| R-3 | **Скан вешает worker-слот** (зависший gitleaks/pip-audit) → стоит конвейер всех проектов (общий инстанс, `max_concurrency`). | Низк. / Выс. | `security_scan_timeout_s` (300) на каждый внешний вызов; never-raise degrade-вердикт; гейт ПЕРВЫМ на ребре (фейлит до дорогих rebase/rebuild). |
| R-4 | **Исключение гейта роняет `advance_stage`** → встаёт движок. | Низк. / Выс. | Двойной never-raise guard (внешний+внутренний) как `check_branch_mergeable`; AC-16/TC-11. |
| R-5 | **Скан после rebase обвиняет задачу в CVE из `main`** → петля. | — (устранён дизайном) | Гейт исполняется ДО merge-gate (скан ветки до rebase); Р-1. |
| R-6 | **Отсутствие бинаря `gitleaks` в образе** (забыт в Dockerfile) → гейт всегда degrade. | Низк. / Средн. | Установка в Dockerfile (I-1), pinned-версия; TC-11 (нет бинаря → `(False,reason)`, never-raise); проверяется на staging (8501) до прода. |
| R-7 | **pip-audit без severity (UNKNOWN)** → либо ложный блок, либо пропуск. | Средн. / Средн. | UNKNOWN → warning (не блок), логируется; осознанный анти-петля компромисс; ужесточение — follow-up. |
| R-8 | **Self-hosting: гейт трогает прод** (рестарт/деплой). | — (запрещено дизайном) | Гейт только читает/сканирует; AC-19/TC-21; прод-деплой ORCH-022 — через staging-гейт. |
| R-9 | **Drift вердикта vs артефакта** (возврат ≠ frontmatter). | Низк. / Средн. | Единый источник: гейт пишет артефакт → читает обратно через `parse_security_status` → возвращает (Р-5); AC-8. |
| R-10 | **Регресс существующих гейтов/стадий** (сломан `QG_CHECKS`/`STAGE_TRANSITIONS`). | Низк. / Выс. | `STAGE_TRANSITIONS` не меняется; новый чек — аддитивно в реестр; полный прогон `tests/` (TC-20); staging-гейт перед прод. |
| R-11 | **v1 Python-only** — секреты/CVE в не-Python стеке (JS/Android) не ловятся. | — (вне scope v1, A3) | Условность scope; точка расширения мульти-стек/SAST (BR-14); зависимость ORCH-9/15 зафиксирована. |
| R-12 | **Стоимость времени** на каждом прогоне задачи. | Низк. / Низк. | Таймаут; гейт первым (ранний выход); только self-hosting по умолчанию. |

View File

@@ -0,0 +1,74 @@
---
type: review
work_item_id: ORCH-022
verdict: APPROVED
version: 1
---
# Review ORCH-022
## Summary
Security-гейт (secret-scanning `gitleaks` + dependency audit `pip-audit`) реализован как
детерминированный под-гейт ребра `deploy-staging → deploy`, исполняемый ПЕРВЫМ среди
edge-под-гейтов — в точности по ADR-001 (Вариант M) и эталонному паттерну соседей
(merge-gate ORCH-043 / image-freshness ORCH-058): leaf-модуль `src/security_gate.py`
(never-raise) + тонкая обёртка `check_security_gate` в `QG_CHECKS` (lazy-import, нет цикла)
+ врезка `_handle_security_gate` ПЕРВОЙ в блоке `current_stage == "deploy-staging"`.
`STAGE_TRANSITIONS` и схема БД не тронуты. Все 772 теста зелёные (25 из них —
security-специфичные: `test_security_gate.py`, `test_qg_security.py`,
`test_stage_engine_security_gate.py`). Документация обновлена полностью и в этом же PR.
### Соответствие ТЗ (02-trz)
- FR-1 secret-scan offline `origin/main..HEAD`, любой секрет вне аллоулиста → FAIL ✓
- FR-2 dep-audit по severity (`HIGH` дефолт), MEDIUM/LOW/UNKNOWN → warning ✓
- FR-3 машинный вердикт ТОЛЬКО из frontmatter `17-security-report.md`, negative-токен
авторитетен, write→read-back (единый источник истины) ✓
- FR-4 FAIL → откат на `development` + developer-retry (cap 3) + `task_desc` с дословными
находками (ORCH-046) ✓
- FR-5 условность `security_gate_enabled` / `security_gate_repos` (пусто → self-hosting) ✓
- FR-6 never-raise + таймаут `security_scan_timeout_s`
- FR-7 наблюдаемость (Telegram при degraded/FAIL, лог при PASS) ✓
- §6 без миграций БД, §7 инварианты соблюдены (STAGE_TRANSITIONS/QG_CHECKS консистентны,
gate не деплоит/не рестартит прод) ✓
### Соответствие ADR (06-adr/ADR-001 + global adr-0012)
Р-1 (размещение ПЕРВЫМ, до merge-gate, до захвата merge-lease → lease не освобождается),
Р-2 (gitleaks pinned Go-бинарь в Dockerfile, pip-audit в requirements), Р-3 (fail-open
degrade + флаг `security_dep_audit_fail_closed`), Р-4 (пороги, UNKNOWN→warning), Р-5
(артефакт + read-back), Р-6 (откат/cap/эскалация), Р-7 (lazy-import, double-guard
never-raise), Р-8 (self-hosting safety) — все реализованы как описано.
### Критерии приёмки (03)
AC-1..AC-21 покрыты тестами TC-01..TC-21 (incl. rollback TC-16, verbatim task_desc TC-17,
cap+blocked TC-18, PASS-advance TC-19, no-deploy-on-FAIL TC-21). AC-20 (документация) —
подтверждён ниже.
## Findings
### P0 — Blocker
- нет
### P1 — Must fix
- нет
### P2 — Should fix
- нет
### P3 — Nice-to-have
- Глобальный `docs/architecture/adr/adr-0012-security-gate.md` помечен `Статус: proposed`,
тогда как per-WI `06-adr/ADR-001``Accepted`. Косметическая рассинхронизация статуса,
на функциональность/гейты не влияет.
## Документация
Обновлена в том же PR (AC-20, CLAUDE.md §6 соблюдён):
- `CLAUDE.md` — раздел «Артефакты задачи» (добавлен `17-security-report.md`) + строка о
машинных вердиктах (`security_status:`).
- `docs/architecture/README.md` — реестр `QG_CHECKS` (`check_security_gate (ORCH-022)`),
новый раздел «Security-гейт …», статусная сноска внизу.
- `docs/architecture/adr/adr-0012-security-gate.md` — новый global ADR (+ per-WI ADR-001).
- `CHANGELOG.md` — подробная запись в `[Unreleased] / Added`.
- `.env.example` — все шесть `ORCH_SECURITY_*` с комментариями.
- `Dockerfile` (pinned gitleaks), `requirements.txt` (pip-audit), `.gitleaks.toml` (корень,
правила + аллоулист) — инфраструктура версионирована.
Статус: документация = golden source — синхронна с кодом. Замечаний нет.

View File

@@ -0,0 +1,76 @@
---
type: test-report
work_item_id: ORCH-022
result: PASS
---
# Test Report — ORCH-022
Security-гейт: secret-scanning (gitleaks) + dependency audit (pip-audit) как под-гейт
ребра `deploy-staging → deploy`.
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Дата: 2026-06-07
- Ветка: `feature/ORCH-022-security-secret-scanning`
- Review verdict: APPROVED (`12-review.md`)
## Smoke test API (prod 8500, self-hosting — не трогаем контейнер)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | `{"status":"ok","service":"orchestrator"}` — OK |
| `GET /status` | OK (active task ORCH-022 в stage=testing виден) |
| `GET /queue` | OK (counts/resilience/reconcile/reaper/post_deploy присутствуют) |
## Результаты (привязка к 04-test-plan.yaml)
| TC ID | Описание | Тест | Результат |
|-------|----------|------|-----------|
| TC-01 | Секрет в diff → FAIL, secrets_found>=1, причина называет находку | test_security_gate.py::test_tc01_secret_in_diff_fails | PASS |
| TC-02 | Чистая ветка → PASS, secrets_found=0 | test_tc02_clean_branch_passes | PASS |
| TC-03 | Аллоулист подавляет заведомо-безопасное | test_tc03_allowlisted_match_does_not_fail | PASS |
| TC-04 | HIGH/CRITICAL CVE при пороге HIGH → FAIL, deps_blocking>=1 | test_tc04_high_cve_at_high_threshold_blocks | PASS |
| TC-05 | Только MEDIUM/LOW → PASS, deps_warning>=1 | test_tc05_only_medium_low_warns_passes | PASS |
| TC-06 | Конфиг порога severity влияет на классификацию | test_tc06_threshold_config_changes_classification | PASS |
| TC-07 | Недоступный фид → детерминированный degrade (fail-open default / fail-closed strict) | test_tc07_degraded_feed_failopen_default_failclosed_strict | PASS |
| TC-08 | Вердикт ТОЛЬКО из frontmatter; negative-токен авторитетен | test_tc08_verdict_only_from_frontmatter | PASS |
| TC-09 | Нет/битый frontmatter → (False, reason) fail-closed | test_tc09_missing_or_broken_frontmatter_failclosed | PASS |
| TC-10 | Артефакт 17-security-report.md с валидным frontmatter + телом | test_tc10_artifact_has_valid_frontmatter_and_body | PASS |
| TC-11 | Нет бинаря / исключение → (False, reason), never-raise | test_tc11_missing_binary_failclosed_never_raises | PASS |
| TC-12 | Таймаут → детерминированный fail-closed, без зависания | test_tc12_timeout_is_deterministic_failclosed | PASS |
| TC-13 | Не-self репо при пустом scope → (True, N/A) мгновенно | test_qg_security.py::test_tc13_non_self_repo_empty_scope_is_na | PASS |
| TC-14 | ORCH_SECURITY_GATE_ENABLED=false → no-op pass | test_tc14_disabled_is_noop_pass | PASS |
| TC-15 | Зарегистрирован в QG_CHECKS и диспетчеризуется _run_qg | test_tc15_registered_in_qg_checks / test_tc15_dispatched_by_run_qg | PASS |
| TC-16 | FAIL → откат на development, enqueue developer, notify_qg_failure | test_stage_engine_security_gate.py::test_tc16_fail_rolls_back_and_enqueues_developer | PASS |
| TC-17 | task_desc несёт дословную причину (ORCH-046) | test_tc17_task_desc_has_verbatim_findings | PASS |
| TC-18 | После MAX_DEVELOPER_RETRIES (3) → set_issue_blocked + Telegram | test_tc18_retry_cap_blocks_and_alerts | PASS |
| TC-19 | PASS → штатное продвижение конвейера | test_tc19_pass_advances_normally | PASS |
| TC-20 | STAGE_TRANSITIONS не изменён; тесты стадий зелёные | tests/test_stages.py (полный прогон) | PASS |
| TC-21 | Гейт не вызывает деплой-хук/рестарт прод (self-hosting safety) | test_tc21_fail_never_triggers_deploy | PASS |
Все 21 TC покрыты и зелёные. Соответствие критериям приёмки (03-acceptance-criteria):
AC-1..AC-21 закрыты соответствующими TC (AC-N ↔ TC-N для N=1..21; AC-20 «документация»
подтверждён в review 12-review.md).
## Вывод pytest
### Security-специфичные тесты (25 шт.)
```
tests/test_security_gate.py ............... (15)
tests/test_qg_security.py ...... (6)
tests/test_stage_engine_security_gate.py ..... (5)
======================== 25 passed, 1 warning in 0.49s =========================
```
### Полный регресс
```
======================= 772 passed, 1 warning in 14.70s ========================
```
(1 warning — PydanticDeprecatedSince20 в src/config.py, не связан с ORCH-022,
существовал до задачи.)
## Итог
**PASS** — полный регресс 772/772 зелёный, 25 security-тестов покрывают все 21 TC
плана и AC-1..AC-21, smoke-тесты API прод-инстанса OK. Прод-контейнер в процессе
тестирования не затронут (тесты офлайн/изолированы). Задача готова к стадии deploy-staging.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-022
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,14 @@
---
post_deploy_status: HEALTHY
action_taken: NONE
work_item: ORCH-022
window_s: 900
checks_total: 30
checks_failed: 0
---
# Post-deploy log — ORCH-021 post-deploy monitor
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.