Files
OMT-SM/docs/THREAT_MODEL.md
T

79 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Threat Model — OMT / Recharge Cell Shop
> **Purpose.** Document, before any code is written, every realistic way an
> employee (or a colluding pair) can steal money or goods from the shop, and
> the specific control the system must enforce to block each path. Every later
> migration, RLS policy, trigger, and UI rule must trace back to a row in this
> table.
>
> This file is **append-only** in spirit: when a new theft vector is
> discovered, add a row; do not delete history.
## Actors
| Actor | Description |
| ---------- | -------------------------------------------------------------- |
| `cashier` | Operates a till during a shift. Highest fraud-risk role. |
| `manager` | Approves voids, refunds, overrides. Can collude with cashier. |
| `owner` | Read-everything. Surprise inspections. Sets prices/fees. |
| `auditor` | Read-only third party (accountant). |
| `customer` | May be an accomplice (fake refund, fake cancellation). |
## Trust boundaries
1. **Browser/POS ↔ Supabase**: client is hostile. Never trust client-supplied
prices, fees, FX rates, timestamps, user IDs, or shift IDs.
2. **App role ↔ Postgres**: even the service role must not be able to `DELETE`
from the ledger or rewrite hashes. Enforce with table grants + triggers.
3. **Shop ↔ Provider (OMT / Alfa / touch / Bank)**: the provider's statement
is the source of truth for reconciliation. Any local row not present on the
provider statement is suspect.
## Theft vectors and required controls
| # | Vector | Control (must exist before go-live) | Enforced in |
| -- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| 1 | Pocket cash, never enter the transaction | Mandatory printed receipt, sequential `reference_no` per shop, customer SMS with reference, gap-detection report | DB + server |
| 2 | Enter transaction, then edit / delete it after customer leaves | Append-only ledger; `UPDATE`/`DELETE` revoked; void requires reason + manager PIN; row hash chain | DB triggers |
| 3 | Recharge own/friend's number flagged as "test" | No "test" flag exists; every recharge debits stock or e-float | Schema |
| 4 | Sell a scratch card, keep cash, claim "lost card" | Voucher serial scanned on intake **and** on sale; loss requires manager approval; variance assigned to cashier | DB + UI |
| 5 | Use shop OMT terminal to send to themselves at zero / reduced fee | Fee schedule server-side and immutable per cashier; OMT statement reconciliation; alert on cashier as sender or beneficiary | DB + recon job |
| 6 | Manipulate FX rate (e.g. "I gave 89,000 LBP/USD instead of 90,000") | `fx_rates` populated by scheduled job; cashier selects, never types; rate stamped on each txn | DB + cron |
| 7 | Skim cash from drawer, blame "shortfall" | Blind closing count (declared before expected revealed); per-cashier variance trend; chronic shortage alert | UI + report |
| 8 | Collect customer cash, "deposit later", never deposit | Forced shift close before leaving; aging-outstanding alert > N hours; bank-deposit reconciliation | DB + recon |
| 9 | Share login with a colleague | Per-user PIN re-prompt, device binding, session timeout, no shared accounts | Auth |
| 10 | Print fake receipts on a second printer | Receipt carries server-signed QR (JWT of `txn_id`); spot-scan by owner returns DB row or "FAKE" | Server |
| 11 | Refund/void to themselves | Void/refund requires manager PIN on same device; cashier cannot self-approve; daily void report per cashier | DB + UI |
| 12 | Sell recharge below price ("friend discount") | Price list server-controlled; cashier UI has no price field; override only by manager and logged | DB + UI |
| 13 | Take goods (phones/accessories) without sale | Per-shift inventory count; CCTV ↔ txn time sync; variance report | UI + ops |
| 14 | Tamper with the closing count | Declared count entered first and locked; expected revealed only after; both stored | DB |
| 15 | Accomplice customer "cancels" after cash handed | Cancellations require manager + reason + photo of voided receipt; CCTV cross-check | UI + ops |
| 16 | Structure large transfers under multiple fake walk-in identities | KYC threshold per (customer, day) and (beneficiary, week); customer record mandatory ≥ threshold | DB + AML report |
| 17 | Off-hours transaction when nobody is watching | Shift hours per shop; after-hours flag + alert | DB + alert |
| 18 | Managercashier collusion to mass-void real sales | Void rate per (cashier, manager) pair trended; owner-only weekly review; voids count against shift variance | Report |
| 19 | Replay an old OMT receipt to a new customer | `external_ref` unique per provider; duplicate detection on insert | DB constraint |
| 20 | Cashier opens a second, undeclared till on the same device | One open shift per `till_id`; device fingerprint pinned to till | DB + auth |
| 21 | Cashier marks recharge "failed at provider" and keeps cash | Provider e-recharge response stored as `external_ref` and reconciled; "failed" requires provider failure id | DB + recon |
| 22 | Cashier "exchanges currency" at a worse rate than recorded, pocketing the spread | FX swap is a typed `cash_movement` with both legs at the system rate; deviation requires manager override | DB |
| 23 | Cashier deletes their browser data to "lose" pending offline transactions | Offline queue persisted with server-issued idempotency key; missing key sequences flagged on reconnect | Client + server |
| 24 | Insider (developer/DBA) silently edits the database | Hash chain on ledger; daily hash anchor exported off-site; restricted DB roles; audit of all DDL and privileged SQL | DB + ops |
| 25 | Backdated transaction to fit a doctored shift count | `occurred_at` server-side `now()`; cashier cannot set; backdate only by owner role with reason | DB |
## Non-negotiables (no go-live without these)
1. RLS on every table; no table is publicly readable or writable.
2. `INSERT`-only ledger with row hash chain; `DELETE` revoked from every role.
3. All money writes go through `SECURITY DEFINER` Postgres functions, not raw
table writes.
4. Server-controlled prices, fees, and FX rates. No cashier-typed money rules.
5. Blind cash close per shift, with declared-vs-expected variance stored.
6. External reconciliation (OMT, Alfa, touch, bank) before any month-close.
7. Per-user account, device-bound, with PIN re-prompt for sensitive ops.
8. Receipt with signed QR linking back to the ledger row.
## Review cadence
- Every new feature PR must reference at least one row above (or add one).
- Quarterly walk-through: pick 5 random rows, demonstrate the control still
works on a staging environment.