@chdlabs/my-shield 0.2.0 → 0.3.1
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/README.md +212 -226
- package/package.json +5 -1
- package/src/admin/otpGate.js +370 -0
- package/src/client/collector.js +303 -53
- package/src/client/index.js +2 -1
- package/src/client/shieldClient.js +121 -0
- package/src/core/ipResolver.js +225 -0
- package/src/index.d.ts +152 -10
- package/src/index.js +113 -4
- package/src/middleware/behavioralGuard.js +172 -24
- package/src/middleware/honeypot.js +6 -4
- package/src/middleware/notFoundCollector.js +6 -4
- package/src/middleware/shieldGuard.js +3 -0
- package/src/storage/BaseAdapter.js +58 -0
- package/src/storage/CallbackStorageAdapter.js +69 -0
- package/src/storage/FileStorageAdapter.js +174 -0
- package/src/storage/MemoryAdapter.js +56 -0
- package/src/storage/index.js +53 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Client IP Resolution & Gateway Protection
|
|
3
|
+
*
|
|
4
|
+
* Provides safe IP extraction across multi-hop reverse proxies, Cloudflare,
|
|
5
|
+
* Nginx, HAProxy, and containerized Docker/Kubernetes environments while
|
|
6
|
+
* defending against header spoofing and accidental gateway blacklisting.
|
|
7
|
+
*
|
|
8
|
+
* @author ChdLabs
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
import net from 'node:net';
|
|
12
|
+
|
|
13
|
+
export class IPResolver {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} [options={}]
|
|
16
|
+
* @param {boolean|string|string[]} [options.trustProxy='loopback,linklocal,uniquelocal']
|
|
17
|
+
* @param {boolean} [options.protectGateways=true]
|
|
18
|
+
* @param {string[]} [options.protectedSubnets] Additional subnets/IPs to protect from bans
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.trustProxy = options.trustProxy !== undefined ? options.trustProxy : 'loopback,linklocal,uniquelocal';
|
|
22
|
+
this.gatewayProtection = options.protectGateways !== false;
|
|
23
|
+
this.protectedSubnets = options.protectedSubnets || [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Clean and normalize raw IP string
|
|
28
|
+
* - Strips surrounding whitespace
|
|
29
|
+
* - Strips IPv6 zone index (e.g. %eth0)
|
|
30
|
+
* - Strips IPv6 port brackets (e.g. [2001:db8::1]:8080 -> 2001:db8::1)
|
|
31
|
+
* - Strips IPv4 port (e.g. 198.51.100.42:54321 -> 198.51.100.42)
|
|
32
|
+
* - Normalizes IPv6-mapped IPv4 (e.g. ::ffff:192.0.2.1 -> 192.0.2.1)
|
|
33
|
+
* @param {string} rawIp
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
normalize(rawIp) {
|
|
37
|
+
if (!rawIp || typeof rawIp !== 'string') return '';
|
|
38
|
+
let ip = rawIp.trim();
|
|
39
|
+
|
|
40
|
+
// 1. Strip IPv6 bracketed port: [2001:db8::1]:8080
|
|
41
|
+
if (ip.startsWith('[') && ip.includes(']')) {
|
|
42
|
+
const closingIdx = ip.indexOf(']');
|
|
43
|
+
ip = ip.substring(1, closingIdx);
|
|
44
|
+
} else if (ip.includes(':')) {
|
|
45
|
+
// Check if it's an IPv4 with port: 1.2.3.4:5678 (contains exactly one ':')
|
|
46
|
+
const colonCount = (ip.match(/:/g) || []).length;
|
|
47
|
+
if (colonCount === 1) {
|
|
48
|
+
const parts = ip.split(':');
|
|
49
|
+
if (net.isIPv4(parts[0])) {
|
|
50
|
+
ip = parts[0];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. Strip IPv6 zone index: fe80::1%eth0 -> fe80::1
|
|
56
|
+
if (ip.includes('%')) {
|
|
57
|
+
ip = ip.split('%')[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Normalize IPv6-mapped IPv4: ::ffff:192.168.1.1 -> 192.168.1.1
|
|
61
|
+
const lower = ip.toLowerCase();
|
|
62
|
+
if (lower.startsWith('::ffff:') && lower.indexOf('.', 7) !== -1) {
|
|
63
|
+
const candidateV4 = ip.substring(7);
|
|
64
|
+
if (net.isIPv4(candidateV4)) {
|
|
65
|
+
return candidateV4;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return ip;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if string is a valid IPv4 or IPv6 address
|
|
74
|
+
* @param {string} ip
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
isValid(ip) {
|
|
78
|
+
if (!ip || typeof ip !== 'string') return false;
|
|
79
|
+
return net.isIP(ip) !== 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if an IPv4 or IPv6 address is private, loopback, or link-local
|
|
84
|
+
* @param {string} ip
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
isPrivateOrLoopback(ip) {
|
|
88
|
+
const clean = this.normalize(ip);
|
|
89
|
+
if (!this.isValid(clean)) return false;
|
|
90
|
+
|
|
91
|
+
if (net.isIPv4(clean)) {
|
|
92
|
+
const octets = clean.split('.').map(Number);
|
|
93
|
+
const [o1, o2] = octets;
|
|
94
|
+
|
|
95
|
+
// 127.0.0.0/8 (Loopback)
|
|
96
|
+
if (o1 === 127) return true;
|
|
97
|
+
// 10.0.0.0/8 (Private)
|
|
98
|
+
if (o1 === 10) return true;
|
|
99
|
+
// 172.16.0.0/12 (Private including Docker 172.17.x.x)
|
|
100
|
+
if (o1 === 172 && o2 >= 16 && o2 <= 31) return true;
|
|
101
|
+
// 192.168.0.0/16 (Private)
|
|
102
|
+
if (o1 === 192 && o2 === 168) return true;
|
|
103
|
+
// 169.254.0.0/16 (Link-local)
|
|
104
|
+
if (o1 === 169 && o2 === 254) return true;
|
|
105
|
+
// 0.0.0.0
|
|
106
|
+
if (clean === '0.0.0.0') return true;
|
|
107
|
+
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (net.isIPv6(clean)) {
|
|
112
|
+
const lower = clean.toLowerCase();
|
|
113
|
+
// ::1 (Loopback)
|
|
114
|
+
if (lower === '::1' || lower === '0:0:0:0:0:0:0:1') return true;
|
|
115
|
+
// fe80::/10 (Link-local)
|
|
116
|
+
if (lower.startsWith('fe8') || lower.startsWith('fe9') || lower.startsWith('fea') || lower.startsWith('feb')) return true;
|
|
117
|
+
// fc00::/7 (Unique local / private: fc00::/8 and fd00::/8)
|
|
118
|
+
if (lower.startsWith('fc') || lower.startsWith('fd')) return true;
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if an IP is a protected container gateway or loopback that should NEVER be banned
|
|
128
|
+
* (e.g. 172.17.0.1 Docker bridge, 127.0.0.1, ::1)
|
|
129
|
+
* @param {string} ip
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
isProtectedGateway(ip) {
|
|
133
|
+
if (!this.gatewayProtection) return false;
|
|
134
|
+
const clean = this.normalize(ip);
|
|
135
|
+
if (!this.isValid(clean)) return false;
|
|
136
|
+
|
|
137
|
+
// Loopback is always protected
|
|
138
|
+
if (clean === '127.0.0.1' || clean === '::1' || clean.startsWith('127.')) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Docker default bridge gateway and bridge subnet: 172.17.0.0/16
|
|
143
|
+
if (net.isIPv4(clean)) {
|
|
144
|
+
const octets = clean.split('.').map(Number);
|
|
145
|
+
// Default docker bridge network gateway: 172.17.0.1
|
|
146
|
+
if (octets[0] === 172 && octets[1] === 17) {
|
|
147
|
+
// If it ends with .1 (standard docker/podman host bridge gateway)
|
|
148
|
+
if (octets[3] === 1) return true;
|
|
149
|
+
}
|
|
150
|
+
// Standard docker compose default gateway: 172.18.0.1, 172.19.0.1, 172.20.0.1
|
|
151
|
+
if (octets[0] === 172 && octets[1] >= 17 && octets[1] <= 31 && octets[3] === 1) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check any user-defined protected subnets/IPs
|
|
157
|
+
for (const subnet of this.protectedSubnets) {
|
|
158
|
+
if (clean === subnet) return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Extract client IP safely from Express request
|
|
166
|
+
* @param {Object} req Express request
|
|
167
|
+
* @returns {string} Normalized client IP
|
|
168
|
+
*/
|
|
169
|
+
resolve(req) {
|
|
170
|
+
if (!req) return '127.0.0.1';
|
|
171
|
+
|
|
172
|
+
// 1. Direct exposure mode (if trustProxy: false, ignore all proxy headers)
|
|
173
|
+
if (this.trustProxy === false) {
|
|
174
|
+
const direct = req.socket?.remoteAddress || req.connection?.remoteAddress || req.ip || '127.0.0.1';
|
|
175
|
+
const clean = this.normalize(direct);
|
|
176
|
+
return this.isValid(clean) ? clean : '127.0.0.1';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const headers = req.headers || {};
|
|
180
|
+
|
|
181
|
+
// 2. Priority check for single-value reverse proxy headers
|
|
182
|
+
const singleHeaders = [
|
|
183
|
+
'cf-connecting-ip', // Cloudflare
|
|
184
|
+
'true-client-ip', // Akamai & Cloudflare Enterprise
|
|
185
|
+
'x-real-ip', // Nginx proxy_set_header X-Real-IP $remote_addr
|
|
186
|
+
'x-client-ip' // Apache & other reverse proxies
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
for (const h of singleHeaders) {
|
|
190
|
+
const val = headers[h];
|
|
191
|
+
if (val && typeof val === 'string') {
|
|
192
|
+
const clean = this.normalize(val);
|
|
193
|
+
if (this.isValid(clean)) {
|
|
194
|
+
return clean;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 3. Multi-hop X-Forwarded-For chain parsing
|
|
200
|
+
const xff = headers['x-forwarded-for'];
|
|
201
|
+
if (xff && typeof xff === 'string') {
|
|
202
|
+
const ips = xff
|
|
203
|
+
.split(',')
|
|
204
|
+
.map(s => this.normalize(s))
|
|
205
|
+
.filter(s => this.isValid(s));
|
|
206
|
+
|
|
207
|
+
if (ips.length > 0) {
|
|
208
|
+
// Find first public IP (skipping private reverse proxy intermediate hops)
|
|
209
|
+
const publicIp = ips.find(ip => !this.isPrivateOrLoopback(ip));
|
|
210
|
+
if (publicIp) {
|
|
211
|
+
return publicIp;
|
|
212
|
+
}
|
|
213
|
+
// If all IPs in chain are private, return the outermost client (first in XFF)
|
|
214
|
+
return ips[0];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 4. Fallback to Express req.ip or socket remote address
|
|
219
|
+
const fallback = req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || '127.0.0.1';
|
|
220
|
+
const cleanFallback = this.normalize(fallback);
|
|
221
|
+
return this.isValid(cleanFallback) ? cleanFallback : '127.0.0.1';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default IPResolver;
|
package/src/index.d.ts
CHANGED
|
@@ -47,13 +47,25 @@ export interface AdminOptions {
|
|
|
47
47
|
enabled?: boolean;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export interface BehavioralVerdict {
|
|
51
|
+
isHuman: boolean;
|
|
52
|
+
probability: number;
|
|
53
|
+
score: number;
|
|
54
|
+
latencyMs: number;
|
|
55
|
+
ip?: string;
|
|
56
|
+
mode?: 'block' | 'audit' | 'header-only';
|
|
57
|
+
reason?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
export interface BehavioralGuardOptions {
|
|
51
61
|
threshold?: number;
|
|
62
|
+
mode?: 'block' | 'audit' | 'header-only';
|
|
52
63
|
headerName?: string;
|
|
53
64
|
replayWindowMs?: number;
|
|
54
65
|
allowFallbackOnError?: boolean;
|
|
55
66
|
autoBlockIP?: boolean;
|
|
56
67
|
onBotDetected?: (req: any, res: any, meta: any) => void;
|
|
68
|
+
onVerdict?: (verdict: BehavioralVerdict, req: any, res: any) => void;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
export interface BiometricsOptions {
|
|
@@ -73,8 +85,44 @@ export interface BehavioralVerificationResult {
|
|
|
73
85
|
latencyMs: number;
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
export interface FileStorageOptions {
|
|
89
|
+
type?: 'file';
|
|
90
|
+
directory?: string;
|
|
91
|
+
filename?: string;
|
|
92
|
+
debounceMs?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CallbackStorageOptions {
|
|
96
|
+
onLoad?: () => Promise<Array<{ ip: string; reason: string; metadata?: any; expiresAt?: number | null }>>;
|
|
97
|
+
onBlock?: (ip: string, reason: string, metadata?: any, expiresAt?: number | null) => Promise<void>;
|
|
98
|
+
onUnblock?: (ip: string) => Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type StorageOption = 'memory' | 'file' | FileStorageOptions | CallbackStorageOptions | BaseStorageAdapter;
|
|
102
|
+
|
|
103
|
+
export interface IPResolverOptions {
|
|
104
|
+
trustProxy?: boolean | string | string[];
|
|
105
|
+
protectGateways?: boolean;
|
|
106
|
+
protectedSubnets?: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface OtpGateOptions {
|
|
110
|
+
adminEmail?: string;
|
|
111
|
+
jwtSecret?: string;
|
|
112
|
+
secret?: string;
|
|
113
|
+
cookieName?: string;
|
|
114
|
+
codeTtlMs?: number;
|
|
115
|
+
sessionTtlMs?: number;
|
|
116
|
+
maxAttempts?: number;
|
|
117
|
+
sendOtp?: (email: string, code: string) => Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
|
|
76
120
|
export interface ShieldOptions {
|
|
77
121
|
whitelist?: string[];
|
|
122
|
+
trustProxy?: boolean | string | string[];
|
|
123
|
+
protectGateways?: boolean;
|
|
124
|
+
protectedSubnets?: string[];
|
|
125
|
+
storage?: StorageOption;
|
|
78
126
|
bloom?: BloomOptions;
|
|
79
127
|
lru?: LRUOptions;
|
|
80
128
|
honeypot?: HoneypotOptions;
|
|
@@ -100,9 +148,110 @@ export interface AdminRouterOptions {
|
|
|
100
148
|
authMiddleware?: RequestHandler | RequestHandler[];
|
|
101
149
|
}
|
|
102
150
|
|
|
151
|
+
export class BaseStorageAdapter {
|
|
152
|
+
shield: any;
|
|
153
|
+
init(shield: any): Promise<void>;
|
|
154
|
+
loadBlockedIPs(): Promise<Array<{ ip: string; reason: string; metadata?: any; expiresAt?: number | null }>>;
|
|
155
|
+
saveBlockedIP(ip: string, reason: string, metadata?: any, expiresAt?: number | null): Promise<void>;
|
|
156
|
+
removeBlockedIP(ip: string): Promise<void>;
|
|
157
|
+
close(): Promise<void>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class MemoryAdapter extends BaseStorageAdapter {}
|
|
161
|
+
export class FileStorageAdapter extends BaseStorageAdapter {
|
|
162
|
+
constructor(options?: FileStorageOptions & { logger?: any });
|
|
163
|
+
flushSync(): void;
|
|
164
|
+
}
|
|
165
|
+
export class CallbackStorageAdapter extends BaseStorageAdapter {
|
|
166
|
+
constructor(options?: CallbackStorageOptions & { logger?: any });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createStorageAdapter(storageOption?: StorageOption, logger?: any): BaseStorageAdapter;
|
|
170
|
+
|
|
171
|
+
export class IPResolver {
|
|
172
|
+
constructor(options?: IPResolverOptions);
|
|
173
|
+
normalize(rawIp: string): string;
|
|
174
|
+
isValid(ip: string): boolean;
|
|
175
|
+
isPrivateOrLoopback(ip: string): boolean;
|
|
176
|
+
isProtectedGateway(ip: string): boolean;
|
|
177
|
+
resolve(req: any): string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export class OTPGate {
|
|
181
|
+
constructor(options?: OtpGateOptions & { logger?: any });
|
|
182
|
+
generateCode(): string;
|
|
183
|
+
createSessionToken(email: string): string;
|
|
184
|
+
verifySessionToken(token: string): { valid: boolean; email?: string };
|
|
185
|
+
middleware(): RequestHandler;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createOtpGate(options?: OtpGateOptions & { logger?: any }): RequestHandler;
|
|
189
|
+
|
|
190
|
+
export type PointerType = 'mouse' | 'touch' | 'pen' | string;
|
|
191
|
+
|
|
192
|
+
export interface PointerTelemetryCollectorOptions {
|
|
193
|
+
windowLength?: number;
|
|
194
|
+
sampleIntervalMs?: number;
|
|
195
|
+
maxBufferSize?: number;
|
|
196
|
+
maxIdleMs?: number;
|
|
197
|
+
minPoints?: number;
|
|
198
|
+
flushOnConsumption?: boolean;
|
|
199
|
+
captureTouch?: boolean;
|
|
200
|
+
minTapDwellMs?: number;
|
|
201
|
+
maxTapDwellMs?: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type MouseTelemetryCollectorOptions = PointerTelemetryCollectorOptions;
|
|
205
|
+
|
|
206
|
+
export interface ShieldClientOptions extends PointerTelemetryCollectorOptions {
|
|
207
|
+
autoInjectTripwire?: boolean;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface InstrumentFetchOptions {
|
|
211
|
+
headerName?: string;
|
|
212
|
+
endpoints?: string[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface PointerTelemetryPayload {
|
|
216
|
+
velocity: number[];
|
|
217
|
+
isTrusted: boolean;
|
|
218
|
+
timestamp: number;
|
|
219
|
+
pointerType?: PointerType;
|
|
220
|
+
source?: 'trajectory' | 'tap' | string;
|
|
221
|
+
dwellMs?: number;
|
|
222
|
+
jitter?: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class PointerTelemetryCollector {
|
|
226
|
+
windowLength: number;
|
|
227
|
+
sampleIntervalMs: number;
|
|
228
|
+
maxBufferSize: number;
|
|
229
|
+
constructor(options?: PointerTelemetryCollectorOptions);
|
|
230
|
+
start(): void;
|
|
231
|
+
stop(): void;
|
|
232
|
+
getPayload(): PointerTelemetryPayload | null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const MouseTelemetryCollector: typeof PointerTelemetryCollector;
|
|
236
|
+
export type MouseTelemetryCollector = PointerTelemetryCollector;
|
|
237
|
+
|
|
238
|
+
export class ShieldClient {
|
|
239
|
+
collector: PointerTelemetryCollector;
|
|
240
|
+
constructor(options?: ShieldClientOptions);
|
|
241
|
+
start(): void;
|
|
242
|
+
stop(): void;
|
|
243
|
+
getPayload(): PointerTelemetryPayload | null;
|
|
244
|
+
instrumentFetch(options?: InstrumentFetchOptions): void;
|
|
245
|
+
instrumentAxios(axiosInstance: any, options?: InstrumentFetchOptions): void;
|
|
246
|
+
}
|
|
247
|
+
|
|
103
248
|
export class Shield extends EventEmitter {
|
|
249
|
+
ipResolver: IPResolver;
|
|
250
|
+
storageAdapter: BaseStorageAdapter;
|
|
251
|
+
|
|
104
252
|
constructor(options?: ShieldOptions);
|
|
105
253
|
|
|
254
|
+
init(): Promise<this>;
|
|
106
255
|
middleware(): RequestHandler;
|
|
107
256
|
honeypot(): RequestHandler;
|
|
108
257
|
notFoundCollector(): RequestHandler;
|
|
@@ -110,7 +259,10 @@ export class Shield extends EventEmitter {
|
|
|
110
259
|
costThrottle(options?: CostThrottleOptions): RequestHandler;
|
|
111
260
|
keyLeakDetector(): RequestHandler;
|
|
112
261
|
behavioralGuard(options?: BehavioralGuardOptions): RequestHandler;
|
|
262
|
+
strictBotGuard(options?: BehavioralGuardOptions): RequestHandler;
|
|
263
|
+
otpGate(options?: OtpGateOptions): RequestHandler;
|
|
113
264
|
verifyBiometrics(velocity: number[] | Float64Array): BehavioralVerificationResult;
|
|
265
|
+
verifyTapDwell(dwellMs: number): BehavioralVerificationResult;
|
|
114
266
|
|
|
115
267
|
blockIP(ip: string, reason?: string, metadata?: Record<string, any>, customTtlMs?: number | null): void;
|
|
116
268
|
unblockIP(ip: string): void;
|
|
@@ -148,16 +300,6 @@ export class DualRidgeClassifier {
|
|
|
148
300
|
predict(features: Float32Array | number[]): { score: number; probability: number; isHuman: boolean };
|
|
149
301
|
}
|
|
150
302
|
|
|
151
|
-
export class MouseTelemetryCollector {
|
|
152
|
-
windowLength: number;
|
|
153
|
-
sampleIntervalMs: number;
|
|
154
|
-
maxBufferSize: number;
|
|
155
|
-
constructor(options?: { windowLength?: number; sampleIntervalMs?: number; maxBufferSize?: number });
|
|
156
|
-
start(): void;
|
|
157
|
-
stop(): void;
|
|
158
|
-
getPayload(): { velocity: number[]; isTrusted: boolean; timestamp: number } | null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
303
|
export function injectTripwire(): void;
|
|
162
304
|
export function createShield(options?: ShieldOptions): Shield;
|
|
163
305
|
export default createShield;
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,9 @@ import { BehavioralShieldService } from './middleware/behavioralGuard.js';
|
|
|
21
21
|
import { MiniRocket } from './biometrics/minirocket.js';
|
|
22
22
|
import { DualRidgeClassifier } from './biometrics/ridge.js';
|
|
23
23
|
import { createAdminRouter } from './admin/adminRouter.js';
|
|
24
|
+
import { IPResolver } from './core/ipResolver.js';
|
|
25
|
+
import { createStorageAdapter } from './storage/index.js';
|
|
26
|
+
import { createOtpGate, OTPGate } from './admin/otpGate.js';
|
|
24
27
|
|
|
25
28
|
export class Shield extends EventEmitter {
|
|
26
29
|
/**
|
|
@@ -136,6 +139,47 @@ export class Shield extends EventEmitter {
|
|
|
136
139
|
// 9. Admin Dashboard Configuration (Default: false)
|
|
137
140
|
const adminOpts = options.admin || {};
|
|
138
141
|
this.adminEnabled = Boolean(adminOpts.enabled);
|
|
142
|
+
|
|
143
|
+
// 10. Enterprise Client IP Resolution & Gateway Protection
|
|
144
|
+
this.ipResolver = new IPResolver({
|
|
145
|
+
trustProxy: options.trustProxy,
|
|
146
|
+
protectGateways: options.protectGateways !== false,
|
|
147
|
+
protectedSubnets: options.protectedSubnets || []
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 11. Pluggable Storage Adapter (Memory, File, or Database Callbacks)
|
|
151
|
+
this.storageAdapter = createStorageAdapter(options.storage, this.logger);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Asynchronous initialization: hydrates unexpired threats from persistent storage
|
|
156
|
+
* into Bloom filter and LRU cache.
|
|
157
|
+
* @returns {Promise<this>}
|
|
158
|
+
*/
|
|
159
|
+
async init() {
|
|
160
|
+
await this.storageAdapter.init(this);
|
|
161
|
+
try {
|
|
162
|
+
const blockedList = await this.storageAdapter.loadBlockedIPs();
|
|
163
|
+
if (Array.isArray(blockedList)) {
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
for (const item of blockedList) {
|
|
166
|
+
if (item && item.ip) {
|
|
167
|
+
const cleanIp = this.ipResolver ? this.ipResolver.normalize(item.ip) : item.ip.trim();
|
|
168
|
+
if (!this.whitelist.has(cleanIp) && !this.ipResolver.isProtectedGateway(cleanIp)) {
|
|
169
|
+
this.bloom.add(cleanIp);
|
|
170
|
+
const ttl = item.expiresAt ? Math.max(1, item.expiresAt - now) : null;
|
|
171
|
+
this.lru.set(cleanIp, { reason: item.reason || 'restored_threat', metadata: item.metadata || {} }, ttl);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (blockedList.length > 0) {
|
|
176
|
+
this.logger.info?.(`[SHIELD] Hydrated ${blockedList.length} persistent threats into Bloom & LRU.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
this.logger.error?.('[SHIELD:STORAGE] Error during startup threat hydration:', err.message);
|
|
181
|
+
}
|
|
182
|
+
return this;
|
|
139
183
|
}
|
|
140
184
|
|
|
141
185
|
/**
|
|
@@ -191,6 +235,32 @@ export class Shield extends EventEmitter {
|
|
|
191
235
|
return this.behavioralShieldService.middleware(opts);
|
|
192
236
|
}
|
|
193
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Route-level strict behavioral bot defense decorator
|
|
240
|
+
* Designed specifically for mission-critical routes like /login, /checkout, /register
|
|
241
|
+
* Automatically rejects bots with 403 and blacklists their IP in Shield L1.
|
|
242
|
+
* @param {Object} [opts]
|
|
243
|
+
*/
|
|
244
|
+
strictBotGuard(opts = {}) {
|
|
245
|
+
return this.behavioralGuard({
|
|
246
|
+
mode: 'block',
|
|
247
|
+
autoBlockIP: true,
|
|
248
|
+
threshold: 0.50,
|
|
249
|
+
...opts
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Shield Standalone OTP Authentication Gate Middleware
|
|
255
|
+
* @param {Object} [options]
|
|
256
|
+
*/
|
|
257
|
+
otpGate(options = {}) {
|
|
258
|
+
return createOtpGate({
|
|
259
|
+
logger: this.logger,
|
|
260
|
+
...options
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
194
264
|
/**
|
|
195
265
|
* Verify kinematics velocity series against MiniRocket model
|
|
196
266
|
* @param {number[]|Float64Array} velocityArray
|
|
@@ -199,6 +269,14 @@ export class Shield extends EventEmitter {
|
|
|
199
269
|
return this.behavioralShieldService.verify(velocityArray);
|
|
200
270
|
}
|
|
201
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Verify mobile tap dwell kinetics against calibrated human distribution
|
|
274
|
+
* @param {number} dwellMs
|
|
275
|
+
*/
|
|
276
|
+
verifyTapDwell(dwellMs) {
|
|
277
|
+
return this.behavioralShieldService.verifyTapDwell(dwellMs);
|
|
278
|
+
}
|
|
279
|
+
|
|
202
280
|
/**
|
|
203
281
|
* Block an IP address immediately
|
|
204
282
|
* @param {string} ip
|
|
@@ -208,16 +286,28 @@ export class Shield extends EventEmitter {
|
|
|
208
286
|
*/
|
|
209
287
|
blockIP(ip, reason = 'manual_block', metadata = {}, customTtlMs = null) {
|
|
210
288
|
if (!ip || typeof ip !== 'string') return;
|
|
211
|
-
const cleanIp = ip.trim();
|
|
289
|
+
const cleanIp = this.ipResolver ? this.ipResolver.normalize(ip) : ip.trim();
|
|
212
290
|
|
|
213
|
-
// Whitelisted IPs can never be blocked
|
|
291
|
+
// Whitelisted IPs and protected gateways can never be blocked
|
|
214
292
|
if (this.whitelist.has(cleanIp)) return;
|
|
293
|
+
if (this.ipResolver && this.ipResolver.isProtectedGateway(cleanIp)) {
|
|
294
|
+
this.logger.warn?.(`[SHIELD] Block attempted on protected gateway/loopback (${cleanIp}). Block prevented.`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
215
297
|
|
|
216
298
|
// 1. Add to Bloom filter (for sub-0.1ms pass-through check)
|
|
217
299
|
this.bloom.add(cleanIp);
|
|
218
300
|
|
|
219
301
|
// 2. Add to LRU confirmed threat cache (with TTL)
|
|
220
302
|
this.lru.set(cleanIp, { reason, metadata }, customTtlMs);
|
|
303
|
+
|
|
304
|
+
// 3. Persist to storage adapter
|
|
305
|
+
const expiresAt = customTtlMs ? Date.now() + customTtlMs : (this.lru.ttlMs ? Date.now() + this.lru.ttlMs : null);
|
|
306
|
+
try {
|
|
307
|
+
this.storageAdapter?.saveBlockedIP(cleanIp, reason, metadata, expiresAt);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
this.logger.error?.('[SHIELD:STORAGE] Error saving blocked IP to storage:', err.message);
|
|
310
|
+
}
|
|
221
311
|
}
|
|
222
312
|
|
|
223
313
|
/**
|
|
@@ -227,8 +317,14 @@ export class Shield extends EventEmitter {
|
|
|
227
317
|
*/
|
|
228
318
|
unblockIP(ip) {
|
|
229
319
|
if (!ip || typeof ip !== 'string') return;
|
|
230
|
-
const cleanIp = ip.trim();
|
|
320
|
+
const cleanIp = this.ipResolver ? this.ipResolver.normalize(ip) : ip.trim();
|
|
231
321
|
this.lru.delete(cleanIp);
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
this.storageAdapter?.removeBlockedIP(cleanIp);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
this.logger.error?.('[SHIELD:STORAGE] Error removing blocked IP from storage:', err.message);
|
|
327
|
+
}
|
|
232
328
|
}
|
|
233
329
|
|
|
234
330
|
/**
|
|
@@ -328,8 +424,9 @@ export class Shield extends EventEmitter {
|
|
|
328
424
|
* Graceful shutdown: flush in-memory state to 404_intelligence.json
|
|
329
425
|
*/
|
|
330
426
|
async shutdown() {
|
|
331
|
-
this.logger.info?.('[SHIELD] Graceful shutdown initiated. Flushing
|
|
427
|
+
this.logger.info?.('[SHIELD] Graceful shutdown initiated. Flushing state to disk...');
|
|
332
428
|
await this.filePersistence.stop();
|
|
429
|
+
await this.storageAdapter?.close?.();
|
|
333
430
|
this.notFoundHandler.destroy();
|
|
334
431
|
this.costThrottleInstance.destroy();
|
|
335
432
|
this.keyLeakDetectorInstance.destroy();
|
|
@@ -349,5 +446,17 @@ export function createShield(options = {}) {
|
|
|
349
446
|
export { MiniRocket } from './biometrics/minirocket.js';
|
|
350
447
|
export { DualRidgeClassifier } from './biometrics/ridge.js';
|
|
351
448
|
export { BehavioralShieldService } from './middleware/behavioralGuard.js';
|
|
449
|
+
export { IPResolver } from './core/ipResolver.js';
|
|
450
|
+
export { OTPGate, createOtpGate } from './admin/otpGate.js';
|
|
451
|
+
export {
|
|
452
|
+
BaseStorageAdapter,
|
|
453
|
+
MemoryAdapter,
|
|
454
|
+
FileStorageAdapter,
|
|
455
|
+
CallbackStorageAdapter,
|
|
456
|
+
createStorageAdapter
|
|
457
|
+
} from './storage/index.js';
|
|
458
|
+
export { ShieldClient } from './client/shieldClient.js';
|
|
459
|
+
export { PointerTelemetryCollector, MouseTelemetryCollector } from './client/collector.js';
|
|
460
|
+
export { injectTripwire } from './client/index.js';
|
|
352
461
|
|
|
353
462
|
export default createShield;
|