@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.
@@ -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;