@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.
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/package.json +45 -0
- package/src/admin/adminRouter.js +221 -0
- package/src/admin/dashboard/dashboard.css +534 -0
- package/src/admin/dashboard/dashboard.js +610 -0
- package/src/admin/dashboard/index.html +266 -0
- package/src/client/shield-client.js +63 -0
- package/src/core/bloomFilter.js +191 -0
- package/src/core/lruCache.js +178 -0
- package/src/core/ringBuffer.js +87 -0
- package/src/index.d.ts +97 -0
- package/src/index.js +317 -0
- package/src/intelligence/autoPromoter.js +91 -0
- package/src/intelligence/collector.js +219 -0
- package/src/intelligence/filePersistence.js +226 -0
- package/src/middleware/costThrottle.js +75 -0
- package/src/middleware/honeypot.js +215 -0
- package/src/middleware/keyLeakDetector.js +98 -0
- package/src/middleware/notFoundCollector.js +171 -0
- package/src/middleware/shieldGuard.js +93 -0
|
@@ -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;
|