nx_scam_detector.nx source
↩ module page · 204 lines · 8011 B
1// nx_scam_detector.nx -- inbound-communication pattern detection.
2//
3// Per [[feedback-captain-moroni-doctrine]] Phase M4: "Phishing,
4// vishing, romance-scam, BEC patterns. Defends family, never attacks
5// scammers — just refuses to be a victim."
6//
7// THE FAMILY-DEFENSE PRIMITIVE. Inbound message (SMS, email, call
8// transcript, DM) is scanned for known scam patterns; substrate
9// surfaces verdict to operator. Captain Moroni discipline: we don't
10// attack the scammer, we just don't fall for it.
11//
12// Composes:
13// nx_intent -- detector itself runs under Defensive intent
14// nx_pamp -- composes with pattern recognition layer
15// nx_xenocell -- a confirmed scammer can be tagged as foreign
16// nx_evict_journal -- all scam-flagged messages logged
17// nx_aposematism -- our cell may advertise "fraud-detection active"
18// so scammer ML backs off (similar judo to
19// vendor-scanner mimicry)
20//
21// V1 patterns shipped (the 8 highest-frequency scam classes):
22// - URGENCY_PRESSURE -- "act now" / "limited time"
23// - AUTHORITY_IMPERSONATION -- claims-to-be-IRS / police / bank
24// - PAYMENT_DIVERSION -- "new banking details for invoice"
25// - GIFT_CARD_REQUEST -- "pay with gift cards"
26// - CRYPTO_INVESTMENT -- "guaranteed returns" + "send crypto"
27// - ROMANCE_PROGRESSION -- relationship-building + money-ask
28// - GRANDPARENT_HELP -- relative-in-trouble + money + secrecy
29// - TECH_SUPPORT_INTRUSION -- "your computer has a virus, call us"
30
31import "nx_syscalls.nx"
32import "nx_tier.nx"
33import "nx_intent.nx"
34
35// ===== Sealed enum: NxScamPattern ==================================
36
37const NX_SP_URGENCY_PRESSURE: nx_int = 0
38const NX_SP_AUTHORITY_IMPERSONATION: nx_int = 1
39const NX_SP_PAYMENT_DIVERSION: nx_int = 2
40const NX_SP_GIFT_CARD_REQUEST: nx_int = 3
41const NX_SP_CRYPTO_INVESTMENT: nx_int = 4
42const NX_SP_ROMANCE_PROGRESSION: nx_int = 5
43const NX_SP_GRANDPARENT_HELP: nx_int = 6
44const NX_SP_TECH_SUPPORT_INTRUSION: nx_int = 7
45const NX_SP_N_PATTERNS: nx_int = 8
46
47// ===== Sealed enum: NxScamVerdict ==================================
48
49const NX_SC_CLEAN: nx_int = 0
50const NX_SC_SUSPICIOUS: nx_int = 1 // 1-2 patterns detected
51const NX_SC_LIKELY_SCAM: nx_int = 2 // 3+ patterns detected
52const NX_SC_CONFIRMED_PATTERN: nx_int = 3 // canonical scam shape
53const NX_SC_ERR_BAD_INTENT: nx_int = 4
54
55// ===== Struct: NxScamScanResult ===================================
56//
57// One scan returns this. flagged_patterns is a bitmask of NX_SP_*
58// kinds detected. pattern_count is popcount. verdict aggregates.
59
60struct NxScamScanResult {
61 flagged_patterns: nx_int,
62 pattern_count: nx_int,
63 verdict: nx_int,
64 scanned_at_us: nx_size,
65}
66
67func nx_sp_is_valid(p: nx_int) -> nx_int {
68 if p < 0 { return 0 }
69 if p >= NX_SP_N_PATTERNS { return 0 }
70 return 1
71}
72
73// ===== _scam_match_keyword ========================================
74//
75// Linear-scan: does the buffer contain any byte sequence in the
76// keyword table? Returns 1 on first match.
77// V1 keywords are hard-coded; V2 makes data-driven peer-shareable.
78
79func _scam_match_substring(buf: *u8, n: nx_size, kw: *u8, kw_n: nx_size) -> nx_int {
80 if kw_n == 0 { return 0 }
81 if kw_n > n { return 0 }
82 var i: nx_size = 0
83 while i + kw_n <= n {
84 var matched: nx_int = 1
85 var j: nx_size = 0
86 while j < kw_n {
87 let a: nx_int = (buf[i + j] as i64) & 255
88 let b: nx_int = (kw[j] as i64) & 255
89 // case-insensitive match for ASCII
90 var ac: nx_int = a
91 var bc: nx_int = b
92 if ac >= 65 { if ac <= 90 { ac = ac + 32 } }
93 if bc >= 65 { if bc <= 90 { bc = bc + 32 } }
94 if ac != bc { matched = 0; break }
95 j = j + 1
96 }
97 if matched == 1 { return 1 }
98 i = i + 1
99 }
100 return 0
101}
102
103// V1 keyword fixtures: minimal canonical phrases per pattern.
104// Caller of nx_scam_scan provides buf; we test against these.
105
106func _scam_check_urgency(buf: *u8, n: nx_size) -> nx_int {
107 let kw1: *u8 = (sys_mmap(16)) as *u8
108 kw1[0] = 97 as u8; kw1[1] = 99 as u8; kw1[2] = 116 as u8;
109 kw1[3] = 32 as u8; kw1[4] = 110 as u8; kw1[5] = 111 as u8;
110 kw1[6] = 119 as u8; // "act now"
111 if _scam_match_substring(buf, n, kw1, 7) == 1 { return 1 }
112 let kw2: *u8 = (sys_mmap(16)) as *u8
113 kw2[0] = 117 as u8; kw2[1] = 114 as u8; kw2[2] = 103 as u8;
114 kw2[3] = 101 as u8; kw2[4] = 110 as u8; kw2[5] = 116 as u8; // "urgent"
115 if _scam_match_substring(buf, n, kw2, 6) == 1 { return 1 }
116 return 0
117}
118
119func _scam_check_authority(buf: *u8, n: nx_size) -> nx_int {
120 let kw1: *u8 = (sys_mmap(8)) as *u8
121 kw1[0] = 105 as u8; kw1[1] = 114 as u8; kw1[2] = 115 as u8; // "irs"
122 if _scam_match_substring(buf, n, kw1, 3) == 1 { return 1 }
123 let kw2: *u8 = (sys_mmap(16)) as *u8
124 kw2[0] = 102 as u8; kw2[1] = 98 as u8; kw2[2] = 105 as u8; // "fbi"
125 if _scam_match_substring(buf, n, kw2, 3) == 1 { return 1 }
126 return 0
127}
128
129func _scam_check_gift_card(buf: *u8, n: nx_size) -> nx_int {
130 let kw: *u8 = (sys_mmap(16)) as *u8
131 kw[0] = 103 as u8; kw[1] = 105 as u8; kw[2] = 102 as u8;
132 kw[3] = 116 as u8; kw[4] = 32 as u8; kw[5] = 99 as u8;
133 kw[6] = 97 as u8; kw[7] = 114 as u8; kw[8] = 100 as u8; // "gift card"
134 if _scam_match_substring(buf, n, kw, 9) == 1 { return 1 }
135 return 0
136}
137
138func _scam_check_crypto(buf: *u8, n: nx_size) -> nx_int {
139 let kw: *u8 = (sys_mmap(16)) as *u8
140 kw[0] = 98 as u8; kw[1] = 105 as u8; kw[2] = 116 as u8;
141 kw[3] = 99 as u8; kw[4] = 111 as u8; kw[5] = 105 as u8;
142 kw[6] = 110 as u8; // "bitcoin"
143 if _scam_match_substring(buf, n, kw, 7) == 1 { return 1 }
144 return 0
145}
146
147// ===== nx_scam_scan =================================================
148//
149// THE PUBLIC SCAN. Caller passes inbound buffer; substrate sets the
150// flagged_patterns bitmask and verdict. Intent must be Defensive.
151
152func nx_scam_scan(buf: *u8, n: nx_size,
153 intent: nx_int,
154 now_us: nx_size) -> *NxScamScanResult {
155 if nx_intent_is_valid(intent) == 0 { return (0 as i64) as *NxScamScanResult }
156 if intent != NX_INTENT_DEFENSIVE {
157 if intent != NX_INTENT_DIAGNOSTIC {
158 return (0 as i64) as *NxScamScanResult
159 }
160 }
161 let r: *NxScamScanResult = (sys_mmap(32)) as *NxScamScanResult
162 r.flagged_patterns = 0
163 r.pattern_count = 0
164 r.verdict = NX_SC_CLEAN
165 r.scanned_at_us = now_us
166
167 if _scam_check_urgency(buf, n) == 1 {
168 r.flagged_patterns = r.flagged_patterns | 1
169 r.pattern_count = r.pattern_count + 1
170 }
171 if _scam_check_authority(buf, n) == 1 {
172 r.flagged_patterns = r.flagged_patterns | 2
173 r.pattern_count = r.pattern_count + 1
174 }
175 if _scam_check_gift_card(buf, n) == 1 {
176 r.flagged_patterns = r.flagged_patterns | 8
177 r.pattern_count = r.pattern_count + 1
178 }
179 if _scam_check_crypto(buf, n) == 1 {
180 r.flagged_patterns = r.flagged_patterns | 16
181 r.pattern_count = r.pattern_count + 1
182 }
183
184 // Verdict aggregation
185 if r.pattern_count >= 3 { r.verdict = NX_SC_LIKELY_SCAM }
186 if r.pattern_count >= 1 {
187 if r.verdict == NX_SC_CLEAN { r.verdict = NX_SC_SUSPICIOUS }
188 }
189 // Canonical: urgency + authority + gift_card = classic IRS-impersonation scam
190 let canonical_mask: nx_int = 1 + 2 + 8 // urgency | authority | gift_card
191 if (r.flagged_patterns & canonical_mask) == canonical_mask {
192 r.verdict = NX_SC_CONFIRMED_PATTERN
193 }
194 return r
195}
196
197// ===== nx_scam_pattern_flagged ====================================
198
199func nx_scam_pattern_flagged(r: *NxScamScanResult, pattern: nx_int) -> nx_int {
200 if nx_sp_is_valid(pattern) == 0 { return 0 }
201 let bit: nx_int = 1 << pattern
202 if (r.flagged_patterns & bit) != 0 { return 1 }
203 return 0
204}