@sparkleideas/browser 3.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,528 @@
1
+ /**
2
+ * @sparkleideas/browser - Security Integration
3
+ * AIDefence integration for URL validation, PII detection, and threat scanning
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ // ============================================================================
9
+ // Security Types
10
+ // ============================================================================
11
+
12
+ export interface ThreatScanResult {
13
+ safe: boolean;
14
+ threats: Threat[];
15
+ pii: PIIMatch[];
16
+ score: number; // 0-1 (1 = safe)
17
+ scanDuration: number;
18
+ }
19
+
20
+ export interface Threat {
21
+ type: ThreatType;
22
+ severity: 'low' | 'medium' | 'high' | 'critical';
23
+ description: string;
24
+ location?: string;
25
+ mitigation?: string;
26
+ }
27
+
28
+ export type ThreatType =
29
+ | 'xss'
30
+ | 'injection'
31
+ | 'phishing'
32
+ | 'malware'
33
+ | 'data-exfiltration'
34
+ | 'credential-theft'
35
+ | 'insecure-protocol'
36
+ | 'suspicious-redirect'
37
+ | 'blocked-domain';
38
+
39
+ export interface PIIMatch {
40
+ type: PIIType;
41
+ value: string;
42
+ masked: string;
43
+ location: string;
44
+ confidence: number;
45
+ }
46
+
47
+ export type PIIType =
48
+ | 'email'
49
+ | 'phone'
50
+ | 'ssn'
51
+ | 'credit-card'
52
+ | 'api-key'
53
+ | 'password'
54
+ | 'address'
55
+ | 'name';
56
+
57
+ export interface SecurityConfig {
58
+ enableUrlValidation: boolean;
59
+ enablePIIDetection: boolean;
60
+ enableThreatScanning: boolean;
61
+ blockedDomains: string[];
62
+ allowedDomains: string[];
63
+ maxRedirects: number;
64
+ requireHttps: boolean;
65
+ piiMaskingEnabled: boolean;
66
+ }
67
+
68
+ // ============================================================================
69
+ // Validation Schemas
70
+ // ============================================================================
71
+
72
+ export const URLValidationSchema = z.string().url().refine(
73
+ (url) => {
74
+ try {
75
+ const parsed = new URL(url);
76
+ return ['http:', 'https:'].includes(parsed.protocol);
77
+ } catch {
78
+ return false;
79
+ }
80
+ },
81
+ { message: 'Invalid URL or unsupported protocol' }
82
+ );
83
+
84
+ // ============================================================================
85
+ // PII Detection Patterns
86
+ // ============================================================================
87
+
88
+ const PII_PATTERNS: Record<PIIType, RegExp> = {
89
+ email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
90
+ phone: /\b(\+?1[-.\s]?)?(\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}\b/g,
91
+ ssn: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g,
92
+ 'credit-card': /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
93
+ 'api-key': /\b(?:sk[-_]|api[-_]?key[-_]?|token[-_]?)[a-zA-Z0-9]{20,}\b/gi,
94
+ password: /(?:password|passwd|pwd)[\s:=]+[^\s]{6,}/gi,
95
+ address: /\b\d+\s+[\w\s]+(?:street|st|avenue|ave|road|rd|boulevard|blvd|drive|dr|lane|ln|way|court|ct)\b/gi,
96
+ name: /(?:^|\s)(?:Mr|Mrs|Ms|Dr|Prof)\.?\s+[A-Z][a-z]+\s+[A-Z][a-z]+(?:\s|$)/g,
97
+ };
98
+
99
+ // ============================================================================
100
+ // Known Threats
101
+ // ============================================================================
102
+
103
+ const PHISHING_INDICATORS = [
104
+ 'login-verify',
105
+ 'account-update',
106
+ 'security-alert',
107
+ 'verify-account',
108
+ 'suspended-account',
109
+ 'confirm-identity',
110
+ 'unusual-activity',
111
+ ];
112
+
113
+ const SUSPICIOUS_TLDs = ['.tk', '.ml', '.ga', '.cf', '.gq', '.xyz', '.top', '.work', '.click'];
114
+
115
+ const BLOCKED_DOMAINS_DEFAULT = [
116
+ 'bit.ly', // URL shorteners (can hide malicious URLs)
117
+ 'tinyurl.com',
118
+ 'goo.gl',
119
+ 't.co',
120
+ // Add more as needed
121
+ ];
122
+
123
+ // ============================================================================
124
+ // Security Scanner Class
125
+ // ============================================================================
126
+
127
+ export class BrowserSecurityScanner {
128
+ private config: SecurityConfig;
129
+
130
+ constructor(config: Partial<SecurityConfig> = {}) {
131
+ this.config = {
132
+ enableUrlValidation: true,
133
+ enablePIIDetection: true,
134
+ enableThreatScanning: true,
135
+ blockedDomains: [...BLOCKED_DOMAINS_DEFAULT],
136
+ allowedDomains: [],
137
+ maxRedirects: 5,
138
+ requireHttps: true,
139
+ piiMaskingEnabled: true,
140
+ ...config,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Full security scan of a URL before navigation
146
+ */
147
+ async scanUrl(url: string): Promise<ThreatScanResult> {
148
+ const startTime = Date.now();
149
+ const threats: Threat[] = [];
150
+ const pii: PIIMatch[] = [];
151
+
152
+ // Validate URL format
153
+ if (this.config.enableUrlValidation) {
154
+ const urlThreats = this.validateUrl(url);
155
+ threats.push(...urlThreats);
156
+ }
157
+
158
+ // Check for blocked domains
159
+ if (this.isBlockedDomain(url)) {
160
+ threats.push({
161
+ type: 'blocked-domain',
162
+ severity: 'high',
163
+ description: 'This domain is on the blocked list',
164
+ location: url,
165
+ mitigation: 'Use the original URL instead of a shortener',
166
+ });
167
+ }
168
+
169
+ // Check for phishing indicators
170
+ if (this.config.enableThreatScanning) {
171
+ const phishingThreats = this.detectPhishing(url);
172
+ threats.push(...phishingThreats);
173
+ }
174
+
175
+ // Calculate safety score
176
+ const score = this.calculateSafetyScore(threats);
177
+
178
+ return {
179
+ safe: score >= 0.7 && !threats.some((t) => t.severity === 'critical'),
180
+ threats,
181
+ pii,
182
+ score,
183
+ scanDuration: Date.now() - startTime,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Scan content for PII before filling forms
189
+ */
190
+ scanContent(content: string, context: string = 'unknown'): ThreatScanResult {
191
+ const startTime = Date.now();
192
+ const threats: Threat[] = [];
193
+ const pii: PIIMatch[] = [];
194
+
195
+ if (!this.config.enablePIIDetection) {
196
+ return { safe: true, threats, pii, score: 1, scanDuration: Date.now() - startTime };
197
+ }
198
+
199
+ // Detect PII in content
200
+ for (const [type, pattern] of Object.entries(PII_PATTERNS) as [PIIType, RegExp][]) {
201
+ const matches = content.matchAll(new RegExp(pattern.source, pattern.flags));
202
+ for (const match of matches) {
203
+ const value = match[0];
204
+ pii.push({
205
+ type,
206
+ value,
207
+ masked: this.maskPII(value, type),
208
+ location: context,
209
+ confidence: this.calculatePIIConfidence(value, type),
210
+ });
211
+ }
212
+ }
213
+
214
+ // Add threat if sensitive PII found
215
+ const sensitivePII = pii.filter((p) => ['ssn', 'credit-card', 'api-key', 'password'].includes(p.type));
216
+ if (sensitivePII.length > 0) {
217
+ threats.push({
218
+ type: 'data-exfiltration',
219
+ severity: 'high',
220
+ description: `Sensitive data detected: ${sensitivePII.map((p) => p.type).join(', ')}`,
221
+ location: context,
222
+ mitigation: 'Consider masking or removing sensitive data before processing',
223
+ });
224
+ }
225
+
226
+ const score = this.calculateSafetyScore(threats);
227
+
228
+ return {
229
+ safe: score >= 0.7,
230
+ threats,
231
+ pii,
232
+ score,
233
+ scanDuration: Date.now() - startTime,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Validate input before filling a form field
239
+ */
240
+ validateInput(value: string, fieldType: string): ThreatScanResult {
241
+ const threats: Threat[] = [];
242
+ const pii: PIIMatch[] = [];
243
+
244
+ // Check for injection patterns
245
+ if (this.containsInjection(value)) {
246
+ threats.push({
247
+ type: 'injection',
248
+ severity: 'critical',
249
+ description: 'Potential injection attack detected in input',
250
+ location: fieldType,
251
+ mitigation: 'Sanitize input before submission',
252
+ });
253
+ }
254
+
255
+ // Check for XSS
256
+ if (this.containsXSS(value)) {
257
+ threats.push({
258
+ type: 'xss',
259
+ severity: 'critical',
260
+ description: 'Potential XSS attack detected in input',
261
+ location: fieldType,
262
+ mitigation: 'Escape HTML entities before submission',
263
+ });
264
+ }
265
+
266
+ // Detect PII in input
267
+ const contentScan = this.scanContent(value, fieldType);
268
+ pii.push(...contentScan.pii);
269
+
270
+ const score = this.calculateSafetyScore(threats);
271
+
272
+ return {
273
+ safe: score >= 0.7,
274
+ threats,
275
+ pii,
276
+ score,
277
+ scanDuration: 0,
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Sanitize input by removing detected threats
283
+ */
284
+ sanitizeInput(value: string): string {
285
+ let sanitized = value;
286
+
287
+ // Escape HTML entities
288
+ sanitized = sanitized
289
+ .replace(/&/g, '&amp;')
290
+ .replace(/</g, '&lt;')
291
+ .replace(/>/g, '&gt;')
292
+ .replace(/"/g, '&quot;')
293
+ .replace(/'/g, '&#x27;');
294
+
295
+ // Remove script tags
296
+ sanitized = sanitized.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
297
+
298
+ // Remove event handlers
299
+ sanitized = sanitized.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '');
300
+
301
+ return sanitized;
302
+ }
303
+
304
+ /**
305
+ * Mask PII for safe logging/display
306
+ */
307
+ maskPII(value: string, type: PIIType): string {
308
+ if (!this.config.piiMaskingEnabled) return value;
309
+
310
+ switch (type) {
311
+ case 'email': {
312
+ const [local, domain] = value.split('@');
313
+ return `${local[0]}${'*'.repeat(Math.min(local.length - 1, 5))}@${domain}`;
314
+ }
315
+ case 'phone':
316
+ return value.replace(/\d(?=\d{4})/g, '*');
317
+ case 'ssn':
318
+ return '***-**-' + value.slice(-4);
319
+ case 'credit-card':
320
+ return '**** **** **** ' + value.replace(/\D/g, '').slice(-4);
321
+ case 'api-key':
322
+ return value.slice(0, 8) + '*'.repeat(Math.min(value.length - 8, 20));
323
+ case 'password':
324
+ return '********';
325
+ default:
326
+ return '*'.repeat(value.length);
327
+ }
328
+ }
329
+
330
+ // Private helpers
331
+
332
+ private validateUrl(url: string): Threat[] {
333
+ const threats: Threat[] = [];
334
+
335
+ try {
336
+ const parsed = new URL(url);
337
+
338
+ // Check HTTPS requirement
339
+ if (this.config.requireHttps && parsed.protocol !== 'https:') {
340
+ threats.push({
341
+ type: 'insecure-protocol',
342
+ severity: 'medium',
343
+ description: 'URL uses insecure HTTP protocol',
344
+ location: url,
345
+ mitigation: 'Use HTTPS for secure communication',
346
+ });
347
+ }
348
+
349
+ // Check for suspicious TLDs
350
+ if (SUSPICIOUS_TLDs.some((tld) => parsed.hostname.endsWith(tld))) {
351
+ threats.push({
352
+ type: 'phishing',
353
+ severity: 'medium',
354
+ description: 'URL uses a suspicious top-level domain',
355
+ location: parsed.hostname,
356
+ mitigation: 'Verify the legitimacy of this domain',
357
+ });
358
+ }
359
+
360
+ // Check for IP address in URL (often suspicious)
361
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(parsed.hostname)) {
362
+ threats.push({
363
+ type: 'suspicious-redirect',
364
+ severity: 'medium',
365
+ description: 'URL points to an IP address instead of a domain',
366
+ location: parsed.hostname,
367
+ mitigation: 'Legitimate sites typically use domain names',
368
+ });
369
+ }
370
+ } catch {
371
+ threats.push({
372
+ type: 'malware',
373
+ severity: 'high',
374
+ description: 'Invalid URL format',
375
+ location: url,
376
+ });
377
+ }
378
+
379
+ return threats;
380
+ }
381
+
382
+ private isBlockedDomain(url: string): boolean {
383
+ try {
384
+ const parsed = new URL(url);
385
+ const hostname = parsed.hostname.toLowerCase();
386
+
387
+ // Check allowed domains first
388
+ if (
389
+ this.config.allowedDomains.length > 0 &&
390
+ this.config.allowedDomains.some((d) => hostname.includes(d.toLowerCase()))
391
+ ) {
392
+ return false;
393
+ }
394
+
395
+ // Check blocked domains
396
+ return this.config.blockedDomains.some((d) => hostname.includes(d.toLowerCase()));
397
+ } catch {
398
+ return false;
399
+ }
400
+ }
401
+
402
+ private detectPhishing(url: string): Threat[] {
403
+ const threats: Threat[] = [];
404
+ const urlLower = url.toLowerCase();
405
+
406
+ // Check for phishing indicators in URL
407
+ for (const indicator of PHISHING_INDICATORS) {
408
+ if (urlLower.includes(indicator)) {
409
+ threats.push({
410
+ type: 'phishing',
411
+ severity: 'high',
412
+ description: `URL contains phishing indicator: ${indicator}`,
413
+ location: url,
414
+ mitigation: 'Verify this is a legitimate request from the service',
415
+ });
416
+ break;
417
+ }
418
+ }
419
+
420
+ // Check for lookalike domains (typosquatting)
421
+ const commonDomains = ['google', 'facebook', 'amazon', 'microsoft', 'apple', 'paypal', 'bank'];
422
+ for (const domain of commonDomains) {
423
+ const pattern = new RegExp(`${domain.split('').join('[^a-z]?')}`, 'i');
424
+ if (pattern.test(urlLower) && !urlLower.includes(`.${domain}.`)) {
425
+ const parsed = new URL(url);
426
+ if (!parsed.hostname.includes(domain)) {
427
+ threats.push({
428
+ type: 'phishing',
429
+ severity: 'high',
430
+ description: `URL may be impersonating ${domain}`,
431
+ location: url,
432
+ mitigation: 'Verify you are on the official domain',
433
+ });
434
+ }
435
+ }
436
+ }
437
+
438
+ return threats;
439
+ }
440
+
441
+ private containsInjection(value: string): boolean {
442
+ const injectionPatterns = [
443
+ /['";]/,
444
+ /--/,
445
+ /\/\*/,
446
+ /\bOR\b.*\b=\b/i,
447
+ /\bAND\b.*\b=\b/i,
448
+ /\bUNION\b.*\bSELECT\b/i,
449
+ /\bDROP\b.*\bTABLE\b/i,
450
+ /\bINSERT\b.*\bINTO\b/i,
451
+ ];
452
+
453
+ return injectionPatterns.some((pattern) => pattern.test(value));
454
+ }
455
+
456
+ private containsXSS(value: string): boolean {
457
+ const xssPatterns = [
458
+ /<script[^>]*>/i,
459
+ /javascript:/i,
460
+ /\bon\w+\s*=/i,
461
+ /<img[^>]+onerror/i,
462
+ /<svg[^>]+onload/i,
463
+ /data:text\/html/i,
464
+ ];
465
+
466
+ return xssPatterns.some((pattern) => pattern.test(value));
467
+ }
468
+
469
+ private calculateSafetyScore(threats: Threat[]): number {
470
+ if (threats.length === 0) return 1;
471
+
472
+ const severityWeights = { low: 0.1, medium: 0.25, high: 0.4, critical: 0.6 };
473
+ let penalty = 0;
474
+
475
+ for (const threat of threats) {
476
+ penalty += severityWeights[threat.severity];
477
+ }
478
+
479
+ return Math.max(0, 1 - penalty);
480
+ }
481
+
482
+ private calculatePIIConfidence(value: string, type: PIIType): number {
483
+ // Basic confidence calculation based on pattern match quality
484
+ switch (type) {
485
+ case 'email':
486
+ return value.includes('.') && value.includes('@') ? 0.95 : 0.7;
487
+ case 'ssn':
488
+ return /^\d{3}-\d{2}-\d{4}$/.test(value) ? 0.95 : 0.8;
489
+ case 'credit-card':
490
+ return value.replace(/\D/g, '').length === 16 ? 0.9 : 0.7;
491
+ case 'api-key':
492
+ return value.length > 30 ? 0.85 : 0.6;
493
+ default:
494
+ return 0.7;
495
+ }
496
+ }
497
+ }
498
+
499
+ // ============================================================================
500
+ // Factory Functions
501
+ // ============================================================================
502
+
503
+ let defaultScanner: BrowserSecurityScanner | null = null;
504
+
505
+ export function getSecurityScanner(config?: Partial<SecurityConfig>): BrowserSecurityScanner {
506
+ if (!defaultScanner || config) {
507
+ defaultScanner = new BrowserSecurityScanner(config);
508
+ }
509
+ return defaultScanner;
510
+ }
511
+
512
+ /**
513
+ * Quick check if a URL is safe to navigate
514
+ */
515
+ export async function isUrlSafe(url: string): Promise<boolean> {
516
+ const scanner = getSecurityScanner();
517
+ const result = await scanner.scanUrl(url);
518
+ return result.safe;
519
+ }
520
+
521
+ /**
522
+ * Quick check if content contains PII
523
+ */
524
+ export function containsPII(content: string): boolean {
525
+ const scanner = getSecurityScanner();
526
+ const result = scanner.scanContent(content);
527
+ return result.pii.length > 0;
528
+ }