Skip to content

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

Open
velvetway wants to merge 30 commits into
mainfrom
feat/risk-graph-viz
Open

risk-graph: stacked layout, 5-col Sankey + client-side what-if#5
velvetway wants to merge 30 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>
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