Appearance
Advisory intelligence contract (pilot)
All Waspada FMS alerts are advisory intelligence. The bank owns any freeze / risk-score decision.
Machine-readable fields (gateway-signed)
| Field | Pilot value | Notes |
|---|---|---|
disposition | advisory | actionable requires corroboration_count ≥ CORROBORATION_ACTIONABLE_THRESHOLD (default 3). Unreachable with a single design partner. |
client_reported_confidence | 0–1 | SDK extraction_confidence — client-claimed, not gateway-assessed. Signed for integrity only. |
mule_key | HMAC-SHA256(consortium_pepper, "waspada-mule-key/v2"|account|bic) | Stable identity for dedupe / corroboration. Keyed, not a bare hash — an unkeyed digest of a short account number is GPU-enumerable and would be re-identifiable. |
mule_key_epoch | integer | Which pepper produced mule_key. Join on (mule_key, mule_key_epoch); 0 = retired unkeyed construction. |
corroboration_count | 1 | This submission only (Phase A). Partner-signed counts only — see source tier. |
action_requested | MONITOR | Never ELEVATE_RISK_SCORE while advisory. |
urgency | HIGH / CRITICAL | Enrichment signal (e.g. young domain). Not a freeze instruction. |
advisory_notice | fixed string | Also covered by gateway_signature. |
source_tier | partner_signed (pilot) | First-class trust field — see below. |
Do not wire urgency alone to an auto-freeze. Verify gateway_signature over the canonical signed payload (includes disposition / client_reported_confidence / mule_key / mule_key_epoch — contract v2).
STIX egress stamps the same advisory framing on indicator and threat-actor objects (x_waspada_disposition, real mapped confidence — never a fabricated 95). Auto-actionable STIX indicators are suppressed while disposition is advisory (federation rule).
Analyst dispositions
POST /api/v1/alerts/disposition records { alert_id, mule_key, disposition } as a structured disposition_recorded log event (Phase A). There is no queryable store until Phase B.
Source tier & public-triage quarantine (ADV-005)
Status: WhatsApp link-only entry is live (
wa.me→ on-device sandbox). Anonymous gateway ingest /triage_quarantinewriter / magic-link mint are not shipped yet.
Pilot today: only thepartner_signedpath is bank-affecting. Quarantine table andsource_tierenforcement land with public ingest — not after.
Crowd / anonymous reports are a poisoning vector into corroboration and FMS. Most “report a scam” apps let crowd reports drive blocklists directly. Waspada’s pilot posture: public triage cannot reach bank FMS because the ingest door is closed — not because a runtime source_tier / quarantine check refuses it yet. The four checkpoints below are the ship gate before any public ingest opens. That is the honest differentiator for the CISO pack.
Taxonomy (source_tier)
| Tier | Origin | Trust |
|---|---|---|
partner_signed | Partner RS256 JWT ingest | May increment corroboration, may reach FMS (advisory), may federate via STIX under advisory rules. |
public_triage | Anonymous sandbox / WhatsApp magic-link funnel | Quarantine tier. Never bank-affecting on its own. |
analyst_promoted | A public_triage indicator an analyst explicitly reviewed | Analyst is the accountable party; only then may the indicator count / federate as if partner-grade (still advisory until corroboration threshold). |
mule_key remains the join key across tiers. Promotion happens only via (a) analyst action that sets analyst_promoted, or (b) an independent partner_signed event for the same mule_key.
Hard “never”s — public_triage (named checkpoints)
| Invariant | Enforcement point |
|---|---|
Never increments corroboration.report_count | Corroboration UPSERT in persistAlert / alerts-repo — skip when source_tier = public_triage. |
Never creates a delivery_outbox row / reaches a bank FMS | enqueueDelivery — refuse / no-op for public_triage. |
Never counts toward the corroboration_count that gates advisory → actionable | resolveAdvisoryContract — only partner_signed (+ post-promotion analyst_promoted) reports contribute. |
| Never emits an auto-actionable STIX indicator SDO | STIX formatter — suppress actionable indicators while disposition is advisory; public_triage never raises disposition. |
Never is written to alerts | Public ingest routes to triage_quarantine, not alerts. |
What public_triage may do
- Extract and show indicators to the submitting user on-device (sandbox).
- Persist to a separate
triage_quarantinetable (encrypted envelopes / tenancy pattern aligned with B0 where applicable). - Surface on an analyst queue for review.
- Join later to a
partner_signedevent on the samemule_key(partner event remains the bank-facing authority).
Quarantine store governance (PDPA)
Anonymous-sourced account numbers are still pseudonymous personal data. triage_quarantine is its own governed surface — not an ungoverned side pool.
| Control | Rule |
|---|---|
| Retention | 30 days default (retention_expires_at) — shorter than the 90-day partner alerts default. |
| Erasure | Crypto-shred / delete row + purge mule_key from quarantine indexes; same backup-bound discipline as partner stores. |
| DPIA | Own line in DPIA — purpose, legal basis, retention, poisoning residual risk. |
| Runbook | Row in Retention & erasure. |
STIX federation
Automated STIX federation suppresses auto-actionable indicator SDOs while disposition is advisory. public_triage origin never produces actionable disposition. Federation of quarantined indicators (if any) must carry x_waspada_source_tier: public_triage and remain non-actionable until promotion + contract rules allow.
CI gate: verifyStixFederationSafety / tests/stix-federation.test.ts — formatter omits patterned indicator SDOs while disposition=advisory; crafted advisory+pattern bundles fail with opaque reason STIX_ADVISORY_PATTERNED_INDICATOR.
Implementation gate
Do not ship WhatsApp magic-link mint, anonymous sandbox→gateway ingest, or a triage_quarantine writer until:
source_tieris present on the ingest/alert path and checked at the four checkpoints above, and- Mint endpoints (if any) are authenticated + rate-limited (never an open link-minting oracle).
Until then, public sandbox stays client-local (draft police report on-device; no anonymous gateway write into alerts / corroboration / outbox).
Abuse resistance & fairness (ADV-006 framing)
last-verified:
615b0d6(main) · doctrine aligned with ADV-005 + B0 corroboration schema
Audience: CISO / legal — answer to “you don’t collect the victim’s identity, so isn’t this abusable?”
Separate three concerns
| # | Concern | Waspada stance |
|---|---|---|
| 1 | Victim PII (name, NRIC, phone) | Deliberately not collected / stripped. Privacy moat — not an authentication control. |
| 2 | Submitter authentication (who sent the report) | Orthogonal to #1. Partner path = RS256 JWT (iss / tenant). Public path = untrusted → quarantine. |
| 3 | Report verifiability (is the claim true?) | Unsolvable from telemetry alone. Every indicator is a signal, not court evidence. |
Not collecting victim PII does not mean “accept anonymous unauthenticated bank-affecting reports.” It means: authenticate the channel, quarantine the crowd, keep output advisory.
What already holds
| Control | What it does | Where |
|---|---|---|
| Partner JWT | Bounds abuse to an onboarded, contractual, revocable institution — not the open internet | Ingest auth |
| ADV-005 quarantine | public_triage never bumps corroboration, never FMS, never actionable STIX alone | Source tier |
| Advisory + bank owns freeze | No single report auto-freezes | This contract |
| Distinct-tenant corroboration schema | PRIMARY KEY (mule_key, reporting_tenant_id) + CHECK (reporting_tenant_id = tenant_id) — one partner cannot appear as N independent reporters for the same mule | B0 corroboration table |
| Pilot gate | Phase A always corroboration_count = 1 → disposition stays advisory | resolveAdvisoryContract |
Phase C invariant (must enforce when multi-partner corroboration ships): corroboration_count that gates advisory → actionable is COUNT(DISTINCT reporting_tenant_id) for that mule_key, not SUM(report_count). A single partner may increment its own report_count; that must never alone cross CORROBORATION_ACTIONABLE_THRESHOLD.
Residual abuse vectors (open — named for CISO review)
| Residual | Risk | Status | Required control |
|---|---|---|---|
| Griefing / noise flood | Alert fatigue; repeated flags on a legitimate account | Partial — JWT + rate limits exist; no behavioural anomaly layer yet | Per-iss / per-mule_key rate + anomaly detection before promotion / federation |
| Analyst-promotion social engineering | Quarantine escape hatch launders a fake into partner-grade signal | Doctrine only | Dual-review or maker-checker; show provenance (source_tier, original channel); log accountable analyst_id |
| Unverifiable claims | Signed ≠ honest; no victim testimony or bank ledger in Waspada | By design | Pitch as intelligence, not evidence; bank must join to their own txn data |
| Wrongful flag of alleged mule | Accused account holder may be innocent / mistyped | Inherent | Advisory-only; corroboration; human-in-loop; appeals / reversal (below) |
Fairness — the harder PDPA probe
Data we do process about the alleged perpetrator (account / BIC → mule_key) can implicate an innocent third party (dupe mule, OCR error, revenge report). Reporter privacy does not erase that duty. Ethical license to operate:
- Advisory disposition + human / bank decision
- Distinct-partner corroboration before any stronger disposition
- Documented appeals / reversal path for a wrongly flagged account
Appeals / reversal (pilot process — operational, not fully productized)
| Step | Owner | Action |
|---|---|---|
| 1. Challenge received | Partner bank / Waspada ops | Record mule_key, alert_id(s), challenger channel (bank case ID preferred — not victim NRIC in our store) |
| 2. Disposition | Analyst / partner | Mark false_positive / dismissed via disposition API or partner case system |
| 3. Containment | Engineering | Suppress further FMS re-drive for that mule_key under partner policy; purge quarantine rows if public-sourced |
| 4. Erasure | Per retention & erasure | Crypto-shred alert row + purge mule_key from corroboration / quarantine indexes when legally required |
Pilot: process is manual / partner-led. Product automation (self-serve appeal portal) is not claimed.
What we still do not claim
- That anonymity is “safe” without quarantine
- That JWT signature proves the underlying scam happened
- That Waspada can adjudicate innocence of an account holder
- Automated freeze from public or single-partner reports
- That extraction / OCR /
blinded_hash/ client confidence proves a slip is genuine
Evidential integrity — fake slips & false flags (ADV-006b)
Audience: CISO — “What if someone uploads a Photoshopped / GenAI Maybank slip to frame a legitimate account?”
Transcriber ≠ authenticator
The extraction pipeline (OCR / regex / NER) reads pixels. It cannot, by design, tell a genuine bank slip from an edited or AI-generated image. In Malaysia, fake “payment successful” receipts are already a scam tool; convincing fakes are cheap.
| Thing people hope will save them | Why it doesn’t |
|---|---|
blinded_hash / gateway signature | Authenticates bytes after extraction, not truth of the claim |
| Extraction accuracy (P/R on corpus) | Measures transcription fidelity, not authenticity |
| On-device / edge model | Attacker controls device and input |
client_reported_confidence | Computed on a hostile device — assertable |
A fake slip is the concrete instance of concern #3 above: every slip is an assertion; a fake slip is a false assertion undetectable at extraction.
What actually contains fake slips
Do not promise “we detect Photoshop.” Contain blast radius:
| Layer | Effect on a fake slip |
|---|---|
| ADV-005 quarantine | Public / WhatsApp fake → never corroboration / FMS alone |
| Advisory + bank owns freeze | One fake triggers nothing automated |
| Distinct-tenant corroboration | Actionable needs N independent partners (Phase C invariant) — hard to forge across banks |
| Bank ledger join (FI-side) | Ultimate ground truth: “Did this account actually receive that transfer?” — Waspada does not hold the ledger |
Targeted false-flag (innocent merchant / personal account on a fabricated “I paid this scammer” slip) is the high-impact version. Contained only by advisory + corroboration + appeals — catastrophic if a bank treats the feed as freeze authority.
Cheap structural validation (edge — filter lazy fakes, not perfect ones)
Shipped today (partial): known Malaysian BIC set; account digits in a broad 8–16 range; DuitNow “no account” treated as correct null.
Roadmap (near-free, on-device): bank-specific account length / format when BIC is known (e.g. Maybank 12-digit expectations); reject or down-score STRUCTURAL_MISMATCH — kills low-effort garbage, not sophisticated fakes.
Forgery / tamper scoring (later — confidence input, never a hard gate)
Template match / metadata / GenAI artifacts against a labeled real-vs-fake MY-slip corpus. Score only; hostile devices can defeat on-device scoring. Corroboration remains the backstop. Do not enter an arms race that bloats the SDK into a “fake detector” product.
Source reputation (roadmap)
Sources whose reports never corroborate get down-weighted; flood patterns on iss / mule_key get throttled — ties to the griefing residual above.
Contract language (say this to banks)
We extract and correlate indicators; we do not — and cannot — prove a slip is genuine. Authenticity is established by independent corroboration and the bank’s own transaction view, never by trusting one image. Waspada output is advisory intelligence, not evidence for automated freeze.
Build vs claim
| Item | Status |
|---|---|
| Intelligence-not-evidence doctrine | Claimed — this page + advisory fields |
| Quarantine + advisory + distinct-tenant schema | Shipped / designed |
| Bank-specific structural mismatch abort | Partial — tighten regex / BIC↔length map |
| Forgery scoring as confidence | Not built — labeled corpus first |
| Behavioural source reputation | Not built |
| Productized appeals portal | Not built — partner-led process above |