Skip to main content

ADR-0131: Sanctions FP-suppression never-suppress hardening — force-invalidation on (re)designation, determination bypass, expiry/re-confirmation

Date: 2026-07-24 Status: Accepted Deciders: Adrian (Soft4U BV), Claude Opus 4.8 (implementation + safety analysis)

Decision context:

  • Latency: negligible. The force-invalidation adds one indexed sanctioned_record_id IN (delta) UPDATE per sanctions refresh (bounded by the day's delta) and one audit write per invalidated rule; it runs only inside the already-dark-launched immediate-designation re-screen path (immediate_designation_rescreen_enabled). check_active_rule gains one invalidated_at IS NULL predicate (same index). Not measured because it rides an existing daily-refresh background pass, not the request path.
  • Dependency surface: zero new packages. Reuses AuditService (ADR-0064/0109 hash chain), get_admin_session (ADR-0050), and the existing suppression service/model.
  • Debuggability: an invalidated rule is queryable (list_rules(status="invalidated")), each invalidation writes an immutable sanctions_suppression_redesignation_invalidated audit event carrying the record id + prior rationale, and a LOUD logger.warning("(re)designated while suppressed: …") fires. Failure mode of the audit write is logged at ERROR but never re-suppresses the rule.
  • Reversibility: schema is Alembic 096 (apply/downgrade verified). The determination-bypass is a builder addition + a read-site preference (sanctions_matches_raw with fallback). Force-invalidation is coupled to the existing immediate_designation_rescreen_enabled flag. No standalone new flag.
  • Blast radius: additive. check_active_rule only ever WITHHOLDS suppression (fail-closed), never adds it. The officer-facing sanctions_matches / EBA #513 band is byte-unchanged; only a new parallel sanctions_matches_raw feeds the deterministic determinations. With the re-screen flag off, invalidated_at is never set, so behaviour is identical to pre-#587.
  • Alternative considered: blanket-remove Tier-2 suppression (rejected — destroys the legitimate FP-suppression use the officers rely on, and the constraint is to close the STALE/re-designated/bypass cases, not suppression itself).

Context

The tenant-editable sanctions false-positive suppression subsystem (ADR-0045, Tier 2 — officer-originated learned rules persisted to sanctions_suppression_rules) lets a compliance officer mark a sanctions match as a false positive so it stops firing. A whole-app design review (AMLR readiness epic #528, Mini-Wave 0, issue #587) found this is the single most dangerous "hide a signal" mechanism in the codebase, violating the never-suppress doctrine (ADR-0067) in three ways:

  1. Stale rule silences a fresh Art. 26(4) hit. The immediate-on-designation re-screen (ADR-0128) re-screens active customers when the OpenSanctions refresh (re)adds a designation. But the re-screen's global path (evaluate_new_designation_match) inspected only the requires_review bucket — a customer with a Tier-2 suppression rule had the fresh hit routed into suppressed_by_rule, where it was not counted. A stale FP rule could therefore silence the very hit the re-designation re-screen exists to catch.

  2. Suppressed FP hides a sanctioned owner from the ADR-0127 determination. The deterministic sanctioned-ownership/control test (AMLR Art. 20(1)(d), EU 50%-rule) consumes investigation_result["sanctions_matches"], which sanctions_match_builder built from the post-suppression requires_review bucket only. A Tier-2 rule moved a hit into suppressed_by_rule, so a sanctioned owner an officer had suppressed was invisible to the ownership graph test — it could never floor the subject to CRITICAL.

  3. No expiry-driven re-confirmation / invalidation dimension. A suppression rule carried a 12-month expires_at but nothing invalidated a rule when the underlying record was re-designated, and there was no re-confirmation timestamp — the rule silently rode its original expiry regardless of list changes.

Additionally, sanctions_suppression_rules carried only a tenant_isolation RLS policy (migrations 054/060) and no admin_bypass — the #561 single-policy gap — so a cross-tenant admin-session sweep would see zero rows.

Decision

Close the hole with three fail-closed guarantees, keeping the legitimate FP-suppression use intact (a genuine, still-valid FP on an UNCHANGED list stays suppressed).

1. Force-invalidation on (re)designation (the core fix). On the OpenSanctions refresh, the ADR-0128 delta (compute_designation_delta) drives a new cross-tenant pass, invalidate_redesignated_suppressions: for every active FP-suppression rule whose sanctioned_record_id is in the fresh delta, stamp invalidated_at + invalidation_reason (SanctionsSuppressionService.force_invalidate_for_designations). An invalidated rule FAILS CLOSED in check_active_rule (it does not suppress) until an officer re-confirms — "a re-designation always wins". Each invalidation writes an immutable, per-tenant hash-chained sanctions_suppression_redesignation_invalidated audit event (ADR-0064/0109) and logs a LOUD "(re)designated while suppressed" warning; the subsequent screen sweep, now un-suppressed, raises the normal per-case Art. 26(4) monitoring alert into the officer's EDD queue. The pass runs under get_admin_session (the delta is global) and is coupled to the existing immediate_designation_rescreen_enabled flag.

2. Deterministic determinations BYPASS suppression. A new sanctions_match_builder.build_raw_sanctions_matches builds a parallel investigation_result["sanctions_matches_raw"] from requires_review plus suppressed_by_rule (Tier-2 officer-editable suppression bypassed; each such record stamped fp_suppression_bypassed=True to preserve the officer's prior-FP context). The ADR-0127 ownership test reads sanctions_matches_raw (fallback to sanctions_matches for older investigations); the ADR-0128 evaluate_new_designation_match now also scans suppressed_by_rule. Tier-1 evidence-based auto_dismissed (≥2 formal discriminators contradict — a DIFFERENT identity) stays EXCLUDED: it is regulator-defensible evidence, not the tenant-editable stale-able hole, and re-surfacing it would violate presence ≠ evidence. The officer-facing sanctions_matches / EBA #513 band is unchanged.

3. Governance hardening. Alembic 096 adds last_reconfirmed_at, invalidated_at, invalidation_reason to sanctions_suppression_rules, the missing admin_bypass RLS policy (closing the #561 gap), and a partial index on sanctioned_record_id. record_dismissal becomes the re-confirmation path: a re-dismissal clears invalidated_at/invalidation_reason and stamps last_reconfirmed_at (an audited officer re-review WITH the re-designation known). list_rules excludes invalidated rules from active and adds an invalidated bucket for the dashboard. The officer-facing config panel is a separate follow-up (#583); this ADR is backend + audit + the alert only.

Consequences

Positive

  • A stale FP-suppression rule can never silence a fresh Art. 26(4) designation hit nor hide a sanctioned owner from the ADR-0127 CRITICAL floor — the named never-suppress hole is closed.
  • Fail-closed by construction: an invalidated / un-reconfirmed / expired rule does not suppress; the deterministic determinations bypass suppression entirely (defense in depth with the force-invalidation).
  • Every invalidation is immutably audited and loud; a re-designation is surfaced to the officer.
  • The admin_bypass gap on sanctions_suppression_rules (a latent cross-tenant defect) is fixed.

Negative

  • A legitimately-suppressed Tier-2 name-only FP now also surfaces in the ownership test as a labelled sanctioned_name_candidate (medium, never a floor). This is added scrutiny/noise on a previously-quiet surface; mitigated by the fp_suppression_bypassed provenance marker and the two-lane design (name-only never floors). Acceptable under never-suppress (surface, don't drop, don't inflate).
  • Force-invalidation is coupled to immediate_designation_rescreen_enabled (dark-launched OFF); until that flag flips, a re-designation does not auto-invalidate a rule. The always-on parts (check_active_rule fail-closed, raw-view bypass) still protect the determinations regardless.
  • One more Alembic migration on the hot suppression table.

Neutral

  • sanctions_matches_raw is a new investigation-result key carried alongside sanctions_matches; consumers that read sanctions_matches are unaffected.

Alternatives Considered

Alternative 1: Blanket-remove Tier-2 suppression

  • Delete officer-originated suppression entirely so nothing can ever hide a hit.
  • Why rejected: destroys the legitimate, regulator-recognised FP-suppression use (a verified name-collision on an unchanged list). The mandate is to close the stale/re-designated/bypass cases, not suppression itself.

Alternative 2: Include Tier-1 auto-dismissed hits in the raw determination view too

  • Feed every screened hit (including evidence-based Tier-1 auto-dismissals) to the deterministic determinations for maximal "raw".
  • Why rejected: a Tier-1 auto-dismissal is ≥2 unambiguous formal-discriminator contradictions — regulator-defensible EVIDENCE the hit is a different identity, recomputed per scan, non-editable. Re-surfacing it as an ownership candidate would fabricate a signal against its own evidence (presence ≠ evidence). Only the tenant-editable, stale-able Tier-2 layer is the hole.

Alternative 3: Emit a standalone rule-level MonitoringAlert instead of relying on the re-screen

  • Raise a MonitoringAlert per invalidated rule.
  • Why rejected: a suppression rule carries no case_id, and MonitoringAlert/route_detection are per-case. The immutable audit event is the correct rule-level record; the officer is told loudly via the normal per-case Art. 26(4) alert the (now un-suppressed) re-screen raises.