devfortress-sdk 4.2.0 → 4.2.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/dist/abuseipdb.d.ts +10 -0
- package/dist/abuseipdb.js +121 -0
- package/dist/agent-security.d.ts +96 -0
- package/dist/agent-security.js +390 -0
- package/dist/agent.d.ts +61 -0
- package/dist/agent.js +177 -0
- package/dist/browser.d.ts +0 -27
- package/dist/browser.js +0 -33
- package/dist/circuit-breaker.d.ts +0 -41
- package/dist/circuit-breaker.js +1 -42
- package/dist/client.d.ts +0 -13
- package/dist/client.js +1 -19
- package/dist/devfortress.d.ts +64 -0
- package/dist/devfortress.js +758 -0
- package/dist/index.d.ts +0 -32
- package/dist/index.js +0 -40
- package/dist/internal-closed-loop-engine.d.ts +123 -0
- package/dist/internal-closed-loop-engine.js +683 -0
- package/dist/middleware/express.d.ts +0 -6
- package/dist/middleware/express.js +11 -41
- package/dist/quick.d.ts +0 -16
- package/dist/quick.js +0 -25
- package/dist/tier-gate.d.ts +38 -0
- package/dist/tier-gate.js +132 -0
- package/dist/token-alias.d.ts +47 -0
- package/dist/token-alias.js +312 -0
- package/dist/types.d.ts +0 -37
- package/dist/types.js +0 -10
- package/dist/unified-audit.d.ts +70 -0
- package/dist/unified-audit.js +171 -0
- package/package.json +2 -15
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DevFortress = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const internal_closed_loop_engine_1 = require("./internal-closed-loop-engine");
|
|
10
|
+
const circuit_breaker_1 = require("./circuit-breaker");
|
|
11
|
+
const unified_audit_1 = require("./unified-audit");
|
|
12
|
+
const tier_gate_1 = require("./tier-gate");
|
|
13
|
+
const DEFAULT_ENDPOINT = 'https://www.devfortress.net/api/events/ingest';
|
|
14
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
15
|
+
const DEFAULT_RETRIES = 3;
|
|
16
|
+
const WEBHOOK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
17
|
+
function deriveBaseUrl(endpoint) {
|
|
18
|
+
const url = new URL(endpoint);
|
|
19
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
20
|
+
if (parts.length > 0 && parts[parts.length - 1] === 'ingest') {
|
|
21
|
+
parts.pop();
|
|
22
|
+
}
|
|
23
|
+
url.pathname = '/' + parts.join('/');
|
|
24
|
+
return url.toString().replace(/\/$/, '');
|
|
25
|
+
}
|
|
26
|
+
const memoryCache = new Map();
|
|
27
|
+
function memGet(key) {
|
|
28
|
+
const entry = memoryCache.get(key);
|
|
29
|
+
if (!entry)
|
|
30
|
+
return null;
|
|
31
|
+
if (Date.now() > entry.expiresAt) {
|
|
32
|
+
memoryCache.delete(key);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return entry.value;
|
|
36
|
+
}
|
|
37
|
+
function memSet(key, value, ttlSeconds) {
|
|
38
|
+
memoryCache.set(key, {
|
|
39
|
+
value,
|
|
40
|
+
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
class DevFortress {
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.handlers = new Map();
|
|
46
|
+
this.internalEngine = null;
|
|
47
|
+
this.circuitBreaker = null;
|
|
48
|
+
if (!config.apiKey) {
|
|
49
|
+
throw new Error('DevFortress: apiKey is required');
|
|
50
|
+
}
|
|
51
|
+
if (!config.appId) {
|
|
52
|
+
throw new Error('DevFortress: appId is required');
|
|
53
|
+
}
|
|
54
|
+
this.apiKey = config.apiKey;
|
|
55
|
+
this.appId = config.appId;
|
|
56
|
+
this.environment = config.environment || 'production';
|
|
57
|
+
this.debug = config.debug || false;
|
|
58
|
+
this.endpoint = config.endpoint || DEFAULT_ENDPOINT;
|
|
59
|
+
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
60
|
+
this.retries = config.retries || DEFAULT_RETRIES;
|
|
61
|
+
this.enrichment = config.enrichment;
|
|
62
|
+
this.responseWebhook = config.responseWebhook;
|
|
63
|
+
this.cacheConfig = config.cache;
|
|
64
|
+
const tier = (config.tier || 'starter');
|
|
65
|
+
this.tierGate = new tier_gate_1.TierGate(tier, {
|
|
66
|
+
hybridAddonEnabled: config.hybridAddonEnabled,
|
|
67
|
+
});
|
|
68
|
+
this.mode = this.tierGate.getEffectiveMode(config.mode);
|
|
69
|
+
this.auditTrail = new unified_audit_1.UnifiedAuditTrail({ debug: this.debug });
|
|
70
|
+
if (this.mode === 'internal' || this.mode === 'hybrid') {
|
|
71
|
+
const iclConfig = {
|
|
72
|
+
failMode: config.internalCL?.failMode ?? 'closed',
|
|
73
|
+
enableExternalRelay: this.mode === 'hybrid',
|
|
74
|
+
tier2Scorer: config.internalCL
|
|
75
|
+
?.tier2Scorer,
|
|
76
|
+
blockThreshold: config.internalCL?.blockThreshold ?? 85,
|
|
77
|
+
rateLimitMax: config.internalCL?.rateLimitMax ?? 100,
|
|
78
|
+
rateLimitWindowMs: config.internalCL?.rateLimitWindowMs ?? 60000,
|
|
79
|
+
debug: this.debug,
|
|
80
|
+
};
|
|
81
|
+
this.internalEngine = new internal_closed_loop_engine_1.InternalClosedLoopEngine(iclConfig);
|
|
82
|
+
}
|
|
83
|
+
if (this.mode === 'hybrid') {
|
|
84
|
+
this.circuitBreaker = new circuit_breaker_1.PlatformCircuitBreaker({
|
|
85
|
+
failureThreshold: config.circuitBreaker?.failureThreshold ?? 3,
|
|
86
|
+
recoveryTimeMs: config.circuitBreaker?.recoveryTimeMs ?? 60000,
|
|
87
|
+
onStateChange: (from, to, reason) => {
|
|
88
|
+
this.log('warn', `Circuit breaker: ${from} → ${to} (${reason})`);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (this.endpoint.startsWith('http://') &&
|
|
93
|
+
!this.endpoint.includes('localhost') &&
|
|
94
|
+
!this.endpoint.includes('127.0.0.1')) {
|
|
95
|
+
this.log('warn', 'Using non-HTTPS endpoint. API key will be transmitted in cleartext.');
|
|
96
|
+
}
|
|
97
|
+
this.axios = axios_1.default.create({
|
|
98
|
+
baseURL: this.endpoint,
|
|
99
|
+
timeout: this.timeout,
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
'X-DevFortress-Key': this.apiKey,
|
|
103
|
+
'X-DevFortress-App': this.appId,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const baseApiUrl = deriveBaseUrl(this.endpoint);
|
|
107
|
+
this.apiAxios = axios_1.default.create({
|
|
108
|
+
baseURL: baseApiUrl,
|
|
109
|
+
timeout: this.timeout,
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
'X-DevFortress-Key': this.apiKey,
|
|
113
|
+
'X-DevFortress-App': this.appId,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
this.log('info', `DevFortress SDK v4.0.0 initialized (app=${this.appId}, mode=${this.mode}, tier=${tier})`);
|
|
117
|
+
}
|
|
118
|
+
getMode() {
|
|
119
|
+
return this.mode;
|
|
120
|
+
}
|
|
121
|
+
getAudit() {
|
|
122
|
+
return this.auditTrail;
|
|
123
|
+
}
|
|
124
|
+
getTierGate() {
|
|
125
|
+
return this.tierGate;
|
|
126
|
+
}
|
|
127
|
+
getInternalEngine() {
|
|
128
|
+
return this.internalEngine;
|
|
129
|
+
}
|
|
130
|
+
getCircuitBreakerDiagnostics() {
|
|
131
|
+
return this.circuitBreaker?.getDiagnostics() ?? null;
|
|
132
|
+
}
|
|
133
|
+
async observe(req, options) {
|
|
134
|
+
const limitCheck = this.tierGate.checkEventLimit();
|
|
135
|
+
if (!limitCheck.allowed) {
|
|
136
|
+
this.log('warn', 'Daily event limit reached, skipping observe()');
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
this.tierGate.recordEvent();
|
|
140
|
+
try {
|
|
141
|
+
const r = req;
|
|
142
|
+
const method = r.method || 'GET';
|
|
143
|
+
const path = r.url || r.path || '/';
|
|
144
|
+
const headers = r.headers || {};
|
|
145
|
+
let ip = '0.0.0.0';
|
|
146
|
+
if (!options?.skipEnrichment && this.enrichment?.getIP) {
|
|
147
|
+
ip = this.enrichment.getIP(req) || ip;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const forwarded = headers['x-forwarded-for'];
|
|
151
|
+
ip =
|
|
152
|
+
(typeof forwarded === 'string'
|
|
153
|
+
? forwarded.split(',')[0]?.trim()
|
|
154
|
+
: undefined) ||
|
|
155
|
+
headers['x-real-ip'] ||
|
|
156
|
+
ip;
|
|
157
|
+
}
|
|
158
|
+
let userId = null;
|
|
159
|
+
let sessionId = null;
|
|
160
|
+
if (!options?.skipEnrichment && this.enrichment) {
|
|
161
|
+
if (this.enrichment.getUserId) {
|
|
162
|
+
userId = await Promise.resolve(this.enrichment.getUserId(req));
|
|
163
|
+
}
|
|
164
|
+
if (this.enrichment.getSessionId) {
|
|
165
|
+
sessionId = await Promise.resolve(this.enrichment.getSessionId(req));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const userAgent = typeof headers['user-agent'] === 'string'
|
|
169
|
+
? headers['user-agent']
|
|
170
|
+
: null;
|
|
171
|
+
if (this.mode === 'internal') {
|
|
172
|
+
return this.observeInternal({
|
|
173
|
+
ip,
|
|
174
|
+
method,
|
|
175
|
+
path,
|
|
176
|
+
userAgent: userAgent || undefined,
|
|
177
|
+
userId,
|
|
178
|
+
sessionId,
|
|
179
|
+
}, options);
|
|
180
|
+
}
|
|
181
|
+
else if (this.mode === 'hybrid') {
|
|
182
|
+
return this.observeHybrid({
|
|
183
|
+
ip,
|
|
184
|
+
method,
|
|
185
|
+
path,
|
|
186
|
+
userAgent: userAgent || undefined,
|
|
187
|
+
userId,
|
|
188
|
+
sessionId,
|
|
189
|
+
}, options);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
return this.observeExternal({ ip, method, path, userAgent, userId, sessionId }, options);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
this.log('error', `observe() failed: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async observeExternal(parsed, options) {
|
|
201
|
+
const startTime = performance.now();
|
|
202
|
+
try {
|
|
203
|
+
const payload = {
|
|
204
|
+
eventType: 'custom',
|
|
205
|
+
ip: parsed.ip,
|
|
206
|
+
method: parsed.method,
|
|
207
|
+
path: parsed.path,
|
|
208
|
+
userAgent: parsed.userAgent,
|
|
209
|
+
userId: parsed.userId,
|
|
210
|
+
sessionId: parsed.sessionId,
|
|
211
|
+
appId: this.appId,
|
|
212
|
+
environment: this.environment,
|
|
213
|
+
timestamp: new Date().toISOString(),
|
|
214
|
+
metadata: {
|
|
215
|
+
...options?.meta,
|
|
216
|
+
clMode: options?.meta?.clMode || this.mode,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
const response = await this.sendWithRetry(payload);
|
|
220
|
+
const data = response.data;
|
|
221
|
+
const latencyMs = performance.now() - startTime;
|
|
222
|
+
this.auditTrail.record({
|
|
223
|
+
eventId: data.eventId || data.event_id || `ext_${Date.now().toString(36)}`,
|
|
224
|
+
timestamp: Date.now(),
|
|
225
|
+
source: 'external',
|
|
226
|
+
tier: 0,
|
|
227
|
+
decision: data.flagged ? 'block' : 'allow',
|
|
228
|
+
score: data.confidence ? data.confidence * 100 : 0,
|
|
229
|
+
severity: data.flagged ? 'high' : 'low',
|
|
230
|
+
latencyMs: Math.round(latencyMs),
|
|
231
|
+
ip: parsed.ip,
|
|
232
|
+
path: parsed.path,
|
|
233
|
+
method: parsed.method,
|
|
234
|
+
matchedRules: [],
|
|
235
|
+
userId: parsed.userId,
|
|
236
|
+
sessionId: parsed.sessionId,
|
|
237
|
+
});
|
|
238
|
+
if (data.flagged) {
|
|
239
|
+
return {
|
|
240
|
+
flagged: true,
|
|
241
|
+
event_id: data.eventId || data.event_id || '',
|
|
242
|
+
confidence: data.confidence || 0,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
this.log('error', `observeExternal() failed: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async observeInternal(parsed, options) {
|
|
253
|
+
if (!this.internalEngine) {
|
|
254
|
+
this.log('error', 'Internal engine not initialized');
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
const iclRequest = {
|
|
258
|
+
ip: parsed.ip,
|
|
259
|
+
method: parsed.method,
|
|
260
|
+
path: parsed.path,
|
|
261
|
+
userAgent: parsed.userAgent,
|
|
262
|
+
userId: parsed.userId,
|
|
263
|
+
sessionId: parsed.sessionId,
|
|
264
|
+
agentId: options?.meta?.agent_id,
|
|
265
|
+
toolName: options?.meta?.tool_name,
|
|
266
|
+
};
|
|
267
|
+
const result = await this.internalEngine.evaluate(iclRequest);
|
|
268
|
+
this.auditTrail.record({
|
|
269
|
+
eventId: result.eventId,
|
|
270
|
+
timestamp: Date.now(),
|
|
271
|
+
source: 'internal',
|
|
272
|
+
tier: result.tier,
|
|
273
|
+
decision: result.decision,
|
|
274
|
+
score: result.score,
|
|
275
|
+
severity: result.score >= 85
|
|
276
|
+
? 'critical'
|
|
277
|
+
: result.score >= 60
|
|
278
|
+
? 'high'
|
|
279
|
+
: result.score >= 35
|
|
280
|
+
? 'medium'
|
|
281
|
+
: 'low',
|
|
282
|
+
latencyMs: result.evaluationTimeUs / 1000,
|
|
283
|
+
ip: parsed.ip,
|
|
284
|
+
path: parsed.path,
|
|
285
|
+
method: parsed.method,
|
|
286
|
+
matchedRules: result.matchedRules,
|
|
287
|
+
userId: parsed.userId,
|
|
288
|
+
agentId: options?.meta?.agent_id,
|
|
289
|
+
sessionId: parsed.sessionId,
|
|
290
|
+
});
|
|
291
|
+
if (result.decision !== 'allow' && this.tierGate.canBlock()) {
|
|
292
|
+
memSet(`blocked:ip:${parsed.ip}`, '1', 3600);
|
|
293
|
+
await this.fireThreatHandlers(result, parsed, options);
|
|
294
|
+
}
|
|
295
|
+
if (result.decision !== 'allow') {
|
|
296
|
+
return {
|
|
297
|
+
flagged: true,
|
|
298
|
+
event_id: result.eventId,
|
|
299
|
+
confidence: result.score / 100,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
async observeHybrid(parsed, options) {
|
|
305
|
+
const internalResult = await this.observeInternal(parsed, options);
|
|
306
|
+
if (internalResult?.flagged) {
|
|
307
|
+
this.relayToExternalAsync(parsed, options).catch(() => { });
|
|
308
|
+
return internalResult;
|
|
309
|
+
}
|
|
310
|
+
if (this.circuitBreaker && this.circuitBreaker.shouldCallExternal()) {
|
|
311
|
+
try {
|
|
312
|
+
const externalResult = await this.observeExternal({ ...parsed, userAgent: parsed.userAgent || null }, options);
|
|
313
|
+
this.circuitBreaker.recordSuccess();
|
|
314
|
+
if (externalResult?.flagged) {
|
|
315
|
+
this.auditTrail.record({
|
|
316
|
+
eventId: externalResult.event_id,
|
|
317
|
+
timestamp: Date.now(),
|
|
318
|
+
source: 'hybrid',
|
|
319
|
+
tier: 0,
|
|
320
|
+
decision: 'block',
|
|
321
|
+
score: externalResult.confidence * 100,
|
|
322
|
+
severity: 'high',
|
|
323
|
+
latencyMs: 0,
|
|
324
|
+
ip: parsed.ip,
|
|
325
|
+
path: parsed.path,
|
|
326
|
+
method: parsed.method,
|
|
327
|
+
matchedRules: ['external_enrichment'],
|
|
328
|
+
userId: parsed.userId,
|
|
329
|
+
sessionId: parsed.sessionId,
|
|
330
|
+
fallbackMode: false,
|
|
331
|
+
});
|
|
332
|
+
return externalResult;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
this.circuitBreaker.recordFailure();
|
|
338
|
+
this.log('warn', 'External CL unavailable, relying on internal-only');
|
|
339
|
+
this.auditTrail.record({
|
|
340
|
+
eventId: `fb_${Date.now().toString(36)}`,
|
|
341
|
+
timestamp: Date.now(),
|
|
342
|
+
source: 'hybrid',
|
|
343
|
+
tier: 0,
|
|
344
|
+
decision: 'allow',
|
|
345
|
+
score: 0,
|
|
346
|
+
severity: 'low',
|
|
347
|
+
latencyMs: 0,
|
|
348
|
+
ip: parsed.ip,
|
|
349
|
+
path: parsed.path,
|
|
350
|
+
method: parsed.method,
|
|
351
|
+
matchedRules: ['fallback_to_internal'],
|
|
352
|
+
userId: parsed.userId,
|
|
353
|
+
sessionId: parsed.sessionId,
|
|
354
|
+
fallbackMode: true,
|
|
355
|
+
});
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
this.log('debug', 'Circuit open, using internal-only mode');
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async relayToExternalAsync(parsed, options) {
|
|
365
|
+
if (!this.circuitBreaker?.shouldCallExternal())
|
|
366
|
+
return;
|
|
367
|
+
try {
|
|
368
|
+
await this.observeExternal({ ...parsed, userAgent: parsed.userAgent || null }, options);
|
|
369
|
+
this.circuitBreaker?.recordSuccess();
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
this.circuitBreaker?.recordFailure();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async fireThreatHandlers(result, parsed, options) {
|
|
376
|
+
const severity = result.score >= 85
|
|
377
|
+
? 'critical'
|
|
378
|
+
: result.score >= 60
|
|
379
|
+
? 'high'
|
|
380
|
+
: result.score >= 35
|
|
381
|
+
? 'medium'
|
|
382
|
+
: 'low';
|
|
383
|
+
const threatEvent = {
|
|
384
|
+
event_id: result.eventId,
|
|
385
|
+
threat_type: result.matchedRules.join(', ') || 'internal_detection',
|
|
386
|
+
severity,
|
|
387
|
+
confidence: result.score / 100,
|
|
388
|
+
composite_score: result.score,
|
|
389
|
+
ip: parsed.ip,
|
|
390
|
+
identity: {
|
|
391
|
+
user_id: parsed.userId,
|
|
392
|
+
session_id: parsed.sessionId,
|
|
393
|
+
},
|
|
394
|
+
endpoint: parsed.path,
|
|
395
|
+
method: parsed.method,
|
|
396
|
+
timestamp: new Date().toISOString(),
|
|
397
|
+
meta: {
|
|
398
|
+
...(options?.meta || {}),
|
|
399
|
+
source: 'internal',
|
|
400
|
+
tier: result.tier,
|
|
401
|
+
evaluation_time_us: result.evaluationTimeUs,
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
const specificHandlers = this.handlers.get(severity) || [];
|
|
405
|
+
const wildcardHandlers = this.handlers.get('*') || [];
|
|
406
|
+
for (const handler of [...specificHandlers, ...wildcardHandlers]) {
|
|
407
|
+
try {
|
|
408
|
+
await handler(threatEvent);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
this.log('error', `Threat handler error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async enrichEvent(eventId, additionalData) {
|
|
416
|
+
if (this.mode === 'internal') {
|
|
417
|
+
this.log('debug', `enrichEvent() is a no-op in internal mode`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
await this.apiAxios.patch(`/enrich/${eventId}`, additionalData);
|
|
422
|
+
this.log('debug', `Enriched event ${eventId}`);
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
this.log('error', `enrichEvent() failed: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async isBlocked(ip, options) {
|
|
429
|
+
if (this.internalEngine) {
|
|
430
|
+
if (this.internalEngine.isIPBlocked(ip))
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (memGet(`blocked:ip:${ip}`))
|
|
434
|
+
return true;
|
|
435
|
+
if (options?.userId && memGet(`blocked:user:${options.userId}`))
|
|
436
|
+
return true;
|
|
437
|
+
if (options?.sessionId && memGet(`blocked:session:${options.sessionId}`))
|
|
438
|
+
return true;
|
|
439
|
+
if (this.mode === 'internal')
|
|
440
|
+
return false;
|
|
441
|
+
try {
|
|
442
|
+
const response = await this.apiAxios.get('/blocked', {
|
|
443
|
+
params: { ip, userId: options?.userId, sessionId: options?.sessionId },
|
|
444
|
+
});
|
|
445
|
+
const blocked = response.data.blocked;
|
|
446
|
+
if (blocked) {
|
|
447
|
+
memSet(`blocked:ip:${ip}`, '1', this.cacheConfig?.ttl || 3600);
|
|
448
|
+
}
|
|
449
|
+
return blocked;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
this.log('warn', `isBlocked() check failed for ${ip}, failing open`);
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
onThreatDetected(severity, handler) {
|
|
457
|
+
if (!this.handlers.has(severity)) {
|
|
458
|
+
this.handlers.set(severity, []);
|
|
459
|
+
}
|
|
460
|
+
this.handlers.get(severity).push(handler);
|
|
461
|
+
this.log('debug', `Registered threat handler for severity: ${severity}`);
|
|
462
|
+
}
|
|
463
|
+
async reportActionTaken(eventId, report) {
|
|
464
|
+
if (this.mode === 'internal') {
|
|
465
|
+
this.log('debug', `reportActionTaken() recorded locally in internal mode`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
await this.apiAxios.post('/actions/confirm', {
|
|
470
|
+
event_id: eventId,
|
|
471
|
+
...report,
|
|
472
|
+
timestamp: new Date().toISOString(),
|
|
473
|
+
});
|
|
474
|
+
this.log('info', `Action confirmed for event ${eventId}: ${report.actions.join(', ')}`);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
this.log('error', `reportActionTaken() failed: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async handleWebhook(rawBody, signature, timestamp) {
|
|
481
|
+
if (!this.responseWebhook?.secret) {
|
|
482
|
+
this.log('error', 'handleWebhook() called but no webhook secret configured');
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
const webhookTime = new Date(timestamp).getTime();
|
|
486
|
+
if (isNaN(webhookTime) || Date.now() - webhookTime > WEBHOOK_MAX_AGE_MS) {
|
|
487
|
+
this.log('warn', 'Webhook rejected: timestamp too old or invalid');
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
const expectedSignature = crypto_1.default
|
|
491
|
+
.createHmac('sha256', this.responseWebhook.secret)
|
|
492
|
+
.update(`${timestamp}.${rawBody}`)
|
|
493
|
+
.digest('hex');
|
|
494
|
+
const sigBuffer = Buffer.from(signature, 'hex');
|
|
495
|
+
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
|
|
496
|
+
if (sigBuffer.length !== expectedBuffer.length ||
|
|
497
|
+
!crypto_1.default.timingSafeEqual(sigBuffer, expectedBuffer)) {
|
|
498
|
+
this.log('warn', 'Webhook rejected: invalid signature');
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
let payload;
|
|
502
|
+
try {
|
|
503
|
+
payload = JSON.parse(rawBody);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
this.log('error', 'Webhook rejected: invalid JSON body');
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const threatEvent = {
|
|
510
|
+
event_id: payload.event_id,
|
|
511
|
+
threat_type: payload.threat_type,
|
|
512
|
+
severity: payload.severity,
|
|
513
|
+
confidence: payload.confidence,
|
|
514
|
+
composite_score: payload.composite_score,
|
|
515
|
+
ip: payload.ip,
|
|
516
|
+
identity: payload.identity,
|
|
517
|
+
geo: payload.geo,
|
|
518
|
+
abuseipdb: payload.abuseipdb,
|
|
519
|
+
endpoint: payload.endpoint,
|
|
520
|
+
method: payload.method,
|
|
521
|
+
timestamp: payload.timestamp,
|
|
522
|
+
meta: payload.meta || {},
|
|
523
|
+
};
|
|
524
|
+
if (this.tierGate.canBlock()) {
|
|
525
|
+
if (payload.severity === 'critical' || payload.severity === 'high') {
|
|
526
|
+
memSet(`blocked:ip:${payload.ip}`, '1', this.cacheConfig?.ttl || 3600);
|
|
527
|
+
if (payload.identity.user_id) {
|
|
528
|
+
memSet(`blocked:user:${payload.identity.user_id}`, '1', this.cacheConfig?.ttl || 3600);
|
|
529
|
+
}
|
|
530
|
+
if (this.internalEngine) {
|
|
531
|
+
this.internalEngine.blockIP(payload.ip, `webhook:${payload.threat_type}`, this.cacheConfig?.ttl || 3600);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
this.auditTrail.record({
|
|
536
|
+
eventId: payload.event_id,
|
|
537
|
+
timestamp: Date.now(),
|
|
538
|
+
source: 'external',
|
|
539
|
+
tier: 0,
|
|
540
|
+
decision: 'webhook_response',
|
|
541
|
+
score: payload.composite_score,
|
|
542
|
+
severity: payload.severity,
|
|
543
|
+
latencyMs: 0,
|
|
544
|
+
ip: payload.ip,
|
|
545
|
+
path: payload.endpoint,
|
|
546
|
+
method: payload.method,
|
|
547
|
+
matchedRules: [payload.threat_type],
|
|
548
|
+
userId: payload.identity.user_id,
|
|
549
|
+
sessionId: payload.identity.session_id,
|
|
550
|
+
});
|
|
551
|
+
const specificHandlers = this.handlers.get(payload.severity) || [];
|
|
552
|
+
const wildcardHandlers = this.handlers.get('*') || [];
|
|
553
|
+
for (const handler of [...specificHandlers, ...wildcardHandlers]) {
|
|
554
|
+
try {
|
|
555
|
+
await handler(threatEvent);
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
this.log('error', `Threat handler error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
this.log('info', `Webhook processed: ${payload.event_id} [${payload.severity}] ${payload.threat_type}`);
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
blockIP(ip, ttlSeconds) {
|
|
565
|
+
const error = this.tierGate.validateBlockAction('block_ip');
|
|
566
|
+
if (error) {
|
|
567
|
+
this.log('warn', error);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
memSet(`blocked:ip:${ip}`, '1', ttlSeconds || this.cacheConfig?.ttl || 3600);
|
|
571
|
+
if (this.internalEngine) {
|
|
572
|
+
this.internalEngine.blockIP(ip, 'manual', ttlSeconds);
|
|
573
|
+
}
|
|
574
|
+
this.log('info', `IP blocked: ${ip}`);
|
|
575
|
+
}
|
|
576
|
+
unblockIP(ip) {
|
|
577
|
+
memoryCache.delete(`blocked:ip:${ip}`);
|
|
578
|
+
if (this.internalEngine) {
|
|
579
|
+
this.internalEngine.unblockIP(ip);
|
|
580
|
+
}
|
|
581
|
+
this.log('info', `IP unblocked: ${ip}`);
|
|
582
|
+
}
|
|
583
|
+
async trackEvent(event) {
|
|
584
|
+
const limitCheck = this.tierGate.checkEventLimit();
|
|
585
|
+
if (!limitCheck.allowed) {
|
|
586
|
+
return { success: false };
|
|
587
|
+
}
|
|
588
|
+
this.tierGate.recordEvent();
|
|
589
|
+
if (this.mode === 'internal' && this.internalEngine) {
|
|
590
|
+
const result = await this.internalEngine.evaluate({
|
|
591
|
+
ip: event.ip,
|
|
592
|
+
method: event.method || 'GET',
|
|
593
|
+
path: event.path || '/',
|
|
594
|
+
userAgent: event.userAgent,
|
|
595
|
+
});
|
|
596
|
+
if (result.decision !== 'allow' && this.tierGate.canBlock()) {
|
|
597
|
+
if (event.severity === 'HIGH' || event.severity === 'CRITICAL') {
|
|
598
|
+
const severity = event.severity.toLowerCase();
|
|
599
|
+
const handlers = [
|
|
600
|
+
...(this.handlers.get(severity) || []),
|
|
601
|
+
...(this.handlers.get('*') || []),
|
|
602
|
+
];
|
|
603
|
+
const threatEvent = {
|
|
604
|
+
event_id: result.eventId,
|
|
605
|
+
threat_type: event.eventType,
|
|
606
|
+
severity,
|
|
607
|
+
confidence: result.score / 100,
|
|
608
|
+
composite_score: result.score,
|
|
609
|
+
ip: event.ip,
|
|
610
|
+
identity: { user_id: null, session_id: null },
|
|
611
|
+
endpoint: event.path || '/',
|
|
612
|
+
method: event.method || 'GET',
|
|
613
|
+
timestamp: new Date().toISOString(),
|
|
614
|
+
meta: event.metadata || {},
|
|
615
|
+
};
|
|
616
|
+
for (const handler of handlers) {
|
|
617
|
+
try {
|
|
618
|
+
await Promise.resolve(handler(threatEvent));
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
this.log('error', `Threat handler error: ${e instanceof Error ? e.message : 'Unknown'}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return { success: true, eventId: result.eventId };
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const payload = {
|
|
630
|
+
eventType: event.eventType,
|
|
631
|
+
ip: event.ip,
|
|
632
|
+
severity: event.severity || 'MEDIUM',
|
|
633
|
+
method: event.method || 'GET',
|
|
634
|
+
requestPath: event.path,
|
|
635
|
+
userAgent: event.userAgent,
|
|
636
|
+
statusCode: event.statusCode,
|
|
637
|
+
metadata: {
|
|
638
|
+
...event.metadata,
|
|
639
|
+
appId: this.appId,
|
|
640
|
+
environment: this.environment,
|
|
641
|
+
sdk_version: '4.0.0',
|
|
642
|
+
mode: this.mode,
|
|
643
|
+
},
|
|
644
|
+
timestamp: new Date().toISOString(),
|
|
645
|
+
};
|
|
646
|
+
const response = await this.sendWithRetry(payload);
|
|
647
|
+
const data = response.data;
|
|
648
|
+
if (data.success) {
|
|
649
|
+
this.log('debug', `trackEvent(${event.eventType}) sent for ${event.ip}`);
|
|
650
|
+
if (event.severity === 'HIGH' || event.severity === 'CRITICAL') {
|
|
651
|
+
const severity = event.severity.toLowerCase();
|
|
652
|
+
const handlers = [
|
|
653
|
+
...(this.handlers.get(severity) || []),
|
|
654
|
+
...(this.handlers.get('*') || []),
|
|
655
|
+
];
|
|
656
|
+
const threatEvent = {
|
|
657
|
+
event_id: data.eventId || '',
|
|
658
|
+
threat_type: event.eventType,
|
|
659
|
+
severity,
|
|
660
|
+
confidence: 0.9,
|
|
661
|
+
composite_score: event.severity === 'CRITICAL' ? 95 : 75,
|
|
662
|
+
ip: event.ip,
|
|
663
|
+
identity: { user_id: null, session_id: null },
|
|
664
|
+
endpoint: event.path || '/',
|
|
665
|
+
method: event.method || 'GET',
|
|
666
|
+
timestamp: new Date().toISOString(),
|
|
667
|
+
meta: event.metadata || {},
|
|
668
|
+
};
|
|
669
|
+
for (const handler of handlers) {
|
|
670
|
+
try {
|
|
671
|
+
await Promise.resolve(handler(threatEvent));
|
|
672
|
+
}
|
|
673
|
+
catch (e) {
|
|
674
|
+
this.log('error', `Threat handler error: ${e instanceof Error ? e.message : 'Unknown'}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return { success: !!data.success, eventId: data.eventId };
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
this.log('error', `trackEvent() failed: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
683
|
+
return { success: false };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async registerTestIPs(ips) {
|
|
687
|
+
if (this.mode === 'internal') {
|
|
688
|
+
this.log('debug', 'registerTestIPs() is a no-op in internal mode');
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
const response = await this.apiAxios.post('/test-ips', { ips });
|
|
693
|
+
const data = response.data;
|
|
694
|
+
this.log('info', `Registered ${ips.length} test IPs: ${ips.join(', ')}`);
|
|
695
|
+
return data.success;
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
this.log('error', `registerTestIPs() failed: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async testConnection() {
|
|
703
|
+
if (this.mode === 'internal') {
|
|
704
|
+
return this.internalEngine !== null;
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
const response = await this.axios.post('', {
|
|
708
|
+
eventType: 'custom',
|
|
709
|
+
ip: '127.0.0.1',
|
|
710
|
+
appId: this.appId,
|
|
711
|
+
metadata: { test: true, sdk_version: '4.0.0', mode: this.mode },
|
|
712
|
+
timestamp: new Date().toISOString(),
|
|
713
|
+
});
|
|
714
|
+
return response.data.success === true;
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async sendWithRetry(payload, attempt = 1) {
|
|
721
|
+
try {
|
|
722
|
+
const response = await this.axios.post('', payload);
|
|
723
|
+
return response;
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
const axiosError = error;
|
|
727
|
+
if (axiosError.response?.status && axiosError.response.status < 500) {
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
if (attempt < this.retries) {
|
|
731
|
+
const backoff = Math.pow(2, attempt) * 1000;
|
|
732
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
733
|
+
return this.sendWithRetry(payload, attempt + 1);
|
|
734
|
+
}
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
log(level, message) {
|
|
739
|
+
if (!this.debug && level === 'debug')
|
|
740
|
+
return;
|
|
741
|
+
const prefix = '[DevFortress]';
|
|
742
|
+
switch (level) {
|
|
743
|
+
case 'error':
|
|
744
|
+
console.error(`${prefix} ${message}`);
|
|
745
|
+
break;
|
|
746
|
+
case 'warn':
|
|
747
|
+
console.warn(`${prefix} ${message}`);
|
|
748
|
+
break;
|
|
749
|
+
case 'debug':
|
|
750
|
+
case 'info':
|
|
751
|
+
if (this.debug) {
|
|
752
|
+
console.log(`${prefix} ${message}`);
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
exports.DevFortress = DevFortress;
|