JavaScript · 14106 bytes Raw Blame History
1 // Pattern Learning System for Scammer Detection
2 // Uses machine learning-inspired techniques to identify and learn scammer patterns
3
4 export class PatternLearning {
5 constructor() {
6 this.patterns = new Map();
7 this.weights = new Map();
8 this.threshold = 0.7;
9 this.learningRate = 0.1;
10 this.decayRate = 0.95;
11 this.minConfidence = 0.6;
12 this.init();
13 }
14
15 async init() {
16 await this.loadPatterns();
17 await this.loadWeights();
18 this.startPeriodicTraining();
19 }
20
21 async loadPatterns() {
22 const stored = await chrome.storage.local.get('learned_patterns');
23 if (stored.learned_patterns) {
24 stored.learned_patterns.forEach(pattern => {
25 this.patterns.set(pattern.id, pattern);
26 });
27 }
28
29 // Load default patterns
30 this.loadDefaultPatterns();
31 }
32
33 loadDefaultPatterns() {
34 const defaults = [
35 {
36 id: 'urgent_action',
37 type: 'keyword_cluster',
38 features: ['urgent', 'immediate', 'act now', 'expire', 'limited time'],
39 weight: 0.8,
40 confidence: 0.9
41 },
42 {
43 id: 'money_request',
44 type: 'keyword_cluster',
45 features: ['send money', 'wire transfer', 'gift card', 'payment', 'fee'],
46 weight: 0.9,
47 confidence: 0.95
48 },
49 {
50 id: 'personal_info',
51 type: 'keyword_cluster',
52 features: ['social security', 'password', 'account number', 'pin', 'verification code'],
53 weight: 0.85,
54 confidence: 0.9
55 },
56 {
57 id: 'suspicious_link',
58 type: 'url_pattern',
59 features: ['bit.ly', 'tinyurl', 'short.link', 'click.here'],
60 weight: 0.75,
61 confidence: 0.8
62 },
63 {
64 id: 'impersonation',
65 type: 'keyword_cluster',
66 features: ['official', 'authorized', 'representative', 'department', 'agency'],
67 weight: 0.7,
68 confidence: 0.75
69 },
70 {
71 id: 'threat_language',
72 type: 'keyword_cluster',
73 features: ['suspended', 'terminated', 'legal action', 'arrest', 'prosecution'],
74 weight: 0.85,
75 confidence: 0.85
76 }
77 ];
78
79 defaults.forEach(pattern => {
80 if (!this.patterns.has(pattern.id)) {
81 this.patterns.set(pattern.id, pattern);
82 this.weights.set(pattern.id, pattern.weight);
83 }
84 });
85 }
86
87 async loadWeights() {
88 const stored = await chrome.storage.local.get('pattern_weights');
89 if (stored.pattern_weights) {
90 Object.entries(stored.pattern_weights).forEach(([id, weight]) => {
91 this.weights.set(id, weight);
92 });
93 }
94 }
95
96 async savePatterns() {
97 const patterns = Array.from(this.patterns.values());
98 await chrome.storage.local.set({ learned_patterns: patterns });
99 }
100
101 async saveWeights() {
102 const weights = Object.fromEntries(this.weights);
103 await chrome.storage.local.set({ pattern_weights: weights });
104 }
105
106 analyzeMessage(message, metadata = {}) {
107 const analysis = {
108 score: 0,
109 matchedPatterns: [],
110 features: [],
111 confidence: 0,
112 recommendation: 'monitor'
113 };
114
115 // Extract features from message
116 const features = this.extractFeatures(message, metadata);
117 analysis.features = features;
118
119 // Check against learned patterns
120 this.patterns.forEach((pattern, patternId) => {
121 const match = this.matchPattern(features, pattern);
122 if (match.score > this.minConfidence) {
123 analysis.matchedPatterns.push({
124 id: patternId,
125 type: pattern.type,
126 score: match.score,
127 weight: this.weights.get(patternId) || 0.5
128 });
129 }
130 });
131
132 // Calculate overall score
133 if (analysis.matchedPatterns.length > 0) {
134 const weightedSum = analysis.matchedPatterns.reduce((sum, pattern) =>
135 sum + (pattern.score * pattern.weight), 0
136 );
137 const totalWeight = analysis.matchedPatterns.reduce((sum, pattern) =>
138 sum + pattern.weight, 0
139 );
140
141 analysis.score = weightedSum / totalWeight;
142 analysis.confidence = this.calculateConfidence(analysis.matchedPatterns);
143 }
144
145 // Determine recommendation
146 if (analysis.score > 0.9) {
147 analysis.recommendation = 'block';
148 } else if (analysis.score > 0.7) {
149 analysis.recommendation = 'warn';
150 } else if (analysis.score > 0.5) {
151 analysis.recommendation = 'monitor_closely';
152 }
153
154 return analysis;
155 }
156
157 extractFeatures(message, metadata) {
158 const features = {
159 keywords: [],
160 urls: [],
161 patterns: [],
162 metrics: {},
163 behavioral: []
164 };
165
166 const text = message.content || message.text || '';
167 const lowerText = text.toLowerCase();
168
169 // Extract keywords
170 features.keywords = this.extractKeywords(lowerText);
171
172 // Extract URLs
173 features.urls = this.extractUrls(text);
174
175 // Extract patterns
176 features.patterns = this.extractPatterns(text);
177
178 // Calculate metrics
179 features.metrics = {
180 length: text.length,
181 wordCount: text.split(/\s+/).length,
182 uppercaseRatio: (text.match(/[A-Z]/g) || []).length / text.length,
183 punctuationCount: (text.match(/[!?]/g) || []).length,
184 numberCount: (text.match(/\d+/g) || []).length,
185 dollarSignCount: (text.match(/\$/g) || []).length
186 };
187
188 // Behavioral features
189 if (metadata.responseTime) {
190 features.behavioral.push({
191 type: 'response_speed',
192 value: metadata.responseTime < 1000 ? 'instant' : 'normal'
193 });
194 }
195
196 if (metadata.messageCount) {
197 features.behavioral.push({
198 type: 'message_frequency',
199 value: metadata.messageCount > 10 ? 'high' : 'normal'
200 });
201 }
202
203 return features;
204 }
205
206 extractKeywords(text) {
207 const keywords = [];
208 const commonScamWords = [
209 'urgent', 'verify', 'suspended', 'confirm', 'prize', 'winner',
210 'congratulations', 'claim', 'refund', 'irs', 'tax', 'arrest',
211 'legal', 'bitcoin', 'investment', 'guaranteed', 'risk free'
212 ];
213
214 commonScamWords.forEach(word => {
215 if (text.includes(word)) {
216 keywords.push(word);
217 }
218 });
219
220 return keywords;
221 }
222
223 extractUrls(text) {
224 const urlRegex = /(https?:\/\/[^\s]+)/g;
225 const urls = text.match(urlRegex) || [];
226
227 return urls.map(url => ({
228 url,
229 shortened: this.isShortened(url),
230 suspicious: this.isSuspiciousUrl(url)
231 }));
232 }
233
234 isShortened(url) {
235 const shorteners = ['bit.ly', 'tinyurl.com', 'short.link', 'ow.ly', 'goo.gl'];
236 return shorteners.some(shortener => url.includes(shortener));
237 }
238
239 isSuspiciousUrl(url) {
240 // Check for typosquatting and suspicious patterns
241 const suspicious = [
242 'amaz0n', 'payp4l', 'mircosoft', 'goggle',
243 'faceb00k', 'app1e', 'netf1ix'
244 ];
245
246 return suspicious.some(pattern => url.toLowerCase().includes(pattern));
247 }
248
249 extractPatterns(text) {
250 const patterns = [];
251
252 // Phone number pattern
253 if (/[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,5}[-\s\.]?[0-9]{1,5}/.test(text)) {
254 patterns.push('phone_number');
255 }
256
257 // Email pattern
258 if (/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/.test(text)) {
259 patterns.push('email_address');
260 }
261
262 // Money amount pattern
263 if (/\$[\d,]+(\.\d{2})?/.test(text)) {
264 patterns.push('money_amount');
265 }
266
267 // Verification code pattern
268 if (/\b\d{4,6}\b/.test(text) && text.includes('code')) {
269 patterns.push('verification_code');
270 }
271
272 return patterns;
273 }
274
275 matchPattern(features, pattern) {
276 let score = 0;
277 let matches = 0;
278
279 if (pattern.type === 'keyword_cluster') {
280 // Check how many keywords from the pattern are in the message
281 pattern.features.forEach(keyword => {
282 if (features.keywords.includes(keyword.toLowerCase())) {
283 matches++;
284 }
285 });
286
287 score = matches / pattern.features.length;
288 } else if (pattern.type === 'url_pattern') {
289 // Check URLs
290 features.urls.forEach(urlInfo => {
291 pattern.features.forEach(urlPattern => {
292 if (urlInfo.url.includes(urlPattern)) {
293 matches++;
294 }
295 });
296 });
297
298 score = matches > 0 ? 1 : 0;
299 } else if (pattern.type === 'behavioral') {
300 // Check behavioral patterns
301 features.behavioral.forEach(behavior => {
302 if (pattern.features.includes(behavior.type)) {
303 matches++;
304 }
305 });
306
307 score = matches / pattern.features.length;
308 }
309
310 return { score, matches };
311 }
312
313 calculateConfidence(matchedPatterns) {
314 if (matchedPatterns.length === 0) return 0;
315
316 // Confidence increases with more pattern matches
317 const baseConfidence = matchedPatterns.reduce((sum, p) => sum + p.score, 0) / matchedPatterns.length;
318 const diversityBonus = Math.min(matchedPatterns.length * 0.1, 0.3);
319
320 return Math.min(baseConfidence + diversityBonus, 1);
321 }
322
323 async addPattern(messageData, scammerScore) {
324 const features = this.extractFeatures(messageData);
325
326 // Only learn from high-confidence scammer messages
327 if (scammerScore < this.threshold) return;
328
329 // Update weights for matched patterns (reinforcement)
330 const analysis = this.analyzeMessage(messageData);
331 analysis.matchedPatterns.forEach(pattern => {
332 const currentWeight = this.weights.get(pattern.id) || 0.5;
333 const newWeight = currentWeight + (this.learningRate * (scammerScore - currentWeight));
334 this.weights.set(pattern.id, Math.min(newWeight, 1));
335 });
336
337 // Check if this represents a new pattern
338 const novelty = this.calculateNovelty(features);
339 if (novelty > 0.3) {
340 await this.createNewPattern(features, scammerScore);
341 }
342
343 // Save updated weights
344 await this.saveWeights();
345
346 // Notify dashboard
347 this.notifyLearning(features, scammerScore);
348 }
349
350 calculateNovelty(features) {
351 // Check how different these features are from existing patterns
352 let maxSimilarity = 0;
353
354 this.patterns.forEach(pattern => {
355 const match = this.matchPattern(features, pattern);
356 maxSimilarity = Math.max(maxSimilarity, match.score);
357 });
358
359 return 1 - maxSimilarity;
360 }
361
362 async createNewPattern(features, confidence) {
363 const id = `learned_${Date.now()}`;
364
365 const newPattern = {
366 id,
367 type: 'learned',
368 features: {
369 keywords: features.keywords.slice(0, 10),
370 patterns: features.patterns,
371 metrics: features.metrics
372 },
373 weight: confidence * 0.7, // Start with lower weight
374 confidence,
375 learnedAt: new Date().toISOString(),
376 occurrences: 1
377 };
378
379 this.patterns.set(id, newPattern);
380 this.weights.set(id, newPattern.weight);
381
382 await this.savePatterns();
383
384 console.log('[PatternLearning] New pattern learned:', id);
385
386 return newPattern;
387 }
388
389 async updateFromServer(serverPatterns) {
390 let updated = 0;
391
392 serverPatterns.forEach(serverPattern => {
393 const existing = this.patterns.get(serverPattern.id);
394
395 if (!existing || serverPattern.confidence > existing.confidence) {
396 this.patterns.set(serverPattern.id, serverPattern);
397 this.weights.set(serverPattern.id, serverPattern.weight);
398 updated++;
399 }
400 });
401
402 if (updated > 0) {
403 await this.savePatterns();
404 await this.saveWeights();
405 console.log(`[PatternLearning] Updated ${updated} patterns from server`);
406 }
407
408 return updated;
409 }
410
411 async getPatterns() {
412 return Array.from(this.patterns.values()).map(pattern => ({
413 ...pattern,
414 weight: this.weights.get(pattern.id) || 0.5,
415 effectiveness: this.calculateEffectiveness(pattern.id)
416 }));
417 }
418
419 calculateEffectiveness(patternId) {
420 // Track how effective each pattern is at detecting scammers
421 // This would be based on true/false positive rates in production
422 const weight = this.weights.get(patternId) || 0.5;
423 const pattern = this.patterns.get(patternId);
424
425 if (!pattern) return 0;
426
427 // Simple effectiveness score based on weight and confidence
428 return (weight * 0.7 + (pattern.confidence || 0.5) * 0.3);
429 }
430
431 startPeriodicTraining() {
432 // Decay weights periodically to adapt to changing patterns
433 setInterval(() => {
434 this.decayWeights();
435 }, 6 * 60 * 60 * 1000); // Every 6 hours
436 }
437
438 async decayWeights() {
439 let decayed = 0;
440
441 this.weights.forEach((weight, patternId) => {
442 // Don't decay core patterns below minimum
443 const pattern = this.patterns.get(patternId);
444 const minWeight = pattern && pattern.type !== 'learned' ? 0.5 : 0.1;
445
446 const newWeight = Math.max(weight * this.decayRate, minWeight);
447 if (newWeight !== weight) {
448 this.weights.set(patternId, newWeight);
449 decayed++;
450 }
451 });
452
453 if (decayed > 0) {
454 await this.saveWeights();
455 console.log(`[PatternLearning] Decayed ${decayed} pattern weights`);
456 }
457 }
458
459 async exportPatterns() {
460 const patterns = await this.getPatterns();
461
462 return {
463 version: '1.0',
464 timestamp: new Date().toISOString(),
465 patterns: patterns.sort((a, b) => b.effectiveness - a.effectiveness),
466 statistics: {
467 total: patterns.length,
468 learned: patterns.filter(p => p.type === 'learned').length,
469 averageConfidence: patterns.reduce((sum, p) => sum + (p.confidence || 0), 0) / patterns.length
470 }
471 };
472 }
473
474 notifyLearning(features, score) {
475 // Notify dashboard about learning event
476 chrome.runtime.sendMessage({
477 type: 'DASHBOARD_UPDATE',
478 data: {
479 event: 'pattern_learned',
480 features: features.keywords.slice(0, 5),
481 score,
482 timestamp: new Date().toISOString()
483 }
484 }).catch(() => {
485 // Dashboard might not be open
486 });
487 }
488
489 async resetLearning() {
490 // Reset to default patterns only
491 this.patterns.clear();
492 this.weights.clear();
493 this.loadDefaultPatterns();
494
495 await this.savePatterns();
496 await this.saveWeights();
497
498 console.log('[PatternLearning] Reset to default patterns');
499 }
500 }
501
502 // Export for use in service worker
503 if (typeof module !== 'undefined' && module.exports) {
504 module.exports = PatternLearning;
505 }