Закрыли часть проблем с безопасностью + мелкие починки
This commit is contained in:
+475
-306
@@ -1,99 +1,266 @@
|
||||
# module-05. Проектная спецификация заглушки `message-safety`
|
||||
# module-05. Проектная спецификация `message-safety`
|
||||
|
||||
> Статус: целевая спецификация тестовой заглушки MVP, строго реализующей правила данного задания.
|
||||
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md), [`module-04-redis.md`](module-04-redis.md).
|
||||
> Статус: целевая production-спецификация MVP.
|
||||
> Канонические источники: [`README.md`](../architectory/README.md), [`arch-00-glossary.md`](../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../architectory/arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md).
|
||||
|
||||
## 1. Назначение и ограничение
|
||||
## 1. Назначение и приоритет
|
||||
|
||||
Сервис — internal stub для проверки orchestration `api-backend`, а не реальный moderation/antivirus engine. Он доступен только в Docker network и реализует канонические пути arch-02:
|
||||
`message-safety` — внутренний сервис, который до отправки сообщения в Bitrix24 проверяет пользовательский текст, содержащиеся в нём ссылки и файлы из S3-quarantine.
|
||||
|
||||
- `POST /internal/safety/v1/messages/check`;
|
||||
- `GET /internal/safety/v1/messages/tasks/{task_id}`;
|
||||
- `GET /health/live`;
|
||||
- `GET /health/ready`.
|
||||
Сервис закрывает угрозы, поступающие через пользовательское сообщение:
|
||||
|
||||
Сервис не публикуется через nginx, не получает JWT пользователя, не перемещает S3 objects, не отправляет сообщения в Bitrix и не хранит бизнес-историю.
|
||||
- управляющие и prompt-injection конструкции, направленные на оператора или последующую автоматическую обработку;
|
||||
- опасные URL-схемы, URL с credentials и ссылки на private/link-local/metadata адреса;
|
||||
- HTML/script-like payloads, способные стать активным содержимым при небезопасном отображении;
|
||||
- подмену типа файла, несоответствие заявленного MIME фактическому формату и checksum;
|
||||
- вредоносные файлы, обнаруживаемые антивирусными сигнатурами.
|
||||
|
||||
## 2. Главное отличие тестовой заглушки
|
||||
Спецификация детализирует архитектуру, но не меняет её. При конфликте приоритет имеют `arch-00`…`arch-05`. Канонические domain outcomes:
|
||||
|
||||
По базовой архитектуре final deny у Message Safety обычно `403`. Для этой заглушки пользователь явно задал особый task-контракт: `GET task` независимо возвращает примерно с равной вероятностью `203`, `200` или **`400`**.
|
||||
- `200 allow`;
|
||||
- `403 deny`;
|
||||
- `203 pending` с последующим sticky `200` или `403`.
|
||||
|
||||
Здесь `400` на валидном `GET task` — **финальный отрицательный verdict/error заглушки**, а не malformed HTTP request. `api-backend` обязан трактовать его как terminal safety rejection и отображать публично как `422 message_blocked`, выставляя `safety_status=blocked`, `delivery_status=rejected`, без вызова Bitrix. Клиенту raw internal `400` не проксируется.
|
||||
Test-only правила по первому символу, случайные verdict и terminal `400 stub_final_error` в production-контракт не входят.
|
||||
|
||||
Это намеренное test-only расширение текущей таблицы arch-02 (`200/203/403`). Перед использованием не как заглушки arch-02 и contract tests должны быть обновлены либо `400` должен быть заменён на канонический `403`. Существующие arch-файлы в рамках этой задачи не изменяются.
|
||||
## 2. Границы ответственности
|
||||
|
||||
## 3. Технологический профиль
|
||||
### 2.1. Сервис отвечает за
|
||||
|
||||
- Python 3.12+, FastAPI, Pydantic v2, Uvicorn.
|
||||
- Redis asyncio client, DB2.
|
||||
- OpenTelemetry, JSON logging.
|
||||
- pytest/anyio, HTTPX ASGI client, real Redis integration tests.
|
||||
- Без PostgreSQL и S3 для этой stub-реализации; их будущая интеграция находится вне scope.
|
||||
- строгую валидацию internal DTO;
|
||||
- нормализацию и rule-based проверку текста;
|
||||
- извлечение и проверку ссылок;
|
||||
- валидацию file metadata и фактического формата;
|
||||
- чтение файла из S3-quarantine по read-only credentials;
|
||||
- вычисление authoritative SHA-256;
|
||||
- антивирусную проверку файла через ClamAV;
|
||||
- выбор sync/async режима;
|
||||
- создание и исполнение async safety tasks;
|
||||
- sticky final verdict, verdict cache и audit в схеме `message_safety`;
|
||||
- task coordination/cache/rate limits в Redis DB2;
|
||||
- internal API, health, метрики, трассировку и безопасные JSON-логи.
|
||||
|
||||
## 4. Приоритет правил
|
||||
### 2.2. Сервис не отвечает за
|
||||
|
||||
Перед классификацией текст нормализуется. Правила применяются строго в порядке:
|
||||
- JWT пользователя, согласия и авторизацию доступа пользователя к диалогу;
|
||||
- edge/API rate limits;
|
||||
- загрузку файла и выдачу presigned URL;
|
||||
- запись пользовательского сообщения и статусов в `han_app`;
|
||||
- copy/promote файла из quarantine в S3-data и удаление объекта;
|
||||
- доставку в Bitrix24, realtime и пользовательский текст ошибки;
|
||||
- анализ входящих сообщений оператора;
|
||||
- ML-модерацию смысла, токсичности или правдивости текста.
|
||||
|
||||
1. validation/auth: invalid DTO или service token обрабатываются до бизнес-правил;
|
||||
2. нормализация;
|
||||
3. если первый Unicode code point нормализованного текста — кириллическая `ф` или `Ф`, вернуть `403 deny`;
|
||||
4. иначе если первый code point — десятичная цифра, создать task и вернуть `203 pending`;
|
||||
5. любой иной текст, включая пустой после допустимой нормализации, вернуть `200 allow`.
|
||||
Этими операциями владеет `api-backend` или соответствующий архитектурный модуль.
|
||||
|
||||
Таким образом, после нормализации строка не может одновременно начинаться и с `ф/Ф`, и с цифры. Rule `ф/Ф` записан раньше для явности. Для file-only request без текста default — `200 allow`; заглушка не сканирует файл.
|
||||
## 3. Threat model MVP
|
||||
|
||||
## 5. Нормализация
|
||||
### 3.1. Текст и ссылки
|
||||
|
||||
Детерминированный pipeline:
|
||||
|
||||
1. требовать JSON UTF-8;
|
||||
2. заменить `CRLF/CR` на `LF`;
|
||||
3. Unicode normalization `NFKC`;
|
||||
4. удалить leading Unicode whitespace (`lstrip`);
|
||||
5. не менять регистр всей строки и не удалять punctuation;
|
||||
6. ограничить текст max length до значения internal DTO (ориентир 10 000 code points).
|
||||
|
||||
Примеры:
|
||||
|
||||
| Вход | После нормализации | Результат |
|
||||
| Угроза | Контроль | Результат |
|
||||
|---|---|---|
|
||||
| `"Файл"` | `"Файл"` | 403 |
|
||||
| `" фраза"` | `"фраза"` | 403 |
|
||||
| `"\u00a07 дней"` | `"7 дней"` | 203 + task |
|
||||
| `"+7..."` | `"+7..."` | 200 |
|
||||
| `"документ"` | `"документ"` | 200 |
|
||||
| `"abc"` | `"abc"` | 200 |
|
||||
| `""`/whitespace | `""` | 200 |
|
||||
| Prompt/control injection | Версионированные Unicode-aware rules | `403 deny` |
|
||||
| Попытка выдать текст за system/developer instruction | Нормализация + rule pack | `403 deny` |
|
||||
| Script/active-content payload | Правила для script, event-handler и опасных embedding-конструкций | `403 deny` |
|
||||
| Опасная URL-схема | Разрешены только `http` и `https` для распознанных web URL | `403 deny` |
|
||||
| URL с userinfo/credentials | Запрет `user:password@host` | `403 deny` |
|
||||
| SSRF-ссылка | DNS/IP classification, запрет private, loopback, link-local, multicast, unspecified и metadata endpoints | `403 deny` |
|
||||
| Обход Unicode/whitespace | NFKC, CRLF→LF, Unicode whitespace handling | Проверка нормализованного текста |
|
||||
| ReDoS/DoS правилами | Линейные/ограниченные regex, лимиты текста, URL и времени | `400` или dependency error |
|
||||
|
||||
«Цифра» означает Unicode category `Nd` после NFKC, не только ASCII `[0-9]`.
|
||||
Rules не заменяют безопасный rendering. Frontend и Bitrix integration обязаны экранировать текст; safety является дополнительным барьером, а не HTML sanitizer.
|
||||
|
||||
## 6. Authentication и common headers
|
||||
### 3.2. Файлы
|
||||
|
||||
Каждый `/internal/safety/v1/*` требует:
|
||||
| Угроза | Контроль | Результат |
|
||||
|---|---|---|
|
||||
| Недопустимый размер/MIME | Сверка DTO с allow-list и лимитами | `403 deny` |
|
||||
| Подмена MIME | Magic-byte/content sniffing, сверка declared MIME | `403 deny` |
|
||||
| Подмена содержимого после complete | Полный SHA-256 против DTO checksum | `403 deny` |
|
||||
| Malware | ClamAV scan актуальными сигнатурами | `403 deny` |
|
||||
| Архивная бомба/ресурсное истощение | Лимиты размера, stream scan, ClamAV limits/timeouts | deny при policy hit; error при сбое |
|
||||
| Polyglot/неоднозначный формат | Строгий формат detector и deny при mismatch/ambiguity | `403 deny` |
|
||||
| Повтор известного файла | Cache по SHA-256 + versions | Sticky cached verdict |
|
||||
|
||||
MVP принимает только типы из `chat.attachments.allowed_extensions` и `chat.attachments.allowed_mime_types`, при `chat.attachments.max_size_mb`. Расширение проверяет `api-backend` до вызова safety; `message-safety` независимо проверяет MIME и фактический формат байтов. Internal DTO не содержит имени файла, поэтому сервис не выводит расширение из object key.
|
||||
|
||||
### 3.3. Вне threat model MVP
|
||||
|
||||
- zero-day malware, отсутствующий в сигнатурах и эвристиках выбранного AV;
|
||||
- OCR изображений и semantic analysis PDF;
|
||||
- password-protected/encrypted containers: в MVP они запрещаются, если содержимое нельзя полностью проверить;
|
||||
- DLP/поиск персональных данных, токсичности и запрещённой тематики;
|
||||
- переход по пользовательской ссылке и анализ удалённой страницы.
|
||||
|
||||
## 4. Общий pipeline
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
postCheck[POST_check] --> auth[Auth_and_DTO]
|
||||
auth --> kind{content_kind}
|
||||
kind -->|text| normalizeText[Normalize_text]
|
||||
normalizeText --> textRules[Text_rules]
|
||||
textRules --> linkRules[Link_pipeline]
|
||||
linkRules --> syncVerdict[200_or_403]
|
||||
kind -->|file| metadata[Metadata_validation]
|
||||
metadata --> cache{SHA256_cache}
|
||||
cache -->|hit| cached[Sticky_200_or_403]
|
||||
cache -->|miss| task[203_and_task]
|
||||
task --> worker[File_worker]
|
||||
worker --> objectRead[S3_stream_and_SHA256]
|
||||
objectRead --> formatCheck[Format_validation]
|
||||
formatCheck --> avScan[ClamAV_scan]
|
||||
avScan --> finalVerdict[Persist_sticky_verdict]
|
||||
finalVerdict --> taskGet[GET_task_200_or_403]
|
||||
```
|
||||
|
||||
Приоритет:
|
||||
|
||||
1. service authentication, body/content-type/size и DTO validation;
|
||||
2. idempotency/fingerprint conflict;
|
||||
3. нормализация;
|
||||
4. обязательные проверки для соответствующего `content_kind`;
|
||||
5. любой deny имеет приоритет над allow;
|
||||
6. инфраструктурная ошибка не превращается ни в allow, ни в domain deny.
|
||||
|
||||
## 5. Текстовый pipeline
|
||||
|
||||
### 5.1. Нормализация
|
||||
|
||||
Pipeline детерминирован:
|
||||
|
||||
1. принять только JSON UTF-8;
|
||||
2. заменить `CRLF`/`CR` на `LF`;
|
||||
3. Unicode normalization `NFKC`;
|
||||
4. удалить leading Unicode whitespace;
|
||||
5. сохранить исходный регистр и punctuation для rules;
|
||||
6. ограничить текст internal DTO до 10 000 Unicode code points;
|
||||
7. вычислить SHA-256 нормализованного текста для correlation/cache без хранения текста.
|
||||
|
||||
`content_kind=text` требует поле `text`; пустой текст отклоняется upstream `api-backend`. `content_kind=file` допускает пустой `text`; текстовые rules тогда не запускаются.
|
||||
|
||||
### 5.2. Rule engine
|
||||
|
||||
Rules поставляются как статический read-only bundle приложения. Динамический код, regex или rule definitions из запроса запрещены.
|
||||
|
||||
Каждое правило содержит:
|
||||
|
||||
- стабильный `rule_id`;
|
||||
- `reason_code`;
|
||||
- severity;
|
||||
- scope (`text`, `url`, `file_metadata`);
|
||||
- action (`deny`);
|
||||
- `rules_version`;
|
||||
- тестовые positive/negative cases.
|
||||
|
||||
Начальный rule pack MVP:
|
||||
|
||||
- `text.prompt_instruction_override` — конструкции вида «игнорируй предыдущие инструкции» и эквиваленты на поддерживаемых языках;
|
||||
- `text.prompt_role_impersonation` — попытка обозначить пользовательский фрагмент как system/developer/tool instruction;
|
||||
- `text.prompt_secret_extraction` — запрос раскрыть system prompt, credentials, tokens или внутренние инструкции;
|
||||
- `text.active_script` — `<script>`, inline event handlers и эквивалентные active-content шаблоны;
|
||||
- `url.forbidden_scheme`;
|
||||
- `url.credentials_present`;
|
||||
- `url.private_destination`.
|
||||
|
||||
Совпадение должно учитывать границы токенов и контекст, чтобы обычное обсуждение терминов не блокировалось простым substring match. Regex обязаны иметь ограниченную сложность и проходить ReDoS tests. Rule pack загружается и компилируется на startup; ошибка делает service not-ready.
|
||||
|
||||
Обычный текст без срабатывания обязательных rules получает `200 allow`. Неуверенное отсутствие совпадения не является deny. Ошибка rule engine является `500`, но не allow.
|
||||
|
||||
## 6. Pipeline ссылок
|
||||
|
||||
Сервис извлекает URL Unicode-aware parser-ом, а не одним regex.
|
||||
|
||||
Для каждой ссылки:
|
||||
|
||||
1. ограничить количество ссылок в сообщении и длину URL;
|
||||
2. выполнить canonical parsing без автоматического исправления malformed URL;
|
||||
3. разрешить только `http` и `https`;
|
||||
4. запретить userinfo/credentials;
|
||||
5. нормализовать IDNA host и отклонить malformed/confusable host;
|
||||
6. для literal IP применить IP policy;
|
||||
7. для hostname выполнить DNS resolve через доверенный resolver и проверить все A/AAAA;
|
||||
8. запретить loopback, RFC1918/ULA, link-local, multicast, unspecified, reserved ranges и cloud metadata endpoints.
|
||||
|
||||
Сервис не загружает содержимое URL и не следует redirects. Поэтому URL scan не создаёт исходящий HTTP SSRF. DNS failure или malformed URL, явно распознанный как ссылка, возвращает deny по policy; недоступность resolver для всех ссылок является dependency error.
|
||||
|
||||
Лимиты URL должны быть техническими env/settings, документированными в `arch-04` до реализации.
|
||||
|
||||
## 7. Файловый pipeline
|
||||
|
||||
### 7.1. Fast path POST
|
||||
|
||||
До создания задачи сервис:
|
||||
|
||||
- валидирует attachment DTO;
|
||||
- проверяет допустимый MIME и `size_bytes`;
|
||||
- проверяет checksum `sha256:<64 lowercase hex>`;
|
||||
- проверяет, что `quarantine_object_key` соответствует разрешённому opaque key contract и не содержит traversal/control characters;
|
||||
- ищет cache по `(sha256, scanner_engine, signatures_version, rules_version)`.
|
||||
|
||||
Явно недопустимые metadata дают sync `403 deny`. Cache hit возвращает sticky `200` или `403`. Cache miss создаёт задачу и возвращает `203 pending`.
|
||||
|
||||
Сервис не доверяет key как path и не строит произвольный URL. S3 client обращается только к configured quarantine bucket с virtual-hosted addressing и read-only credentials.
|
||||
|
||||
### 7.2. Worker
|
||||
|
||||
Worker:
|
||||
|
||||
1. атомарно claim-ит pending task с lease;
|
||||
2. открывает S3 object stream с ограничением bytes/time;
|
||||
3. вычисляет authoritative SHA-256;
|
||||
4. сверяет фактический размер и checksum с DTO;
|
||||
5. определяет реальный формат по содержимому;
|
||||
6. сверяет detector result с declared MIME;
|
||||
7. запрещает encrypted/password-protected и неподдерживаемые containers;
|
||||
8. передаёт поток в `clamd` через internal network;
|
||||
9. сохраняет sticky final verdict и audit;
|
||||
10. записывает cache только для terminal результата;
|
||||
11. завершает lease.
|
||||
|
||||
Чистый файл получает allow только если успешно завершились **все** обязательные проверки. `ClamAV FOUND`, checksum mismatch, format mismatch, unsupported encrypted content или policy limit дают deny с отдельным `rule_id`.
|
||||
|
||||
ClamAV timeout, protocol error, недоступность S3/DB/Redis или потеря lease не являются deny. Task остаётся pending/retryable в пределах deadline; после исчерпания retry получает terminal infrastructure failure, который API отдаёт как `503`, а не `403`.
|
||||
|
||||
### 7.3. AV runtime
|
||||
|
||||
MVP использует отдельный `clamd` sidecar/service в private Docker network:
|
||||
|
||||
- порт не публикуется наружу;
|
||||
- сигнатуры обновляет `freshclam`;
|
||||
- readiness требует daemon PING и допустимый возраст signatures;
|
||||
- limits согласованы с максимальным размером файла;
|
||||
- контейнер non-root, read-only root filesystem где возможно, отдельный writable volume только для signatures/runtime;
|
||||
- worker не передаёт в clamd object key, имя пользователя или иные PII.
|
||||
|
||||
Недоступность AV переводит `/health/ready` в `503` и запрещает новые file allow.
|
||||
|
||||
## 8. Internal API
|
||||
|
||||
Все `/internal/safety/v1/*` доступны только `api-backend` в private network.
|
||||
|
||||
Headers:
|
||||
|
||||
```text
|
||||
X-Service-Token: ${MESSAGE_SAFETY_SERVICE_TOKEN}
|
||||
X-Request-ID: UUID/ULID (если нет — сервис создаёт)
|
||||
X-Request-ID: UUID/ULID; при отсутствии генерируется сервисом
|
||||
traceparent: optional W3C
|
||||
Content-Type: application/json; charset=utf-8
|
||||
```
|
||||
|
||||
Token сравнивается constant-time. Missing/invalid token → `401` или `403` internal auth error; выбран единый `401 service_unauthorized`, без подсказки о значении. Health не требует token внутри network либо использует отдельную ops policy.
|
||||
Token сравнивается constant-time. Missing/invalid token → `401 service_unauthorized` без подсказок.
|
||||
|
||||
## 7. DTO `POST .../check`
|
||||
|
||||
Stub принимает минимальный versioned DTO, совместимый с потребностями api-backend:
|
||||
### 8.1. POST `/internal/safety/v1/messages/check`
|
||||
|
||||
```json
|
||||
{
|
||||
"message_id": "uuid",
|
||||
"content_kind": "text",
|
||||
"text": "Фраза",
|
||||
"text": "Текст сообщения",
|
||||
"attachment": null
|
||||
}
|
||||
```
|
||||
|
||||
Для file:
|
||||
|
||||
```json
|
||||
{
|
||||
"message_id": "uuid",
|
||||
@@ -104,162 +271,58 @@ Stub принимает минимальный versioned DTO, совместим
|
||||
"quarantine_object_key": "opaque",
|
||||
"mime_type": "application/pdf",
|
||||
"size_bytes": 12345,
|
||||
"checksum": "sha256:..."
|
||||
"checksum": "sha256:<64-lowercase-hex>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Неизвестные поля запрещены. `content_kind=text` требует text field (пустой разрешён именно stub default); `file` допускает attachment metadata, но не читает S3. `message_id` нужен для correlation/idempotency, не для выбора verdict.
|
||||
Неизвестные поля запрещены. `text` и `file` — строгий discriminated union.
|
||||
|
||||
## 8. Ответы `POST .../check`
|
||||
|
||||
### `200 allow`
|
||||
`200`:
|
||||
|
||||
```json
|
||||
{
|
||||
"verdict": "allow",
|
||||
"rule_id": "stub.default_allow",
|
||||
"rule_id": "safety.all_checks_passed",
|
||||
"rules_version": "2026-01-01"
|
||||
}
|
||||
```
|
||||
|
||||
### `403 deny` для `ф/Ф`
|
||||
`403`:
|
||||
|
||||
```json
|
||||
{
|
||||
"verdict": "deny",
|
||||
"rule_id": "stub.starts_with_cyrillic_ef",
|
||||
"reason_code": "stub_blocked",
|
||||
"rule_id": "file.malware_detected",
|
||||
"reason_code": "message_blocked",
|
||||
"rules_version": "2026-01-01"
|
||||
}
|
||||
```
|
||||
|
||||
### `203 pending` для цифры
|
||||
`203`:
|
||||
|
||||
```json
|
||||
{
|
||||
"verdict": "pending",
|
||||
"task_id": "uuid",
|
||||
"poll_after_ms": 2000,
|
||||
"expires_at": "2026-07-10T12:15:00Z",
|
||||
"expires_at": "2026-07-29T15:00:00Z",
|
||||
"rules_version": "2026-01-01"
|
||||
}
|
||||
```
|
||||
|
||||
Все три — нормальные domain outcomes. `403` не участвует в circuit breaker failure count.
|
||||
### 8.2. GET `/internal/safety/v1/messages/tasks/{task_id}`
|
||||
|
||||
## 9. Task storage Redis DB2
|
||||
- running/retryable → `203 pending`;
|
||||
- sticky allow → `200 allow`;
|
||||
- sticky deny → `403 deny`;
|
||||
- malformed UUID → `400 validation_error`;
|
||||
- unknown/expired → `404 task_not_found`;
|
||||
- dependency failure → `503`.
|
||||
|
||||
Ключ:
|
||||
После final verdict повторный GET возвращает тот же HTTP status, verdict, `rule_id` и `rules_version`. Task не удаляется до retention expiry.
|
||||
|
||||
```text
|
||||
han:safety:task:{task_id}
|
||||
```
|
||||
|
||||
HASH/JSON v1:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"message_id": "uuid",
|
||||
"created_at_ms": 0,
|
||||
"poll_count": 0,
|
||||
"rng_context": "optional-test-only",
|
||||
"rules_version": "2026-01-01"
|
||||
}
|
||||
```
|
||||
|
||||
TTL `MESSAGE_SAFETY_TASK_TTL_SEC`, default 900 seconds, должен быть больше `MESSAGE_SAFETY_TASK_POLL_MAX_SEC` (300) плюс network/recovery margin. Текст, attachment key и checksum в Redis не нужны. Создание task и TTL атомарны. Коллизия UUID повторяется bounded.
|
||||
|
||||
`message_id → task_id` dedup key допустим для идемпотентного повторного POST:
|
||||
|
||||
```text
|
||||
han:safety:task-by-message:{message_id} -> task_id
|
||||
```
|
||||
|
||||
с тем же TTL; reserve обоих keys выполняется Lua. Повтор одинакового check возвращает тот же active task. Если fingerprint изменился для того же message id — `409 safety_request_conflict`.
|
||||
|
||||
## 10. `GET .../tasks/{task_id}`
|
||||
|
||||
Сначала проверяются token, UUID и существование task. Затем **на каждый GET независимо** выбирается один из трёх outcomes с вероятностью примерно 1/3:
|
||||
|
||||
- `203 pending`;
|
||||
- `200 allow`;
|
||||
- `400 stub_final_error` (terminal deny/error).
|
||||
|
||||
Предыдущий `200` или `400` не фиксируется как sticky verdict в Redis по буквальному требованию «дальнейший GET случайно и независимо». Следовательно, повторный GET того же task после terminal ответа теоретически может вернуть другой outcome. `api-backend` обязан прекратить polling на первом terminal `200/400`, поэтому противоречие снаружи не возникает.
|
||||
|
||||
Это поведение специально тестовое и не годится для production moderation. Для безопасной recovery production service должен сохранять sticky final verdict; переход потребует изменения режима/контракта.
|
||||
|
||||
### Ответы
|
||||
|
||||
`203`:
|
||||
|
||||
```json
|
||||
{"verdict":"pending","task_id":"uuid","poll_after_ms":2000}
|
||||
```
|
||||
|
||||
`200`:
|
||||
|
||||
```json
|
||||
{"verdict":"allow","task_id":"uuid","rule_id":"stub.random_allow"}
|
||||
```
|
||||
|
||||
`400` terminal:
|
||||
|
||||
```json
|
||||
{
|
||||
"verdict":"deny",
|
||||
"task_id":"uuid",
|
||||
"error":{
|
||||
"code":"stub_final_error",
|
||||
"message":"Stub task returned a final negative verdict",
|
||||
"request_id":"uuid",
|
||||
"details":{"terminal":true}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Для malformed `task_id` используется `400 validation_error`, но его envelope имеет `verdict` отсутствующий и `details.terminal` отсутствует/false. Для неизвестного/expired task — `404 task_not_found`. Api-backend различает terminal stub `400` строго по schema/code, а не по одному HTTP status.
|
||||
|
||||
## 11. Worker/poll model
|
||||
|
||||
Реальный worker не требуется. Task создаётся сразу, а GET эмулирует состояние worker случайным outcome. Контракт остаётся таким же, как для async orchestration: check создаёт `task_id`, api-backend poll-ит GET внутри исходного user POST.
|
||||
|
||||
Опциональный `SAFETY_STUB_WORKER_MODE=emulated_on_poll` — единственный режим MVP. Будущий worker mode не должен менять endpoint/DTO, но final verdict тогда становится sticky.
|
||||
|
||||
Api-backend:
|
||||
|
||||
```text
|
||||
POST check
|
||||
200 -> allow
|
||||
403 -> deny -> public 422 message_blocked
|
||||
203 -> poll GET
|
||||
GET 203 -> continue
|
||||
GET 200 -> allow
|
||||
GET 400 + code=stub_final_error + terminal=true
|
||||
-> deny -> public 422 message_blocked
|
||||
other 400 -> dependency contract error, not message verdict
|
||||
timeout/5xx/redis unavailable -> public 503/504
|
||||
```
|
||||
|
||||
## 12. Randomness и deterministic testing
|
||||
|
||||
Production-like stub default использует криптографически достаточный process RNG либо `random.Random` с entropy seed; распределение не является security decision.
|
||||
|
||||
RNG внедряется через интерфейс `VerdictRng.choice()`. Test implementations:
|
||||
|
||||
- sequence RNG: `pending, allow, final_error`;
|
||||
- seeded RNG через `SAFETY_STUB_RNG_SEED` только при `APP_ENV=test`;
|
||||
- forced outcome через dependency override, не public header.
|
||||
|
||||
В production-like env seed/forced mode вызывает startup failure, чтобы внешний caller не управлял verdict. Статистический test на большой выборке проверяет каждую долю в допустимом диапазоне (например, 0.30–0.36), но основные tests используют sequence RNG и не flaky.
|
||||
|
||||
«Независимо» означает новый RNG draw на каждый валидный GET; poll count/предыдущий outcome не влияют на draw.
|
||||
|
||||
## 13. Error semantics
|
||||
|
||||
Internal envelope:
|
||||
### 8.3. Error envelope
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -272,187 +335,293 @@ Internal envelope:
|
||||
}
|
||||
```
|
||||
|
||||
| HTTP | Code | Retry/смысл |
|
||||
| HTTP | Code/смысл | Retry |
|
||||
|---|---|---|
|
||||
| 400 | `validation_error` | malformed, не terminal verdict |
|
||||
| 400 | `stub_final_error` + verdict deny | terminal task verdict, не malformed |
|
||||
| 401 | `service_unauthorized` | не retry без исправления secret |
|
||||
| 403 | domain `deny` POST | terminal safety verdict |
|
||||
| 404 | `task_not_found` | expired/unknown, dependency contract failure |
|
||||
| 409 | `safety_request_conflict` | message id с другим fingerprint |
|
||||
| 429 | `rate_limit_exceeded` | retry по `Retry-After` |
|
||||
| 500 | `internal_error` | retry/circuit |
|
||||
| 503 | `redis_unavailable` | retry/circuit |
|
||||
| `400` | malformed DTO/path | нет без исправления |
|
||||
| `401` | `service_unauthorized` | нет без исправления secret |
|
||||
| `403` | domain `deny` | нет |
|
||||
| `404` | `task_not_found` | dependency reconciliation |
|
||||
| `409` | `safety_request_conflict` | нет |
|
||||
| `429` | `rate_limit_exceeded` | по `Retry-After` |
|
||||
| `500` | `internal_error` | да |
|
||||
| `503` | dependency/scanner/storage unavailable | да |
|
||||
|
||||
Domain `403` и terminal stub `400` не считаются infrastructure failure circuit breaker.
|
||||
Domain `403` не считается circuit breaker failure.
|
||||
|
||||
## 14. Idempotency и concurrency
|
||||
## 9. Idempotency и state model
|
||||
|
||||
POST fingerprint = SHA-256 canonical normalized DTO без request-id/token. Lua reserve обеспечивает один task на `(message_id,fingerprint)` в TTL. Concurrent duplicate получает тот же task id.
|
||||
Fingerprint = SHA-256 canonical normalized DTO без token/request-id/trace headers.
|
||||
|
||||
GET атомарно проверяет существование и увеличивает `poll_count`; RNG draw выполняется независимо. Удалять task после terminal нельзя, иначе повтор получил бы 404 и нарушил независимый test behavior. TTL выполняет cleanup.
|
||||
- одинаковый `(message_id, fingerprint)` возвращает тот же активный task или sticky final verdict;
|
||||
- тот же `message_id` с другим fingerprint → `409 safety_request_conflict`;
|
||||
- concurrent duplicate создаёт одну task;
|
||||
- final verdict изменять запрещено;
|
||||
- transient error не кэшируется как allow/deny;
|
||||
- повторный worker delivery безопасен через task state compare-and-set.
|
||||
|
||||
## 15. Health
|
||||
Task states:
|
||||
|
||||
`GET /health/live`: только process/event loop, всегда без Redis call.
|
||||
```text
|
||||
pending -> processing -> allowed
|
||||
-> denied
|
||||
processing -> pending (retry with lease expiry)
|
||||
processing -> failed (infrastructure retries exhausted)
|
||||
```
|
||||
|
||||
`GET /health/ready` проверяет:
|
||||
`allowed`, `denied`, `failed` terminal и sticky. `failed` не является safety deny.
|
||||
|
||||
- env/token/rules version валидны;
|
||||
- Redis DB2 auth, PING и короткий SET/GET/DEL с TTL;
|
||||
- RNG provider доступен;
|
||||
- OpenAPI schema загружена.
|
||||
## 10. Хранение данных
|
||||
|
||||
Redis down → `503 {"status":"not_ready","components":{"redis":"down"}}`. Текстовые sync rules технически вычислимы, но service целиком not-ready, а digit check возвращает 503, чтобы не выдавать task без storage.
|
||||
### 10.1. PostgreSQL schema `message_safety`
|
||||
|
||||
## 16. Observability
|
||||
`safety_tasks`:
|
||||
|
||||
JSON fields: timestamp, level, `service.name=message-safety`, module, event, request_id, trace_id/span_id, route, status, duration, rule_id, verdict, task_age_bucket, poll_count bucket, error_code.
|
||||
- `id`, `message_id`, `attachment_id`;
|
||||
- request fingerprint и content hash;
|
||||
- task status, attempts, lease owner/until, next attempt/deadline;
|
||||
- declared MIME/size/checksum;
|
||||
- verdict, `rule_id`, `reason_code`;
|
||||
- rules/scanner/signatures versions;
|
||||
- timestamps и common audit fields.
|
||||
|
||||
Не логируются service token, message text, attachment key/name, checksum, DTO body или PII. Разрешены message/task UUID при принятой retention либо их hash.
|
||||
`verdict_cache`:
|
||||
|
||||
- content SHA-256;
|
||||
- rules/scanner/signatures versions;
|
||||
- sticky verdict и rule;
|
||||
- expiry/created timestamps;
|
||||
- unique versioned cache key.
|
||||
|
||||
`safety_audit`:
|
||||
|
||||
- request/task identifiers;
|
||||
- event (`received`, `task_created`, `rule_hit`, `scan_completed`, `dependency_failed`);
|
||||
- verdict/rule/version;
|
||||
- duration and technical error category;
|
||||
- timestamp.
|
||||
|
||||
Raw message text, file bytes, object key, filename, service token и AV stream в PostgreSQL не сохраняются. Для correlation используются UUID и cryptographic hashes.
|
||||
|
||||
### 10.2. Redis DB2
|
||||
|
||||
Redis используется для:
|
||||
|
||||
- short-lived task lookup;
|
||||
- dedup/reservation coordination;
|
||||
- hot verdict cache;
|
||||
- leases/locks;
|
||||
- internal rate limits.
|
||||
|
||||
PostgreSQL остаётся durable source of truth. Потеря Redis не должна менять sticky final verdict; сервис восстанавливает state из PostgreSQL. Redis unavailable делает service not-ready и file task creation недоступным.
|
||||
|
||||
TTL task должен превышать `MESSAGE_SAFETY_TASK_POLL_MAX_SEC` плюс recovery/network margin. Verdict cache TTL задаётся отдельно и инвалидируется версиями rules/scanner/signatures.
|
||||
|
||||
## 11. Согласование с `api-backend`
|
||||
|
||||
```text
|
||||
POST check
|
||||
200 -> allow -> text accepted or file promote -> Bitrix delivery
|
||||
403 -> blocked/rejected -> no Bitrix -> public 422 message_blocked
|
||||
203 -> persist han_app.safety_tasks -> poll GET
|
||||
GET 203 -> continue
|
||||
GET 200 -> allow branch
|
||||
GET 403 -> deny branch
|
||||
timeout/5xx -> public 503/504, no promote, no Bitrix
|
||||
```
|
||||
|
||||
`message-safety` не перемещает и не удаляет S3 object. На deny это идемпотентно делает `api-backend`. Клиент не получает internal `203`.
|
||||
|
||||
## 12. Security controls
|
||||
|
||||
- internal network only; endpoint не публикуется через nginx и host port;
|
||||
- unique service token только из env/secret mount;
|
||||
- strict DTO, body/text/URL/file limits и запрет unknown fields;
|
||||
- read-only S3-quarantine access, без list/write/delete;
|
||||
- no arbitrary URL fetch, no redirect following;
|
||||
- egress allow-list только DNS, S3, PostgreSQL, Redis, OTLP и clamd по назначению;
|
||||
- parameterized SQL и least-privilege DB role только на schema `message_safety`;
|
||||
- Redis ACL только DB2 и prefixes `han:safety:*`;
|
||||
- dependency pinning, SBOM/image scanning;
|
||||
- non-root, read-only root fs, tmpfs `/tmp`, dropped capabilities, no-new-privileges;
|
||||
- OpenAPI UI выключен в production;
|
||||
- безопасные generic errors без stack/internal addresses;
|
||||
- правила и AV signatures обновляются только trusted deployment process.
|
||||
|
||||
## 13. Observability и privacy
|
||||
|
||||
JSON logs:
|
||||
|
||||
- `timestamp`, `level`, `service.name=message-safety`, `module`, `event`;
|
||||
- `request_id`, `trace_id`, `span_id`, route, status, duration;
|
||||
- verdict, `rule_id`, rules/scanner/signatures version;
|
||||
- task state, attempt, age/size bucket;
|
||||
- dependency/error code.
|
||||
|
||||
Запрещено логировать message text, extracted URL целиком, file bytes, object key/name, checksum целиком, DTO body и service token. URL host и hash допустимы только при утверждённой retention policy; по умолчанию логируется category/hash.
|
||||
|
||||
Metrics:
|
||||
|
||||
- requests/latency/errors по route/status;
|
||||
- check outcomes allow/deny/pending;
|
||||
- task GET outcomes pending/allow/final_error;
|
||||
- observed distribution;
|
||||
- task create/dedup/conflict/not-found/expired;
|
||||
- Redis latency/error/pool;
|
||||
- auth rejects, rate limit;
|
||||
- RNG mode как low-cardinality info;
|
||||
- readiness.
|
||||
- request rate/latency/status по route;
|
||||
- checks/verdicts по `content_kind`, `allow|deny|pending|error`;
|
||||
- rule hits по low-cardinality `rule_id`;
|
||||
- task queue/age/attempts/lease conflicts/timeouts;
|
||||
- scan latency/bytes buckets/AV outcome;
|
||||
- signatures age/version info;
|
||||
- cache hit/miss;
|
||||
- PostgreSQL/Redis/S3/ClamAV latency and errors;
|
||||
- auth rejects/rate limit/readiness.
|
||||
|
||||
Trace связывается с api-backend через `traceparent`, `X-Request-ID` возвращается.
|
||||
Telemetry collector unavailable не влияет на safety verdict и readiness.
|
||||
|
||||
## 17. Security
|
||||
## 14. Health
|
||||
|
||||
- только Docker backend network, без nginx/public route и host port;
|
||||
- constant-time token compare, secret только env/secret mount;
|
||||
- strict JSON schema/max body/max text;
|
||||
- no dynamic code/rules from request;
|
||||
- Redis ACL только DB2 prefixes;
|
||||
- non-root, read-only root fs, tmpfs `/tmp`, dropped capabilities;
|
||||
- OpenAPI docs UI production отключён, committed YAML остаётся;
|
||||
- CORS не нужен internal service;
|
||||
- rate limit по service identity/network защищает от accidental loops;
|
||||
- error response не раскрывает internal host/stack/secret.
|
||||
`GET /health/live` проверяет только process/event loop.
|
||||
|
||||
## 18. Docker и env
|
||||
`GET /health/ready` проверяет:
|
||||
|
||||
```text
|
||||
message-safety/
|
||||
app/
|
||||
main.py
|
||||
api/{routes,schemas,errors,auth}.py
|
||||
application/{classifier,tasks}.py
|
||||
infrastructure/{redis,rng,observability}.py
|
||||
settings.py
|
||||
tests/{unit,integration,contract}/
|
||||
openapi.yaml
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
- settings, secret и rules bundle;
|
||||
- PostgreSQL read/write в schema `message_safety`;
|
||||
- Redis DB2 PING и prefixed SET/GET/DEL;
|
||||
- worker heartbeat/lease processing;
|
||||
- S3-quarantine Head/Get read permission на безопасный canary object;
|
||||
- ClamAV PING и допустимый возраст signatures;
|
||||
- OpenAPI schema availability.
|
||||
|
||||
Критическая dependency down → `503`:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "not_ready",
|
||||
"components": {
|
||||
"postgres": "ok",
|
||||
"redis": "ok",
|
||||
"s3_quarantine": "ok",
|
||||
"worker": "ok",
|
||||
"antivirus": "down",
|
||||
"rules": "ok"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Compose: `expose: 8080`, networks `backend`,`observability`, без `ports`, depends_on Redis health, собственный retry startup.
|
||||
Health не требует service token внутри private ops network и не раскрывает credentials/hostnames.
|
||||
|
||||
Env:
|
||||
## 15. Configuration
|
||||
|
||||
Канонические существующие env:
|
||||
|
||||
```text
|
||||
APP_ENV=production-like
|
||||
MESSAGE_SAFETY_PORT=8080
|
||||
MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:<secret>@<host>:5433/han_chat
|
||||
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/2
|
||||
MESSAGE_SAFETY_SERVICE_TOKEN=<secret>
|
||||
MESSAGE_SAFETY_RULES_VERSION=2026-01-01
|
||||
MESSAGE_SAFETY_TASK_TTL_SEC=900
|
||||
MESSAGE_SAFETY_POLL_AFTER_MS=2000
|
||||
SAFETY_STUB_WORKER_MODE=emulated_on_poll
|
||||
SAFETY_STUB_RNG_SEED=
|
||||
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
|
||||
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
|
||||
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
|
||||
MESSAGE_SAFETY_FILE_SCAN_TIMEOUT_SEC=60
|
||||
SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru
|
||||
SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine
|
||||
SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY=<secret>
|
||||
SELECTEL_S3_QUARANTINE_READ_SECRET_KEY=<secret>
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
||||
```
|
||||
|
||||
Новые env (`TASK_TTL`, `POLL_AFTER`, stub mode/seed) требуют внесения в arch-04 перед реализацией production config; здесь они зафиксированы как предложение.
|
||||
ClamAV endpoint, signature max age, worker concurrency, retry/lease, task/cache TTL, text/URL limits и detector version требуют добавления в `arch-04` до реализации. Бизнес allow-list типов/размера остаётся в `app_settings`; `api-backend` передаёт согласованные metadata, а safety использует versioned runtime snapshot.
|
||||
|
||||
## 19. OpenAPI
|
||||
Startup отклоняет placeholders, insecure production defaults, несовместимые timeout/TTL и отсутствующие обязательные параметры.
|
||||
|
||||
`message-safety/openapi.yaml` OpenAPI 3.1 обязателен и включает:
|
||||
## 16. OpenAPI
|
||||
|
||||
- security scheme `X-Service-Token`;
|
||||
- check request union text/file;
|
||||
- exact 200/203/403 responses POST;
|
||||
- exact 200/203/400/404 responses GET;
|
||||
- discriminator между malformed 400 и terminal stub 400;
|
||||
`message-safety/openapi.yaml` OpenAPI 3.1 обязателен и содержит:
|
||||
|
||||
- `X-Service-Token` security scheme;
|
||||
- common request/trace headers;
|
||||
- examples, max lengths, UUID/checksum formats;
|
||||
- health endpoints.
|
||||
- strict discriminated union text/file;
|
||||
- exact POST responses `200/203/400/401/403/409/429/500/503`;
|
||||
- exact GET responses `200/203/400/401/403/404/429/500/503`;
|
||||
- domain deny отдельно от error envelope;
|
||||
- UUID/checksum/max length examples;
|
||||
- health contracts.
|
||||
|
||||
Generated/runtime schema сравнивается с committed artifact. Contract test api-backend отдельно закрепляет mapping terminal `400 stub_final_error → 422 message_blocked`.
|
||||
Runtime schema сравнивается с committed artifact contract test. Terminal `400 stub_final_error` отсутствует.
|
||||
|
||||
## 20. Тестовая матрица
|
||||
## 17. Тестовая матрица
|
||||
|
||||
### Unit
|
||||
|
||||
- NFKC/whitespace/Unicode `Nd`;
|
||||
- `ф`, `Ф`, fullwidth variants, punctuation/default;
|
||||
- exact rule priority;
|
||||
- DTO union/limits;
|
||||
- injected sequence and seeded RNG;
|
||||
- error discrimination and log redaction.
|
||||
- NFKC, CRLF, Unicode whitespace, max length;
|
||||
- positive/negative cases каждого text rule;
|
||||
- границы токенов и false-positive corpus;
|
||||
- ReDoS/time budget всех regex;
|
||||
- URL parsing, IDNA, schemes, credentials и IP ranges IPv4/IPv6;
|
||||
- DTO union, fingerprint и rule priority;
|
||||
- MIME/magic/checksum decision table;
|
||||
- sticky state transitions;
|
||||
- log redaction.
|
||||
|
||||
### Integration
|
||||
|
||||
- Redis DB2 task/dedup/TTL/atomic concurrency;
|
||||
- same message same/different fingerprint;
|
||||
- task expiration;
|
||||
- Redis outage/reconnect;
|
||||
- ACL rejection outside prefix;
|
||||
- poll count concurrency.
|
||||
- PostgreSQL migration/constraints/idempotency/audit;
|
||||
- Redis DB2 reserve/cache/TTL/lease concurrency/recovery;
|
||||
- S3 read-only stream, object changed/missing/timeout;
|
||||
- ClamAV clean, EICAR, FOUND, timeout, daemon down, stale signatures;
|
||||
- full SHA-256 and size mismatch;
|
||||
- duplicate concurrent request and worker retry;
|
||||
- dependency recovery without changed final verdict.
|
||||
|
||||
### Contract
|
||||
|
||||
- POST `документ`/default 200, `ф/Ф` 403, digit 203;
|
||||
- GET independent 203/200/400;
|
||||
- terminal 400 schema versus malformed 400;
|
||||
- text allow and each deny category;
|
||||
- file cache hit, `203` then sticky `200`, `203` then sticky `403`;
|
||||
- no random/non-sticky outcomes;
|
||||
- malformed `400` never treated as deny;
|
||||
- auth missing/wrong/correct;
|
||||
- request id/trace propagation;
|
||||
- api-backend mapping to public 422 and no Bitrix call;
|
||||
- request-id and trace propagation;
|
||||
- api-backend maps only domain `403` to public `422 message_blocked`;
|
||||
- deny/timeout never calls Bitrix and never promotes quarantine;
|
||||
- OpenAPI runtime parity.
|
||||
|
||||
### Statistical/failure
|
||||
### Security/failure
|
||||
|
||||
- 30k+ GET draws approximately 1/3 each with non-flaky tolerance;
|
||||
- prior outcome does not influence next seeded sequence;
|
||||
- API sync wait terminates on first 200/400;
|
||||
- repeated 203 reaches timeout behavior;
|
||||
- Redis restart loses ephemeral task safely and API returns dependency error;
|
||||
- no text/token/object key in logs.
|
||||
- EICAR and safe corpus;
|
||||
- malformed/polyglot/encrypted files;
|
||||
- decompression and parser resource limits;
|
||||
- URL private/link-local/metadata/IPv4-mapped-IPv6 cases;
|
||||
- DNS rebinding simulation;
|
||||
- no raw text/token/URL/object key/checksum in logs/traces/errors;
|
||||
- S3 credentials cannot list/write/delete;
|
||||
- Redis ACL and PostgreSQL schema isolation;
|
||||
- telemetry outage does not affect verdict.
|
||||
|
||||
## 21. Definition of Done
|
||||
## 18. Definition of Done
|
||||
|
||||
- канонические endpoint paths arch-02 реализованы;
|
||||
- правило normalized `ф/Ф → 403`, digit → `203 task`, others → `200` покрыто;
|
||||
- каждый valid task GET независимо даёт 203/200/terminal 400 примерно 1/3;
|
||||
- distinction terminal vs malformed 400 формально задано;
|
||||
- api-backend contract mapping terminal 400 → public 422 проверен;
|
||||
- Redis DB2 atomic task/dedup/TTL и degraded behavior готовы;
|
||||
- RNG injected, deterministic tests не flaky, prod seed запрещён;
|
||||
- service token/network/ACL/container hardening проверены;
|
||||
- health, JSON logs, metrics/traces без PII/secrets;
|
||||
- OpenAPI 3.1 committed и contract tests зелёные;
|
||||
- контейнер запускается в root Compose без published port;
|
||||
- intentional divergence с arch-02 либо принята как stub exception, либо arch-02 обновлён до production implementation.
|
||||
- production `200/203/403` contract реализован без stub divergence;
|
||||
- text rules и URL pipeline покрывают threat model и false-positive corpus;
|
||||
- files проходят metadata, authoritative SHA-256, format detector и ClamAV;
|
||||
- final task verdict sticky и durable;
|
||||
- idempotency/concurrency/recovery доказаны тестами;
|
||||
- PG schema `message_safety`, Redis DB2 и S3 read-only работают по least privilege;
|
||||
- cache versioned rules/scanner/signatures и не сохраняет transient errors;
|
||||
- api-backend mapping allow/deny/timeout проверен end-to-end;
|
||||
- blocked/failed content не попадает в Bitrix и не promote-ится;
|
||||
- health/readiness отражает PG/Redis/S3/workers/ClamAV/rules;
|
||||
- OpenAPI 3.1 и runtime parity зелёные;
|
||||
- logs/metrics/traces не содержат содержимое сообщений, файлов и secrets;
|
||||
- hardened containers запускаются без public port;
|
||||
- runbook описывает signature update, stale signatures, AV outage, retry и rollback.
|
||||
|
||||
## 22. Решения, допущения и TBD
|
||||
## 19. Переход с текущей заглушки и follow-up
|
||||
|
||||
**Решения:** normalizer NFKC+lstrip; Unicode `Nd`; default allow; emulation on GET без worker; independent non-sticky outcomes; `400 stub_final_error` terminal и преобразуется API в 422.
|
||||
Текущая реализация в `codebase/backend/message-safety/` остаётся test stub и **не соответствует** этой production-спецификации. Для перехода отдельной задачей необходимо:
|
||||
|
||||
**Допущения:** пустой/file-only text попадает в default 200; `message_id` передаётся internal DTO; Redis task TTL 900 секунд достаточен для MVP tests.
|
||||
1. удалить правила `ф/Ф`, digit task и random RNG;
|
||||
2. удалить terminal `400 stub_final_error`;
|
||||
3. реализовать sticky canonical `403` для async deny;
|
||||
4. подключить PostgreSQL schema `message_safety`, Redis DB2, S3 read-only и ClamAV workers;
|
||||
5. добавить migrations, полный OpenAPI и тестовую матрицу;
|
||||
6. синхронизировать `module-01-api-backend.md` и код api-backend, удалив test-only mapping `stub_final_error`;
|
||||
7. синхронизировать `module-09-observability.md` и dashboards, удалив stub distribution panels/маркировку;
|
||||
8. добавить новые technical env и ClamAV deployment contract в `arch-04`/`arch-03` до production реализации.
|
||||
|
||||
**TBD:**
|
||||
До выполнения перехода контейнер должен быть явно маркирован как stub и не считаться production security control.
|
||||
|
||||
- S1 формально обновить arch-02 для test-only terminal 400 или вернуть production 403;
|
||||
- S2 окончательный internal DTO/fingerprint в OpenAPI;
|
||||
- S3 добавить новые env в arch-04;
|
||||
- S4 точный Redis task TTL относительно extended recovery module-01;
|
||||
- S5 sticky final verdict при переходе от stub к реальному Safety;
|
||||
- S6 реальные file/link checks, PostgreSQL schema и S3 read-only — вне scope заглушки.
|
||||
|
||||
Reference in New Issue
Block a user