@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,215 @@
1
+ /**
2
+ * Static & Dynamic Route Honeypot Traps
3
+ * Catches vulnerability scanners probing for known sensitive paths,
4
+ * silently returning a 200 OK decoy after a brief delay while instantly
5
+ * blacklisting the IP in the Bloom filter and LRU cache.
6
+ *
7
+ * @author ChdLabs
8
+ * @license MIT
9
+ */
10
+
11
+ export const DEFAULT_TRAP_ROUTES = [
12
+ // WordPress / PHP probes
13
+ '/wp-admin/', '/wp-includes/', '/wp-login.php', '/wp-content/',
14
+ '/xmlrpc.php', '/wp-json/',
15
+
16
+ // Environment / config file probes
17
+ '/.env', '/.env.local', '/.env.production',
18
+ '/.git/config', '/.git/HEAD',
19
+ '/.aws/credentials', '/.docker/config.json',
20
+
21
+ // Database dumps
22
+ '/backup.sql', '/db.sql', '/dump.sql', '/database.sql',
23
+
24
+ // Admin panels
25
+ '/phpMyAdmin/', '/phpmyadmin/', '/adminer.php',
26
+ '/server-status', '/server-info',
27
+
28
+ // Debug / actuator endpoints
29
+ '/api/v1/debug', '/api/v1/admin/config',
30
+ '/actuator/health', '/actuator/env',
31
+ '/debug/vars', '/debug/pprof/',
32
+
33
+ // Common exploit paths
34
+ '/cgi-bin/', '/shell', '/cmd', '/console',
35
+
36
+ // Client DOM Tripwire
37
+ '/api/__shield_trap'
38
+ ];
39
+
40
+ export class Honeypot {
41
+ /**
42
+ * @param {Object} options
43
+ * @param {boolean} [options.enabled=true]
44
+ * @param {Array<string>} [options.additionalRoutes=[]] Custom trap paths
45
+ * @param {number} [options.delayMs=500] Delay before responding with silent 200 OK
46
+ * @param {Object} options.shield Shield core instance
47
+ * @param {Object} [options.logger=console]
48
+ */
49
+ constructor({
50
+ enabled = true,
51
+ additionalRoutes = [],
52
+ delayMs = 500,
53
+ shield,
54
+ logger = console
55
+ } = {}) {
56
+ this.enabled = enabled;
57
+ this.delayMs = Math.max(0, delayMs);
58
+ this.shield = shield;
59
+ this.logger = logger;
60
+
61
+ // Set of exact match trap routes
62
+ this.exactTraps = new Set();
63
+ // Array of prefix match trap routes (ending with /)
64
+ this.prefixTraps = new Set();
65
+ // Promoted/dynamically added routes tracked with metadata
66
+ this.routeMetadata = new Map();
67
+
68
+ // Register default trap routes
69
+ for (const route of DEFAULT_TRAP_ROUTES) {
70
+ this.addRoute(route, 'builtin');
71
+ }
72
+
73
+ // Register custom trap routes
74
+ if (Array.isArray(additionalRoutes)) {
75
+ for (const route of additionalRoutes) {
76
+ if (typeof route === 'string') {
77
+ this.addRoute(route, 'custom');
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Normalize path for matching
85
+ * @param {string} rawPath
86
+ * @returns {string}
87
+ */
88
+ normalizePath(rawPath) {
89
+ if (!rawPath || typeof rawPath !== 'string') return '/';
90
+ const cleaned = rawPath.split('?')[0].split('#')[0].trim();
91
+ return cleaned.replace(/\/+/g, '/') || '/';
92
+ }
93
+
94
+ /**
95
+ * Add a route to the active trap list
96
+ * @param {string} route
97
+ * @param {string} [source='dynamic']
98
+ */
99
+ addRoute(route, source = 'dynamic') {
100
+ const normalized = this.normalizePath(route);
101
+ if (normalized.endsWith('/') && normalized.length > 1) {
102
+ this.prefixTraps.add(normalized.toLowerCase());
103
+ } else {
104
+ this.exactTraps.add(normalized.toLowerCase());
105
+ }
106
+ this.routeMetadata.set(normalized, {
107
+ source,
108
+ addedAt: new Date().toISOString(),
109
+ hitCount: 0
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Remove a trap route
115
+ * @param {string} route
116
+ * @returns {boolean}
117
+ */
118
+ removeRoute(route) {
119
+ const normalized = this.normalizePath(route);
120
+ const lower = normalized.toLowerCase();
121
+ const removed = this.exactTraps.delete(lower) || this.prefixTraps.delete(lower);
122
+ this.routeMetadata.delete(normalized);
123
+ return removed;
124
+ }
125
+
126
+ /**
127
+ * Check if a given request path triggers a honeypot trap
128
+ * @param {string} path
129
+ * @returns {boolean}
130
+ */
131
+ isTrap(path) {
132
+ if (!this.enabled || !path) return false;
133
+ const normalized = this.normalizePath(path).toLowerCase();
134
+
135
+ // Check exact match
136
+ if (this.exactTraps.has(normalized)) {
137
+ return true;
138
+ }
139
+
140
+ // Check directory/prefix match
141
+ for (const prefix of this.prefixTraps) {
142
+ if (normalized.startsWith(prefix)) {
143
+ return true;
144
+ }
145
+ }
146
+
147
+ return false;
148
+ }
149
+
150
+ /**
151
+ * Get list of routes that were auto-promoted or restored
152
+ * @returns {string[]}
153
+ */
154
+ getPromotedRoutes() {
155
+ const promoted = [];
156
+ for (const [route, meta] of this.routeMetadata.entries()) {
157
+ if (meta.source === 'auto_promoted' || meta.source === 'restored_from_file' || meta.source === 'manual_promotion') {
158
+ promoted.push(route);
159
+ }
160
+ }
161
+ return promoted;
162
+ }
163
+
164
+ /**
165
+ * Express middleware handler
166
+ */
167
+ middleware() {
168
+ return (req, res, next) => {
169
+ if (!this.enabled || !this.isTrap(req.path)) {
170
+ return next();
171
+ }
172
+
173
+ // Extract client IP
174
+ const ip = (req.headers['x-forwarded-for']?.split(',')[0]?.trim()) ||
175
+ req.socket?.remoteAddress ||
176
+ req.ip ||
177
+ '127.0.0.1';
178
+
179
+ const matchedPath = this.normalizePath(req.path);
180
+ const meta = this.routeMetadata.get(matchedPath);
181
+ if (meta) {
182
+ meta.hitCount = (meta.hitCount || 0) + 1;
183
+ }
184
+
185
+ // 1. Immediately block IP in Bloom filter and LRU cache
186
+ if (this.shield) {
187
+ this.shield.blockIP(ip, 'honeypot_trap', { path: matchedPath });
188
+ this.shield.recordEvent({
189
+ type: 'honeypot',
190
+ ip,
191
+ path: matchedPath,
192
+ method: req.method,
193
+ userAgent: req.headers['user-agent'] || '',
194
+ timestamp: Date.now()
195
+ });
196
+ }
197
+
198
+ this.logger.warn?.(`[SHIELD] Honeypot triggered by ${ip} on path "${matchedPath}" -> IP Blacklisted`);
199
+
200
+ // 2. Delayed Silent 200 OK decoy response
201
+ const respond = () => {
202
+ if (res.headersSent) return;
203
+ res.status(200)
204
+ .set('Content-Type', 'text/html')
205
+ .send('<html><head><title>Loading...</title></head><body></body></html>');
206
+ };
207
+
208
+ if (this.delayMs > 0) {
209
+ setTimeout(respond, this.delayMs);
210
+ } else {
211
+ respond();
212
+ }
213
+ };
214
+ }
215
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * API Key / Token Leak Detector Middleware
3
+ * Identifies tokens (e.g. JWT/PQC jti claims) used concurrently
4
+ * across multiple distinct IP addresses within a short sliding window.
5
+ *
6
+ * @author ChdLabs
7
+ * @license MIT
8
+ */
9
+
10
+ export class KeyLeakDetector {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {number} [options.windowMs=300000] Window to monitor (default 5 min)
14
+ * @param {number} [options.maxIpCount=2] Max concurrent IPs before flagging
15
+ * @param {Object} options.shield Shield core instance
16
+ * @param {Object} [options.logger=console]
17
+ */
18
+ constructor({
19
+ windowMs = 5 * 60 * 1000,
20
+ maxIpCount = 2,
21
+ shield,
22
+ logger = console
23
+ } = {}) {
24
+ this.windowMs = windowMs;
25
+ this.maxIpCount = maxIpCount;
26
+ this.shield = shield;
27
+ this.logger = logger;
28
+
29
+ // jti -> { ips: Set<string>, firstSeen: number }
30
+ this.jtiTracker = new Map();
31
+
32
+ this.cleanupTimer = setInterval(() => {
33
+ const now = Date.now();
34
+ for (const [jti, rec] of this.jtiTracker.entries()) {
35
+ if (now - rec.firstSeen > this.windowMs) {
36
+ this.jtiTracker.delete(jti);
37
+ }
38
+ }
39
+ }, 60_000);
40
+
41
+ if (typeof this.cleanupTimer.unref === 'function') {
42
+ this.cleanupTimer.unref();
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Middleware to inspect authenticated requests
48
+ * (Expects req.user?.jti or req.token?.jti to be populated by auth middleware)
49
+ */
50
+ middleware() {
51
+ return (req, res, next) => {
52
+ const jti = req.user?.jti || req.token?.jti || req.user?.id;
53
+ if (!jti) {
54
+ return next();
55
+ }
56
+
57
+ const ip = (req.headers['x-forwarded-for']?.split(',')[0]?.trim()) ||
58
+ req.socket?.remoteAddress ||
59
+ req.ip ||
60
+ '127.0.0.1';
61
+
62
+ const now = Date.now();
63
+ let rec = this.jtiTracker.get(jti);
64
+
65
+ if (!rec || (now - rec.firstSeen > this.windowMs)) {
66
+ rec = { ips: new Set([ip]), firstSeen: now };
67
+ this.jtiTracker.set(jti, rec);
68
+ } else {
69
+ rec.ips.add(ip);
70
+
71
+ if (rec.ips.size > this.maxIpCount) {
72
+ const ipList = Array.from(rec.ips);
73
+ this.logger.warn?.(`[SHIELD] Security alert: Token ${jti} used from ${rec.ips.size} distinct IPs: ${ipList.join(', ')}`);
74
+
75
+ if (this.shield) {
76
+ this.shield.recordEvent({
77
+ type: 'key_leak_detected',
78
+ jti,
79
+ ips: ipList,
80
+ path: req.path,
81
+ timestamp: now
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ next();
88
+ };
89
+ }
90
+
91
+ destroy() {
92
+ if (this.cleanupTimer) {
93
+ clearInterval(this.cleanupTimer);
94
+ this.cleanupTimer = null;
95
+ }
96
+ this.jtiTracker.clear();
97
+ }
98
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * 404 Intelligence Collector & Anomaly Detector
3
+ * Captures all 404 status codes into the intelligence engine.
4
+ * Automatically bans IPs that probe >= 5 unique 404 paths within a 60-second window.
5
+ *
6
+ * @author ChdLabs
7
+ * @license MIT
8
+ */
9
+
10
+ export class NotFoundCollector {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {number} [options.anomalyThreshold=5] Unique 404s before IP is banned
14
+ * @param {number} [options.anomalyWindowMs=60000] Sliding window duration in ms
15
+ * @param {boolean} [options.captureAll=true]
16
+ * @param {Object} options.collector IntelligenceCollector instance
17
+ * @param {Object} options.shield Shield core instance
18
+ * @param {Object} [options.logger=console]
19
+ */
20
+ constructor({
21
+ anomalyThreshold = 5,
22
+ anomalyWindowMs = 60_000,
23
+ captureAll = true,
24
+ collector,
25
+ shield,
26
+ logger = console
27
+ } = {}) {
28
+ this.anomalyThreshold = Math.max(1, anomalyThreshold);
29
+ this.anomalyWindowMs = Math.max(1000, anomalyWindowMs);
30
+ this.captureAll = captureAll;
31
+ this.collector = collector;
32
+ this.shield = shield;
33
+ this.logger = logger;
34
+
35
+ // Sliding window tracker: ip -> { paths: Set<string>, firstSeen: number }
36
+ this.ipWindows = new Map();
37
+
38
+ // Periodic pruning of expired IP windows every 60 seconds
39
+ this.cleanupTimer = setInterval(() => {
40
+ this.pruneWindows();
41
+ }, 60_000);
42
+
43
+ if (typeof this.cleanupTimer.unref === 'function') {
44
+ this.cleanupTimer.unref();
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Prune expired sliding windows to keep memory strictly bounded
50
+ */
51
+ pruneWindows() {
52
+ const now = Date.now();
53
+ for (const [ip, window] of this.ipWindows.entries()) {
54
+ if (now - window.firstSeen > this.anomalyWindowMs) {
55
+ this.ipWindows.delete(ip);
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Handle a detected 404 event
62
+ * @param {Object} req Express request
63
+ */
64
+ handle404(req) {
65
+ const ip = (req.headers['x-forwarded-for']?.split(',')[0]?.trim()) ||
66
+ req.socket?.remoteAddress ||
67
+ req.ip ||
68
+ '127.0.0.1';
69
+
70
+ const path = req.path || req.originalUrl || '/';
71
+ const userAgent = req.headers['user-agent'] || '';
72
+
73
+ // If IP is whitelisted, skip anomaly banning
74
+ const isWhitelisted = this.shield?.isWhitelisted(ip);
75
+
76
+ // 1. Record in 404 intelligence collector
77
+ if (this.captureAll && this.collector) {
78
+ this.collector.record404({ path, ip, userAgent });
79
+ }
80
+
81
+ if (isWhitelisted) {
82
+ return;
83
+ }
84
+
85
+ // 2. Sliding window anomaly evaluation
86
+ const now = Date.now();
87
+ let window = this.ipWindows.get(ip);
88
+
89
+ if (!window || (now - window.firstSeen > this.anomalyWindowMs)) {
90
+ window = {
91
+ paths: new Set([path]),
92
+ firstSeen: now
93
+ };
94
+ this.ipWindows.set(ip, window);
95
+ } else {
96
+ window.paths.add(path);
97
+
98
+ // Anomaly threshold reached!
99
+ if (window.paths.size >= this.anomalyThreshold) {
100
+ const uniqueCount = window.paths.size;
101
+ const pathList = Array.from(window.paths);
102
+
103
+ this.logger.warn?.(`[SHIELD] 404 Anomaly threshold reached for IP ${ip} (${uniqueCount} unique paths in ${Math.round((now - window.firstSeen) / 1000)}s) -> Blacklisting IP`);
104
+
105
+ if (this.shield) {
106
+ this.shield.blockIP(ip, '404_anomaly', {
107
+ threshold: this.anomalyThreshold,
108
+ uniquePathsCount: uniqueCount,
109
+ paths: pathList
110
+ });
111
+
112
+ this.shield.recordEvent({
113
+ type: '404_anomaly',
114
+ ip,
115
+ path,
116
+ uniqueCount,
117
+ paths: pathList,
118
+ userAgent,
119
+ timestamp: now
120
+ });
121
+ }
122
+
123
+ // Reset window for this banned IP
124
+ this.ipWindows.delete(ip);
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Express middleware
131
+ */
132
+ middleware() {
133
+ return (req, res, next) => {
134
+ let handled = false;
135
+
136
+ // Hook into response finish event to detect any 404 response
137
+ res.on('finish', () => {
138
+ if (res.statusCode === 404 && !handled) {
139
+ handled = true;
140
+ this.handle404(req);
141
+ }
142
+ });
143
+
144
+ // If an API route reaches here without being handled by app routes, send a 404 response
145
+ // Non-API GET requests pass through to the SPA fallback
146
+ if (req.path.startsWith('/api') || req.method !== 'GET') {
147
+ if (!res.headersSent) {
148
+ handled = true;
149
+ this.handle404(req);
150
+ return res.status(404).json({
151
+ error: 'Not Found',
152
+ path: req.path
153
+ });
154
+ }
155
+ }
156
+
157
+ next();
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Cleanup timer on shutdown
163
+ */
164
+ destroy() {
165
+ if (this.cleanupTimer) {
166
+ clearInterval(this.cleanupTimer);
167
+ this.cleanupTimer = null;
168
+ }
169
+ this.ipWindows.clear();
170
+ }
171
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * L1 Shield Guard Middleware
3
+ * High-performance dual-tier IP reputation gatekeeper:
4
+ * 1. Check Whitelist (instant pass)
5
+ * 2. Bloom Filter Lookup (< 0.1ms; if false -> instant pass)
6
+ * 3. LRU Cache Lookup (confirms whether bloom positive is a real threat)
7
+ *
8
+ * @author ChdLabs
9
+ * @license MIT
10
+ */
11
+
12
+ export class ShieldGuard {
13
+ /**
14
+ * @param {Object} options
15
+ * @param {Object} options.bloom BloomFilter instance
16
+ * @param {Object} options.lru LRUCache instance
17
+ * @param {Object} options.shield Shield core instance
18
+ * @param {number} [options.blockStatusCode=403]
19
+ * @param {Object} [options.logger=console]
20
+ */
21
+ constructor({
22
+ bloom,
23
+ lru,
24
+ shield,
25
+ blockStatusCode = 403,
26
+ logger = console
27
+ } = {}) {
28
+ this.bloom = bloom;
29
+ this.lru = lru;
30
+ this.shield = shield;
31
+ this.blockStatusCode = blockStatusCode;
32
+ this.logger = logger;
33
+ }
34
+
35
+ /**
36
+ * Extract client IP safely from headers or socket
37
+ * @param {Object} req
38
+ * @returns {string}
39
+ */
40
+ getClientIP(req) {
41
+ const forwarded = req.headers['x-forwarded-for'];
42
+ if (forwarded && typeof forwarded === 'string') {
43
+ const first = forwarded.split(',')[0].trim();
44
+ if (first) return first;
45
+ }
46
+ return req.socket?.remoteAddress || req.ip || '127.0.0.1';
47
+ }
48
+
49
+ /**
50
+ * Express middleware
51
+ */
52
+ middleware() {
53
+ return (req, res, next) => {
54
+ const ip = this.getClientIP(req);
55
+
56
+ // 1. Whitelist Bypass
57
+ if (this.shield?.isWhitelisted(ip)) {
58
+ return next();
59
+ }
60
+
61
+ // 2. L1 Bloom Filter Check (< 0.1ms)
62
+ // If false -> IP is definitely clean, pass immediately!
63
+ if (!this.bloom.has(ip)) {
64
+ return next();
65
+ }
66
+
67
+ // 3. Bloom returned true: Check LRU cache to verify it's not a statistical false positive
68
+ const threat = this.lru.get(ip);
69
+ if (!threat) {
70
+ // False positive in Bloom filter (or threat expired from LRU) -> Pass request!
71
+ return next();
72
+ }
73
+
74
+ // Confirmed threat!
75
+ this.shield?.recordBlock();
76
+ this.shield?.recordEvent({
77
+ type: 'blocked_request',
78
+ ip,
79
+ path: req.path,
80
+ method: req.method,
81
+ reason: threat.reason || 'blacklist',
82
+ timestamp: Date.now()
83
+ });
84
+
85
+ this.logger.warn?.(`[SHIELD] Blocked request from ${ip} to ${req.method} ${req.path} (${threat.reason || 'blacklist'})`);
86
+
87
+ res.status(this.blockStatusCode).json({
88
+ error: 'Access denied by security shield',
89
+ code: 'SHIELD_BLOCKED'
90
+ });
91
+ };
92
+ }
93
+ }