diff --git a/src/govoplan_core/core/sanctions.py b/src/govoplan_core/core/sanctions.py
new file mode 100644
index 0000000..e02a6e9
--- /dev/null
+++ b/src/govoplan_core/core/sanctions.py
@@ -0,0 +1,152 @@
+from __future__ import annotations
+
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass, field
+from datetime import datetime
+from typing import Protocol, runtime_checkable
+
+
+SANCTIONS_SNAPSHOT_CONTRACT_VERSION = "1"
+CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS = (
+ "connectors.sanctionsSnapshotProvider"
+)
+
+
+@dataclass(frozen=True, slots=True)
+class SanctionsSnapshotReference:
+ ref: str
+ tenant_id: str
+ provider_id: str
+ publisher: str
+ jurisdiction: str
+ list_type: str
+ source_id: str
+ source_version: str
+ publication_at: datetime | None
+ effective_at: datetime | None
+ acquired_at: datetime
+ content_type: str
+ byte_count: int
+ sha256: str
+ parser_version: str
+ raw_evidence_ref: str
+ connector_run_id: str
+ signature_evidence: Mapping[str, object] = field(
+ default_factory=dict
+ )
+ licence_notes: str | None = None
+ trust_notes: str | None = None
+ transport_evidence: Mapping[str, object] = field(
+ default_factory=dict
+ )
+ contract_version: str = SANCTIONS_SNAPSHOT_CONTRACT_VERSION
+
+ def __post_init__(self) -> None:
+ if self.contract_version != SANCTIONS_SNAPSHOT_CONTRACT_VERSION:
+ raise ValueError(
+ "Unsupported sanctions snapshot contract version."
+ )
+ required = (
+ self.ref,
+ self.tenant_id,
+ self.provider_id,
+ self.publisher,
+ self.jurisdiction,
+ self.list_type,
+ self.source_id,
+ self.source_version,
+ self.content_type,
+ self.sha256,
+ self.parser_version,
+ self.raw_evidence_ref,
+ self.connector_run_id,
+ )
+ if not all(value.strip() for value in required):
+ raise ValueError(
+ "Sanctions snapshot references require complete provenance."
+ )
+ if self.byte_count < 1:
+ raise ValueError(
+ "Sanctions snapshot evidence must not be empty."
+ )
+ if (
+ len(self.sha256) != 64
+ or any(character not in "0123456789abcdef" for character in self.sha256)
+ ):
+ raise ValueError(
+ "Sanctions snapshot SHA-256 evidence is invalid."
+ )
+
+
+@dataclass(frozen=True, slots=True)
+class SanctionsSnapshotPayload:
+ snapshot: SanctionsSnapshotReference
+ content: bytes
+
+ def __post_init__(self) -> None:
+ if len(self.content) != self.snapshot.byte_count:
+ raise ValueError(
+ "Sanctions snapshot content length does not match metadata."
+ )
+
+
+@runtime_checkable
+class SanctionsSnapshotProvider(Protocol):
+ def list_snapshots(
+ self,
+ session: object,
+ principal: object,
+ *,
+ limit: int = 100,
+ ) -> Sequence[SanctionsSnapshotReference]:
+ ...
+
+ def get_snapshot(
+ self,
+ session: object,
+ principal: object,
+ *,
+ snapshot_ref: str,
+ ) -> SanctionsSnapshotReference | None:
+ ...
+
+ def read_snapshot(
+ self,
+ session: object,
+ principal: object,
+ *,
+ snapshot_ref: str,
+ ) -> SanctionsSnapshotPayload:
+ ...
+
+
+def sanctions_snapshot_provider(
+ registry: object | None,
+) -> SanctionsSnapshotProvider | None:
+ if (
+ registry is None
+ or not hasattr(registry, "has_capability")
+ or not hasattr(registry, "capability")
+ or not registry.has_capability(
+ CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS
+ )
+ ):
+ return None
+ provider = registry.capability(
+ CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS
+ )
+ return (
+ provider
+ if isinstance(provider, SanctionsSnapshotProvider)
+ else None
+ )
+
+
+__all__ = [
+ "CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS",
+ "SANCTIONS_SNAPSHOT_CONTRACT_VERSION",
+ "SanctionsSnapshotPayload",
+ "SanctionsSnapshotProvider",
+ "SanctionsSnapshotReference",
+ "sanctions_snapshot_provider",
+]
diff --git a/src/govoplan_core/settings.py b/src/govoplan_core/settings.py
index 99fed2f..9c13237 100644
--- a/src/govoplan_core/settings.py
+++ b/src/govoplan_core/settings.py
@@ -49,8 +49,8 @@ class Settings(BaseSettings):
default=(
"tenancy,organizations,identity,idm,access,admin,dashboard,policy,"
"audit,campaigns,files,mail,calendar,poll,scheduling,connectors,"
- "datasources,dataflow,workflow,views,search,postbox,notifications,"
- "docs,ops"
+ "datasources,dataflow,workflow,views,search,risk_compliance,"
+ "postbox,notifications,docs,ops"
),
alias="ENABLED_MODULES",
)
diff --git a/tests/test_sanctions_contract.py b/tests/test_sanctions_contract.py
new file mode 100644
index 0000000..83b029b
--- /dev/null
+++ b/tests/test_sanctions_contract.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+from datetime import datetime, timezone
+import hashlib
+import unittest
+
+from govoplan_core.core.sanctions import (
+ CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS,
+ SanctionsSnapshotPayload,
+ SanctionsSnapshotProvider,
+ SanctionsSnapshotReference,
+ sanctions_snapshot_provider,
+)
+
+
+class _Provider:
+ def list_snapshots(self, session, principal, *, limit=100):
+ del session, principal, limit
+ return ()
+
+ def get_snapshot(self, session, principal, *, snapshot_ref):
+ del session, principal, snapshot_ref
+ return None
+
+ def read_snapshot(self, session, principal, *, snapshot_ref):
+ del session, principal, snapshot_ref
+ raise LookupError
+
+
+class _Registry:
+ def __init__(self, provider):
+ self.provider = provider
+
+ def has_capability(self, name):
+ return name == CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS
+
+ def capability(self, name):
+ return self.provider if self.has_capability(name) else None
+
+
+class SanctionsContractTests(unittest.TestCase):
+ def test_snapshot_evidence_is_versioned_and_bounded(self) -> None:
+ content = b""
+ snapshot = SanctionsSnapshotReference(
+ ref="sanctions-snapshot:snapshot-1",
+ tenant_id="tenant-1",
+ provider_id="un.security_council",
+ publisher="United Nations Security Council",
+ jurisdiction="UN",
+ list_type="consolidated",
+ source_id="unsc-consolidated",
+ source_version="2026-07-22",
+ publication_at=None,
+ effective_at=None,
+ acquired_at=datetime.now(timezone.utc),
+ content_type="application/xml",
+ byte_count=len(content),
+ sha256=hashlib.sha256(content).hexdigest(),
+ parser_version="unsc-xml-v1",
+ raw_evidence_ref="connector-evidence:snapshot-1",
+ connector_run_id="run-1",
+ )
+
+ payload = SanctionsSnapshotPayload(
+ snapshot=snapshot,
+ content=content,
+ )
+ provider = _Provider()
+
+ self.assertEqual(content, payload.content)
+ self.assertIsInstance(provider, SanctionsSnapshotProvider)
+ self.assertIs(
+ provider,
+ sanctions_snapshot_provider(_Registry(provider)),
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/webui/package-lock.json b/webui/package-lock.json
index b0f5437..993dd17 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -26,6 +26,7 @@
"@govoplan/organizations-webui": "file:../../govoplan-organizations/webui",
"@govoplan/policy-webui": "file:../../govoplan-policy/webui",
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
+ "@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui",
"@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui",
"@govoplan/search-webui": "file:../../govoplan-search/webui",
"@govoplan/views-webui": "file:../../govoplan-views/webui",
@@ -378,6 +379,22 @@
}
}
},
+ "../../govoplan-risk-compliance/webui": {
+ "name": "@govoplan/risk-compliance-webui",
+ "version": "0.1.8",
+ "peerDependencies": {
+ "@govoplan/core-webui": "^0.1.14",
+ "lucide-react": "^1.23.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": ">=7.18.2 <8"
+ },
+ "peerDependenciesMeta": {
+ "@govoplan/core-webui": {
+ "optional": true
+ }
+ }
+ },
"../../govoplan-scheduling/webui": {
"name": "@govoplan/scheduling-webui",
"version": "0.1.11",
@@ -1243,6 +1260,10 @@
"resolved": "../../govoplan-postbox/webui",
"link": true
},
+ "node_modules/@govoplan/risk-compliance-webui": {
+ "resolved": "../../govoplan-risk-compliance/webui",
+ "link": true
+ },
"node_modules/@govoplan/scheduling-webui": {
"resolved": "../../govoplan-scheduling/webui",
"link": true
diff --git a/webui/package.json b/webui/package.json
index 41c0494..31b41a3 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -57,6 +57,7 @@
"@govoplan/ops-webui": "file:../../govoplan-ops/webui",
"@govoplan/policy-webui": "file:../../govoplan-policy/webui",
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
+ "@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui",
"@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui",
"@govoplan/search-webui": "file:../../govoplan-search/webui",
"@govoplan/views-webui": "file:../../govoplan-views/webui",
diff --git a/webui/src/platform/modules.ts b/webui/src/platform/modules.ts
index d3986a2..5e2fa7c 100644
--- a/webui/src/platform/modules.ts
+++ b/webui/src/platform/modules.ts
@@ -1,4 +1,4 @@
-import { Activity, Bell, BookUser, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, Form, Inbox, LayoutDashboard, LayoutTemplate, Mail, Mails, RadioTower, Shield, Users, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
+import { Activity, Bell, BookUser, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, Form, Inbox, LayoutDashboard, LayoutTemplate, Mail, Mails, RadioTower, Shield, ShieldCheck, Users, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
import installedWebModules from "virtual:govoplan-installed-modules";
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
import {
@@ -68,6 +68,7 @@ const iconByName: Record = {
operator: RadioTower,
"radio-tower": RadioTower,
reports: ClipboardPenLine,
+ "shield-check": ShieldCheck,
templates: LayoutTemplate,
users: Users,
waypoints: Waypoints,
diff --git a/webui/vite.config.ts b/webui/vite.config.ts
index 42a7021..8b93d65 100644
--- a/webui/vite.config.ts
+++ b/webui/vite.config.ts
@@ -29,6 +29,7 @@ const defaultWebModulePackages = [
"@govoplan/ops-webui",
"@govoplan/policy-webui",
"@govoplan/postbox-webui",
+ "@govoplan/risk-compliance-webui",
"@govoplan/scheduling-webui",
"@govoplan/views-webui",
"@govoplan/workflow-webui"
@@ -129,6 +130,7 @@ export default defineConfig({
fileURLToPath(new URL('../../govoplan-ops/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-policy/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-postbox/webui', import.meta.url)),
+ fileURLToPath(new URL('../../govoplan-risk-compliance/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-scheduling/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))