Перейти к содержимому

LLD — низкоуровневый дизайн

LLD (Low-Level Design) — низкоуровневый (детальный) архитектурный дизайн. Описывает, как реализуется то, что зафиксировано в BRD: декомпозиция системы по уровням C4 (контейнеры, компоненты), сценарии взаимодействия (sequence) и спецификации внешних API. LLD — рабочая карта для команды разработки; при расхождении с BRD приоритет у BRD.

Проект: Система интеллектуального мониторинга мерчантов Версия: 0.1 (черновик) Базовый документ: BRD Ответственный: А. Зубик (архитектор)


Многоуровневое проектирование сервиса Система интеллектуального мониторинга мерчантов от контейнерной диаграммы (C2) до спецификации внешнего API.

Скоуп этого документа. Контейнеры Verdict API и Verdict Cache (Redis) показаны на C2 как design-задел (пунктирные интеграционные точки). По решению заказчика Verdict API в работу не берём — он оставлен в дизайне как зафиксированный контракт на будущее, на случай интеграции с PSP-шлюзом. В текущей реализации hits и вердикты пишутся в PostgreSQL, потребитель — только UI комплаенс-офицера и Reporter. Цель раздела — зафиксировать границы ответственности и точки интеграции так, чтобы при возможном расширении модель данных Scanner Engine не пришлось перекраивать.

Актуализация под реализацию. Диаграммы ниже сохраняют исходный проектный замысел (self-hosted LLM, vision/OCR, SSO). Фактически реализовано иначе: AI-слой — LLM-классификация и семантика через эмбеддинги облачного провайдера Mistral (+ vector DB Qdrant); OCR/vision и SSO вне области (см. BRD и RTM). Контейнеры читать с этой поправкой.

Принципы (унаследованы из практики lesson-05-lld):

  1. Contract First. Сначала фиксируется OpenAPI на внешнюю границу — Verdict API. Контракт страхует интеграционные риски с платёжным шлюзом, где SLA жёстче всего (p95 ≤ 200 мс).
  2. C4-уровни, по одному на диаграмму. Одна абстракция на диаграмму — без «слоёного пирога».
  3. Стрелки = глагол + технология. Каждая связь подписана действием и транспортом (HTTPS/JSON, gRPC, S3, AMQP и т.п.).
  4. High Cohesion + Low Coupling. Crawler / Scanner / AI Verifier — отдельные сервисы; AI-слой не знает деталей краула, общается через очередь и кандидатов.
  5. Резилентность — в LLD. Circuit Breaker / Retries / Timeouts на внешних клиентах (LLM, vision, headless). Деградация краула не должна влиять на доступность Verdict API (NFR-Availability).
  6. Single source of truth. Один владелец на одно правило (тариф проверяет Backend на инициации, не Scanner на каждой странице).

flowchart LR
  officer([Комплаенс-офицер])
  admin([Администратор сервиса])
  gw[[Платёжный шлюз / PSP]]
  merchant[[Сайт мерчанта]]
  sso[[Keycloak / Корпоративный SSO]]
  llm[[LLM Provider · Qwen self-hosted]]
  vision[[Vision / OCR Provider]]
  smtp[[SMTP / Webhook receiver]]

  subgraph SH[Система интеллектуального мониторинга мерчантов]
    ui[Admin Web UI<br/>React / TypeScript]
    api[Backend / Control Plane<br/>Python · FastAPI]
    verdict[Verdict API · фаза 3<br/>Python · FastAPI · stateless]
    sched[Crawl Scheduler<br/>Celery beat · окно рекраула по тарифу]
    crawler[Crawler Worker<br/>httpx · respect robots.txt]
    headless[Headless Renderer<br/>Playwright · JS fallback FR-06]
    scanner[Scanner Engine<br/>Aho-Corasick + pymorphy3 ru/be]
    ai[AI Verifier · Premium<br/>LLM / embeddings / vision]
    reporter[Reporter<br/>PDF · история · экспорт]
    bus[(Message Bus<br/>RabbitMQ / Redis Streams)]
    pg[(SQL DB · PostgreSQL<br/>tenants · sites · hits · verdicts · audit)]
    qdr[(Vector DB · Qdrant<br/>embeddings стоп-фраз · Premium)]
    s3[(Object Storage · S3<br/>HTML-снимки · скриншоты · immutable)]
    cache[(Verdict Cache · Redis · фаза 3<br/>verdict by domain/URL · TTL=recrawl_interval)]
  end

  officer --> ui
  admin --> ui
  ui -->|REST · HTTPS/JSON| api
  ui -->|OIDC| sso
  api --> sso

  gw -. "фаза 3: GET /v1/verdict · HTTPS/JSON · JWT" .-> verdict
  verdict -. фаза 3 .-> cache
  verdict -. фаза 3 · miss .-> pg

  api --> pg
  api --> sched
  sched -->|enqueue crawl job| bus
  bus --> crawler
  crawler -->|HTTPS GET| merchant
  crawler -.->|JS-heavy → fallback| headless
  headless -->|HTTPS GET| merchant
  crawler -->|publish page| bus
  bus --> scanner
  scanner --> pg
  scanner --> s3
  scanner -. Premium candidates .-> bus
  bus --> ai
  ai -->|HTTPS/JSON| llm
  ai -->|HTTPS/JSON| vision
  ai --> qdr
  ai --> pg
  scanner -. "фаза 3: invalidate" .-> cache
  ai -. "фаза 3: invalidate" .-> cache

  api -->|alerts FR-17| smtp
  reporter --> pg
  reporter --> s3
  reporter -->|PDF FR-19| officer
КонтейнерЗачемПочему отдельный (deploy-критерий)
Admin Web UIУправление сайтами, стоп-листами, разметка срабатываний (FR-03, FR-04, FR-18)React SPA, отдельный CDN/static, релизится чаще API
Backend / Control PlaneMulti-tenant CRUD, тарифы (FR-02), управление расписанием, алертинг (FR-17)Бизнес-правила и SSO; не должен страдать от нагрузки краула
Verdict API (фаза 3)Низколатентный (p95 ≤ 200 мс) вердикт по URL/домену для платёжного шлюза (FR-15, FR-16)Отдельный сервис из-за SLA: деградация краула не имеет права валить hot path оплаты (NFR-Availability ≥ 99.5%). В MVP не реализуется — контракт зафиксирован заранее, см. раздел 5
Crawl SchedulerОчередь рекраула, окна и лимиты по тарифу (FR-13, FR-02)Workload-pattern «фоновые задачи + cron», изолируется от API
Crawler WorkerHTTP-обход страниц, robots.txt, троттлинг по домену (FR-05)I/O-bound, скейлится по входной очереди, отдельный namespace
Headless RendererJS-рендеринг страниц, где статический обход вернул мало текста (FR-06)Тяжёлые ресурсы (Chromium); только Premium / по необходимости
Scanner EngineИзвлечение текста, морфологический матчинг ru/be, анти-обфускация (FR-07, FR-08, FR-09)Ядро MVP; CPU-bound, скейлится отдельно. Раскрыт в разделе 3
AI Verifier (Premium)LLM-классификация (FR-10), семантический поиск (FR-21), OCR/vision (FR-11)GPU-нода / дорогой downstream; получает только кандидатов, не сырые страницы (инвариант экономики Премиума)
ReporterDaily/ondemand PDF-отчёты, история (FR-14, FR-19, FR-20)CronJob, batch-workload
Message BusРазвязка Crawler → Scanner → AI Verifier; backpressure, retriesЛюбой временный лаг AI не блокирует краул
SQL DB (PostgreSQL)Тенанты, сайты, стоп-листы, hits, verdicts, audit logManaged-сервис, HA, бэкапы
Vector DB (Qdrant)Эмбеддинги стоп-фраз и страниц для семантики (FR-21, Premium)Специализированное хранилище, только Premium
Object Storage (S3)HTML-снимки и скриншоты — immutable, append-only с TTL по политике (FR-12)Lifecycle-rules, дешёвое хранение
Verdict Cache (Redis) (фаза 3)Кэш вердиктов по домену/URL для p95 ≤ 200 мсБез кэша Verdict API не уложится в SLA. В MVP не нужен — потребитель один (UI), читает напрямую из PG

Архитектурные инварианты, отражённые на C2

Заголовок раздела «Архитектурные инварианты, отражённые на C2»
  • Verdict API не запускает краул. Только чтение кэша, при miss — fallback в PG. Никаких синхронных вызовов в Scanner/AI.
  • Двухступенчатый пайплайн. Crawler → Scanner (дешёвый детерминированный проход) → опционально AI Verifier (для Premium, по кандидатам). Не инвертировать.
  • Один владелец на тариф. Лимиты (частота, глубина, число сайтов) применяет Crawl Scheduler на инициации. Scanner и AI Verifier не валидируют тариф на каждой странице.
  • Immutable evidence. S3-снимки пишутся append-only и привязаны к scan_id — это юридическая защита перед регулятором / платёжной системой.

flowchart LR
  bus[[Message Bus<br/>page.ready]]
  pg[(SQL DB)]
  s3[(Object Storage)]
  cache[(Verdict Cache)]
  busOut[[Message Bus<br/>candidate.ready · Premium]]

  subgraph SE[Scanner Engine]
    ctrl[Consumer<br/>aiopika · ack only after persist]
    extr[Text Extractor<br/>trafilatura · отсечь nav/menu/footer · FR-07]
    norm[Normalizer<br/>casefold · NFC · diacritics · FR-08]
    deob[Anti-obfuscation Layer<br/>гомоглифы · разрядка · транслит · FR-09]
    morph[Morphology Engine<br/>pymorphy3 ru/be · lemma index]
    ac[Aho-Corasick Matcher<br/>per-tenant automaton · FR-08]
    agg[Candidate Aggregator<br/>группировка hits по странице + категория]
    evi[Evidence Writer<br/>HTML+screenshot ref · immutable · FR-12]
    persist[Verdict Updater<br/>upsert hits · пересчёт verdict · BR]
    invalidator[Cache Invalidator<br/>домен/URL keys]
    premium{{Premium?<br/>tenant.tier == premium}}
  end

  bus --> ctrl
  ctrl --> extr --> norm --> deob --> morph --> ac --> agg
  agg --> evi --> s3
  agg --> persist --> pg
  persist --> invalidator --> cache
  agg --> premium
  premium -->|yes · только кандидаты| busOut
  premium -->|no · стоп| persist
КомпонентНазначениеГраница ответственности
ConsumerЧтение page.ready из шины, ack только после записи в PG/S3Не знает деталей матчинга; гарантирует at-least-once
Text ExtractorИзвлекает значимый текст (статья, описание товара), отсекает nav/menu/footer (FR-07)Не делает нормализацию и не классифицирует
Normalizercasefold, NFC, удаление диакритики, нормализация пробеловЧистый функционал, без I/O
Anti-obfuscation LayerРазрядка букв («з а п р е щ»), гомоглифы (кириллица/латиница), транслит (FR-09). На Basic — базовая, на Premium — продвинутаяИзолирует «магию» — единственное место в Scanner, чувствительное к подписке
Morphology Enginepymorphy3 для ru/be: лемматизация текста и стоп-фраз (FR-08)Не лезет в матчинг; отдаёт лемматизированную форму
Aho-Corasick MatcherОдин автомат на тенанта, перестраивается при изменении стоп-листаТолько точные/морфо-совпадения; семантика — не сюда
Candidate AggregatorГруппирует hits по странице, добавляет категорию нарушенияРешает, что считается «кандидатом» для Premium-пайплайна
Evidence WriterСохраняет HTML-снимок и ссылку на скриншот в S3 (FR-12); пишет immutable recordОдин раз, без перезаписи
Verdict UpdaterUpsert hits в PG, пересчёт site.verdict по бизнес-правилу: «1+ подтверждённый prohibited → non-compliant»Учитывает FR-18 (ложные не считаются)
Cache InvalidatorСбрасывает ключи Verdict Cache по домену и URLЕдинственная запись в Redis из Scanner — иначе race с AI Verifier
Premium-gateРешает, отправлять ли кандидатов в AI Verifier по tenant.tierТариф проверяется здесь, не на каждой странице до этого
  • Анти-обфускация выделена отдельным слоем — её сложность (юникод-классы, гомоглифы) изолирована от матчера; матчер видит «чистый» нормализованный поток.
  • Aho-Corasick + морфология — это и есть тот «дешёвый первый проход», который выполняется для обоих тарифов. Все дорогие модели работают только по выходу Candidate Aggregator.
  • Evidence Writer перед Verdict Updater — порядок специально такой: сначала фиксируем доказательство (S3 immutable), потом отмечаем hit в PG. Это даёт юридическую гарантию: каждый зафиксированный hit имеет привязанное доказательство.
  • Cache Invalidator только в Scanner — даже если AI Verifier потом понизит уверенность hit’а, инвалидация уже сделана; Verdict API увидит обновлённое состояние при следующем чтении.

4. Sequence — «Платёжный шлюз спрашивает вердикт» (FR-15, FR-16) (фаза 3)

Заголовок раздела «4. Sequence — «Платёжный шлюз спрашивает вердикт» (FR-15, FR-16) (фаза 3)»

Hot path при оплате: SLA p95 ≤ 200 мс, не вызывает live-краул, читает только закэшированный вердикт. Не входит в MVP — сценарий зафиксирован для фиксации интеграционного контракта с PSP-шлюзом.

sequenceDiagram
  autonumber
  actor B as Покупатель
  participant GW as Платёжный шлюз
  participant VA as Verdict API
  participant RC as Verdict Cache (Redis)
  participant PG as SQL DB
  participant L as Audit log

  B->>GW: Pay (merchant_id, URL)
  GW->>VA: GET /v1/verdict?domain=…&url=… (JWT)
  VA->>VA: validate JWT, tenant scope
  VA->>RC: GET verdict:{tenant}:{url_hash}

  alt cache hit (горячий путь)
    RC-->>VA: {status, confidence, scanned_at}
    VA-->>GW: 200 OK · verdict
  else cache miss (cold)
    RC-->>VA: nil
    VA->>PG: SELECT verdict FROM site WHERE … (read replica)
    PG-->>VA: row | not_found
    alt found
      VA->>RC: SETEX verdict:… TTL=recrawl_interval
      VA-->>GW: 200 OK · verdict
    else not_found
      VA-->>GW: 200 OK · {status: "not_monitored"}
    end
  end

  Note over VA,L: Async — никогда в hot path
  VA-)L: append audit (tenant, url, decision, latency_ms)

  GW->>GW: if status=="warning" → show banner / require 2FA
  GW->>GW: if status=="block"   → reject transaction
  GW-->>B: UI: continue / warning / blocked

Связность C2/C3 ↔ Sequence (критерий приёмки):

ШагКонтейнер / компонент
1–2Платёжный шлюз → Verdict API (внешняя граница)
3Verdict API: JWT + tenant scope
4–5Verdict Cache (Redis) — горячий путь
6–9SQL DB read replica + populate cache (cold path)
10Audit log — async, не в hot path
11–13Решение шлюза по status (clean / warning / block)

После того как Scanner Engine нашёл кандидата, дальнейшая судьба зависит от тарифа:

  • Basic. Аggregator сразу зовёт Verdict Updater → hit фиксируется как prohibited.unverified. Вердикт сайта — бинарный.
  • Premium. Aggregator публикует candidate.ready в шину; AI Verifier (LLM + embeddings + vision) ставит confidence. Только после ответа Verifier’а пишется итоговый hit с confidence и категорией; Cache Invalidator срабатывает в обоих случаях.

Это прямое следствие инварианта «двухступенчатый пайплайн» из BRD: дорогие модели не гоняются по всем страницам.


Контракт фиксируется заранее (Contract First), чтобы при интеграции с банком-эквайером не пришлось перекраивать модель данных Scanner Engine. В фазе 1 (MVP) этот сервис не разворачивается. Полная спецификация — artifacts/verdict-openapi.yaml (TODO, при выходе на фазу 3). Ключевые решения:

openapi: 3.1.0
info:
title: Shield Verdict API
version: 1.0.0
description: |
Низколатентный вердикт комплаенса по домену/URL мерчанта для
встраивания в платёжный поток (FR-15, FR-16). SLA: p95 ≤ 200 мс.
servers:
- url: https://verdict.shield.example/v1
paths:
/verdict:
get:
operationId: getVerdict
summary: Получить вердикт по домену или конкретному URL
security: [{ bearerAuth: [] }]
parameters:
- { in: query, name: domain, schema: { type: string }, required: true,
description: "ASCII/IDN-домен мерчанта; нижний регистр" }
- { in: query, name: url, schema: { type: string, format: uri }, required: false,
description: "Конкретный URL страницы; если опущен — вердикт по домену" }
responses:
'200':
description: Вердикт получен (включая not_monitored — это валидный исход, не ошибка)
content:
application/json:
schema: { $ref: '#/components/schemas/Verdict' }
examples:
clean:
value:
status: clean
scanned_at: "2026-05-27T18:42:11Z"
tier: basic
warning_premium:
value:
status: warning
confidence: 0.83
categories: ["prohibited.tobacco"]
scanned_at: "2026-05-28T03:11:00Z"
tier: premium
evidence_ref: "s3://shield-evidence/2026/05/28/abc123/"
not_monitored:
value: { status: not_monitored }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/RateLimited' }
'503': { $ref: '#/components/responses/UpstreamUnavailable' }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
schemas:
Verdict:
type: object
required: [status]
properties:
status:
type: string
enum: [clean, warning, block, not_monitored, needs_attention]
confidence:
type: number
minimum: 0
maximum: 1
description: "Только для Premium-тенанта; для Basic вердикт бинарный (BR из раздела 5 BRD)."
categories:
type: array
items: { type: string }
scanned_at: { type: string, format: date-time }
tier: { type: string, enum: [basic, premium] }
evidence_ref:
type: string
description: "Ссылка на immutable доказательства (FR-12), только при status=warning|block."
responses:
Unauthorized: { description: JWT отсутствует/невалиден,
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } }
RateLimited:
description: Лимит запросов по тенанту/тарифу превышен
headers:
Retry-After: { schema: { type: integer }, description: "Секунды до следующей попытки" }
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
UpstreamUnavailable:
description: Кэш и БД одновременно недоступны (Circuit Breaker open)
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
  • not_monitored — это 200 OK, не 404. Платёжный шлюз спрашивает «есть ли у тебя данные по этому мерчанту»; отсутствие — валидный бизнес-исход, обрабатывается тем же путём, что и clean. (Параллель с lesson-05: unrecognized интент тоже остался в 200, а не уходил в 422.) HTTP-статус описывает судьбу HTTP-обмена, а не бизнес-исход.
  • confidence только для Premium. Бизнес-правило BR из BRD: Basic — бинарный вердикт; Premium — с уверенностью. В контракте это выражено nullable-полем + комментарием.
  • evidence_ref указывается только при warning/block. Это прямая привязка к FR-12 (доказательства) и юридическая прослеживаемость.
  • 503 явный. Когда Redis и реплика PG разом недоступны — Circuit Breaker open, шлюз должен переключиться на свой fallback (например, всегда warning). Лучше явный 503, чем медленный таймаут на 200 мс SLA.
  • Retry-After в 429. Лимиты по тарифу: жадный клиент должен знать, когда повторять.
  • Версионирование URI (/v1/...). Verdict API — внешний контракт, ломающие изменения требуют новой версии в URL.
  • Security — Bearer JWT с tenant scope. Изоляция тенантов на уровне токена; Verdict API проверяет scope == domain.owner_tenant.

6. Соответствие критериям многоуровневого проектирования

Заголовок раздела «6. Соответствие критериям многоуровневого проектирования»
  • Соответствие нотации C4. C2 — контейнеры, C3 — компоненты внутри Scanner Engine; обе диаграммы Mermaid, связи направлены, на каждой стрелке указан транспорт.
  • Связность C3 ↔ Sequence. Hot path (Verdict API) использует Verdict Cache → SQL DB → audit. Scanner-пайплайн привязан к шагам Premium handoff. Каждый шаг sequence ссылается на свой компонент или контейнер.
  • Качество API. Спецификация содержит схему Verdict, три примера ответа (clean, warning_premium, not_monitored), коды ошибок 401/429/503 с RFC 7807 (application/problem+json), заголовок Retry-After, security-схему JWT.

  • TLS-mTLS со шлюзом vs JWT-only. При входе банка-эквайера может потребоваться mTLS поверх JWT — решается на этапе интеграции.
  • Webhook от Backend в шлюз для push-инвалидации кэша при срочном изменении вердикта — фаза 2.
  • OCR-конвейер. Картинки идут через Crawler → Scanner.ImageRefExtractor → AI Verifier.Vision. Описывается отдельно при выходе фазы 2.
  • Стратегия миграции стоп-листов при обновлении (rebuild автомата без даунтайма). Решается в HLD реализации Scanner.

8. Расширение продукта: домен Андеррайтинг + Onboarding

Заголовок раздела «8. Расширение продукта: домен Андеррайтинг + Onboarding»

Модули Onboarding и Андеррайтинг реализованы в admin (контроль-плейн), без отдельных сервисов и без изменения контракта со сканером (admin↔scanner — прежний Scanner API). Подробный план и состав — admin/ROADMAP-modules.md. Доменная модель (схемой владеет admin, миграции):

classDiagram
  Merchant "1" --> "*" Site : websites
  Merchant "1" --> "*" MerchantPerson : директора/UBO
  Merchant "1" --> "*" MerchantAddress
  Merchant "1" --> "*" MerchantDocument
  Merchant "1" --> "*" InvestigateCase : кейсы
  InvestigateCase "1" --> "*" CaseIndicator : 49 индикаторов
  InvestigateCase "1" --> "1" CremAssessment : CREM
  InvestigateCase "1" --> "*" CaseRecommendation : checklist
  Application ..> Merchant : convert()
  Application ..> InvestigateCase : convert()
  IndicatorDefinition ..> CaseIndicator : каталог (snapshot)
  MccReference ..> InvestigateCase : регуляторика по MCC

Поток: Application (заявка) →convert()Merchant (+структура) + InvestigateCase (на created: наполнение индикаторами каталога, авто-деривация контент-индикаторов из сканера/MCC, расчёт risk score, генерация recommendation checklist) → разметка офицером → решение Accept/Decline (проекция на Merchant.lifecycle_status) → PDF-отчёт.

Risk score — вычитательная модель (RiskScoreCalculator): старт 100%, вычеты по сработавшим индикаторам (red/amber) и за непроведённое ревью обязательных, пол 1%; уровень LOW/MEDIUM/HIGH. CREM (CremCalculator) — кредитная экспозиция (FEaD/EtPR/сценарии/залоги), чистые формулы. Разделение модулей в UI — nav-группами одной панели (не отдельные панели — см. ROADMAP «Упаковка»).

Каналы intake Application (FR-30/31): ручной ввод и CSV (UI), API ingest (POST /api/onboarding/applications, per-tenant Bearer tenants.ingest_token) и публичная форма (/onboarding/{form_token}, без создания юзера). Все → Application(source=…)convert().

Связка кейс ↔ мониторинг (FR-46): завершение скана сайта (Scanner API sites/{id}/scanned) у мерчанта с открытым кейсом → InvestigateCase::refreshFromScanner() (пересбор scanner-индикаторов

  • пересчёт score), не затирая ручную разметку; ежегодный rerun high-risk (investigate:rerun-due по расписанию); частота мониторинга сайта auto наследуется от риск-категории мерчанта.

Transaction Laundering (FR-50, частично): portfolio cross-check / corporate structure (PHP), whois + reverse-IP, SiteReveal, outgoing/backlinks и related-domains/редиректы (сканер → Scanner API), тест-карты (ручной ввод). Остаток — входные слова на внешних ресурсах, гео-клоакинг, load balancing.

RBAC (FR-01): роли super_admin/admin/officer/auditor (read-only) — spatie-пермишены (Filament Shield), BelongsToTenant-scope; админ может отключать пользователей (disabled_atcanAccessPanel=false).


Модуль защиты бренда от внешних цифровых угроз. Реализуется в admin (control-plane + Filament UI) и в scanner (новые команды мониторинга). Существующие контракт Scanner API, БД и инфраструктура (S3, Qdrant) переиспользуются без изменений.

classDiagram
  class ProtectedAsset {
    +id
    +tenant_id
    +name: string
    +type: enum web|mobile|social
    +url_or_handle: string
    +status: enum active|paused
    +last_fingerprinted_at: timestamp
  }

  class WebFingerprint {
    +id
    +protected_asset_id
    +tenant_id
    +version: int
    +favicon_hash: string
    +dom_signature: json
    +color_palette: json
    +brand_keywords: json
    +form_patterns: json
    +visual_embedding_ref: string
    +computed_at: timestamp
  }

  class MobileFingerprint {
    +id
    +protected_asset_id
    +tenant_id
    +version: int
    +package_name: string
    +bundle_id: string
    +cert_sha256: string
    +icon_hash: string
    +store_urls: json
    +brand_keywords: json
    +computed_at: timestamp
  }

  class PhishingTicket {
    +id
    +tenant_id
    +protected_asset_id
    +type: enum domain|webpage|app|social
    +threat_url: string
    +source: enum auto_dns|auto_web|auto_app|auto_social|manual
    +status: enum monitoring|in_progress|blocked
    +detected_at: timestamp
    +status_changed_at: timestamp
    +assigned_to: FK users
    +takedown_sent_at: timestamp
  }

  class PhishingEvidence {
    +id
    +tenant_id
    +ticket_id
    +screenshot_path: string
    +html_snapshot_path: string
    +geo_cloaking_detected: bool
    +detection_meta: json
    +captured_at: timestamp
  }

  class TakedownRequest {
    +id
    +tenant_id
    +ticket_id
    +target_type: enum registrar|hosting|platform
    +target_name: string
    +sent_at: timestamp
    +response_status: string
    +notes: text
  }

  ProtectedAsset "1" --> "*" WebFingerprint : версии web-отпечатков (FR-61)
  ProtectedAsset "1" --> "*" MobileFingerprint : версии mobile-отпечатков (FR-62)
  ProtectedAsset "1" --> "*" PhishingTicket : угрозы по активу
  PhishingTicket "1" --> "*" PhishingEvidence : immutable доказательства
  PhishingTicket "1" --> "*" TakedownRequest : жалобы на блокировку

Инварианты схемы:

  • WebFingerprint и MobileFingerprint — две отдельные таблицы (web_fingerprints, mobile_fingerprints). Атрибуты веб-отпечатка никогда не смешиваются с атрибутами мобильного — нет NULL-колонок «не своего» типа.
  • brand_keywords присутствует в обеих таблицах намеренно: у веб-актива извлекаются из DOM, у мобильного — из названия и описания в сторе. Источники разные, значения могут различаться.
  • Обе таблицы хранят историю версий (version автоинкремент); актуальная — последняя по computed_at. Старые не удаляются.
  • PhishingEvidence — immutable: $timestamps = false, только captured_at, без UPDATE (тот же паттерн, что Evidence, FR-12).
  • PhishingTicket имеет BelongsToTenant-scope и soft-deletes.
  • visual_embedding_ref в WebFingerprint — путь к вектору в Qdrant-коллекции phishing_fingerprints.

flowchart LR
  adminApi[[Admin Scanner API\n/api/scanner/*]]
  pg[(PostgreSQL)]
  s3[(S3 / MinIO)]
  qdr[(Qdrant\nphishing_fingerprints)]
  ct[[CT Logs\ncrt.sh API]]
  dns[[Passive DNS\nAPI]]
  store[[Store APIs\nGoogle Play · App Store]]
  apk[[APK агрегаторы\nAPKPure · APKMirror]]
  social[[Platform APIs\nTelegram · X · Instagram]]

  subgraph BPE[Brand Protection Engine — scanner]
    sched[BrandScheduler\nrun каждые N часов по тарифу]

    subgraph FP[Fingerprint Pipeline — FR-61 / FR-62]
      fpcrawl[WebCrawler\nкраул официального сайта]
      fpext[FingerprintExtractor\nfavicon pHash · DOM sig · palette · keywords]
      fpemb[EmbeddingBuilder\nвизуальный embedding → Qdrant]
      fpmob[MobileFingerprinter\nStore API → icon hash · cert · metadata]
    end

    subgraph MON[Monitoring Pipeline — FR-63–FR-67]
      dns_mon[DomainMonitor\nCT logs · passive DNS · typosquatting FR-63]
      web_mon[WebSimilarityScanner\nFR-64: DOM · favicon сравнение]
      emb_mon[EmbeddingMatcher\nFR-65: Qdrant kNN по visual embedding]
      app_mon[AppMonitor\nFR-66: Store + APK агрегаторы]
      soc_mon[SocialMonitor\nFR-67: Platform API поиск]
    end

    subgraph EVI[Evidence Pipeline — FR-70]
      screener[ThreatScreener\nприоритизация кандидатов]
      evicap[EvidenceCapture\nскриншот · HTML-снимок · geo-cloaking check]
    end

    ticketw[TicketWriter\nPOST /api/scanner/phishing-tickets идемпотентно]
  end

  sched --> FP
  sched --> MON
  fpcrawl -->|страницы| fpext --> fpemb --> qdr
  fpext --> adminApi
  fpmob --> adminApi
  dns_mon --> ct & dns
  web_mon & emb_mon --> qdr
  emb_mon --> qdr
  app_mon --> store & apk
  soc_mon --> social
  MON --> screener --> evicap
  evicap --> s3
  evicap --> ticketw --> adminApi

Ключевые решения компонентов:

КомпонентРешениеОбоснование
FingerprintExtractorПереиспользует crawler.py + extractor.pyОфициальный сайт — тот же HTTP-краул, только цель — отпечаток, не стоп-слова
EmbeddingBuilderКоллекция phishing_fingerprints в существующем QdrantНе добавляет инфраструктуры; коллекция изолирована от stop_phrases
DomainMonitorcrt.sh API (Certificate Transparency) + пассивный DNSCT-логи покрывают 99%+ новых доменов с TLS за минуты после выдачи сертификата; дёшево и без белых списков
EmbeddingMatcherkNN-поиск по Qdrant с порогом similarityЛовит визуально перекрашенный/переставленный фишинг, который favicon и DOM-хэш не поймают
ThreatScreenerДедупликация по (tenant_id, protected_asset_id, threat_url)Одна угроза — один тикет; повторное обнаружение обновляет detected_count, не плодит дубли
EvidenceCaptureHeadless Chrome (уже есть в admin) через Scanner APIScreencap переиспользует существующий Chromium-стек фазы 2; новый код не нужен
TicketWriterИдемпотентный POST /api/scanner/phishing-ticketsUniq-индекс (tenant_id, protected_asset_id, threat_url) на стороне admin — тот же паттерн, что у hits

Расширение существующего ScannerController (routes/api.php, middleware scanner):

GET /api/scanner/protected-assets → список активов тенанта для сканирования
GET /api/scanner/protected-assets/{id}/web-fingerprint → актуальный web-отпечаток (FR-61)
POST /api/scanner/protected-assets/{id}/web-fingerprint → сохранить web-отпечаток (FR-61)
GET /api/scanner/protected-assets/{id}/mobile-fingerprint → актуальный mobile-отпечаток (FR-62)
POST /api/scanner/protected-assets/{id}/mobile-fingerprint → сохранить mobile-отпечаток (FR-62)
POST /api/scanner/phishing-tickets → создать/обновить тикет (идемпотентно, FR-63–68)
POST /api/scanner/phishing-tickets/{id}/evidence → прикрепить evidence к тикету (FR-70)

Аутентификация — Bearer SCANNER_API_TOKEN, тот же middleware scanner. Тенант определяется токеном (как во всём Scanner API). Новых токенов не вводится.


9.4 Sequence — «Обнаружение домена-тайпосквоттера» (FR-63)

Заголовок раздела «9.4 Sequence — «Обнаружение домена-тайпосквоттера» (FR-63)»
sequenceDiagram
  autonumber
  participant S as BrandScheduler
  participant DM as DomainMonitor
  participant CT as crt.sh API
  participant FP as WebFingerprint (admin DB)
  participant EC as EvidenceCapture
  participant TW as TicketWriter
  participant A as Admin (ScannerController)
  participant O as Офицер (Filament UI)

  S->>A: GET /api/scanner/protected-assets
  A-->>S: [{id, url, brand_keywords, fingerprint_version}]
  S->>DM: monitor(asset)

  DM->>CT: GET /crt.sh?q=%brand%&output=json
  CT-->>DM: [{domain, issued_at}]
  DM->>DM: typosquatting_variants(brand_keywords)
  DM->>DM: filter: уже известные тикеты исключить

  loop для каждого кандидата
    DM->>EC: capture(candidate_url)
    EC-->>DM: {screenshot_s3, html_s3, geo_cloaking}
    DM->>TW: POST /api/scanner/phishing-tickets\n{asset_id, type:domain, url, source:auto_dns, evidence}
    A-->>DM: {ticket_id, created|updated}
  end

  Note over O,A: Async — офицер видит тикеты в UI
  O->>A: GET /phishing-tickets (status=monitoring)
  O->>A: PATCH ticket → status: in_progress
  A->>A: запись takedown_sent_at + TakedownRequest
  O->>A: PATCH ticket → status: blocked

9.5 Sequence — «Снятие web-отпечатка защищаемого сайта» (FR-61)

Заголовок раздела «9.5 Sequence — «Снятие web-отпечатка защищаемого сайта» (FR-61)»
sequenceDiagram
  autonumber
  participant O as Офицер
  participant A as Admin UI
  participant S as FingerprintPipeline (scanner)
  participant CR as WebCrawler
  participant FE as FingerprintExtractor
  participant EB as EmbeddingBuilder
  participant QD as Qdrant
  participant DB as PostgreSQL

  O->>A: Добавить защищаемый актив (тип: web, URL)
  A->>DB: INSERT protected_assets
  A->>S: POST /api/scanner/protected-assets/{id}/fingerprint (trigger)

  S->>CR: crawl(url, depth=1, pages=home+key)
  CR-->>S: HTML страниц + ресурсы

  S->>FE: extract_fingerprint(pages)
  FE-->>S: {favicon_hash, dom_signature, color_palette,\nbrand_keywords, form_patterns, screenshot_s3}

  S->>EB: build_embedding(screenshot_s3)
  EB-->>S: vector[1024]

  S->>QD: upsert(collection=phishing_fingerprints,\nid=asset_id, vector, payload={asset_id, version})
  S->>A: POST /api/scanner/protected-assets/{id}/web-fingerprint\n{favicon_hash, dom_sig, keywords, embedding_ref, ...}
  A->>DB: INSERT web_fingerprints (version++)
  A-->>O: Отпечаток снят · версия N

[auto_detect / manual]
┌─monitoring─┐ офицер подтверждает угрозу
│ │──────────────────────────────► [in_progress]
│ │ │
└────────────┘ takedown отправлен
│ │
│ ▼
│ [blocked] ◄── офицер фиксирует факт блокировки
└── офицер: «ложное срабатывание» ──► [false_positive]
(также из in_progress) (терминальный; whitelist)

Бизнес-правила:

  • Тикет создаётся автоматически при обнаружении (source: auto_*) или вручную офицером (source: manual).
  • Дедупликация: повторное обнаружение той же угрозы (threat_url + protected_asset_id) не создаёт новый тикет — обновляет last_seen_at и инкрементирует detected_count.
  • false_positive — терминальный статус, доступен офицеру из открытых статусов (monitoring, in_progress); благодаря дедупликации работает как whitelist: повторные обнаружения не возвращают тикет в очередь. Ошибочно закрытый тикет можно переоткрыть (reopenmonitoring).
  • Переход monitoring → in_progress фиксирует assigned_to и инициирует TakedownRequest.
  • PhishingEvidence пишется append-only при каждом новом обнаружении (immutable, тот же паттерн, что Evidence в FR-12).
  • Мониторинг продолжается даже для тикетов в статусе in_progress — фишинговые страницы восстанавливаются на других доменах.

9.7 Переиспользование существующей инфраструктуры

Заголовок раздела «9.7 Переиспользование существующей инфраструктуры»
ИнфраструктураТекущее использованиеИспользование в ФИШИНГ-ДЕТЕКТОРе
Scanner API (/api/scanner/*)hits, sites, stop-phrases+ protected-assets, phishing-tickets, evidence
BelongsToTenant traitвсе бизнес-моделиProtectedAsset, WebFingerprint, MobileFingerprint, PhishingTicket
S3 / MinIOHTML-снимки и скриншоты hits (FR-12)PhishingEvidence (скриншоты угроз)
Qdrantэмбеддинги стоп-фраз (FR-21)новая коллекция phishing_fingerprints
Headless Chromeскриншоты evidence фазы 2EvidenceCapture для фишинговых страниц
crawler.py + extractor.pyкраул сайтов мерчантовFingerprintExtractor (краул официального сайта)
Filament Auditableaudit log действий офицератикеты + смена статусов