@purplesquirrel/guardrails-mcp-server 1.0.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/.github/dependabot.yml +21 -0
- package/.github/workflows/ci.yml +36 -0
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/TROUBLESHOOTING.md +299 -0
- package/package.json +26 -0
- package/src/audit/AuditLogger.js +340 -0
- package/src/engine/GuardrailsEngine.js +305 -0
- package/src/filters/OutputFilter.js +296 -0
- package/src/policies/PolicyEngine.js +337 -0
- package/src/validators/InputValidator.js +291 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuditLogger - Comprehensive audit logging for guardrails events
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Event logging with timestamps
|
|
6
|
+
* - Log rotation and retention
|
|
7
|
+
* - Query and filtering
|
|
8
|
+
* - Export capabilities
|
|
9
|
+
* - Metrics aggregation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class AuditLogger {
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.config = {
|
|
15
|
+
maxLogSize: 10000, // Maximum logs to keep in memory
|
|
16
|
+
retentionDays: 30,
|
|
17
|
+
enableConsoleLog: false,
|
|
18
|
+
logLevel: 'info', // 'debug', 'info', 'warn', 'error'
|
|
19
|
+
...config,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
this.logs = [];
|
|
23
|
+
this.metrics = {
|
|
24
|
+
totalRequests: 0,
|
|
25
|
+
blockedRequests: 0,
|
|
26
|
+
filteredOutputs: 0,
|
|
27
|
+
policyViolations: 0,
|
|
28
|
+
validationErrors: 0,
|
|
29
|
+
byType: {},
|
|
30
|
+
byUser: {},
|
|
31
|
+
byHour: {},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
this.logLevels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Log an event
|
|
39
|
+
* @param {Object} event - The event to log
|
|
40
|
+
*/
|
|
41
|
+
async log(event) {
|
|
42
|
+
const logEntry = {
|
|
43
|
+
id: this.generateLogId(),
|
|
44
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
45
|
+
level: event.level || 'info',
|
|
46
|
+
...event,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Check log level
|
|
50
|
+
if (this.logLevels[logEntry.level] < this.logLevels[this.config.logLevel]) {
|
|
51
|
+
return logEntry.id;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Add to logs
|
|
55
|
+
this.logs.push(logEntry);
|
|
56
|
+
|
|
57
|
+
// Update metrics
|
|
58
|
+
this.updateMetrics(logEntry);
|
|
59
|
+
|
|
60
|
+
// Console log if enabled
|
|
61
|
+
if (this.config.enableConsoleLog) {
|
|
62
|
+
this.consoleLog(logEntry);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Rotate logs if needed
|
|
66
|
+
if (this.logs.length > this.config.maxLogSize) {
|
|
67
|
+
this.rotateLogs();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Call external handler if configured
|
|
71
|
+
if (this.config.externalHandler) {
|
|
72
|
+
try {
|
|
73
|
+
await this.config.externalHandler(logEntry);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('External log handler error:', error.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return logEntry.id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update metrics based on log entry
|
|
84
|
+
*/
|
|
85
|
+
updateMetrics(entry) {
|
|
86
|
+
this.metrics.totalRequests++;
|
|
87
|
+
|
|
88
|
+
// Update type counts
|
|
89
|
+
if (!this.metrics.byType[entry.type]) {
|
|
90
|
+
this.metrics.byType[entry.type] = 0;
|
|
91
|
+
}
|
|
92
|
+
this.metrics.byType[entry.type]++;
|
|
93
|
+
|
|
94
|
+
// Update specific counters
|
|
95
|
+
switch (entry.type) {
|
|
96
|
+
case 'RATE_LIMIT_EXCEEDED':
|
|
97
|
+
case 'POLICY_VIOLATION':
|
|
98
|
+
case 'INPUT_VALIDATION_FAILED':
|
|
99
|
+
this.metrics.blockedRequests++;
|
|
100
|
+
break;
|
|
101
|
+
case 'OUTPUT_FILTERED':
|
|
102
|
+
if (entry.redactionsApplied > 0) {
|
|
103
|
+
this.metrics.filteredOutputs++;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (entry.type === 'POLICY_VIOLATION') {
|
|
109
|
+
this.metrics.policyViolations++;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (entry.type === 'INPUT_VALIDATION_FAILED') {
|
|
113
|
+
this.metrics.validationErrors++;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Update user stats
|
|
117
|
+
const userId = entry.context?.userId || 'anonymous';
|
|
118
|
+
if (!this.metrics.byUser[userId]) {
|
|
119
|
+
this.metrics.byUser[userId] = { total: 0, blocked: 0 };
|
|
120
|
+
}
|
|
121
|
+
this.metrics.byUser[userId].total++;
|
|
122
|
+
if (['RATE_LIMIT_EXCEEDED', 'POLICY_VIOLATION', 'INPUT_VALIDATION_FAILED'].includes(entry.type)) {
|
|
123
|
+
this.metrics.byUser[userId].blocked++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Update hourly stats
|
|
127
|
+
const hour = new Date(entry.timestamp).toISOString().slice(0, 13);
|
|
128
|
+
if (!this.metrics.byHour[hour]) {
|
|
129
|
+
this.metrics.byHour[hour] = { total: 0, blocked: 0 };
|
|
130
|
+
}
|
|
131
|
+
this.metrics.byHour[hour].total++;
|
|
132
|
+
if (['RATE_LIMIT_EXCEEDED', 'POLICY_VIOLATION', 'INPUT_VALIDATION_FAILED'].includes(entry.type)) {
|
|
133
|
+
this.metrics.byHour[hour].blocked++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Console log with formatting
|
|
139
|
+
*/
|
|
140
|
+
consoleLog(entry) {
|
|
141
|
+
const levelColors = {
|
|
142
|
+
debug: '\x1b[36m', // cyan
|
|
143
|
+
info: '\x1b[32m', // green
|
|
144
|
+
warn: '\x1b[33m', // yellow
|
|
145
|
+
error: '\x1b[31m', // red
|
|
146
|
+
};
|
|
147
|
+
const reset = '\x1b[0m';
|
|
148
|
+
const color = levelColors[entry.level] || '';
|
|
149
|
+
|
|
150
|
+
console.log(
|
|
151
|
+
`${color}[${entry.timestamp}] [${entry.level.toUpperCase()}] ${entry.type}${reset}`,
|
|
152
|
+
entry.requestId ? `(${entry.requestId})` : '',
|
|
153
|
+
entry.message || ''
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Rotate logs (remove oldest entries)
|
|
159
|
+
*/
|
|
160
|
+
rotateLogs() {
|
|
161
|
+
const cutoff = this.config.maxLogSize * 0.8;
|
|
162
|
+
this.logs = this.logs.slice(-cutoff);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get logs with optional filtering
|
|
167
|
+
* @param {Object} filter - Filter options
|
|
168
|
+
*/
|
|
169
|
+
getLogs(filter = {}) {
|
|
170
|
+
let results = [...this.logs];
|
|
171
|
+
|
|
172
|
+
// Filter by type
|
|
173
|
+
if (filter.type) {
|
|
174
|
+
results = results.filter(l => l.type === filter.type);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Filter by level
|
|
178
|
+
if (filter.level) {
|
|
179
|
+
const minLevel = this.logLevels[filter.level];
|
|
180
|
+
results = results.filter(l => this.logLevels[l.level] >= minLevel);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Filter by time range
|
|
184
|
+
if (filter.startTime) {
|
|
185
|
+
const start = new Date(filter.startTime);
|
|
186
|
+
results = results.filter(l => new Date(l.timestamp) >= start);
|
|
187
|
+
}
|
|
188
|
+
if (filter.endTime) {
|
|
189
|
+
const end = new Date(filter.endTime);
|
|
190
|
+
results = results.filter(l => new Date(l.timestamp) <= end);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Filter by user
|
|
194
|
+
if (filter.userId) {
|
|
195
|
+
results = results.filter(l => l.context?.userId === filter.userId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Filter by request ID
|
|
199
|
+
if (filter.requestId) {
|
|
200
|
+
results = results.filter(l => l.requestId === filter.requestId);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Limit results
|
|
204
|
+
if (filter.limit) {
|
|
205
|
+
results = results.slice(-filter.limit);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get a single log by ID
|
|
213
|
+
*/
|
|
214
|
+
getLog(logId) {
|
|
215
|
+
return this.logs.find(l => l.id === logId);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get aggregated metrics
|
|
220
|
+
*/
|
|
221
|
+
getMetrics() {
|
|
222
|
+
return {
|
|
223
|
+
...this.metrics,
|
|
224
|
+
blockRate: this.metrics.totalRequests > 0
|
|
225
|
+
? (this.metrics.blockedRequests / this.metrics.totalRequests * 100).toFixed(2)
|
|
226
|
+
: 0,
|
|
227
|
+
logsInMemory: this.logs.length,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get request count
|
|
233
|
+
*/
|
|
234
|
+
getRequestCount() {
|
|
235
|
+
return this.metrics.totalRequests;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get blocked count
|
|
240
|
+
*/
|
|
241
|
+
getBlockedCount() {
|
|
242
|
+
return this.metrics.blockedRequests;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Export logs to JSON
|
|
247
|
+
*/
|
|
248
|
+
exportLogs(filter = {}) {
|
|
249
|
+
const logs = this.getLogs(filter);
|
|
250
|
+
return JSON.stringify(logs, null, 2);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Clear all logs
|
|
255
|
+
*/
|
|
256
|
+
clearLogs() {
|
|
257
|
+
this.logs = [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Reset metrics
|
|
262
|
+
*/
|
|
263
|
+
resetMetrics() {
|
|
264
|
+
this.metrics = {
|
|
265
|
+
totalRequests: 0,
|
|
266
|
+
blockedRequests: 0,
|
|
267
|
+
filteredOutputs: 0,
|
|
268
|
+
policyViolations: 0,
|
|
269
|
+
validationErrors: 0,
|
|
270
|
+
byType: {},
|
|
271
|
+
byUser: {},
|
|
272
|
+
byHour: {},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Generate unique log ID
|
|
278
|
+
*/
|
|
279
|
+
generateLogId() {
|
|
280
|
+
return `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Set external log handler
|
|
285
|
+
*/
|
|
286
|
+
setExternalHandler(handler) {
|
|
287
|
+
this.config.externalHandler = handler;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get summary report
|
|
292
|
+
*/
|
|
293
|
+
getSummary(hours = 24) {
|
|
294
|
+
const cutoff = new Date(Date.now() - hours * 3600000);
|
|
295
|
+
const recentLogs = this.logs.filter(l => new Date(l.timestamp) >= cutoff);
|
|
296
|
+
|
|
297
|
+
const summary = {
|
|
298
|
+
period: `Last ${hours} hours`,
|
|
299
|
+
totalRequests: recentLogs.length,
|
|
300
|
+
blocked: recentLogs.filter(l =>
|
|
301
|
+
['RATE_LIMIT_EXCEEDED', 'POLICY_VIOLATION', 'INPUT_VALIDATION_FAILED'].includes(l.type)
|
|
302
|
+
).length,
|
|
303
|
+
filtered: recentLogs.filter(l => l.type === 'OUTPUT_FILTERED').length,
|
|
304
|
+
errors: recentLogs.filter(l => l.level === 'error').length,
|
|
305
|
+
topViolations: this.getTopViolations(recentLogs, 5),
|
|
306
|
+
uniqueUsers: new Set(recentLogs.map(l => l.context?.userId).filter(Boolean)).size,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
summary.successRate = summary.totalRequests > 0
|
|
310
|
+
? ((summary.totalRequests - summary.blocked) / summary.totalRequests * 100).toFixed(2)
|
|
311
|
+
: 100;
|
|
312
|
+
|
|
313
|
+
return summary;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get top violations
|
|
318
|
+
*/
|
|
319
|
+
getTopViolations(logs, limit = 5) {
|
|
320
|
+
const violations = {};
|
|
321
|
+
for (const log of logs) {
|
|
322
|
+
if (log.type === 'POLICY_VIOLATION' && log.policy) {
|
|
323
|
+
const key = log.policy.name || log.policy.id;
|
|
324
|
+
violations[key] = (violations[key] || 0) + 1;
|
|
325
|
+
}
|
|
326
|
+
if (log.type === 'INPUT_VALIDATION_FAILED' && log.violations) {
|
|
327
|
+
for (const v of log.violations) {
|
|
328
|
+
violations[v.type] = (violations[v.type] || 0) + 1;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return Object.entries(violations)
|
|
334
|
+
.sort((a, b) => b[1] - a[1])
|
|
335
|
+
.slice(0, limit)
|
|
336
|
+
.map(([name, count]) => ({ name, count }));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export default AuditLogger;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuardrailsEngine - Core engine for AI agent security and validation
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Input validation and sanitization
|
|
6
|
+
* - Output filtering and redaction
|
|
7
|
+
* - Policy enforcement
|
|
8
|
+
* - Audit logging
|
|
9
|
+
* - Rate limiting
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { InputValidator } from '../validators/InputValidator.js';
|
|
13
|
+
import { OutputFilter } from '../filters/OutputFilter.js';
|
|
14
|
+
import { PolicyEngine } from '../policies/PolicyEngine.js';
|
|
15
|
+
import { AuditLogger } from '../audit/AuditLogger.js';
|
|
16
|
+
|
|
17
|
+
export class GuardrailsEngine {
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
this.config = {
|
|
20
|
+
// Default configuration
|
|
21
|
+
enableInputValidation: true,
|
|
22
|
+
enableOutputFiltering: true,
|
|
23
|
+
enablePolicyEnforcement: true,
|
|
24
|
+
enableAuditLogging: true,
|
|
25
|
+
enableRateLimiting: true,
|
|
26
|
+
maxRequestsPerMinute: 60,
|
|
27
|
+
maxTokensPerRequest: 100000,
|
|
28
|
+
blockedPatterns: [],
|
|
29
|
+
allowedDomains: [],
|
|
30
|
+
sensitiveDataPatterns: [],
|
|
31
|
+
...config,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Initialize components
|
|
35
|
+
this.inputValidator = new InputValidator(this.config);
|
|
36
|
+
this.outputFilter = new OutputFilter(this.config);
|
|
37
|
+
this.policyEngine = new PolicyEngine(this.config);
|
|
38
|
+
this.auditLogger = new AuditLogger(this.config);
|
|
39
|
+
|
|
40
|
+
// Rate limiting state
|
|
41
|
+
this.requestCounts = new Map();
|
|
42
|
+
this.lastCleanup = Date.now();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Process an incoming request through all guardrails
|
|
47
|
+
* @param {Object} request - The request to process
|
|
48
|
+
* @param {Object} context - Additional context (user, session, etc.)
|
|
49
|
+
* @returns {Object} - Processing result with status and modified request
|
|
50
|
+
*/
|
|
51
|
+
async processInput(request, context = {}) {
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
const requestId = this.generateRequestId();
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// 1. Rate limiting check
|
|
57
|
+
if (this.config.enableRateLimiting) {
|
|
58
|
+
const rateLimitResult = this.checkRateLimit(context.userId || 'anonymous');
|
|
59
|
+
if (!rateLimitResult.allowed) {
|
|
60
|
+
await this.auditLogger.log({
|
|
61
|
+
requestId,
|
|
62
|
+
type: 'RATE_LIMIT_EXCEEDED',
|
|
63
|
+
context,
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
allowed: false,
|
|
68
|
+
reason: 'Rate limit exceeded',
|
|
69
|
+
code: 'RATE_LIMIT',
|
|
70
|
+
retryAfter: rateLimitResult.retryAfter,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. Input validation
|
|
76
|
+
if (this.config.enableInputValidation) {
|
|
77
|
+
const validationResult = await this.inputValidator.validate(request, context);
|
|
78
|
+
if (!validationResult.valid) {
|
|
79
|
+
await this.auditLogger.log({
|
|
80
|
+
requestId,
|
|
81
|
+
type: 'INPUT_VALIDATION_FAILED',
|
|
82
|
+
violations: validationResult.violations,
|
|
83
|
+
context,
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
allowed: false,
|
|
88
|
+
reason: 'Input validation failed',
|
|
89
|
+
code: 'VALIDATION_ERROR',
|
|
90
|
+
violations: validationResult.violations,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Apply sanitization
|
|
94
|
+
request = validationResult.sanitizedRequest || request;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Policy enforcement
|
|
98
|
+
if (this.config.enablePolicyEnforcement) {
|
|
99
|
+
const policyResult = await this.policyEngine.evaluate(request, context);
|
|
100
|
+
if (!policyResult.allowed) {
|
|
101
|
+
await this.auditLogger.log({
|
|
102
|
+
requestId,
|
|
103
|
+
type: 'POLICY_VIOLATION',
|
|
104
|
+
policy: policyResult.violatedPolicy,
|
|
105
|
+
context,
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
allowed: false,
|
|
110
|
+
reason: policyResult.reason,
|
|
111
|
+
code: 'POLICY_VIOLATION',
|
|
112
|
+
policy: policyResult.violatedPolicy,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 4. Log successful processing
|
|
118
|
+
if (this.config.enableAuditLogging) {
|
|
119
|
+
await this.auditLogger.log({
|
|
120
|
+
requestId,
|
|
121
|
+
type: 'INPUT_PROCESSED',
|
|
122
|
+
processingTime: Date.now() - startTime,
|
|
123
|
+
context,
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
allowed: true,
|
|
130
|
+
requestId,
|
|
131
|
+
request,
|
|
132
|
+
processingTime: Date.now() - startTime,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
} catch (error) {
|
|
136
|
+
await this.auditLogger.log({
|
|
137
|
+
requestId,
|
|
138
|
+
type: 'PROCESSING_ERROR',
|
|
139
|
+
error: error.message,
|
|
140
|
+
context,
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
allowed: false,
|
|
145
|
+
reason: 'Internal processing error',
|
|
146
|
+
code: 'INTERNAL_ERROR',
|
|
147
|
+
error: error.message,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Process an outgoing response through output filters
|
|
154
|
+
* @param {Object} response - The response to filter
|
|
155
|
+
* @param {Object} context - Additional context
|
|
156
|
+
* @returns {Object} - Filtered response
|
|
157
|
+
*/
|
|
158
|
+
async processOutput(response, context = {}) {
|
|
159
|
+
const startTime = Date.now();
|
|
160
|
+
const requestId = context.requestId || this.generateRequestId();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
if (!this.config.enableOutputFiltering) {
|
|
164
|
+
return { filtered: false, response };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const filterResult = await this.outputFilter.filter(response, context);
|
|
168
|
+
|
|
169
|
+
if (this.config.enableAuditLogging) {
|
|
170
|
+
await this.auditLogger.log({
|
|
171
|
+
requestId,
|
|
172
|
+
type: 'OUTPUT_FILTERED',
|
|
173
|
+
redactionsApplied: filterResult.redactions?.length || 0,
|
|
174
|
+
processingTime: Date.now() - startTime,
|
|
175
|
+
context,
|
|
176
|
+
timestamp: new Date().toISOString(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
filtered: filterResult.modified,
|
|
182
|
+
response: filterResult.response,
|
|
183
|
+
redactions: filterResult.redactions,
|
|
184
|
+
processingTime: Date.now() - startTime,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
} catch (error) {
|
|
188
|
+
await this.auditLogger.log({
|
|
189
|
+
requestId,
|
|
190
|
+
type: 'OUTPUT_FILTER_ERROR',
|
|
191
|
+
error: error.message,
|
|
192
|
+
context,
|
|
193
|
+
timestamp: new Date().toISOString(),
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
filtered: false,
|
|
197
|
+
response,
|
|
198
|
+
error: error.message,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check rate limit for a user
|
|
205
|
+
*/
|
|
206
|
+
checkRateLimit(userId) {
|
|
207
|
+
this.cleanupRateLimits();
|
|
208
|
+
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const windowStart = now - 60000; // 1 minute window
|
|
211
|
+
|
|
212
|
+
if (!this.requestCounts.has(userId)) {
|
|
213
|
+
this.requestCounts.set(userId, []);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const userRequests = this.requestCounts.get(userId);
|
|
217
|
+
const recentRequests = userRequests.filter(t => t > windowStart);
|
|
218
|
+
this.requestCounts.set(userId, recentRequests);
|
|
219
|
+
|
|
220
|
+
if (recentRequests.length >= this.config.maxRequestsPerMinute) {
|
|
221
|
+
const oldestRequest = Math.min(...recentRequests);
|
|
222
|
+
const retryAfter = Math.ceil((oldestRequest + 60000 - now) / 1000);
|
|
223
|
+
return { allowed: false, retryAfter };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
recentRequests.push(now);
|
|
227
|
+
return { allowed: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Cleanup old rate limit entries
|
|
232
|
+
*/
|
|
233
|
+
cleanupRateLimits() {
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
if (now - this.lastCleanup < 60000) return;
|
|
236
|
+
|
|
237
|
+
const windowStart = now - 60000;
|
|
238
|
+
for (const [userId, requests] of this.requestCounts.entries()) {
|
|
239
|
+
const recent = requests.filter(t => t > windowStart);
|
|
240
|
+
if (recent.length === 0) {
|
|
241
|
+
this.requestCounts.delete(userId);
|
|
242
|
+
} else {
|
|
243
|
+
this.requestCounts.set(userId, recent);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
this.lastCleanup = now;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Generate a unique request ID
|
|
251
|
+
*/
|
|
252
|
+
generateRequestId() {
|
|
253
|
+
return `gr_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get current engine statistics
|
|
258
|
+
*/
|
|
259
|
+
getStats() {
|
|
260
|
+
return {
|
|
261
|
+
activeUsers: this.requestCounts.size,
|
|
262
|
+
totalRequests: this.auditLogger.getRequestCount(),
|
|
263
|
+
blockedRequests: this.auditLogger.getBlockedCount(),
|
|
264
|
+
config: {
|
|
265
|
+
inputValidation: this.config.enableInputValidation,
|
|
266
|
+
outputFiltering: this.config.enableOutputFiltering,
|
|
267
|
+
policyEnforcement: this.config.enablePolicyEnforcement,
|
|
268
|
+
rateLimiting: this.config.enableRateLimiting,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Update engine configuration
|
|
275
|
+
*/
|
|
276
|
+
updateConfig(newConfig) {
|
|
277
|
+
this.config = { ...this.config, ...newConfig };
|
|
278
|
+
this.inputValidator.updateConfig(this.config);
|
|
279
|
+
this.outputFilter.updateConfig(this.config);
|
|
280
|
+
this.policyEngine.updateConfig(this.config);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Add a custom policy
|
|
285
|
+
*/
|
|
286
|
+
addPolicy(policy) {
|
|
287
|
+
return this.policyEngine.addPolicy(policy);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Remove a policy
|
|
292
|
+
*/
|
|
293
|
+
removePolicy(policyId) {
|
|
294
|
+
return this.policyEngine.removePolicy(policyId);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get audit logs
|
|
299
|
+
*/
|
|
300
|
+
getAuditLogs(filter = {}) {
|
|
301
|
+
return this.auditLogger.getLogs(filter);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default GuardrailsEngine;
|