Skip to content

Commit b9f110b

Browse files
Add Rust risk register bridge
1 parent 5207d07 commit b9f110b

14 files changed

Lines changed: 1682 additions & 30 deletions

File tree

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ COMPOSE_STAGE=docker compose -f docker-compose.yml -f docker-compose.stage.yml
33
COMPOSE_PROD=docker compose -f docker-compose.yml -f docker-compose.prod.yml
44
COMPOSE_PROD_LLM=docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.llm.yml
55
PYTHON_BIN=$(shell if [ -x .venv/bin/python ]; then echo .venv/bin/python; else echo python; fi)
6-
TEAM_TEST_ENV=GUIDANCE_SCORING_BACKEND=local RISK_SCORING_BACKEND=local REPORT_SUMMARY_BACKEND=local REPORT_SNAPSHOT_BACKEND=local DASHBOARD_SUMMARY_BACKEND=local ASSET_INVENTORY_BACKEND=local PROCESS_REGISTER_BACKEND=local RUST_BACKEND_URL=
6+
TEAM_TEST_ENV=GUIDANCE_SCORING_BACKEND=local RISK_SCORING_BACKEND=local REPORT_SUMMARY_BACKEND=local REPORT_SNAPSHOT_BACKEND=local DASHBOARD_SUMMARY_BACKEND=local ASSET_INVENTORY_BACKEND=local PROCESS_REGISTER_BACKEND=local RISK_REGISTER_BACKEND=local RUST_BACKEND_URL=
77

88
.PHONY: dev-up dev-down stage-up stage-down prod-up prod-down prod-up-llm llm-download backup restore health handbook-pdf local-bootstrap local-check local-test team-test docker-check docker-smoke easy-start prod-readiness rust-build rust-test rust-run canary-daily rust-import-collection rust-sync-recent rust-canary-parity rust-canary-trend rust-canary-import
99

@@ -20,7 +20,7 @@ local-test:
2020

2121
team-test:
2222
$(TEAM_TEST_ENV) $(PYTHON_BIN) manage.py check
23-
$(TEAM_TEST_ENV) $(PYTHON_BIN) manage.py test apps.core apps.reports apps.product_security apps.guidance apps.dashboard apps.assets_app apps.processes apps.vulnerability_intelligence
23+
$(TEAM_TEST_ENV) $(PYTHON_BIN) manage.py test apps.core apps.reports apps.product_security apps.guidance apps.dashboard apps.assets_app apps.processes apps.risks apps.vulnerability_intelligence
2424

2525
docker-check:
2626
$(COMPOSE_DEV) config >/dev/null

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ Der funktionierende Referenzpfad auf Ubuntu 24.04 ist:
160160
- `DASHBOARD_SUMMARY_BACKEND=rust_service`
161161
- `ASSET_INVENTORY_BACKEND=rust_service`
162162
- `PROCESS_REGISTER_BACKEND=rust_service`
163+
- `RISK_REGISTER_BACKEND=rust_service`
163164
- `RUST_STRICT_MODE=True` (erzwingt Rust-Backends ohne Fallback)
164165

165166
Danach kann weiter wie gewohnt gestartet werden:

apps/risks/services.py

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,317 @@
11
"""V20: Risk-Matrix-Service fuer 5x5 Heatmap."""
22
from collections import defaultdict
3+
import json
4+
from dataclasses import dataclass
5+
from datetime import date, datetime
6+
from urllib.error import HTTPError
7+
from urllib.request import Request, urlopen
8+
9+
from django.conf import settings
10+
11+
from .models import Risk
12+
13+
14+
IMPACT_LABELS = dict(Risk.IMPACT_CHOICES)
15+
LIKELIHOOD_LABELS = dict(Risk.LIKELIHOOD_CHOICES)
16+
STATUS_LABELS = dict(Risk.Status.choices)
17+
TREATMENT_LABELS = dict(Risk.Treatment.choices)
18+
19+
20+
@dataclass(frozen=True)
21+
class RiskRelatedRef:
22+
id: int | None
23+
name: str
24+
25+
def __str__(self) -> str:
26+
return self.name
27+
28+
29+
@dataclass(frozen=True)
30+
class RiskBridgeItem:
31+
id: int
32+
tenant: object
33+
tenant_id: int
34+
category: RiskRelatedRef | None
35+
category_id: int | None
36+
process: RiskRelatedRef | None
37+
process_id: int | None
38+
asset: RiskRelatedRef | None
39+
asset_id: int | None
40+
owner: RiskRelatedRef | None
41+
owner_id: int | None
42+
title: str
43+
description: str
44+
threat: str
45+
vulnerability: str
46+
impact: int
47+
impact_label: str
48+
likelihood: int
49+
likelihood_label: str
50+
residual_impact: int | None
51+
residual_impact_label: str | None
52+
residual_likelihood: int | None
53+
residual_likelihood_label: str | None
54+
status: str
55+
status_label: str
56+
treatment_strategy: str
57+
treatment_strategy_label: str
58+
treatment_plan: str
59+
treatment_due_date: date | None
60+
accepted_by: RiskRelatedRef | None
61+
accepted_by_id: int | None
62+
accepted_at: datetime | None
63+
review_date: date | None
64+
created_at: str
65+
updated_at: str
66+
67+
@property
68+
def pk(self) -> int:
69+
return self.id
70+
71+
@property
72+
def score(self) -> int:
73+
return self.impact * self.likelihood
74+
75+
@property
76+
def residual_score(self) -> int | None:
77+
if self.residual_impact and self.residual_likelihood:
78+
return self.residual_impact * self.residual_likelihood
79+
return None
80+
81+
@property
82+
def risk_level(self) -> str:
83+
score = self.score
84+
if score >= 20:
85+
return 'CRITICAL'
86+
if score >= 12:
87+
return 'HIGH'
88+
if score >= 6:
89+
return 'MEDIUM'
90+
return 'LOW'
91+
92+
@property
93+
def risk_level_label(self) -> str:
94+
return {'CRITICAL': 'Kritisch', 'HIGH': 'Hoch', 'MEDIUM': 'Mittel', 'LOW': 'Niedrig'}.get(
95+
self.risk_level,
96+
'–',
97+
)
98+
99+
@property
100+
def severity(self) -> int:
101+
return self.impact
102+
103+
def get_impact_display(self) -> str:
104+
return self.impact_label or IMPACT_LABELS.get(self.impact, str(self.impact))
105+
106+
def get_likelihood_display(self) -> str:
107+
return self.likelihood_label or LIKELIHOOD_LABELS.get(self.likelihood, str(self.likelihood))
108+
109+
def get_status_display(self) -> str:
110+
return self.status_label or STATUS_LABELS.get(self.status, self.status)
111+
112+
def get_treatment_strategy_display(self) -> str:
113+
return self.treatment_strategy_label or TREATMENT_LABELS.get(self.treatment_strategy, self.treatment_strategy)
114+
115+
116+
class RiskRustClient:
117+
@staticmethod
118+
def _base_url() -> str:
119+
base = (getattr(settings, 'RUST_BACKEND_URL', '') or '').strip()
120+
return base.rstrip('/')
121+
122+
@staticmethod
123+
def fetch_risks(request, tenant, timeout: int = 8) -> list[RiskBridgeItem]:
124+
base = RiskRustClient._base_url()
125+
if not base:
126+
raise RuntimeError('RUST_BACKEND_URL ist nicht gesetzt.')
127+
128+
rust_request = RiskRustClient._authenticated_request(request, tenant, f'{base}/api/v1/risks')
129+
with urlopen(rust_request, timeout=timeout) as response:
130+
payload = json.loads(response.read().decode('utf-8'))
131+
132+
tenant_id = RiskRustClient._int_field(payload, 'tenant_id')
133+
if tenant_id != int(tenant.id):
134+
raise RuntimeError('Rust-Risikoliste gehoert nicht zum angefragten Tenant.')
135+
136+
return [
137+
RiskRustClient._risk_from_payload(item, tenant)
138+
for item in payload.get('risks', [])
139+
if isinstance(item, dict)
140+
]
141+
142+
@staticmethod
143+
def fetch_risk_detail(request, tenant, risk_id: int, timeout: int = 8):
144+
base = RiskRustClient._base_url()
145+
if not base:
146+
raise RuntimeError('RUST_BACKEND_URL ist nicht gesetzt.')
147+
148+
rust_request = RiskRustClient._authenticated_request(request, tenant, f'{base}/api/v1/risks/{int(risk_id)}')
149+
try:
150+
with urlopen(rust_request, timeout=timeout) as response:
151+
payload = json.loads(response.read().decode('utf-8'))
152+
except HTTPError as exc:
153+
if exc.code == 404:
154+
return None
155+
raise
156+
157+
risk = payload.get('risk') or {}
158+
if not isinstance(risk, dict):
159+
raise RuntimeError('Rust-Risikodetail hat kein risk-Objekt geliefert.')
160+
risk_tenant_id = RiskRustClient._int_field(risk, 'tenant_id')
161+
if risk_tenant_id != int(tenant.id):
162+
raise RuntimeError('Rust-Risikodetail gehoert nicht zum angefragten Tenant.')
163+
return RiskRustClient._risk_from_payload(risk, tenant)
164+
165+
@staticmethod
166+
def _risk_from_payload(item: dict, tenant) -> RiskBridgeItem:
167+
category_id = RiskRustClient._optional_int_field(item, 'category_id')
168+
process_id = RiskRustClient._optional_int_field(item, 'process_id')
169+
asset_id = RiskRustClient._optional_int_field(item, 'asset_id')
170+
owner_id = RiskRustClient._optional_int_field(item, 'owner_id')
171+
accepted_by_id = RiskRustClient._optional_int_field(item, 'accepted_by_id')
172+
category_name = str(item.get('category_name') or '').strip()
173+
process_name = str(item.get('process_name') or '').strip()
174+
asset_name = str(item.get('asset_name') or '').strip()
175+
owner_display = str(item.get('owner_display') or '').strip()
176+
accepted_by_display = str(item.get('accepted_by_display') or '').strip()
177+
impact = RiskRustClient._int_field(item, 'impact')
178+
likelihood = RiskRustClient._int_field(item, 'likelihood')
179+
status = str(item.get('status') or Risk.Status.IDENTIFIED)
180+
treatment_strategy = str(item.get('treatment_strategy') or '')
181+
return RiskBridgeItem(
182+
id=RiskRustClient._int_field(item, 'id'),
183+
tenant=tenant,
184+
tenant_id=RiskRustClient._int_field(item, 'tenant_id'),
185+
category=RiskRelatedRef(category_id, category_name) if category_name else None,
186+
category_id=category_id,
187+
process=RiskRelatedRef(process_id, process_name) if process_name else None,
188+
process_id=process_id,
189+
asset=RiskRelatedRef(asset_id, asset_name) if asset_name else None,
190+
asset_id=asset_id,
191+
owner=RiskRelatedRef(owner_id, owner_display) if owner_display else None,
192+
owner_id=owner_id,
193+
title=str(item.get('title') or ''),
194+
description=str(item.get('description') or ''),
195+
threat=str(item.get('threat') or ''),
196+
vulnerability=str(item.get('vulnerability') or ''),
197+
impact=impact,
198+
impact_label=str(item.get('impact_label') or IMPACT_LABELS.get(impact, impact)),
199+
likelihood=likelihood,
200+
likelihood_label=str(item.get('likelihood_label') or LIKELIHOOD_LABELS.get(likelihood, likelihood)),
201+
residual_impact=RiskRustClient._optional_int_field(item, 'residual_impact'),
202+
residual_impact_label=RiskRustClient._optional_str_field(item, 'residual_impact_label'),
203+
residual_likelihood=RiskRustClient._optional_int_field(item, 'residual_likelihood'),
204+
residual_likelihood_label=RiskRustClient._optional_str_field(item, 'residual_likelihood_label'),
205+
status=status,
206+
status_label=str(item.get('status_label') or STATUS_LABELS.get(status, status)),
207+
treatment_strategy=treatment_strategy,
208+
treatment_strategy_label=str(
209+
item.get('treatment_strategy_label') or TREATMENT_LABELS.get(treatment_strategy, treatment_strategy)
210+
),
211+
treatment_plan=str(item.get('treatment_plan') or ''),
212+
treatment_due_date=RiskRustClient._date_field(item, 'treatment_due_date'),
213+
accepted_by=RiskRelatedRef(accepted_by_id, accepted_by_display) if accepted_by_display else None,
214+
accepted_by_id=accepted_by_id,
215+
accepted_at=RiskRustClient._datetime_field(item, 'accepted_at'),
216+
review_date=RiskRustClient._date_field(item, 'review_date'),
217+
created_at=str(item.get('created_at') or ''),
218+
updated_at=str(item.get('updated_at') or ''),
219+
)
220+
221+
@staticmethod
222+
def _authenticated_request(request, tenant, url: str) -> Request:
223+
user = getattr(request, 'user', None)
224+
user_id = getattr(user, 'id', None)
225+
if not user_id:
226+
raise RuntimeError('Risk-Rust-Bridge braucht einen authentifizierten User.')
227+
228+
rust_request = Request(url)
229+
rust_request.add_header('Accept', 'application/json')
230+
rust_request.add_header('X-ISCY-Tenant-ID', str(tenant.id))
231+
rust_request.add_header('X-ISCY-User-ID', str(user_id))
232+
user_email = (getattr(user, 'email', '') or '').strip()
233+
if user_email:
234+
rust_request.add_header('X-ISCY-User-Email', user_email)
235+
return rust_request
236+
237+
@staticmethod
238+
def _int_field(payload: dict, key: str) -> int:
239+
return int(payload.get(key) or 0)
240+
241+
@staticmethod
242+
def _optional_int_field(payload: dict, key: str) -> int | None:
243+
value = payload.get(key)
244+
if value in (None, ''):
245+
return None
246+
return int(value)
247+
248+
@staticmethod
249+
def _optional_str_field(payload: dict, key: str) -> str | None:
250+
value = str(payload.get(key) or '').strip()
251+
return value or None
252+
253+
@staticmethod
254+
def _date_field(payload: dict, key: str) -> date | None:
255+
value = str(payload.get(key) or '').strip()
256+
if not value:
257+
return None
258+
return date.fromisoformat(value[:10])
259+
260+
@staticmethod
261+
def _datetime_field(payload: dict, key: str) -> datetime | None:
262+
value = str(payload.get(key) or '').strip()
263+
if not value:
264+
return None
265+
normalized = value.replace('Z', '+00:00')
266+
return datetime.fromisoformat(normalized)
267+
268+
269+
class RiskRegisterBridge:
270+
@staticmethod
271+
def fetch_list(request, tenant):
272+
if tenant is None:
273+
return None
274+
275+
backend = str(getattr(settings, 'RISK_REGISTER_BACKEND', 'rust_service') or '').strip().lower()
276+
if backend != 'rust_service':
277+
return None
278+
279+
if not RiskRustClient._base_url():
280+
if RiskRegisterBridge._strict_rust_mode():
281+
raise RuntimeError('Rust risk register backend ist aktiv, aber RUST_BACKEND_URL ist nicht gesetzt.')
282+
return None
283+
284+
try:
285+
return RiskRustClient.fetch_risks(request, tenant)
286+
except Exception as exc:
287+
if RiskRegisterBridge._strict_rust_mode():
288+
raise RuntimeError('Rust risk register backend ist aktiv, aber nicht erreichbar.') from exc
289+
return None
290+
291+
@staticmethod
292+
def fetch_detail(request, tenant, risk_id: int):
293+
if tenant is None:
294+
return None
295+
296+
backend = str(getattr(settings, 'RISK_REGISTER_BACKEND', 'rust_service') or '').strip().lower()
297+
if backend != 'rust_service':
298+
return None
299+
300+
if not RiskRustClient._base_url():
301+
if RiskRegisterBridge._strict_rust_mode():
302+
raise RuntimeError('Rust risk register backend ist aktiv, aber RUST_BACKEND_URL ist nicht gesetzt.')
303+
return None
304+
305+
try:
306+
return RiskRustClient.fetch_risk_detail(request, tenant, risk_id)
307+
except Exception as exc:
308+
if RiskRegisterBridge._strict_rust_mode():
309+
raise RuntimeError('Rust risk register backend ist aktiv, aber nicht erreichbar.') from exc
310+
return None
311+
312+
@staticmethod
313+
def _strict_rust_mode() -> bool:
314+
return bool(getattr(settings, 'RUST_STRICT_MODE', False))
3315

4316

5317
class RiskMatrixService:

0 commit comments

Comments
 (0)