Skip to content

risk-graph: stacked layout, 5-col Sankey + client-side what-if - #5

Merged
velvetway merged 56 commits into
mainfrom
feat/risk-graph-viz
Aug 30, 2026
Merged

velvetway merged 56 commits into
mainfrom
feat/risk-graph-viz

Conversation

@velvetway

Copy link
Copy Markdown
Owner

Summary

  • Переработка RiskGraphPage под stacked layout: hero (W-вердикт) → sortable список угроз актива → 5-колонок Sankey S→ST→VL→C→DA + панель формулы ПТСЗИ.
  • Client-side what-if симуляция: клик по контролю отключает его, recomputeW зеркалит бэкендный QReactionFromVLs, sticky-бар показывает ΔW и позволяет сохранить заметку в localStorage.
  • Новый bulk endpoint GET /api/risk/asset/:id/attack-paths — возвращает все AttackPath актива + агрегаты (w_max, level, counts) одним запросом.
  • 5 новых backend unit-тестов (ComputeAssetAggregate) + 2 handler-теста + 11 frontend Jest-тестов на pure riskFlow.ts (flow conservation, coverage clamping, disabled controls, severity-weighted splits, negative-severity guard).

Spec: docs/superpowers/specs/2026-05-02-risk-graph-visualization-design.md
Plan: docs/superpowers/plans/2026-05-02-risk-graph-visualization.md

Что меняется на UI

  • /risk/graph/:assetId — теперь страница на 1 актив × все угрозы. ?threat=<id> опционален; без него авто-выбирается max-W.
  • 5-я колонка C(СЗИ) видна на графе; толщины VL→C отражают coverage, VL→DA — (1−cov).
  • Удалён неиспользуемый frontend/src/components/RiskGraphSankey.tsx (legacy).

V2 (документировано в spec, не блокирует merge)

  • AssembleAssetAttackPaths сейчас вызывает AssembleAttackPath в цикле — N+1 (~40–120 запросов на актив с 10–30 угрозами). V2: bulk repo-метод LoadAssetAttackPaths + контракт-тест.
  • RiskGraphPage.test.tsx / ThreatList.test.tsx snapshot-тесты.
  • Просмотр сохранённых what-if заметок (V1 пишет только в localStorage).
  • onParams hero-кнопка (заглушка из spec, V1 без неё).

Test plan

  • go test ./internal/service/... ./internal/transport/... ./internal/domain/... — PASS (backend)
  • cd frontend && CI=true npm test -- --testPathPattern=riskFlow — 11 passed
  • cd frontend && CI=true npm run build — Compiled successfully
  • Manual smoke: запустить backend + frontend, открыть /risk/graph/<assetId> для актива с ≥3 угрозами и ≥1 контролем — проверить hero/список/Sankey/popover/what-if/PDF.
  • Deep-link /risk/graph/<id>?threat=<id> корректно подсвечивает выбранную угрозу.
  • Невалидный ?threat=<id> → toast + fallback к max-W.

🤖 Generated with Claude Code

velvetway and others added 30 commits May 2, 2026 12:48
Stacked layout (hero / threat list / graph), 5-column Sankey with C(СЗИ),
client-side what-if simulation mirroring backend QReactionFromVLs, plus
new bulk endpoint /api/risk/asset/:id/attack-paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 tasks across 4 phases (backend types/aggregate/service/handler,
frontend types/riskFlow lib, 6 components, page rewrite + cleanup)
with TDD-style steps and per-task commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Types for the new bulk attack-paths endpoint that returns all threats
of a single asset in one request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure function that summarizes a slice of AttackPath into WMax, Level,
ThreatCount and UncoveredCount for the asset-level hero block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hardens the ComputeAssetAggregate contract before it is exercised by the
new bulk service method on real DB data: a non-empty input where every
path has all VLs covered must return UncoveredCount=0, and a path with
nil VulnerableLinks must not increment counts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns all relevant attack paths plus an aggregate (WMax, Level, counts)
for a single asset in one call. Skips paths with no sources, vulnerable
links and destructive actions to keep the response noise-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bulk endpoint returning all attack paths and aggregate metrics for an
asset in one request, plus handler unit tests using a stubbed Service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend types matching the new backend bulk endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure functions for the new RiskGraphPage:
- buildSankeyGraph: S→ST→VL→{C|DA} graph with normalized flow
- recomputeW: client-side what-if mirroring backend QReactionFromVLs

10 unit tests cover flow conservation, coverage clamping, disabled
controls, severity-weighted splits and edge cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The severity || 1 fallback only caught zero; negative values produced
negative flow values that d3-sankey rejects. Replaced with a Math.max(1, ...)
clamp and added a regression test that verifies all link values are
non-negative for paths containing a negative-severity VL. Also added the
missing delta assertion to the empty-VLs recomputeW test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top hero card showing asset id, name, aggregate W, level badge, threat
count and uncovered count, with PDF / Параметры / Back action buttons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sortable list of all threats for an asset with W bars, uncovered count
and selection highlighting. Sortable by W (default), uncovered count
or name. Keyboard-navigable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracted formula breakdown panel with optional what-if simulation
overrides (effective q_reaction and W from client-side recompute).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d3-sankey layout with custom React-rendered nodes for the 5-column
S→ST→VL→C→DA graph. Hover highlights, control nodes are clickable
(opens ControlPopover via onControlClick), disabled controls are
visually dimmed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modal popover for a single control: shows id, name, coverage value,
and a toggle button to disable/re-enable in client-side simulation.
Click-outside and Escape close the popover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sticky bottom banner that surfaces an active what-if simulation:
chips for each disabled control (click to re-enable), baseline → simulated
W with ΔW, plus Сбросить and Сохранить заметку actions. Hidden when no
controls are disabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New layout: AssetRiskHero / ThreatList / (AttackFlowSankey + PtsziBreakdown).
Auto-selects max-W threat when ?threat= absent, mirrors selection to
the URL via replaceState, and keeps a what-if simulation in client state
(disabledControls Set) — recompute mirrors backend QReactionFromVLs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaced by AttackFlowSankey on the rewritten RiskGraphPage; the legacy
component was no longer referenced by any route or page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
react-scripts build emits TS2802 because the project's tsconfig has
neither downlevelIteration nor an ES2015+ target. Map.forEach is
supported on the existing target without flags and yields the same
behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move invalid-?threat= toast out of useMemo into a dedicated effect
  (memos should not produce side effects).
- Drop `params` from the URL-sync effect deps to avoid redundant runs.
- Wire onPdf in AssetRiskHero to POST /api/risk/report/pdf and trigger
  a blob download; restores the missing "PDF сценария" hero action.
- Guard AttackFlowSankey against zero-VL paths (d3-sankey crashes on
  graphs with no edges from VL); render a friendly empty state instead.
- ControlPopover: auto-focus the toggle on open, restore focus to the
  invoking element on close (keyboard-only users could not escape it).
- Drop unused nodeIndex map in AttackFlowSankey.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflect what was actually shipped in V1 — the service method calls
AssembleAttackPath in a loop, producing N+1 (~40–120 queries for
typical assets). True bulk repo method (LoadAssetAttackPaths) and
the no-N+1 contract test are deferred to V2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
eslint array-callback-return wants every branch of a sort comparator
to return a value. The compile-time exhaustive switch already covers
all SortKey values; the default is dead but silences the linter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This resolves the react-scripts build error where sub-dependency @types/d3-dispatch (v3.0.7) required TypeScript 5.0+ syntax. Upgraded to 5.1.6 to stay within @typescript-eslint supported limits.
…tly in tests

Remove non-existent `type`/`location` columns from assets seed, widen
fstec/fsb_protection_class to VARCHAR(64), and reset id sequences after
seeding. Drop the schema/seed split in testhelper — all migrations now
run in numeric order and fail the test on error instead of being silently
logged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire `useAuth().logout` to a new IconBtn in the TopBar so users can sign
out from any page. Remove the old App.test.tsx scaffold which no longer
matches the current routing and was failing to compile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pin the model to the source spec in /Расчет идеи/: full S/ST/VL/DA
reference tables, methods of противодействия, the W formula with
Z ∈ {0.5, 1.0}, and an explicit list of asset fields actually used by W.
Drops every mention of the legacy 1-25 / impact×likelihood scale —
this is now the single source of truth for the risk engine and an
anchor for the upcoming legacy purge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delete the parallel legacy risk stack:
  - internal/service/risk/calculator.go (Impact × Likelihood × RegulatoryFactor)
  - internal/service/risk/recommendations.go (rule-based regulatory advice)
  - internal/service/asset/cia_calculator.go (auto-derived C/I/A & criticality)
  - internal/report/pdf.go (legacy PDF report)
plus their tests.

risk.Service now exposes only PTSZI methods: AssembleAttackPath,
AssembleAssetAttackPaths, Overview, ListThreatSources, ListDestructiveActions.
OverviewPoint loses its 1-25 back-compat fields (impact/likelihood/score) and
returns only W and its decomposition.

ZFromAsset is collapsed to the strict thesis form: 0.5 if isolated, else 1.0
(no more prod/stage 0.75 hop). Tests updated accordingly.

HTTP layer: drop POST /api/risk/preview, GET /api/risk/asset/:id, and
POST /api/risk/report/pdf. Asset request/response no longer carry
business_criticality, C/I/A, kii_category, data_category, protection_level,
has_personal_data, personal_data_volume, has_internet_access, type, location
— the PTSZI formula does not use them. The DB columns still exist; Stage 2
will migrate them away. Asset.Create writes neutral defaults (3) for the
remaining NOT NULL CHECK columns until the migration lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add migration 020_drop_legacy_risk_fields:
  - drop tables risk_scenarios, recommendation_templates,
    risk_scenario_recommendations (the old impact×likelihood engine)
  - drop columns from assets: type, location, business_criticality,
    confidentiality, integrity, availability, data_category, protection_level,
    kii_category, has_personal_data, personal_data_volume, has_internet_access
  - drop columns from threats: base_likelihood, attack_vector,
    impact_confidentiality, impact_integrity, impact_availability
  - drop columns from controls: reduces_likelihood_by, reduces_impact_by
  - drop column asset_controls.effectiveness
  - drop unused enum types data_category_type, protection_level,
    kii_category, risk_status, risk_level, recommendation_status

Older migrations (002, 005, 010) still reference these columns; they run
strictly before 020 so the chronological chain replays cleanly on a fresh DB.

Mirror the schema in domain/models.go and the asset/threat repositories,
services, and HTTP DTOs. Threat input now takes q_threat / q_severity (∈[0,1])
directly — base_likelihood is gone from the API.

Repo tests updated to the new struct shapes; full repo suite passes against
the migrated container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ormula

Delete the screens that were shaped by the old impact×likelihood engine:
  - RiskMapPage (5×5 heatmap) + .backup
  - RiskPreviewPage (что-если симулятор) + .backup
  - AssetRiskProfilePage
  - RiskHeatmapSingle component

Drop the corresponding routes (/risk/map, /risk/preview, /assets/:id/risks),
nav entries, command-palette actions, and the old Asset/Threat/Risk*
type definitions. types.ts and the API client now mirror the W-only
backend exactly — no impact, likelihood, score, regulatory_factor,
RiskRecommendation, RegulatoryRecommendation, or преview endpoint.

AssetFormPage rebuilt to the four PTSZI-relevant inputs: name,
description, asset_type_id, owner, environment, is_isolated. КИИ /
ПДн / УЗ-1..4 / data_category sections are gone — the form now states
explicitly that only `is_isolated` (Z) and the asset's deployed controls
(Q^reaction) feed the formula.

AssetsPage and DashboardPage rewired to render W max per asset
(0..1) and «Изолированный (Z=0.5)» / «Открытый (Z=1.0)» chips
instead of the old 1..25 score and КИИ/ПДн tags. The dashboard
quick-action now opens the Risk Graph for the first asset rather
than the dead simulator route.

`npm run build` is clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
internal/report/pdf.go is back, this time PTSZI-only:
  - GenerateAttackPathPDF(*AttackPath) renders one (asset, threat) page
    with the W decomposition (Q^threat / q^threat / Q^reaction / Z),
    the S → ST → VL → DA chain, per-VL coverage status and a
    «what raises Q^reaction» recommendation block.
  - GenerateAssetReportPDF(*AssetAttackPathsResponse) prints the
    asset-level aggregate plus one section per applicable threat.

Both APIs use the embedded NotoSans font (full Cyrillic), no disk
fallback needed.

Two new GET endpoints expose the PDFs:
  - GET /api/risk/report/graph/:asset_id/:threat_id
  - GET /api/risk/report/asset/:asset_id

Smoke tests exercise both generators and assert the output starts with
the %PDF- magic and is non-trivially sized.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
velvetway and others added 26 commits May 5, 2026 13:03
P1 of the FSTEC integration plan. TRUNCATE threats, vulnerabilities,
controls with RESTART IDENTITY CASCADE so the upcoming importers
(P2 ФСТЭК threats, P3 БДУ snapshot, P4 Минцифры catalog) can repopulate
from authoritative sources.

Reference catalogues (asset_types, threat_categories, threat_sources,
destructive_actions, …) are kept; user-created assets are kept; their
orphaned VL/control links die with the cascade.

Verified on the running compose stack: post-migration counts are 0 for
all wiped tables, 9/8/4/7 for assets/asset_types/threat_sources/DA
(unchanged). repo test suite still green against the full migration chain.

Plan doc: docs/superpowers/specs/2026-05-05-fstec-bdu-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9-stage plan (P1..P9) that replaces the 26 sample threats / 5 sample
vulnerabilities / 30 sample software with the authoritative datasets
(227 ФСТЭК threats, 86k БДУ vulnerabilities, 26k Минцифры products).
Introduces VL_categories per the diploma (6 entries), redefines
asset_vulnerabilities as an inventory of CVE evidence linked to a
VL_category, and adds applicability filtering driven by the ФСТЭК
"Объект воздействия" field.

Each stage = one commit; pauses between P5/P6 and after P7 for review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmd/import-fstec-threats — populates `threats` from the official
ФСТЭК thrlist.xlsx (227 entries) hosted in bdu-fstec-mirror.

For each row the importer derives:
  • bdu_id = "УБИ.NNN"
  • source_type + source_threats links from "Источник угрозы" text
    (Внешний → S1+S4, Внутренний → S2+S3, mixed → all four)
  • applies_to_asset_types[] from "Объект воздействия" via a regex
    dictionary (грид/cloud → Cloud+Server, BIOS → Server+Workstation,
    СУБД → Database, сетевой трафик → Network, …)
  • impact_c/i/a + status verbatim from the catalogue
  • q_threat from intruder potential (low/medium/high → 0.3/0.6/0.9)
  • q_severity from CIA flag count (1/2/3 axes → 0.5/0.7/0.9)
  • threat_category_id matched against existing categories by keyword

Migration 031 brings back impact_c/i/a (we dropped them in 020 — they
now serve a different purpose: deriving q_severity and the future
threat ↔ DA mapping), plus applies_to_targets/applies_to_asset_types
and a status column.

Verified end-to-end on the running stack:
  - 227 threats inserted, 660 source_threats edges
  - 156/227 (69%) got asset-type matches automatically
  - 122/227 (54%) got a category
  - /api/risk/overview returns 450 valid pairs (was 0 after P1 wipe)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmd/import-bdu-snapshot — streams the gzipped SQLite snapshot from
bdu-fstec-mirror (~50 MB compressed → 450 MB decompressed) and writes
it atomically to ./data/bdu.sqlite.

internal/bdu — pure-Go (modernc.org/sqlite) wrapper that opens the
snapshot in read-only `immutable=1` mode for fastest reads on a static
file. Exposes:
  • Open / Close / Stats
  • Get(bduID)             — single vulnerability + its CWE codes
  • SoftwareLookup(v, n)   — every vulnerability matching vendor + name
                             (basis for the asset-vulnerability auto-detection
                             coming in P6)

The snapshot lives outside Postgres because it is large and entirely
static between syncs; the server will mount it as a read-only catalogue.
Added /data/ to .gitignore so the 450 MB file never enters version control.

Smoke test exercises the real snapshot when BDU_SNAPSHOT_PATH is set
(skipped in CI, runs locally after an importer run):
  - 86 664 vulnerabilities, 715 615 software rows, 92 457 CWE links
  - Get("BDU:2014-00001") → Schneider Electric Modicon Quantum, CWE-259
  - SoftwareLookup("D-Link", "DSR-500", 5) → 5 hits, CVSS 10.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the hand-crafted ~30-row software_catalog seed with a fresh
import from the unofficial каталогпо.рф API, which mirrors the official
Минцифры реестр российского ПО (~26 094 products).

Schema changes:
- 032 truncates the legacy seed (asset_software cascade detaches user
  rows; user re-attaches via the new asset detail UI in P8).
- 033 widens software_catalog.name and .vendor from VARCHAR(256) to
  TEXT — some каталогпо.рф entries exceed 256 chars (e.g. id=23196).

Importer (cmd/import-minreestr):
- Per-page UPSERT on registry_number (idempotent re-runs).
- Maps subcategories[].name → software_categories.id; default "other".
- Sets is_russian = TRUE (every catalogue entry is by definition
  registered Russian software). FSTEC/FSB flags stay FALSE — that data
  isn't exposed by this API and will be enriched separately.
- Uses stdlib default http.Client; custom Transport with disabled
  keep-alives or pinned HTTP/1.1 reproducibly hangs against
  каталогпо.рф's HTTP/2 endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…es (P5)

До этого момента таблица vulnerabilities двойственно играла роль и
семантического «уязвимого звена» (VL в формуле W), и конкретной
CVE-записи. В дипломной модели ПТСЗИ это разные сущности: VL —
ровно 6 категорий (нештатное ПО, устаревшее ПО, недекларируемое ПО,
обход админом, носители, открытые ОС/ЛВС), а CVE — инвентарь
свидетельств присутствия VL на активе (это будет в P6).

Изменения:

* migration 034: новая vl_categories (6 строк), threat_vulnerable_links
  и vulnerability_controls (→ vl_category_controls) переключены с FK на
  vulnerabilities на FK на vl_categories.
* migration 035: засеваем 11 канонических контролей из диплома
  (Антивирус/МСЭ/Honeypot/DZ/IDS/AD/Резерв/L/Шифрование/DS/DD) и матрицу
  vl_category_controls по таблице соответствия из диплома.
* import-fstec-threats: новая эвристика deriveVLCategoryCodes по
  тексту угрозы; пишет threat_vulnerable_links параллельно с
  source_threats. На реальных 227 УБИ даёт 305 связей с разумным
  распределением (VL2 — 198, VL1 — 57, VL6 — 33, остальные малые).
* domain.VLNode: VulnerabilityID/Severity заменены на CategoryID/Code/
  Description; risk_graph_repository переписан под vl_categories +
  vl_category_controls; q_reaction по-прежнему «доля VL, у которых
  есть хотя бы один control с coverage>0 и внедрённый на активе».
* Frontend: VLNode.vulnerability_id → category_id + code; ST→VL split
  стал равномерным (severity у категорий нет); все юнит-тесты
  riskFlow.test.ts перестроены под новый контракт.

Со 100% контролями на активе W = 0.5 для critical-угроз; без контролей
W достигает 0.93 (что соответствует «critical»). Применимость по типу
актива и presence VL (через CVE) подключим в P6/P7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Раньше asset_vulnerabilities ссылалась на пустую legacy-таблицу
vulnerabilities — оператор мог только вручную добавить «уязвимость без
данных». Теперь это инвентарь конкретных БДУ-записей, привязанных к VL-
категории формулы W. Источник записи: 'auto:asset_software' (когда
сервис обнаружил CVE через bdu.SoftwareLookup) или 'manual'.

Изменения:

* migration 036: новая схема asset_vulnerabilities — bdu_id, cve, cwe,
  vl_category_id, cvss_score, severity_level, source, software_id,
  discovered_at. UNIQUE (asset_id, bdu_id, source, COALESCE(software_id,0))
  обеспечивает идемпотентные re-runs без дублей.
* migration 037: cve расширен до TEXT — БДУ-записи перечисляют десятки
  связанных CVE через пробел и не влезают в VARCHAR(64).
* internal/risk/cwe_to_vl.go: словарь CWE → VL_код (VL1..VL6); fallback
  → VL2 («устаревшее ПО / уязвимости»). Покрытие: code-injection
  (VL1), Trojan/embedded malware (VL3), privilege/auth (VL4),
  info-exposure/network (VL6); всё прочее — VL2. Юнит-тесты.
* internal/bdu/snapshot.go: SoftwareLookup теперь использует
  case-sensitive vendor matching, потому что встроенный SQLite LOWER()
  ASCII-only — для кириллицы он silent no-op и патрерн с LOWER никогда
  не совпадал. Имя — латиница, lowercase сохранён.
* asset_vulnerability service переписан: AutoDetectFromSoftware (через
  bdu+CWE→VL), AddManual (с обогащением из БДУ), RemoveByID,
  RemoveBySoftware (cascade при отвязке ПО).
* software service: AttachToAsset/DetachFromAsset, опциональная
  зависимость VulnerabilityDetector — wired в server.go.
* HTTP routes: POST/DELETE /api/assets/:id/software (с возвратом
  detected_vulnerabilities count), GET /api/assets/:id/software,
  POST/GET /api/assets/:id/vulnerabilities переписан под bdu_id.
* risk_graph_repository.LoadVulnerableLinks: добавил presence_count —
  число активных asset_vulnerabilities этой VL-категории на активе
  (для UI-предупреждения в P8).
* docker-compose: ./data:/data:ro + BDU_SNAPSHOT_PATH=/data/bdu.sqlite,
  чтобы сервер видел снимок (470МБ) при старте.

Проверено вживую: Astra Linux Special Edition → 200 БДУ-записей
(capped) → presence_count=182 на VL2, формула W возвращает 0.77 для
critical-угрозы УБИ.003 на активе без контролей.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Из-за того что после P2 в каталоге 227 угроз ФСТЭК, а у каждой угрозы
импортёр выводит applies_to_asset_types из «Объекта воздействия», теперь
имеет смысл не считать W для каждой пары, а только для применимых.

* internal/service/risk/applicability.go: IsApplicable(asset, threat) —
  пустые targets → universal; конкретные targets без типа актива →
  conservatively применима; конкретные targets и asset.type не входит →
  не применима. С тестами.
* Overview и AssembleAssetAttackPaths пропускают неприменимые пары
  до вызова AssembleAttackPath. Лимит ThreatFilter поднят до 500
  (по умолчанию был 50, после импорта обрезал каталог до первых 50 угроз).
* domain.Threat расширен полями AppliesToTargets, AppliesToAssetTypes,
  ImpactC/I/A, Status (миграция 031 их уже создала, но domain их не
  читал). Repository SELECT/UPDATE адаптированы.

Эффект: /api/risk/overview уменьшился с потенциальных 9×227=2043 до
1126 пар — ~45% шума отфильтровано. Угрозы вида «BadUSB» больше не
показываются для Customer Database, угрозы СУБД — для рабочих станций.

Q^reaction-семантика осталась прежней (covered_VLs / total_threat_VLs):
этого требует диплом. Presence через CVE-инвентарь индицируется
отдельным полем VLNode.presence_count (введено в P6, не входит в W).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Новая страница /assets/:id — это рабочее место оператора по конкретному
активу. Три секции, каждая привязана к новому или существующему API:

* «Установленное ПО» — список из asset_software с кнопкой «Добавить ПО».
  Поиск по software_catalog (debounced); при выборе POST /assets/:id/
  software и сразу видно, сколько БДУ-записей вытащил автодетект.
* «Уязвимости и VL-категории» — asset_vulnerabilities, сгруппированные
  по vl_category_id. Для каждой VL — её код/имя + развернутый список
  CVE с CVSS-чипами (color-coded) и пометкой «авто». Можно вручную
  добавить запись по BDU-id (POST /assets/:id/vulnerabilities) или
  удалить отдельно.
* «Внедрённые контроли» — две полосы chip'ов: «Внедрено» (success-tone,
  click чтобы отвязать) и «Каталог» (ghost-tone, click чтобы привязать).
  Catalogue API: GET /api/controls.
* Header c agg-карточкой W_max / threat_count / uncovered_count и
  кнопкой «Пересчитать W» — дёргает /risk/asset/:id/attack-paths и
  обновляет агрегат без перезагрузки страницы.

Backend:
* control_repository, control service, control_handlers — CRUD над
  controls / asset_controls (без модификации catalog'а, только attach/
  detach). Routes wired в server.go.
* domain.Control получил json-теги (snake_case), чтобы фронт не путался
  с CamelCase, который Go emits по умолчанию.
* api/client.ts расширен типами AssetSoftwareLink, AssetVulnerability,
  Control, AssetAttackPathsResponse и набором новых вызовов.

Живая проверка: Astra Linux → 200 CVE → presence_count в графе риска
видит 4 на VL1 и 182 на VL2; внедрение Антивируса (закрывает VL1+VL2)
обнуляет uncovered и роняет W с 0.93 до 0.50. Аггрегат: 183 угрозы
применимы к Web Application Server, 16 непокрытых VL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
В навигации был пункт «Каталог угроз» с бейджем «БДУ», но реальной
страницы не было — клик по нему открывал AssetsPage по маршруту-fallback.
Теперь это полноценный экран со всеми 227 импортированными УБИ.

Frontend:
* /threats — таблица с колонками БДУ-id, название, источник,
  Q^threat, q^severity, CIA-чипы, applies_to_asset_types (имена через
  /api/asset-types). Поиск по тексту + три фильтра (источник, тип
  актива, нарушаемое свойство C/I/A).
* Drawer-редактор по клику на карандаш: чекбокс-чипы по типам активов,
  текстовое поле «Объект воздействия», поля Q. Submit → PUT /threats/:id.

Backend:
* domain/threat: Update/Create+ResponseDTO теперь включают
  applies_to_targets, applies_to_asset_types, impact_c/i/a, status.
  Раньше PUT молча терял эти поля при ручной правке угрозы.
* Новые справочные endpoint'ы для UI: GET /api/asset-types и
  GET /api/vl-categories — read-only inline-handlers напрямую через pool,
  без отдельного service-слоя (одна тривиальная SELECT-выборка каждый).

Living check:
- /api/threats?limit=3 возвращает applies_to_asset_types и CIA-флаги.
- PUT /api/threats/1 с applies_to_asset_types=[6,9] сохраняет массив.
- /api/asset-types: 8 типов из 002_seed_data.

P9 завершает FSTEC/БДУ/Минцифры-интеграцию из спека
2026-05-05-fstec-bdu-integration.md (P1..P9 готовы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 элементов, 6 цветовых слоёв (sources → ETL → storage → backend
services → API → frontend → user) + доменная модель ПТСЗИ
S→ST→VL→DA + Controls + формула W + workflow.

Открыть: https://excalidraw.com → File → Open → docs/architecture.excalidraw

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drawer на /assets вёл только в /risk/graph/:id и /assets/edit/:id
(старая форма). Новая страница AssetDetailPage из P8 (с тремя
секциями ПО / уязвимости-VL / контроли) была недоступна — никакая
ссылка на /assets/:id из UI не вела.

Теперь в drawer 3 кнопки:
- «Открыть карточку» (primary) → /assets/:id
- «Граф атаки» (outline) → /risk/graph/:id
- «Редактировать форму» (ghost) → /assets/edit/:id

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Передавал в Chip несуществующие tone-значения (critical/high/medium/low),
которые не входят в ChipTone (neutral|accent|success|warn|danger|ghost).
Chip обращается к toneStyles[tone].bg напрямую → undefined → TypeError →
React unmounts → белый экран.

Чиню маппинг:
* levelTone(cvss): danger / warn / neutral
* VlChipColor: VL1→warn, VL2/3/6→danger, VL4→warn, VL5→neutral

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
api.attachControl/attachSoftware/addManualVulnerability падали с
"Unexpected token 'C', \"Created\" is not valid JSON" — Fiber
SendStatus(201) пишет в body литерал "Created", а request<T>
безусловно делал JSON.parse(text).

Теперь request<T> проверяет Content-Type: если не application/json —
возвращает undefined без парсинга.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pgx в binary mode не умеет сканить DATE (OID 1082) в *string и валит
весь /api/software?search=… с "can't scan into dest[7]". Поиск ПО
из карточки актива (P8 SoftwarePicker) был полностью сломан.

Простой fix без миграции типов в domain.Software: добавляю ::text
к registry_date / fstec_certificate_date / fstec_valid_until во всех
3 SELECT'ах software_repository (GetByID, List, ListAssetSoftware).
Модель Software.RegistryDate остаётся *string, JSON-выдача формата
'YYYY-MM-DD' не меняется.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
До этого SoftwareLookup матчил БДУ-уязвимости только по vendor+name,
поэтому Astra Linux 1.7 получал на актив CVE и для 1.6 «Смоленск» и
для всего что было в каталоге — 200 capped из 12 331 без отбора.

Backend:
* internal/bdu/version.go — парсер русских форм версий из БДУ:
  - "-" / "" → catch-all
  - exact (case-insensitive)
  - "от X до Y включительно" → closed [X, Y]
  - "от X до Y" → half-open [X, Y)
  - "до Y включительно" / "до Y" → (-∞, Y] или (-∞, Y)
  - "от X включительно" / "от X" → [X, +∞) или (X, +∞)
  Семвер-сравнение по компонентам через '.', с числовым префиксом
  (поддерживает "8.4.16" vs "8.4.10rc1"). Юнит-тесты на все формы.
* SoftwareLookup получил параметр assetVersion. Если задан — SQL
  сужается до version-кандидатов (точное, '-', range-like) ДО loading
  ORDER BY cvss_score, чтобы редкие версии типа "4.2.6" (3 строки в
  БДУ) не вылетали из топ-1000 случайно. После loading — финальный
  filter через VersionMatches с дедупом по bdu_id.

Frontend:
* AssetDetailPage SoftwarePicker теперь двухшаговый: поиск → выбор →
  поле «Версия (опционально)» → submit. Версия передаётся в POST
  /api/assets/:id/software и используется автодетектом.
* Live-проверка для Astra Linux Special Edition:
    (empty)         → 200 (capped из 12 331)
    1.7             → 200 (capped из 6 835)
    1.5 «Смоленск»  → 200 (capped из 639)
    4.2.6           → 3 (всё, что есть в БДУ для этой версии)
    999.999         → 0 (никакого ложного матча)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Шлюз каталогпо.рф режет HTTP/2-стримы при Content-Length > ~115KB:
limit=100 даёт 300KB, ответ обрезается до 81624 байт, JSON-парсер
падает, импортер до сих пор фаталил на первой неудаче (3001/26094).

Фикс:
- defaultPageSize = 30 (даёт ~98KB, проходит стабильно)
- maxPageSize = 35; --page-size > 35 клампится с warn
- fetchPageWithRetry: 5 попыток, 1s/2s/4s/8s/16s
- Сетевая ошибка после ретраев → пропуск страницы, не Fatalf
- exit 2 при наличии пропущенных страниц
- Таймаут поднят до 90 минут (с лимитом 30 идёт ~22 мин)

Live-проверка: 26094/26094, 0 пропущенных страниц, 0 ретраев.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Закрывает блок «Оценка состояния защищённости» из 7.png диплома:
для каждого актива считаем % соответствия требованиям ИБ-стандартов
на основе внедрённых на нём контролей.

Бэкенд:
- migration 038: справочники compliance_standards (ФСТЭК-17, ISO 27001:2022),
  compliance_requirements (25+17 требований), requirement_controls
  (77 рёбер ↔ нашими 11 контролями с весами покрытия 0..1)
- repository.ComplianceRepository (read-only)
- service/compliance: AssetOverview / AssetByStandard
  алгоритм coverage: max(weight) среди control_id, внедрённых
  на активе через asset_controls; overall = avg по требованиям
- 3 эндпоинта:
    GET /api/compliance/standards
    GET /api/compliance/asset/:assetID
    GET /api/compliance/asset/:assetID/standard/:standardCode

Фронт:
- ComplianceSection: плитки стандартов с прогресс-барами + drill-down
  со списком требований (фильтры all/✓/◐/✗, группировка по категории)
- На каждое требование: цветная метка покрытия, для развёрнутого —
  список covering_controls (внедрено) и missing_controls (что ещё закроет)
- Подключено как 4-я секция на странице карточки актива

Live-проверка на Customer Database (4 контроля внедрено):
  FSTEC_17  → 25.6% (✓3/◐6/✗16/25)
  ISO_27001 → 22.4% (✓1/◐6/✗10/17)
АВЗ.1, АВЗ.2 = 100% (антивирус), ЗИС.18 = 80% (DDoS), ОЦЛ.1 = 30%
(антивирус как косвенный контроль целостности).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Минцифры использует hierarchical классификатор XX.YY (~100 подкатегорий),
наших всего 14 общих кодов (os/dbms/erp/crm/office/antivirus/backup/…).
Точное совпадение по name давало 0 / 26 094.

Маппинг в три прохода:
1) точный subcategoryid → категория (02.07=dbms, 06.04=mail, 09.07=erp …)
2) префикс XX.* (03.*=antivirus, 04.*=development, 07.*=development)
3) keyword по name как страховка
4) fallback в "other"

Распределение после re-import 26094/26094:
  other          17093  65.5%
  monitoring      2068   7.9%
  development     1961   7.5%
  antivirus       1101   4.2%
  web             1087   4.2%
  office           886   3.4%
  crm              480   1.8%
  erp              403   1.5%
  network          251   1.0%
  dbms             229   0.9%
  os               198   0.8%
  backup           197   0.8%
  virtualization   118   0.5%
  mail              22   0.1%
  NULL               1   0.0%

65% "other" — Минцифры включает CAD/BPM/ITSM/мультимедиа, у которых
нет аналога в наших 14 кодах; остальные 35% разнесены по нашим категориям.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Расширение ОСЗ из 7.png диплома: добавлен PDF-отчёт с покрытием
по каждому стандарту и пер-категорным разрезом требований.

Бэкенд:
- internal/report/compliance_pdf.go: GenerateCompliancePDF(asset, standards)
  Структура страниц:
    1. Сводка — плитки по каждому стандарту с overall_score и счётчиками
    2. Отдельная страница на стандарт — требования по категориям
       с цветной меткой (зелёный/жёлтый/оранжевый/красный) и списками
       covering_controls + missing_controls (что бы ещё закрыло).
- service.AssetAllStandards(assetID) — детализация сразу по всем стандартам
- handler GET /api/compliance/asset/:assetID/report.pdf

Фронт:
- ComplianceSection: кнопка «Скачать PDF» в шапке секции
  через authFetch + Blob (тот же паттерн что у risk-PDF)

Live-проверка: Customer Database (4 контроля) → 4 страницы, 35KB:
  стр.1 ФСТЭК-17 25.6% + ISO 22.4%
  стр.2 ФСТЭК-17 детали (АВЗ.1=100%, ОЦЛ.1=30%, ИАФ.1=0% и т.д.)
  стр.3 ISO 27001 детали

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Закрывает блок «Формирование организационно-технической документации»
из 7.png диплома + «Рекомендации по усовершенствованию» с группировкой
по 4 мероприятиям из 8.png (АРМ/ЛВС/ЭДО/конф.инф.).

Бэкенд — 3 PDF-генератора:
- document_passport.go      — «Технический паспорт АС»
                              общие сведения, состав ПО, СЗИ, БДУ-уязвимости,
                              сводная таблица соответствия стандартам
- document_threat_model.go  — «Модель угроз ИБ»
                              формула W, агрегат, перечень угроз, для топ-5
                              отдельные страницы с разбором S→ST→VL→DA
                              и параметрами Q^угр / q^серьёз / Q^реакц / Z
- document_protection_plan.go — «Перечень мер защиты»
                              контроли разнесены по 4 мероприятиям из 8.png,
                              для непокрытых VL — адресные рекомендации
                              (что внедрить из vl_category_controls)

document_pack.go — упаковка нескольких PDF в один ZIP.

document_handlers.go — 4 эндпоинта:
  GET /api/reports/asset/:id/document/passport.pdf
  GET /api/reports/asset/:id/document/threat-model.pdf
  GET /api/reports/asset/:id/document/protection-plan.pdf
  GET /api/reports/asset/:id/documents.zip   (все 3 + compliance отчёт)

Фронт:
- DocumentsMenu — выпадающее меню в шапке карточки актива
  «Документы ▾», 5 опций (полный пакет ZIP + 4 отдельных PDF)
- Скачивание через authFetch + blob (как у risk-PDF)

Live-проверка на Customer Database:
  passport.pdf       27KB,  1 стр
  threat-model.pdf   40KB,  7 стр (общий + 5 разборов)
  protection-plan.pdf 28KB, 2 стр
  documents.zip     117KB,  4 файла

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Добавил отдельную схему docs/risk-formula.excalidraw — фокус на
происхождении 4 параметров формулы W (Q^угроза, q^серьёзность,
Q^реакция, Z): источник данных → маппинг → хранение → пример.

В обоих excalidraw-файлах заменил рукописный Excalifont (fontFamily=5)
на Helvetica (fontFamily=2) + увеличил высоту блоков, чтобы
текст не вылезал за границы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes потребность «не один актив = один отчёт, а вся организация»:
агрегированные метрики по 9 активам, таблица активов с W_max/уровнями/
СЗИ/compliance, топ-15 критических (asset × threat) рисков и единый
PDF-отчёт. Подключено в Sidebar как пункт «Организация», доступно
по /organization.

Live: 9 активов, W_max=0.93 (Web Application Server), 8 critical,
PDF=31KB / 3 стр.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(1) Цели и задачи ЗИ
- Раздел 1.2 в техническом паспорте АС: 4 цели (C/I/A + неотказуемость)
  и 3 задачи защиты информации.

(2) Определение вида обрабатываемой информации
- Migration 039: справочник data_categories (6 кодов: public/internal/pdn/
  confidential/dsp/kii) + поле assets.data_category_id
- Backend: Asset.DataCategoryID, обновлены Create/Update/scan/payload,
  эндпоинт GET /api/data-categories
- UI: SELECT 'Категория обрабатываемой информации' в форме актива
- PDF: строка в общих сведениях паспорта АС.

(3) 2 категории угроз из 8.png
- Migration 040: добавил «Хищение, искажение, блокировка» и
  «Непреднамеренные воздействия»; переименовал «Утечка информации»
  → «Утечка по техническим каналам»
- Backfill threats.threat_category_id по эвристикам name LIKE:
  227 угроз размечены, NULL = 0
  основные: НСД=95, Вредоносное ПО=52, Утечка=22, Отказ в обслуживании=16

(4) Каталог СЗИ (БД ПАСЗИ)
- Новая страница /controls (ControlCatalogPage) — список 11 контролей
  по 4 мероприятиям из 8.png (АРМ/ЛВС/Документооборот/Конф.инф.)
- Для каждого: описание, какие VL закрывает, на скольких активах
  внедрён, какие требования ФСТЭК-17 / ISO 27001 покрывает
- Подключено в Sidebar как «Каталог СЗИ» в секции 'refs'

Live-проверка:
- паспорт PDF: 2 стр., раздел 1.2 «Цели и задачи ЗИ» рендерится
- /api/data-categories: 6 категорий
- /api/controls + UI: 11 контролей по 4 мероприятиям

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Все PDF-отчёты использовали символы ✓ ◐ ✗ → ↳ — NotoSans их не имеет,
рендерились как ⬛-квадраты. Плюс длинные заголовки страниц угроз
вылезали за правый край листа.

Фиксы:
- pdf.go: helper statusGlyph() возвращает «OK / Част. / Нет» вместо ✓◐✗
  glyphCovered/Partial/Missing константы; стрелки → заменены на ASCII '->'
- compliance_pdf.go: метки требований через statusGlyph()
- document_passport.go: row() расширен до labelWidth=80mm; таблица
  соответствия колонки «Закрыто/Частично/Не закрыто/Всего»
- document_protection_plan.go: «[внедрено] / [рекомендуется] / [нет]»
  вместо ✓✗+; «VL <-> control» в footer
- document_threat_model.go: длинный заголовок страницы угрозы режется
  до 55 символов с «…», полное название печатается отдельной строкой
- document_organization.go: ↳ заменены на отступы; «актив-источник»

Также UI:
- AssetDetailPage VulnSection: если ПО привязано но в БДУ нет совпадений,
  показываем нормальное объяснение (3 возможные причины + дисклеймер
  «не означает что ПО безопасно — БДУ покрывает только публичные CVE»)
  вместо вводящего в заблуждение «привяжите ПО» при уже привязанном.

Live: 5 PDF (passport, threat-model, protection-plan, compliance,
organization) теперь рендерятся без ⬛, заголовки помещаются в лист.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 новых файлов тестов (~920 строк) на критичные чистые функции:

cmd/import-fstec-threats/applicability_test.go
  TestDeriveQThreat        — маппинг 'Возможности нарушителя' → Q^угр
                             (low/medium/high → 0.3/0.6/0.9 + edge кейсы)
  TestDeriveQSeverity      — q^серьёзность по числу нарушенных C/I/A

cmd/import-minreestr/categories_test.go
  TestResolveCategory      — 3-проходный маппер subcategoryid в наши
                             14 категорий (точный код / префикс / keyword
                             / fallback в other), приоритеты, edge кейсы

internal/service/compliance/calc_test.go
  TestCalcOverview_*       — алгоритм coverage = max(weight) среди
                             внедрённых controls (5 сценариев: full spread,
                             max-wins, без контролей, full coverage,
                             пустые/без рёбер)

internal/service/control/service_test.go (100% coverage модуля)
  Mock-репозиторий + проверка валидации id и проброса ошибок репо

internal/report/levels_test.go
  TestStatusGlyph          — текстовые маркеры OK/Част./Нет
                             (вместо ✓◐✗ из-за отсутствия глифов в NotoSans)
  TestComplianceLevelRGB   — цвета по баллу соответствия (4 уровня)
  TestLevelRGB / Label     — для уровней риска

internal/report/document_protection_plan_test.go
  TestJoinControls         — склейка контролей в строку (edge: nil/1/N)
  TestCountUncoveredVL     — дедуп uncovered VL по category_id между
                             несколькими угрозами
  TestGroupControlsByMeasure — разнос 11 контролей по 4 мероприятиям
                             из 8.png (АРМ/ЛВС/ЭДО/конф.инф.)

internal/report/documents_smoke_test.go
  Smoke-тесты на 5 PDF-генераторов: %PDF- magic, размер, nil-data
  errors. PackDocuments — ZIP magic + skip empty entries.

Покрытие после:
  internal/risk                   95.2%
  internal/service/control       100.0%
  internal/report                 83.2%
  internal/service/auth           79.5%
  internal/service/risk           42.4%
  internal/bdu                    36.5%
  internal/service/compliance     19.0%

Все тесты — чистые юнит-тесты без БД и сети.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@velvetway
velvetway merged commit 53e151a into main Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant