@chdlabs/my-shield 0.1.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,178 @@
1
+ /**
2
+ * Bounded In-Memory LRU Cache with TTL Expiration
3
+ * Backed by native JavaScript Map preserving insertion order.
4
+ *
5
+ * @author ChdLabs
6
+ * @license MIT
7
+ */
8
+
9
+ export class LRUCache {
10
+ /**
11
+ * @param {Object} options
12
+ * @param {number} [options.maxSize=10000] Maximum entries before evicting least-recently used
13
+ * @param {number} [options.ttlMs=300000] Default Time-To-Live in milliseconds (5 minutes)
14
+ */
15
+ constructor({ maxSize = 10_000, ttlMs = 5 * 60 * 1000 } = {}) {
16
+ this.maxSize = Math.max(1, maxSize);
17
+ this.ttlMs = ttlMs;
18
+ this.cache = new Map();
19
+ this.hits = 0;
20
+ this.misses = 0;
21
+ this.evictions = 0;
22
+ }
23
+
24
+ /**
25
+ * Set or update a key in the cache
26
+ * @param {string} key
27
+ * @param {*} value
28
+ * @param {number} [customTtlMs]
29
+ */
30
+ set(key, value, customTtlMs = null) {
31
+ const effectiveTtl = customTtlMs !== null && customTtlMs !== undefined ? customTtlMs : this.ttlMs;
32
+ const now = Date.now();
33
+ const expiresAt = effectiveTtl > 0 ? now + effectiveTtl : 0;
34
+
35
+ // If exists, delete first to move to the end (most recent)
36
+ if (this.cache.has(key)) {
37
+ this.cache.delete(key);
38
+ } else if (this.cache.size >= this.maxSize) {
39
+ // Evict oldest (first key in Map iterator)
40
+ const oldestKey = this.cache.keys().next().value;
41
+ if (oldestKey !== undefined) {
42
+ this.cache.delete(oldestKey);
43
+ this.evictions++;
44
+ }
45
+ }
46
+
47
+ this.cache.set(key, {
48
+ value,
49
+ expiresAt,
50
+ createdAt: now,
51
+ updatedAt: now
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Retrieve a key from the cache, refreshing its recency
57
+ * @param {string} key
58
+ * @returns {*} Value or undefined if absent or expired
59
+ */
60
+ get(key) {
61
+ const entry = this.cache.get(key);
62
+ if (!entry) {
63
+ this.misses++;
64
+ return undefined;
65
+ }
66
+
67
+ // Check expiration
68
+ if (entry.expiresAt > 0 && Date.now() > entry.expiresAt) {
69
+ this.cache.delete(key);
70
+ this.misses++;
71
+ return undefined;
72
+ }
73
+
74
+ // Refresh position in Map (delete and re-insert)
75
+ this.cache.delete(key);
76
+ entry.updatedAt = Date.now();
77
+ this.cache.set(key, entry);
78
+ this.hits++;
79
+ return entry.value;
80
+ }
81
+
82
+ /**
83
+ * Check if a key exists and is unexpired without modifying LRU order
84
+ * @param {string} key
85
+ * @returns {boolean}
86
+ */
87
+ has(key) {
88
+ const entry = this.cache.get(key);
89
+ if (!entry) return false;
90
+ if (entry.expiresAt > 0 && Date.now() > entry.expiresAt) {
91
+ this.cache.delete(key);
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+
97
+ /**
98
+ * Delete a key from the cache
99
+ * @param {string} key
100
+ * @returns {boolean}
101
+ */
102
+ delete(key) {
103
+ return this.cache.delete(key);
104
+ }
105
+
106
+ /**
107
+ * Clear all entries
108
+ */
109
+ clear() {
110
+ this.cache.clear();
111
+ this.hits = 0;
112
+ this.misses = 0;
113
+ this.evictions = 0;
114
+ }
115
+
116
+ /**
117
+ * Prune expired entries
118
+ * @returns {number} Count of removed entries
119
+ */
120
+ prune() {
121
+ const now = Date.now();
122
+ let pruned = 0;
123
+ for (const [key, entry] of this.cache.entries()) {
124
+ if (entry.expiresAt > 0 && now > entry.expiresAt) {
125
+ this.cache.delete(key);
126
+ pruned++;
127
+ }
128
+ }
129
+ return pruned;
130
+ }
131
+
132
+ /**
133
+ * Current count of active entries
134
+ */
135
+ get size() {
136
+ return this.cache.size;
137
+ }
138
+
139
+ /**
140
+ * Get snapshot array of active unexpired entries
141
+ * @returns {Array<{ key: string, value: any, expiresAt: number, remainingTtlMs: number }>}
142
+ */
143
+ entries() {
144
+ const now = Date.now();
145
+ const result = [];
146
+ for (const [key, entry] of this.cache.entries()) {
147
+ if (entry.expiresAt > 0 && now > entry.expiresAt) {
148
+ this.cache.delete(key);
149
+ continue;
150
+ }
151
+ result.push({
152
+ key,
153
+ value: entry.value,
154
+ createdAt: entry.createdAt,
155
+ expiresAt: entry.expiresAt,
156
+ remainingTtlMs: entry.expiresAt > 0 ? Math.max(0, entry.expiresAt - now) : Infinity
157
+ });
158
+ }
159
+ return result;
160
+ }
161
+
162
+ /**
163
+ * Diagnostic statistics
164
+ */
165
+ getStats() {
166
+ const totalOps = this.hits + this.misses;
167
+ const hitRate = totalOps > 0 ? Number((this.hits / totalOps).toFixed(4)) : 0;
168
+ return {
169
+ size: this.cache.size,
170
+ maxSize: this.maxSize,
171
+ ttlMs: this.ttlMs,
172
+ hits: this.hits,
173
+ misses: this.misses,
174
+ hitRate,
175
+ evictions: this.evictions
176
+ };
177
+ }
178
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Fixed-Size In-Memory Ring Buffer for Security Events
3
+ * Overwrites oldest events once capacity is reached.
4
+ *
5
+ * @author ChdLabs
6
+ * @license MIT
7
+ */
8
+
9
+ export class RingBuffer {
10
+ /**
11
+ * @param {number} [capacity=10000] Maximum events to retain in memory
12
+ */
13
+ constructor(capacity = 10_000) {
14
+ this.capacity = Math.max(10, capacity);
15
+ this.buffer = new Array(this.capacity);
16
+ this.head = 0; // points to next write slot
17
+ this.count = 0; // total valid items currently stored
18
+ this.totalPushed = 0;
19
+ }
20
+
21
+ /**
22
+ * Push a new event into the circular buffer
23
+ * @param {*} item
24
+ */
25
+ push(item) {
26
+ this.buffer[this.head] = item;
27
+ this.head = (this.head + 1) % this.capacity;
28
+ if (this.count < this.capacity) {
29
+ this.count++;
30
+ }
31
+ this.totalPushed++;
32
+ }
33
+
34
+ /**
35
+ * Return all stored items in chronological order (oldest to newest)
36
+ * @returns {Array<*>}
37
+ */
38
+ toArray() {
39
+ const result = new Array(this.count);
40
+ // If buffer has not wrapped around
41
+ if (this.count < this.capacity) {
42
+ for (let i = 0; i < this.count; i++) {
43
+ result[i] = this.buffer[i];
44
+ }
45
+ } else {
46
+ // Buffer has wrapped around: head points to the oldest item
47
+ let index = 0;
48
+ for (let i = this.head; i < this.capacity; i++) {
49
+ result[index++] = this.buffer[i];
50
+ }
51
+ for (let i = 0; i < this.head; i++) {
52
+ result[index++] = this.buffer[i];
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+
58
+ /**
59
+ * Return the most recent n items (newest first)
60
+ * @param {number} [n=50]
61
+ * @returns {Array<*>}
62
+ */
63
+ getRecent(n = 50) {
64
+ const limit = Math.min(n, this.count);
65
+ const result = new Array(limit);
66
+ for (let i = 0; i < limit; i++) {
67
+ // Newest item is at (head - 1 - i + capacity) % capacity
68
+ const index = (this.head - 1 - i + this.capacity) % this.capacity;
69
+ result[i] = this.buffer[index];
70
+ }
71
+ return result;
72
+ }
73
+
74
+ /**
75
+ * Reset the buffer
76
+ */
77
+ clear() {
78
+ this.buffer = new Array(this.capacity);
79
+ this.head = 0;
80
+ this.count = 0;
81
+ this.totalPushed = 0;
82
+ }
83
+
84
+ get size() {
85
+ return this.count;
86
+ }
87
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,97 @@
1
+ import { EventEmitter } from 'events';
2
+ import { RequestHandler, Router } from 'express';
3
+
4
+ export interface BloomOptions {
5
+ expectedItems?: number;
6
+ falsePositiveRate?: number;
7
+ sizeBytes?: number | null;
8
+ }
9
+
10
+ export interface LRUOptions {
11
+ maxSize?: number;
12
+ ttlMs?: number;
13
+ }
14
+
15
+ export interface HoneypotOptions {
16
+ enabled?: boolean;
17
+ additionalRoutes?: string[];
18
+ delayMs?: number;
19
+ }
20
+
21
+ export interface FilePersistenceOptions {
22
+ enabled?: boolean;
23
+ directory?: string;
24
+ filename?: string;
25
+ intervalMs?: number;
26
+ loadOnStartup?: boolean;
27
+ }
28
+
29
+ export interface AutoPromoteOptions {
30
+ enabled?: boolean;
31
+ minHits?: number;
32
+ minUniqueIPs?: number;
33
+ }
34
+
35
+ export interface IntelligenceOptions {
36
+ anomalyThreshold?: number;
37
+ anomalyWindowMs?: number;
38
+ captureAll?: boolean;
39
+ ringBufferSize?: number;
40
+ maxUniquePaths?: number;
41
+ excludePatterns?: RegExp[];
42
+ filePersistence?: FilePersistenceOptions;
43
+ autoPromote?: AutoPromoteOptions;
44
+ }
45
+
46
+ export interface ShieldOptions {
47
+ whitelist?: string[];
48
+ bloom?: BloomOptions;
49
+ lru?: LRUOptions;
50
+ honeypot?: HoneypotOptions;
51
+ intelligence?: IntelligenceOptions;
52
+ blockStatusCode?: number;
53
+ logger?: {
54
+ info?: (...args: any[]) => void;
55
+ warn?: (...args: any[]) => void;
56
+ error?: (...args: any[]) => void;
57
+ };
58
+ }
59
+
60
+ export interface CostThrottleOptions {
61
+ weight?: number;
62
+ maxCost?: number;
63
+ windowMs?: number;
64
+ }
65
+
66
+ export interface AdminRouterOptions {
67
+ authMiddleware?: RequestHandler | RequestHandler[];
68
+ }
69
+
70
+ export class Shield extends EventEmitter {
71
+ constructor(options?: ShieldOptions);
72
+
73
+ middleware(): RequestHandler;
74
+ honeypot(): RequestHandler;
75
+ notFoundCollector(): RequestHandler;
76
+ adminRouter(options?: AdminRouterOptions): Router;
77
+ costThrottle(options?: CostThrottleOptions): RequestHandler;
78
+ keyLeakDetector(): RequestHandler;
79
+
80
+ blockIP(ip: string, reason?: string, metadata?: Record<string, any>, customTtlMs?: number | null): void;
81
+ unblockIP(ip: string): void;
82
+ allowIP(ip: string): void;
83
+ removeAllowedIP(ip: string): void;
84
+ isWhitelisted(ip: string): boolean;
85
+
86
+ recordEvent(event: Record<string, any>): void;
87
+ recordBlock(): void;
88
+
89
+ getStats(): Record<string, any>;
90
+ getIntelligence(): Record<string, any>;
91
+ getBloomHealth(): Record<string, any>;
92
+
93
+ shutdown(): Promise<void>;
94
+ }
95
+
96
+ export function createShield(options?: ShieldOptions): Shield;
97
+ export default createShield;
package/src/index.js ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * @chdlabs/my-shield
3
+ * Zero-latency security shield middleware with Bloom filter, honeypots, and 404 intelligence.
4
+ *
5
+ * @author ChdLabs
6
+ * @license MIT
7
+ */
8
+ import { EventEmitter } from 'events';
9
+ import { BloomFilter } from './core/bloomFilter.js';
10
+ import { LRUCache } from './core/lruCache.js';
11
+ import { RingBuffer } from './core/ringBuffer.js';
12
+ import { IntelligenceCollector } from './intelligence/collector.js';
13
+ import { AutoPromoter } from './intelligence/autoPromoter.js';
14
+ import { FilePersistence } from './intelligence/filePersistence.js';
15
+ import { Honeypot } from './middleware/honeypot.js';
16
+ import { NotFoundCollector } from './middleware/notFoundCollector.js';
17
+ import { ShieldGuard } from './middleware/shieldGuard.js';
18
+ import { CostThrottle } from './middleware/costThrottle.js';
19
+ import { KeyLeakDetector } from './middleware/keyLeakDetector.js';
20
+ import { createAdminRouter } from './admin/adminRouter.js';
21
+
22
+ export class Shield extends EventEmitter {
23
+ /**
24
+ * @param {Object} [options={}]
25
+ */
26
+ constructor(options = {}) {
27
+ super();
28
+ this.options = options;
29
+ this.logger = options.logger || console;
30
+ this.startTime = Date.now();
31
+
32
+ // 1. Whitelist (Set of IPs that are never blocked)
33
+ this.whitelist = new Set(
34
+ (options.whitelist || []).map(ip => String(ip).trim())
35
+ );
36
+
37
+ // 2. Core Probabilistic Data Structures
38
+ const bloomOpts = options.bloom || {};
39
+ this.bloom = new BloomFilter({
40
+ expectedItems: bloomOpts.expectedItems || 100_000,
41
+ falsePositiveRate: bloomOpts.falsePositiveRate || 0.01,
42
+ sizeBytes: bloomOpts.sizeBytes || null
43
+ });
44
+
45
+ const lruOpts = options.lru || {};
46
+ this.lru = new LRUCache({
47
+ maxSize: lruOpts.maxSize || 10_000,
48
+ ttlMs: lruOpts.ttlMs || 5 * 60 * 1000 // 5 minutes default
49
+ });
50
+
51
+ const intelOpts = options.intelligence || {};
52
+ const ringCapacity = intelOpts.ringBufferSize || 10_000;
53
+ this.ringBuffer = new RingBuffer(ringCapacity);
54
+
55
+ // 3. 404 Intelligence Collector
56
+ this.collector = new IntelligenceCollector({
57
+ maxUniquePaths: intelOpts.maxUniquePaths || 50_000,
58
+ excludePatterns: intelOpts.excludePatterns || []
59
+ });
60
+
61
+ // 4. Honeypot Subsystem
62
+ const honeypotOpts = options.honeypot || {};
63
+ this.honeypotInstance = new Honeypot({
64
+ enabled: honeypotOpts.enabled !== false,
65
+ additionalRoutes: honeypotOpts.additionalRoutes || [],
66
+ delayMs: honeypotOpts.delayMs ?? 500,
67
+ shield: this,
68
+ logger: this.logger
69
+ });
70
+
71
+ // 5. Auto-Promoter Subsystem
72
+ const autoPromoteOpts = intelOpts.autoPromote || {};
73
+ this.autoPromoter = new AutoPromoter({
74
+ enabled: autoPromoteOpts.enabled !== false,
75
+ minHits: autoPromoteOpts.minHits || 50,
76
+ minUniqueIPs: autoPromoteOpts.minUniqueIPs || 10,
77
+ collector: this.collector,
78
+ honeypot: this.honeypotInstance,
79
+ logger: this.logger
80
+ });
81
+
82
+ // 6. Single-File Persistence Subsystem
83
+ const fileOpts = intelOpts.filePersistence || {};
84
+ this.filePersistence = new FilePersistence({
85
+ enabled: fileOpts.enabled !== false,
86
+ directory: fileOpts.directory || './data/shield',
87
+ filename: fileOpts.filename || '404_intelligence.json',
88
+ intervalMs: fileOpts.intervalMs || 60 * 60 * 1000, // 1 hour default
89
+ loadOnStartup: fileOpts.loadOnStartup !== false,
90
+ collector: this.collector,
91
+ honeypot: this.honeypotInstance,
92
+ autoPromoter: this.autoPromoter,
93
+ logger: this.logger
94
+ });
95
+
96
+ // Start file persistence timer & initial startup load
97
+ if (fileOpts.enabled !== false) {
98
+ this.filePersistence.start();
99
+ }
100
+
101
+ // 7. Middlewares
102
+ this.guard = new ShieldGuard({
103
+ bloom: this.bloom,
104
+ lru: this.lru,
105
+ shield: this,
106
+ blockStatusCode: options.blockStatusCode || 403,
107
+ logger: this.logger
108
+ });
109
+
110
+ this.notFoundHandler = new NotFoundCollector({
111
+ anomalyThreshold: intelOpts.anomalyThreshold || 5,
112
+ anomalyWindowMs: intelOpts.anomalyWindowMs || 60_000,
113
+ captureAll: intelOpts.captureAll !== false,
114
+ collector: this.collector,
115
+ shield: this,
116
+ logger: this.logger
117
+ });
118
+
119
+ this.costThrottleInstance = new CostThrottle();
120
+ this.keyLeakDetectorInstance = new KeyLeakDetector({
121
+ shield: this,
122
+ logger: this.logger
123
+ });
124
+ }
125
+
126
+ /**
127
+ * L1 Guard Express Middleware (Mount first!)
128
+ */
129
+ middleware() {
130
+ return this.guard.middleware();
131
+ }
132
+
133
+ /**
134
+ * Honeypot Express Middleware (Mount before SPA fallback)
135
+ */
136
+ honeypot() {
137
+ return this.honeypotInstance.middleware();
138
+ }
139
+
140
+ /**
141
+ * 404 Collector & Anomaly Detector Middleware (Mount after app routes)
142
+ */
143
+ notFoundCollector() {
144
+ return this.notFoundHandler.middleware();
145
+ }
146
+
147
+ /**
148
+ * Admin Dashboard Router
149
+ * @param {Object} [options]
150
+ */
151
+ adminRouter(options = {}) {
152
+ return createAdminRouter(this, options);
153
+ }
154
+
155
+ /**
156
+ * Route-level Denial-of-Wallet Cost Throttle Decorator
157
+ * @param {Object} [opts]
158
+ */
159
+ costThrottle(opts) {
160
+ return this.costThrottleInstance.create(opts);
161
+ }
162
+
163
+ /**
164
+ * Route-level API Key / Token Leak Detector Middleware
165
+ */
166
+ keyLeakDetector() {
167
+ return this.keyLeakDetectorInstance.middleware();
168
+ }
169
+
170
+ /**
171
+ * Block an IP address immediately
172
+ * @param {string} ip
173
+ * @param {string} [reason='manual_block']
174
+ * @param {Object} [metadata={}]
175
+ * @param {number} [customTtlMs]
176
+ */
177
+ blockIP(ip, reason = 'manual_block', metadata = {}, customTtlMs = null) {
178
+ if (!ip || typeof ip !== 'string') return;
179
+ const cleanIp = ip.trim();
180
+
181
+ // Whitelisted IPs can never be blocked
182
+ if (this.whitelist.has(cleanIp)) return;
183
+
184
+ // 1. Add to Bloom filter (for sub-0.1ms pass-through check)
185
+ this.bloom.add(cleanIp);
186
+
187
+ // 2. Add to LRU confirmed threat cache (with TTL)
188
+ this.lru.set(cleanIp, { reason, metadata }, customTtlMs);
189
+ }
190
+
191
+ /**
192
+ * Unblock an IP address
193
+ * (Removes from LRU cache; Bloom will say maybe, but LRU confirms clean)
194
+ * @param {string} ip
195
+ */
196
+ unblockIP(ip) {
197
+ if (!ip || typeof ip !== 'string') return;
198
+ const cleanIp = ip.trim();
199
+ this.lru.delete(cleanIp);
200
+ }
201
+
202
+ /**
203
+ * Allow an IP address (whitelist)
204
+ * @param {string} ip
205
+ */
206
+ allowIP(ip) {
207
+ if (!ip || typeof ip !== 'string') return;
208
+ const cleanIp = ip.trim();
209
+ this.whitelist.add(cleanIp);
210
+ this.lru.delete(cleanIp);
211
+ }
212
+
213
+ /**
214
+ * Remove an IP from whitelist
215
+ * @param {string} ip
216
+ */
217
+ removeAllowedIP(ip) {
218
+ if (!ip || typeof ip !== 'string') return;
219
+ this.whitelist.delete(ip.trim());
220
+ }
221
+
222
+ /**
223
+ * Check if an IP is whitelisted
224
+ * @param {string} ip
225
+ * @returns {boolean}
226
+ */
227
+ isWhitelisted(ip) {
228
+ if (!ip || typeof ip !== 'string') return false;
229
+ return this.whitelist.has(ip.trim());
230
+ }
231
+
232
+ /**
233
+ * Record a security event in the ring buffer & emit to real-time subscribers
234
+ * @param {Object} event
235
+ */
236
+ recordEvent(event) {
237
+ const enriched = {
238
+ ...event,
239
+ timestamp: event.timestamp || Date.now()
240
+ };
241
+ this.ringBuffer.push(enriched);
242
+ this.emit('event', enriched);
243
+ }
244
+
245
+ /**
246
+ * Increment blocked threat counter in intelligence
247
+ */
248
+ recordBlock() {
249
+ this.collector.recordBlock();
250
+ }
251
+
252
+ /**
253
+ * Aggregate system diagnostics and statistics
254
+ */
255
+ getStats() {
256
+ const trapsList = [];
257
+ for (const [p, meta] of this.honeypotInstance.routeMetadata.entries()) {
258
+ trapsList.push({
259
+ path: p,
260
+ source: meta.source,
261
+ hitCount: meta.hitCount || 0
262
+ });
263
+ }
264
+
265
+ return {
266
+ uptime: (Date.now() - this.startTime) / 1000,
267
+ blockedIPs: this.lru.size,
268
+ eventsTotal: this.ringBuffer.size,
269
+ bloom: this.bloom.getStats(),
270
+ lru: this.lru.getStats(),
271
+ intelligence: this.collector.getStats(),
272
+ honeypot: {
273
+ activeTraps: trapsList.length,
274
+ promotedTraps: this.honeypotInstance.getPromotedRoutes().length,
275
+ traps: trapsList
276
+ },
277
+ persistence: this.filePersistence.getStatus()
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Current 404 intelligence snapshot
283
+ */
284
+ getIntelligence() {
285
+ return this.collector.getSnapshot(this.honeypotInstance.getPromotedRoutes());
286
+ }
287
+
288
+ /**
289
+ * Bloom filter diagnostic metrics
290
+ */
291
+ getBloomHealth() {
292
+ return this.bloom.getStats();
293
+ }
294
+
295
+ /**
296
+ * Graceful shutdown: flush in-memory state to 404_intelligence.json
297
+ */
298
+ async shutdown() {
299
+ this.logger.info?.('[SHIELD] Graceful shutdown initiated. Flushing intelligence to disk...');
300
+ await this.filePersistence.stop();
301
+ this.notFoundHandler.destroy();
302
+ this.costThrottleInstance.destroy();
303
+ this.keyLeakDetectorInstance.destroy();
304
+ this.logger.info?.('[SHIELD] Shutdown complete.');
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Factory helper to instantiate Shield
310
+ * @param {Object} [options]
311
+ * @returns {Shield}
312
+ */
313
+ export function createShield(options = {}) {
314
+ return new Shield(options);
315
+ }
316
+
317
+ export default createShield;