Skip to main content

SAR/STR Lifecycle & Tipping-Off Boundary

Trust Relay implements the Suspicious Activity / Suspicious Transaction Report (SAR/STR) lifecycle as a fail-closed, no-skip state machine with MLRO four-eyes on the filing decision, plus a runtime tipping-off boundary that keeps investigation internals off every customer-facing surface. This page documents both, and the SAR-first customer-contact gate that ties them together.

Regulatory basis: AMLD Article 33 (obligation to report), Article 39 (prohibition of tipping-off), and AMLR/6AMLD record-keeping (5-year retention of the SAR audit trail). Design decisions: ADR-0071 (lifecycle + gate) and ADR-0070 (four-eyes, reused for the MLRO filing decision).

Lifecycle state machine

SARStatus (app/services/sar_service.py) is a five-state forward-only machine plus a reject branch:

The machine is no-skip: an unknown or out-of-order transition resolves to nothing (the permitted-transitions set is <none — terminal> for terminal states), so a SAR can never be auto-submitted or jump from draft to submitted. Every transition writes an immutable audit_events row (ADR-0064) recording the actor and both parties on the four-eyes steps.

Endpoints (app/api/sar.py)

All state-changing routes are per-case and RBAC-gated (app/api/deps/permissions.py): officers hold SAR_RAISE; SAR_APPROVE and SAR_FILE are mlro+ only. The reportability assessment — the determination that unlocks the SAR-first customer-contact gate — is deliberately SAR_APPROVE (MLRO-level), so an officer cannot self-clear the gate. The GET list is authenticated but not permission-gated (any tenant user may read); tighten it to CASE_READ if SAR visibility must be role-limited.

MethodPathPermissionPurpose
POST/cases/{case_id}/sarSAR_RAISERaise a draft SAR
GET/cases/{case_id}/sarauthenticated (no permission gate)List SARs for the case
POST/cases/{case_id}/sar/{sar_id}/submit-for-mlroSAR_RAISEdraft → pending_mlro
POST/cases/{case_id}/sar/{sar_id}/assessmentSAR_APPROVE (mlro+)Record the structured reportability assessment (MLRO determination)
POST/cases/{case_id}/sar/{sar_id}/mlro-approveSAR_APPROVE (mlro+)pending_mlro → approved (four-eyes)
POST/cases/{case_id}/sar/{sar_id}/mlro-rejectSAR_APPROVE (mlro+)pending_mlro → rejected (four-eyes)
POST/cases/{case_id}/sar/{sar_id}/record-submissionSAR_FILE (mlro+)approved → submitted, capture FIU channel
POST/cases/{case_id}/sar/{sar_id}/acknowledgeSAR_FILE (mlro+)submitted → acknowledged, capture FIU reference

The MLRO approve/reject steps call assert_distinct_approver (reusing app/services/maker_checker.py), so the person who approves the filing must differ from the officer who raised it — the SAR filing decision is itself four-eyes.

Structured reportability assessment

Before a decline/follow-up can reach the customer, an MLRO must record a structured reportability assessment (app/services/sar_assessment.py, DB columns via migration 071). Its AssessmentOutcome is one of required, not_required, or further_info_needed, and it captures the onboarding_interaction disposition (decline_no_sar, decline_sar_filed, defer_edd, other). This artifact is rendered into the case pack (templates/sar_assessment.html) and its inclusion is fail-closed: if the assessment cannot be built, the pack build fails rather than shipping a pack that silently omits it.

SAR-first customer-contact gate

The single most important control here is that no customer contact happens on a case with an open SAR predicate until a signed MLRO assessment exists. Contacting a customer whose case shows a criminal/enforcement signal — asking for "just one more document" — risks tipping off a subject under investigation, which is a criminal offence under AMLD Art. 39.

The trigger and the unlock are single-sourced in app/services/sar_assessment.py (has_sar_trigger_signal / the unlock predicate) and enforced by app/services/customer_contact_gate.py (assert_customer_contact_allowed, raising SARFirstGateError → HTTP 409/423) at four surfaces:

  1. Officer reject / follow_up decisions (app/api/case_decisions.py:187)
  2. Evidence-request PDF issuance (app/api/compliance_docs.py:113)
  3. The customer portal (app/api/portal.py — hold sites return 423)
  4. The portal chat agent (app/api/agent.py)

There is no officer override — the gate unlocks only when an MLRO determination (assessment or a decided lifecycle state) exists. This is unlock-by-determination, not unlock-by-permission.

Tipping-off boundary (default-deny scrub)

Independently of the gate, every customer-facing payload passes through a default-deny field scrubber (app/services/tipping_off.py). scrub_customer_payload removes every SAR-indicative key at any depth of the response, and assert_customer_payload_sar_free runs on the scrubbed payload as a tripwire — it normally passes, but raises TippingOffViolation the instant the scrubber's vocabulary and a leaking field diverge. Because the boundary is default-deny on the field name, a newly-added internal field is withheld from the customer by default rather than leaking until someone remembers to add it to a denylist.

:::note Denylist vs. allowlist The scrubber keys off SAR-indicative field names. A safer-still posture is an explicit allowlist of customer-safe fields; see the known gaps register for the residual items the review flagged around portal payload shaping. :::

Officer UI

The officer drives the SAR flow from SarPanel.tsx (Regulatory Filing tab) — raise → submit to MLRO → record FIU reference → acknowledgement — and the maker-checker hold renders in PendingSecondApproval.tsx. See State Machine → Decision Gates for how the SAR-first gate blocks the decision transition, and Case-Pack Export for how the SAR assessment is sealed into the regulator pack.