Skip to main content

Risk Assessment

Multi-layered risk assessment built exclusively on the EBA (European Banking Authority) risk factor matrix, deterministic red flag detection, a configurable risk scoring system with versioned configurations, and sector-specific risk engines. The risk engine produces quantified risk scores with full traceability to individual risk factors.

As of 2026-03-31 the system is EBA-only: the ARIA risk matrix has been removed and EBA_RISK_MATRIX_ENABLED flag eliminated. EBA scoring is always active.

Components

ModulePurpose
risk_engine.pyCore risk scoring engine aggregating multiple risk signals
risk_matrix_service.pyConfigurable risk matrix management and evaluation
eba_risk_matrix.pyEBA/GL/2021/02 risk factor implementation (7 dimensions, 20 factors, SHA-256 determinism proof)
red_flag_engine.pyDeterministic red flag detection based on jurisdiction-specific rules
precious_metals_risk_engine.pySector-specific risk engine for precious metals dealers
risk_config_service.pyVersioned risk configuration management — load, activate, audit
app/api/risk_config.pyREST endpoints at /api/risk-config/

EBA Risk Matrix

The EBA risk matrix implements EBA/GL/2021/02 (Guidelines on risk factors) as a scored 7-dimension matrix. The overall score is a weighted_max aggregate: a weighted average of dimension scores with a floor boost (FLOOR_BOOST_FACTOR = 0.60) applied when any single dimension exceeds the critical threshold (CRITICAL_DIMENSION_THRESHOLD = 80), consistent with EBA guidance that a critical risk factor should dominate the assessment.

7 dimensions (weights from eba_standard_v1.yaml, EBA_WEIGHTS in eba_risk_matrix.py):

DimensionWeightFactors
Customer0.25ownership_complexity, pep_exposure, sanctions_exposure, adverse_media, business_profile
Geographic0.20jurisdiction_risk, operational_geography, ubo_geography
Product/Service0.15product_complexity, regulatory_status
Delivery Channel0.08non_face_to_face, digital_presence
Transaction0.12financial_profile, transaction_patterns
Network/Association0.10network_risk, shared_address_risk, subsidiary_opacity
Temporal/Historical0.10company_age, filing_regularity, adverse_history

Risk levels:

LevelScore Range
Critical90+
High70–89
Medium40–69
Low20–39
Clear0–19

SHA-256 audit trail: EBARiskResult carries an input_hash and output_hash so auditors can verify stored results without re-running the scorer. The matrix version (eba_standard_v1) is captured in every result, satisfying 5-year AML retention requirements.

_unwrap_score helper (2026-04-13)

eba_risk_matrix.py now defines a module-level _unwrap_score(entry) helper that mirrors ReferenceDataService.get_risk_score so the four inline ref_datasets[...].get(key) lookups in _score_business_profile and _score_product_service_dimension correctly unwrap three supported shapes: a plain numeric, {"score": N, …}, or {"risk_score": N, …}. Previously the fast path returned the whole entry dict which blew up with TypeError: float() argument must be a string or a real number, not 'dict' and sent the reassess_risk activity into a Temporal retry loop. Commit dc5f1d4a.

Unified Risk Configuration

Risk scoring configuration is managed via the RiskConfigService and exposed through the /api/risk-config/ REST API. Configurations are versioned: tenants can create new versions, preview their impact, and activate a version explicitly. Activating a new version records the change in risk_config_audit.

Database Tables

TablePurpose
risk_configurationsVersioned risk config records — scoring model, reference dataset overrides, activation status. RLS-enforced per tenant.
risk_config_auditImmutable audit log for config activations and deactivations. RLS-enforced per tenant.

Both tables have FORCE ROW LEVEL SECURITY and are covered by standard tenant isolation policies.

API Endpoints (/api/risk-config/)

Defined in app/api/risk_config.py. All mutating endpoints require the super_admin role; GET /active and POST /recalculate/{case_id} are open to any authenticated officer.

MethodPathDescription
GET/api/risk-config/activeGet the currently active configuration (no super_admin gate)
GET/api/risk-config/versionsList all config versions for the current tenant (paginated)
GET/api/risk-config/versions/{config_id}Get a specific configuration version
POST/api/risk-config/versionsCreate a new draft by cloning the active config
PUT/api/risk-config/versions/{config_id}Update a draft configuration (validated)
POST/api/risk-config/versions/{config_id}/activateActivate a draft (archives the prior active version)
GET/api/risk-config/versions/{id_a}/diff/{id_b}Recursive diff between two versions
GET/api/risk-config/auditList configuration audit log
POST/api/risk-config/recalculate/{case_id}Recalculate a case's risk under the active config
POST/api/risk-config/batch-reevaluateStart a BatchRiskReEvaluationWorkflow for all active cases

Stale Configuration Detection

When a case was scored under an older configuration version, the case detail page shows a stale config banner with a Recalculate button. Clicking it re-scores the case under the currently active configuration and updates the stored risk score without requiring a full re-investigation.

Admin UI — Risk Configuration Page

The /admin/risk-configuration admin page provides a three-tab interface for managing the active scoring model:

TabPurpose
Scoring ModelView and edit the active EBA dimension weights, factor thresholds, and risk level boundaries
Reference DatasetsInspect and override reference dataset values (FATF lists, PEP tiers, industry risk classifications) used by the EBA matrix
VersionsBrowse all configuration versions, compare diffs, and activate a version

The old /admin/reference-data page now redirects to /admin/risk-configuration.

Recent Fixes (2026-04-06)

Delivery Channel Fix

The delivery_channel dimension weight was incorrectly set to 0 in some configurations, causing the EBA matrix to produce inflated scores. It now carries the EBA-standard weight of 0.08 (8%) in EBA_WEIGHTS. The non_face_to_face factor contributes a positive score (5.0 of its 15-point factor maximum) for cases onboarded through the digital portal, reflecting EBA guidance that non-face-to-face identification carries inherent risk; the digital_presence factor is scored separately (20-point maximum).

PEP Detection Fix

False PEP escalations were occurring when the screening agent found common-name matches without sufficient confidence. The fix tightens the PEP matching threshold: a PEP finding only elevates risk when the match confidence exceeds 0.7 (previously any match triggered escalation). Clean screening results now correctly produce VERIFIED severity in the structured summary.

MCC-Aware License Verification

The verification checks pipeline now includes regulatory license verification that is MCC-aware. The MCC code (assigned by the MCC classifier agent earlier in the pipeline) determines the business vertical, which in turn determines which regulatory licenses are required. Missing licenses produce a hit finding with PSD2 Art. 11 / CRR Art. 8 regulatory basis.

Segment Risk Calibration

EBA dimension weights can be overridden per regulatory segment via apply_risk_calibration(weights, calibration) in eba_risk_matrix.py. The RiskCalibration model carries five dimension multipliers (customer_weight_multiplier, geographic_weight_multiplier, product_weight_multiplier, channel_weight_multiplier, transaction_weight_multiplier); calibration operates at the dimension level, not on individual factors. After multiplication the weights are re-normalised to sum to 1.0. Examples from config/segments/: CZ Banking applies geographic_weight_multiplier = 1.2; BE precious metals applies customer_weight_multiplier = 1.5 and transaction_weight_multiplier = 1.4. Segment calibration is applied during the post_osint risk reassessment checkpoint.

Risk Config 403 Fix

The /api/risk-config/recalculate/{case_id} endpoint was gated behind super_admin role, preventing compliance officers from recalculating risk scores when configurations changed. The gate has been removed -- any authenticated user with tenant access can trigger recalculation (see the # No super_admin check comment in recalculate_case_risk). The endpoint accepts either a case_id (UUID) or a workflow_id (wf_xxx), inserts a new risk_assessments row (preserving history), and updates the case's additional_data.