79 lines
9.1 KiB
Markdown
79 lines
9.1 KiB
Markdown
# 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 | Manager–cashier 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.
|