628 lines
30 KiB
Markdown
628 lines
30 KiB
Markdown
# module-05. Проектная спецификация `message-safety`
|
||
|
||
> Статус: целевая 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. Назначение и приоритет
|
||
|
||
`message-safety` — внутренний сервис, который до отправки сообщения в Bitrix24 проверяет пользовательский текст, содержащиеся в нём ссылки и файлы из S3-quarantine.
|
||
|
||
Сервис закрывает угрозы, поступающие через пользовательское сообщение:
|
||
|
||
- управляющие и prompt-injection конструкции, направленные на оператора или последующую автоматическую обработку;
|
||
- опасные URL-схемы, URL с credentials и ссылки на private/link-local/metadata адреса;
|
||
- HTML/script-like payloads, способные стать активным содержимым при небезопасном отображении;
|
||
- подмену типа файла, несоответствие заявленного MIME фактическому формату и checksum;
|
||
- вредоносные файлы, обнаруживаемые антивирусными сигнатурами.
|
||
|
||
Спецификация детализирует архитектуру, но не меняет её. При конфликте приоритет имеют `arch-00`…`arch-05`. Канонические domain outcomes:
|
||
|
||
- `200 allow`;
|
||
- `403 deny`;
|
||
- `203 pending` с последующим sticky `200` или `403`.
|
||
|
||
Test-only правила по первому символу, случайные verdict и terminal `400 stub_final_error` в production-контракт не входят.
|
||
|
||
## 2. Границы ответственности
|
||
|
||
### 2.1. Сервис отвечает за
|
||
|
||
- строгую валидацию 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-логи.
|
||
|
||
### 2.2. Сервис не отвечает за
|
||
|
||
- JWT пользователя, согласия и авторизацию доступа пользователя к диалогу;
|
||
- edge/API rate limits;
|
||
- загрузку файла и выдачу presigned URL;
|
||
- запись пользовательского сообщения и статусов в `han_app`;
|
||
- copy/promote файла из quarantine в S3-data и удаление объекта;
|
||
- доставку в Bitrix24, realtime и пользовательский текст ошибки;
|
||
- анализ входящих сообщений оператора;
|
||
- ML-модерацию смысла, токсичности или правдивости текста.
|
||
|
||
Этими операциями владеет `api-backend` или соответствующий архитектурный модуль.
|
||
|
||
## 3. Threat model MVP
|
||
|
||
### 3.1. Текст и ссылки
|
||
|
||
| Угроза | Контроль | Результат |
|
||
|---|---|---|
|
||
| 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 |
|
||
|
||
Rules не заменяют безопасный rendering. Frontend и Bitrix integration обязаны экранировать текст; safety является дополнительным барьером, а не HTML sanitizer.
|
||
|
||
### 3.2. Файлы
|
||
|
||
| Угроза | Контроль | Результат |
|
||
|---|---|---|
|
||
| Недопустимый размер/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; при отсутствии генерируется сервисом
|
||
traceparent: optional W3C
|
||
Content-Type: application/json; charset=utf-8
|
||
```
|
||
|
||
Token сравнивается constant-time. Missing/invalid token → `401 service_unauthorized` без подсказок.
|
||
|
||
### 8.1. POST `/internal/safety/v1/messages/check`
|
||
|
||
```json
|
||
{
|
||
"message_id": "uuid",
|
||
"content_kind": "text",
|
||
"text": "Текст сообщения",
|
||
"attachment": null
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"message_id": "uuid",
|
||
"content_kind": "file",
|
||
"text": "",
|
||
"attachment": {
|
||
"attachment_id": "uuid",
|
||
"quarantine_object_key": "opaque",
|
||
"mime_type": "application/pdf",
|
||
"size_bytes": 12345,
|
||
"checksum": "sha256:<64-lowercase-hex>"
|
||
}
|
||
}
|
||
```
|
||
|
||
Неизвестные поля запрещены. `text` и `file` — строгий discriminated union.
|
||
|
||
`200`:
|
||
|
||
```json
|
||
{
|
||
"verdict": "allow",
|
||
"rule_id": "safety.all_checks_passed",
|
||
"rules_version": "2026-01-01"
|
||
}
|
||
```
|
||
|
||
`403`:
|
||
|
||
```json
|
||
{
|
||
"verdict": "deny",
|
||
"rule_id": "file.malware_detected",
|
||
"reason_code": "message_blocked",
|
||
"rules_version": "2026-01-01"
|
||
}
|
||
```
|
||
|
||
`203`:
|
||
|
||
```json
|
||
{
|
||
"verdict": "pending",
|
||
"task_id": "uuid",
|
||
"poll_after_ms": 2000,
|
||
"expires_at": "2026-07-29T15:00:00Z",
|
||
"rules_version": "2026-01-01"
|
||
}
|
||
```
|
||
|
||
### 8.2. GET `/internal/safety/v1/messages/tasks/{task_id}`
|
||
|
||
- 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.
|
||
|
||
### 8.3. Error envelope
|
||
|
||
```json
|
||
{
|
||
"error": {
|
||
"code": "validation_error",
|
||
"message": "Request is invalid",
|
||
"request_id": "uuid",
|
||
"details": {}
|
||
}
|
||
}
|
||
```
|
||
|
||
| HTTP | Code/смысл | Retry |
|
||
|---|---|---|
|
||
| `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` не считается circuit breaker failure.
|
||
|
||
## 9. Idempotency и state model
|
||
|
||
Fingerprint = SHA-256 canonical normalized DTO без token/request-id/trace headers.
|
||
|
||
- одинаковый `(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.
|
||
|
||
Task states:
|
||
|
||
```text
|
||
pending -> processing -> allowed
|
||
-> denied
|
||
processing -> pending (retry with lease expiry)
|
||
processing -> failed (infrastructure retries exhausted)
|
||
```
|
||
|
||
`allowed`, `denied`, `failed` terminal и sticky. `failed` не является safety deny.
|
||
|
||
## 10. Хранение данных
|
||
|
||
### 10.1. PostgreSQL schema `message_safety`
|
||
|
||
`safety_tasks`:
|
||
|
||
- `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.
|
||
|
||
`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:
|
||
|
||
- 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.
|
||
|
||
Telemetry collector unavailable не влияет на safety verdict и readiness.
|
||
|
||
## 14. Health
|
||
|
||
`GET /health/live` проверяет только process/event loop.
|
||
|
||
`GET /health/ready` проверяет:
|
||
|
||
- 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"
|
||
}
|
||
}
|
||
```
|
||
|
||
Health не требует service token внутри private ops network и не раскрывает credentials/hostnames.
|
||
|
||
## 15. Configuration
|
||
|
||
Канонические существующие env:
|
||
|
||
```text
|
||
APP_ENV=production-like
|
||
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_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
|
||
```
|
||
|
||
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.
|
||
|
||
Startup отклоняет placeholders, insecure production defaults, несовместимые timeout/TTL и отсутствующие обязательные параметры.
|
||
|
||
## 16. OpenAPI
|
||
|
||
`message-safety/openapi.yaml` OpenAPI 3.1 обязателен и содержит:
|
||
|
||
- `X-Service-Token` security scheme;
|
||
- common request/trace headers;
|
||
- 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.
|
||
|
||
Runtime schema сравнивается с committed artifact contract test. Terminal `400 stub_final_error` отсутствует.
|
||
|
||
## 17. Тестовая матрица
|
||
|
||
### Unit
|
||
|
||
- 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
|
||
|
||
- 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
|
||
|
||
- 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 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.
|
||
|
||
### Security/failure
|
||
|
||
- 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.
|
||
|
||
## 18. Definition of Done
|
||
|
||
- 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.
|
||
|
||
## 19. Переход с текущей заглушки и follow-up
|
||
|
||
Текущая реализация в `codebase/backend/message-safety/` остаётся test stub и **не соответствует** этой production-спецификации. Для перехода отдельной задачей необходимо:
|
||
|
||
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 реализации.
|
||
|
||
До выполнения перехода контейнер должен быть явно маркирован как stub и не считаться production security control.
|
||
|