openpay: a PEP match is recorded as a PEP match, not an OFAC sanctions hit #269
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/openpay-pep-not-ofac"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What was wrong
op-connect's PEP index (
build_pep_index_from_names) tagged every entrySanctionsList::OfacConsolidated, because the enum had nowhere to put a PEP list. The code comment said the operator surface "should not surface this".But a PEP index is a
Screener, and every screen appendssource_list.label()to that screener's signed, hash-chainedAuditLog. So every politically-exposed-person match was written into the tamper-evident compliance record as "OFAC Consolidated". The two findings call for different handling:An adverse-media index had the same problem: it could only tag its entries with a sanctions authority.
The fix
SanctionsListbecomesWatchList. That is the termScreenerConfig::watchlist_filteralready used.Pep("PEP list") andAdverseMedia("Adverse media").is_sanctions(), which is false for exactly those two.SanctionsListalias keeps the old name compiling.WatchList::Pep.Serialized forms are unchanged, because the variant names stay the same, so stored indexes still load. No published vector or doc carries the label.
Checked
Tests run on the build host:
op-screeningandop-connectpass, including the three new tests:a_pep_match_is_audited_as_a_pep_match_not_a_sanctions_matchscreens a PEP fixture, verifies the audit chain, and checks that the audit row names "PEP list";only_the_authorities_are_sanctions_listsandevery_list_has_its_own_labeluse an exhaustive match, so a new list fails to compile until someone classifies it.Screener::screen(unused async), predates this change.Each of these mutations fails a test:
OfacConsolidatedagain;🤖 Generated with Claude Code
op-connect's PEP index tagged every entry `SanctionsList::OfacConsolidated`, because the enum had no place for a PEP list. Its comment said the operator surface "should not surface this". But a PEP index is a `Screener`, and every screen appends `source_list.label()` to the screener's signed, hash-chained audit log. So each PEP match was written into the tamper-evident compliance record as "OFAC Consolidated": a sanctions designation (block, report) where there was a due-diligence flag. An adverse-media index had the same problem: it too could only tag its entries with a sanctions authority. - `SanctionsList` is now `WatchList`, the term `ScreenerConfig::watchlist_filter` already used. It gains `Pep` ("PEP list") and `AdverseMedia` ("Adverse media"), and `is_sanctions()`, which is false for exactly those two. Its docs say why a match must never be recorded under a list it did not come from. A deprecated `SanctionsList` alias keeps the old name compiling. - `build_pep_index_from_names` tags its entries `WatchList::Pep`. - Serialized forms are unchanged: the enum's variant names stay the same, so stored indexes still load. No published vector or doc carries the label. Tests: - `a_pep_match_is_audited_as_a_pep_match_not_a_sanctions_match` screens a PEP fixture and checks that the audit chain verifies and that its row names "PEP list". - `only_the_authorities_are_sanctions_lists` and `every_list_has_its_own_label` pin the kinds and labels. An exhaustive match makes a new list fail to compile until it is classified. Co-Authored-By: Claude Opus 5.5 <[email protected]>