Skip to main content

Sanctions Screening

Sanctions screening infrastructure for matching entities against international sanctions lists, plus the ADR-0045 three-tier false-positive (FP) suppression pipeline that keeps officer review queues clean without ever deleting a hit.

Screening runs during the OSINT investigation stage and is invoked again, recurrently, by Continuous Monitoring. It covers the subject company plus all natural persons associated with the entity — directors and beneficial owners — against the locally-cached OpenSanctions table, and every raw hit is partitioned into one of three visible buckets: auto_dismissed (Tier 1 evidence), suppressed_by_rule (Tier 2 learned rule), or requires_review (officer must decide).

Screening Coverage — Subject + All Natural Persons (issue #33)

Screening is not director-only. The canonical natural-person screening set merges directors and beneficial owners, so a sanctioned UBO who is not a board member is no longer missed:

persons = merge_screening_persons(directors_detailed, ubos) # dedup; directors first
queries = build_director_queries(persons, last_activity_date=...)

merge_screening_persons (sanctions_screening_pipeline.py) adds directors first, then UBOs, deduplicating by lowercased name. When the same individual appears as both a director (richer record carrying the DOB/nationality discriminators that reduce false positives) and a UBO (often name-only), the more complete director record is kept. This is not fabrication: a name-only record screens by name with UNKNOWN discriminators, which the suppression engine skips rather than scoring as a mismatch (ADR-0045 invariant — never fabricate a discriminator).

The subject company is screened via build_company_query (name + registration country + LEI), and each natural person via build_director_queries, which populates DOB and nationality only from explicit registry fields — never substituting a board-mandate start date for a DOB or the company's registration country for the director's nationality.

Recurring Re-Screening & Evidence Trail

The same screen_entities pipeline is invoked recurrently by Continuous Monitoring for risk-paced re-screening of the onboarded population (ADR-0066). The monitoring check re-runs merge_screening_persons + build_director_queries + screen_entities under a tenant-scoped session, gated by a per-case risk cadence (EDD 90d / CDD 180d / SDD 365d).

Post-suppression results are persisted to the typed, append-only screening_results evidence trail (ADR-0063, issue #45) as timestamped ScreeningResult rows — recording "screened, clean" rows as well as hits, so the trail constitutes ongoing-monitoring proof (AMLR Art. 21/26). The ScreeningListType enum used for these rows gained a WANTED value alongside EU_SANCTIONS, UN_SANCTIONS, OFAC, PEP, and ADVERSE_MEDIA; sanctions classes are always classified ahead of PEP so a hit carrying both topics is never downgraded.

An indeterminate OpenSanctions lookup (ScreeningUnavailableError.error on the result) is never recorded as clean — the monitoring re-screen emits no row for that person, surfaces a WARNING, and does not advance the cadence. See Continuous Monitoring for the full cardinal-rule treatment.

False-Positive Suppression Pipeline (ADR-0045)

The suppression engine is the core of this page. It is evidence-based and always visible — the system can ADD scrutiny but never silently suppress a risk signal. Three tiers:

TierModuleBehavior
Tier 0 — name-token pre-filtersanctions_suppression_integration.pyRuns the two-token surname matcher (score_person_match); auto-dismisses hits whose surname similarity falls below SURNAME_MATCH_THRESHOLD, filtering OpenSanctions prefix/phonetic false positives (e.g. "PERKA"→"Peroutka") before they reach review
Tier 1 — evidence-based auto-dismissalsanctions_fp_suppression.pyDeterministic, no ML. Evaluates up to 6 discriminators (DOB, YOB, nationality, date-of-death vs. activity, LEI, gender) and auto-dismisses only when ≥ 2 independent discriminators contradict
Tier 2 — officer-originated learned rulessanctions_suppression_service.pyPersists officer dismissals as sanctions_suppression_rules (HMAC-SHA256 discriminator hash, tenant-salted, 12-month expiry) and checks active rules at screening time
Tier 3 — periodic re-checksanctions_suppression_service.py (flag_rules_for_review)Conservative housekeeping pass surfacing expiring/expired rules into the dashboard renewal queue; never auto-revokes or auto-extends

Tier 1 requires compositional evidence — a single-discriminator mismatch (which could be a data-entry error) never auto-dismisses. Every Tier-1 evaluation emits a structured sanctions_fp_tier1_evaluated audit event citing the exact discriminator values compared (EU AI Act Art. 12 / Art. 14). Every Tier-2 dismissal requires a non-empty officer rationale (Art. 13) and writes an audit event.

Components

ModulePurpose
sanctions_screening_pipeline.pyOrchestrates per-entity screening: queries OpenSanctions via match_local, normalises rows, calls partition_hits, and aggregates the result for the OSINT payload (screen_entities, build_company_query, merge_screening_persons, build_director_queries, summarize_screening). Raises ScreeningUnavailableError when OpenSanctions data is not loaded (surfaced as .error, never recorded clean)
sanctions_fp_suppression.pyTier 1 deterministic discriminator evaluation (evaluate_suppression, build_audit_event_payload). Defines CustomerDiscriminators and SanctionedRecordDiscriminators
sanctions_suppression_service.pyTier 2/3 SanctionsSuppressionService: record_dismissal, revoke_rule, check_active_rule, list_rules, flag_rules_for_review, plus the tenant-salted HMAC discriminator hash
sanctions_suppression_integration.pyAdapter wiring Tier 0 + Tier 1 + Tier 2 over a hit list (partition_hits); returns the three-bucket PartitionedScreening
sanctions_matcher_service.pyLocal EU sanctions list matching with three tiers: exact (normalized name), fuzzy (Jaro-Winkler), and LLM resolution for the 0.80–0.95 ambiguous zone
sanctions_feed_service.pyEU Consolidated Financial Sanctions List ingestion — fetches/parses the EU XML to produce sanctioned country codes, Redis-cached 24h, with hardcoded fallback
sanctions_constants.pySanctioned and high-risk country sets (EU 833/2014, UN consolidated, OFAC SDN informational) for per-shipment compliance checks
portfolio_service.pyRate-limited parallel Tier 1 batch scans across an entity portfolio (PortfolioService.batch_scan), persisting a Portfolio graph node with CONTAINS edges

API

  • app/api/sanctions.py — screening endpoints (screen, results)
  • app/api/sanctions_suppression.py — suppression-rule CRUD under /api/sanctions/suppression-rules (record dismissal, list by status bucket, revoke)
  • ADR-0045 — Sanctions false-positive suppression (3-tier engine, evidence-based, always visible)
  • ADR-0063 — Typed persisted ScreeningResult evidence trail (issue #45)
  • ADR-0066 — Live risk-paced UBO/director re-screening + general-population monitoring