cipher-shield 1.0.0

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,510 @@
1
+ /**
2
+ * In-Memory IP Blacklist Manager v2.1
3
+ *
4
+ * High-performance IP blacklist system with automatic expiry, cleanup,
5
+ * and threat intelligence tracking. Optimized for concurrent access
6
+ * patterns typical in web security middleware.
7
+ *
8
+ * Features:
9
+ * - Automatic entry expiration to prevent memory leaks
10
+ * - Background cleanup to maintain performance
11
+ * - Threat categorization and statistics
12
+ * - Memory-efficient data structures
13
+ * - Thread-safe operations for concurrent requests
14
+ *
15
+ * @module blacklistMem
16
+ * @version 1.0.0
17
+ * @author Omindu Dissanayaka
18
+ * @license MIT
19
+ */
20
+
21
+ const EventEmitter = require('events');
22
+
23
+ /**
24
+ * Default configuration constants with security limits
25
+ * @readonly
26
+ */
27
+ const DEFAULT_CONFIG = Object.freeze({
28
+ BLACKLIST_EXPIRY: 3600000,
29
+ CLEANUP_INTERVAL: 300000,
30
+ MAX_ENTRIES: 10000,
31
+ MAX_ENTRIES_PER_IP: 5,
32
+ CLEANUP_BATCH_SIZE: 100,
33
+ ENTRY_SIZE_LIMIT: 1000
34
+ });
35
+
36
+ /**
37
+ * Blacklist entry structure
38
+ * @typedef {Object} BlacklistEntry
39
+ * @property {string} reason - Reason for blacklisting
40
+ * @property {number} timestamp - When the IP was blacklisted
41
+ * @property {number} hits - Number of times this IP triggered security
42
+ * @property {string} category - Threat category (e.g., 'ghost_route', 'ai_threat')
43
+ */
44
+
45
+ /**
46
+ * Internal blacklist storage using Map for O(1) lookups with mutex protection
47
+ * @private
48
+ * @type {Map<string, BlacklistEntry>}
49
+ */
50
+ const blacklist = new Map();
51
+
52
+ /**
53
+ * Mutex for thread-safe operations
54
+ * @private
55
+ * @type {boolean}
56
+ */
57
+ let blacklistMutex = false;
58
+
59
+ /**
60
+ * Executes operation with mutex protection to prevent race conditions
61
+ * @private
62
+ * @param {Function} operation - Operation to execute safely
63
+ * @returns {*} Result of the operation
64
+ */
65
+ function withMutex(operation) {
66
+ while (blacklistMutex) {
67
+ }
68
+
69
+ blacklistMutex = true;
70
+ try {
71
+ return operation();
72
+ } finally {
73
+ blacklistMutex = false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Statistics tracking
79
+ * @private
80
+ */
81
+ let stats = {
82
+ totalAdded: 0,
83
+ totalExpired: 0,
84
+ totalRemoved: 0,
85
+ peakSize: 0,
86
+ lastCleanup: Date.now()
87
+ };
88
+
89
+ /**
90
+ * Background cleanup timer
91
+ * @private
92
+ * @type {NodeJS.Timeout|null}
93
+ */
94
+ let cleanupTimer = null;
95
+
96
+ /**
97
+ * Event emitter for monitoring blacklist events
98
+ * @private
99
+ */
100
+ const events = new EventEmitter();
101
+
102
+ /**
103
+ * Starts the automatic background cleanup process
104
+ *
105
+ * Runs periodic cleanup to remove expired entries and maintain performance.
106
+ * Cleanup happens in batches to avoid blocking the event loop.
107
+ *
108
+ * @private
109
+ * @returns {void}
110
+ */
111
+ function startBackgroundCleanup() {
112
+ if (cleanupTimer) {
113
+ clearInterval(cleanupTimer);
114
+ }
115
+
116
+ cleanupTimer = setInterval(() => {
117
+ const now = Date.now();
118
+ const expired = [];
119
+ let processed = 0;
120
+
121
+ for (const [ip, entry] of blacklist.entries()) {
122
+ if (processed >= DEFAULT_CONFIG.CLEANUP_BATCH_SIZE) break;
123
+
124
+ if (now - entry.timestamp > DEFAULT_CONFIG.BLACKLIST_EXPIRY) {
125
+ expired.push(ip);
126
+ processed++;
127
+ }
128
+ }
129
+
130
+ expired.forEach(ip => {
131
+ blacklist.delete(ip);
132
+ stats.totalExpired++;
133
+ });
134
+
135
+ stats.lastCleanup = now;
136
+ stats.peakSize = Math.max(stats.peakSize, blacklist.size);
137
+
138
+ if (expired.length > 0 && process.env.NODE_ENV === 'development') {
139
+ console.log(`[BLACKLIST] Cleaned up ${expired.length} expired entries`);
140
+ }
141
+
142
+ }, DEFAULT_CONFIG.CLEANUP_INTERVAL);
143
+
144
+ cleanupTimer.unref();
145
+ }
146
+
147
+ /**
148
+ * Stops the background cleanup process
149
+ *
150
+ * @private
151
+ * @returns {void}
152
+ */
153
+ function stopBackgroundCleanup() {
154
+ if (cleanupTimer) {
155
+ clearInterval(cleanupTimer);
156
+ cleanupTimer = null;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Initializes the blacklist system
162
+ *
163
+ * Sets up background cleanup and prepares the system for operation.
164
+ * Should be called during application startup.
165
+ *
166
+ * @function initialize
167
+ * @returns {void}
168
+ *
169
+ * @example
170
+ * ```javascript
171
+ * const blacklistMem = require('./blacklistMem');
172
+ * blacklistMem.initialize();
173
+ * ```
174
+ */
175
+ function initialize() {
176
+ blacklist.clear();
177
+
178
+ stats = {
179
+ totalAdded: 0,
180
+ totalExpired: 0,
181
+ totalRemoved: 0,
182
+ peakSize: 0,
183
+ lastCleanup: Date.now()
184
+ };
185
+
186
+ startBackgroundCleanup();
187
+
188
+ if (process.env.NODE_ENV === 'development') {
189
+ console.log('[BLACKLIST] Initialized with background cleanup');
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Adds an IP address to the blacklist with reason tracking
195
+ *
196
+ * Automatically prevents duplicate entries and updates hit counts.
197
+ * Enforces maximum entry limits to prevent memory exhaustion.
198
+ *
199
+ * @function add
200
+ * @param {string} ip - IP address to blacklist
201
+ * @param {string} reason - Reason for blacklisting (e.g., 'ghost_route_access', 'ai_high_threat')
202
+ * @returns {boolean} True if IP was added/updated, false if at capacity limit
203
+ *
204
+ * @example
205
+ * ```javascript
206
+ * blacklistMem.add('192.168.1.100', 'ghost_route_access');
207
+ * blacklistMem.add('10.0.0.1', 'ai_high_threat');
208
+ * ```
209
+ */
210
+ function add(ip, reason = 'unknown') {
211
+ return withMutex(() => {
212
+ if (!ip || typeof ip !== 'string') {
213
+ return false;
214
+ }
215
+
216
+ if (!isValidIP(ip)) {
217
+ return false;
218
+ }
219
+
220
+ const sanitizedReason = sanitizeReason(reason);
221
+
222
+ if (blacklist.size >= DEFAULT_CONFIG.MAX_ENTRIES) {
223
+ if (process.env.NODE_ENV === 'development') {
224
+ console.warn(`[BLACKLIST] At capacity limit (${DEFAULT_CONFIG.MAX_ENTRIES}), cannot add ${ip}`);
225
+ }
226
+ return false;
227
+ }
228
+
229
+ const now = Date.now();
230
+ const existing = blacklist.get(ip);
231
+
232
+ if (existing) {
233
+ if (existing.hits >= DEFAULT_CONFIG.MAX_ENTRIES_PER_IP) {
234
+ if (process.env.NODE_ENV === 'development') {
235
+ console.warn(`[BLACKLIST] IP ${ip} has reached max entries (${DEFAULT_CONFIG.MAX_ENTRIES_PER_IP})`);
236
+ }
237
+ return false;
238
+ }
239
+
240
+ existing.hits = (existing.hits || 1) + 1;
241
+ existing.timestamp = now;
242
+ existing.lastReason = sanitizedReason;
243
+ } else {
244
+ blacklist.set(ip, {
245
+ reason: sanitizedReason,
246
+ timestamp: now,
247
+ hits: 1,
248
+ category: categorizeReason(sanitizedReason)
249
+ });
250
+
251
+ stats.totalAdded++;
252
+ }
253
+
254
+ events.emit('added', { ip, reason: sanitizedReason, existing: !!existing });
255
+
256
+ return true;
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Validates IP address format
262
+ * @private
263
+ * @param {string} ip - IP address to validate
264
+ * @returns {boolean} True if valid IP format
265
+ */
266
+ function isValidIP(ip) {
267
+ const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
268
+ return ipv4Regex.test(ip) || ip === 'unknown';
269
+ }
270
+
271
+ /**
272
+ * Sanitizes blacklist reason to prevent injection and limit size
273
+ * @private
274
+ * @param {string} reason - Raw reason string
275
+ * @returns {string} Sanitized reason
276
+ */
277
+ function sanitizeReason(reason) {
278
+ if (!reason || typeof reason !== 'string') {
279
+ return 'unknown';
280
+ }
281
+
282
+ return reason.substring(0, DEFAULT_CONFIG.ENTRY_SIZE_LIMIT)
283
+ .replace(/[<>'"&]/g, '')
284
+ .trim();
285
+ }
286
+
287
+ /**
288
+ * Categorizes blacklist reasons for better threat intelligence
289
+ *
290
+ * @private
291
+ * @param {string} reason - The blacklist reason
292
+ * @returns {string} Threat category
293
+ */
294
+ function categorizeReason(reason) {
295
+ if (reason.includes('ghost')) return 'ghost_route';
296
+ if (reason.includes('ai')) return 'ai_detected';
297
+ if (reason.includes('aes') || reason.includes('decrypt')) return 'encryption_attack';
298
+ if (reason.includes('shadow')) return 'timing_attack';
299
+ return 'other';
300
+ }
301
+
302
+ /**
303
+ * Checks if an IP address is currently blacklisted
304
+ *
305
+ * Performs automatic cleanup of expired entries during lookup.
306
+ * This lazy cleanup approach minimizes background processing.
307
+ *
308
+ * @function isBlacklisted
309
+ * @param {string} ip - IP address to check
310
+ * @returns {boolean} True if IP is blacklisted and not expired
311
+ *
312
+ * @example
313
+ * ```javascript
314
+ * if (blacklistMem.isBlacklisted('192.168.1.100')) {
315
+ * // Apply shadow ban or blocking
316
+ * }
317
+ * ```
318
+ */
319
+ function isBlacklisted(ip) {
320
+ return withMutex(() => {
321
+ if (!ip || !blacklist.has(ip)) {
322
+ return false;
323
+ }
324
+
325
+ const entry = blacklist.get(ip);
326
+ const age = Date.now() - entry.timestamp;
327
+
328
+ if (age > DEFAULT_CONFIG.BLACKLIST_EXPIRY) {
329
+ blacklist.delete(ip);
330
+ stats.totalExpired++;
331
+
332
+ if (process.env.NODE_ENV === 'development') {
333
+ console.log(`[BLACKLIST] Expired entry removed: ${ip}`);
334
+ }
335
+
336
+ return false;
337
+ }
338
+
339
+ return true;
340
+ });
341
+ }
342
+
343
+ /**
344
+ * Removes an IP address from the blacklist
345
+ *
346
+ * @function remove
347
+ * @param {string} ip - IP address to remove
348
+ * @returns {boolean} True if IP was removed, false if not found
349
+ *
350
+ * @example
351
+ * ```javascript
352
+ * const removed = blacklistMem.remove('192.168.1.100');
353
+ * console.log(removed ? 'IP removed' : 'IP not found');
354
+ * ```
355
+ */
356
+ function remove(ip) {
357
+ const existed = blacklist.delete(ip);
358
+
359
+ if (existed) {
360
+ stats.totalRemoved++;
361
+ events.emit('removed', { ip });
362
+ }
363
+
364
+ return existed;
365
+ }
366
+
367
+ /**
368
+ * Retrieves all current blacklist entries
369
+ *
370
+ * Returns a snapshot of current entries with metadata.
371
+ * Note: This creates a new array, so avoid calling frequently in hot paths.
372
+ *
373
+ * @function getAll
374
+ * @returns {Array<BlacklistEntry & {ip: string}>} Array of blacklist entries
375
+ *
376
+ * @example
377
+ * ```javascript
378
+ * const entries = blacklistMem.getAll();
379
+ * console.log(`Blacklisted IPs: ${entries.length}`);
380
+ * ```
381
+ */
382
+ function getAll() {
383
+ const result = [];
384
+
385
+ for (const [ip, entry] of blacklist.entries()) {
386
+ result.push({
387
+ ip,
388
+ ...entry
389
+ });
390
+ }
391
+
392
+ return result;
393
+ }
394
+
395
+ /**
396
+ * Retrieves detailed statistics about blacklist operations
397
+ *
398
+ * @function getStats
399
+ * @returns {Object} Blacklist statistics
400
+ * @property {number} size - Current number of entries
401
+ * @property {number} totalAdded - Total IPs ever added
402
+ * @property {number} totalExpired - Total IPs expired
403
+ * @property {number} totalRemoved - Total IPs manually removed
404
+ * @property {number} peakSize - Maximum size reached
405
+ * @property {number} lastCleanup - Timestamp of last cleanup
406
+ *
407
+ * @example
408
+ * ```javascript
409
+ * const stats = blacklistMem.getStats();
410
+ * console.log(`Current size: ${stats.size}, Peak: ${stats.peakSize}`);
411
+ * ```
412
+ */
413
+ function getStats() {
414
+ return {
415
+ size: blacklist.size,
416
+ ...stats,
417
+ config: DEFAULT_CONFIG
418
+ };
419
+ }
420
+
421
+ /**
422
+ * Clears all blacklist entries
423
+ *
424
+ * Emergency function to reset the blacklist. Use with caution.
425
+ *
426
+ * @function clear
427
+ * @returns {number} Number of entries cleared
428
+ *
429
+ * @example
430
+ * ```javascript
431
+ * const cleared = blacklistMem.clear();
432
+ * console.log(`Cleared ${cleared} entries`);
433
+ * ```
434
+ */
435
+ function clear() {
436
+ const previousSize = blacklist.size;
437
+ blacklist.clear();
438
+
439
+ stats.peakSize = 0;
440
+ stats.lastCleanup = Date.now();
441
+
442
+ events.emit('cleared', { previousSize });
443
+
444
+ if (process.env.NODE_ENV === 'development') {
445
+ console.log(`[BLACKLIST] Cleared ${previousSize} entries`);
446
+ }
447
+
448
+ return previousSize;
449
+ }
450
+
451
+ /**
452
+ * Gets the current number of blacklisted IPs
453
+ *
454
+ * More efficient than getAll().length for size checks.
455
+ *
456
+ * @function size
457
+ * @returns {number} Current blacklist size
458
+ */
459
+ function size() {
460
+ return blacklist.size;
461
+ }
462
+
463
+ /**
464
+ * Checks if the blacklist contains a specific IP
465
+ *
466
+ * Similar to isBlacklisted() but doesn't perform expiry checks.
467
+ *
468
+ * @function has
469
+ * @param {string} ip - IP address to check
470
+ * @returns {boolean} True if IP exists in blacklist (may be expired)
471
+ */
472
+ function has(ip) {
473
+ return blacklist.has(ip);
474
+ }
475
+
476
+ /**
477
+ * Gets detailed information about a specific blacklisted IP
478
+ *
479
+ * @function getEntry
480
+ * @param {string} ip - IP address to query
481
+ * @returns {BlacklistEntry|null} Entry details or null if not found/expired
482
+ */
483
+ function getEntry(ip) {
484
+ if (!isBlacklisted(ip)) {
485
+ return null;
486
+ }
487
+
488
+ return { ...blacklist.get(ip) };
489
+ }
490
+
491
+ initialize();
492
+
493
+ process.on('exit', stopBackgroundCleanup);
494
+ process.on('SIGINT', stopBackgroundCleanup);
495
+ process.on('SIGTERM', stopBackgroundCleanup);
496
+
497
+ module.exports = {
498
+ initialize,
499
+ add,
500
+ isBlacklisted,
501
+ remove,
502
+ getAll,
503
+ getStats,
504
+ clear,
505
+ size,
506
+ has,
507
+ getEntry,
508
+ events, // For advanced monitoring
509
+ DEFAULT_CONFIG
510
+ };