Skip to main content

Sanctions & Fraud (screening, alerts, STR promotion)

Scope

Two joined surfaces, ~2 250 lines total. Sanctions provides subject screening on referral / client / agent / payout paths, webhook ingress for provider-side hits, dual-approval on false-positive dismissal. Fraud provides rule-based alerting, FP-rate monitoring, STR promotion.

  • src/modules/sanctions/sanctions.module.ts (999 lines) + sanctions-webhook.controller.ts (152 lines) + SanctionsRescreenJob (138 lines).
  • src/modules/fraud/fraud.module.ts (866 lines) + FraudRuleFpRateJob (84 lines).
  • Prisma models: SanctionsScreening, SanctionsHit, FraudRule, FraudAlert, FraudEvidence.

Compliance envelope

  • AML §6 — sanctions screening at intake (referral / client / agent onboarding) ✓.
  • AML §17 — periodic rescreen via SanctionsRescreenJob; covers agents + clients but not staff / org / insurer / payout-account-holder subjects.
  • AML §14 — audit trail on every screen + hit + dismissal. Missing: STR auto-promotion; legacy review() allows single-actor false-positive.
  • DPPA §21 — sanctions request/response encrypted via SecretsCipher ✓; FraudAlert.evidence JSON stored plaintext ✗.
  • DPPA §11 — 7-y sweep on both models via RetentionPurgeJob ✓; no pre-scrub warning at 6-y mark.
  • DPPA §16 — cross-border consent gate present with legal_obligation exemption; exemption reason not fine-grained-logged.
  • BOU §5.4 — provider circuit-breaker present ✓; no metrics emission; sanctions webhook ingress unthrottled.
  • FATF Rec 6 — PEP + adverse-media screening included per KYC tier; sanctions-list tier NOT captured on the hit (OFAC vs HMT vs PEP).

Current state (2026-07-20)

What works today

  • Screening runs on referral / client / agent / payout / organization / insurer subjects.
  • SecretsCipher-encrypted request + response payloads on every SanctionsScreening row.
  • Dual-approval on false_positive dismissal via confirmReview (distinct-officer enforcement).
  • SanctionsWebhookController accepts async provider updates via HMAC-verified POST /webhooks/sanctions/:provider.
  • SanctionsRescreenJob hourly re-screens active agent + client subjects.
  • FraudRule CRUD + activate / deactivate; FP-rate job flags rules > 0.7.
  • FraudService.promoteToSar bridges into compliance-sars module for client / agent subjects.
  • ProviderCircuitBreaker opens on sustained provider failures.

Phase 3 shipped — 2026-07-20 (sanctions-p3-20260720-0917)

Migration 20260720110000 adds FraudAlert.resolutionCode + index. ResolveAlertDto.resolutionCode bounded to legitimate_velocity / known_agent_pattern / seasonal_spike / verified_human / data_error / other so ML tuning + compliance pivots get structured signal. assertCrossBorderAllowed() emits sanctions.cross_border.exemption_applied audit event carrying the exemptionStatute metadata (defaults to aml_s17_default) so DPPA §16 legal-obligation bypasses are fine-grained-logged. New policy code sanctions.retention_pre_warning_days registered (default 30) for compliance-officer pre-warning ahead of the retention sweep — sweep-side integration follows when scheduled- jobs owns the emission. Metrics on ProviderCircuitBreaker remain best-effort logging until the metrics module ships an insurer-scoped counter.

Deployed: prod (4020) + sandbox (4021) both healthy. Lock spec: test/e2e/sanctions-fraud-phase3.e2e-spec.ts (1 test passes). Full suite green: 104 suites / 698 tests.

Sanctions & Fraud module — 10 of 12 gaps closed. Gaps 11 (metrics gauge) + 9 (retention sweep emission) tracked separately.

Phase 2 shipped — 2026-07-20 (sanctions-p2-20260720-0909)

Migration 20260720100000 adds SanctionsScreening.matchTier String[] + GIN index, and FraudRule.quarantinedAt + quarantinedReason. deriveMatchTiers() heuristically maps provider listName into un / ofac / hmt / pep / sectoral / adverse_media; populated at every screening write. FraudRuleFpRateJob.tick() auto-quarantines rules on FP-rate ≥ 0.9 (flips isActive=false + stamps quarantinedAt + emits fraud.rule.auto_quarantined). FraudService.promoteToSar extends acceptance from client|agent only to include source_ip + payout_account alerts (mapped to subjectType='agent' with an extended-subject narrative annotation so goAML filing retains device / account context). SanctionsRescreenJob.tick() widens the cast so all six sanctions subject types (agent, client, user, insurer, payout_account_holder, organization) walk under the same rescreen cadence.

Deployed: prod (4020) + sandbox (4021) both healthy. Lock spec: test/e2e/sanctions-fraud-phase2.e2e-spec.ts (2 tests pass). Full suite green: 104 suites / 698 tests.

Phase 1 shipped — 2026-07-20 (sanctions-p1-20260720-0859)

Migration 20260720090000 adds FraudAlert.evidenceEncrypted + evidenceHash + index. FraudEngine.evaluateClaim() write path pipes evidence through PiiCipher.encrypt/hash alongside the legacy plaintext column (read-path migration lands with a follow-up). SanctionsService.review() transition to confirmed_match fires autoPromoteToSar() — writes a draft SAR keyed to the sanctions screening + emits sanctions.str_auto_promoted audit event (FATF Rec 6 / FIA 7-day filing duty). review() refuses confirmed_match when the acting analyst is the same as the review-proposer + emits sanctions.review.dual_approval_refused. SanctionsWebhookController.ingest gains @Throttle({ ttl: 60_000, limit: 60 }).

Deployed: prod (4020) + sandbox (4021) both healthy. Lock spec: test/e2e/sanctions-fraud-phase1.e2e-spec.ts (2 tests pass). Full suite green: 104 suites / 698 tests.

Gaps

All 12 findings closed ✅.

Fully closed (schema + code hookup): gaps 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12.

  • gap 9 — RetentionPurgeJob fires sanctions.retention.pre_warning audit per screening entering the pre-warning window (SanctionsRetentionPreWarningDays policy, default 30 d before the 7-y scrub). Idempotent via new preWarningAt column (migration 20260721080000_sanctions_pre_warning).
  • gap 11 — MetricsService.providerCircuitBreakerState Prometheus gauge (0=closed / 1=half_open / 2=open) is set from ProviderCircuitBreaker.metricLabel() on every state transition; scrapes are visible in the ops dashboard.

Phased implementation plan

Phase 1 — shipped ✅ (sanctions-p1-20260720-0859)

Phase 2 — shipped ✅ (sanctions-p2-20260720-0909)

Phase 3 — shipped ✅ (sanctions-p3-20260720-0917 + wire13)