@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,296 @@
1
+ /**
2
+ * OutputFilter - Filters and redacts sensitive information from outputs
3
+ *
4
+ * Provides:
5
+ * - PII redaction
6
+ * - Secret/credential filtering
7
+ * - Content policy enforcement
8
+ * - Custom pattern masking
9
+ */
10
+
11
+ export class OutputFilter {
12
+ constructor(config = {}) {
13
+ this.config = config;
14
+
15
+ // Sensitive data patterns with redaction masks
16
+ this.sensitivePatterns = [
17
+ {
18
+ name: 'SSN',
19
+ pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
20
+ mask: '[SSN REDACTED]',
21
+ },
22
+ {
23
+ name: 'CreditCard',
24
+ pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/g,
25
+ mask: '[CARD REDACTED]',
26
+ },
27
+ {
28
+ name: 'Email',
29
+ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
30
+ mask: '[EMAIL REDACTED]',
31
+ partialMask: (match) => {
32
+ const [local, domain] = match.split('@');
33
+ return `${local[0]}***@${domain}`;
34
+ },
35
+ },
36
+ {
37
+ name: 'Phone',
38
+ pattern: /\b(?:\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
39
+ mask: '[PHONE REDACTED]',
40
+ partialMask: (match) => match.replace(/\d(?=\d{4})/g, '*'),
41
+ },
42
+ {
43
+ name: 'IPAddress',
44
+ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
45
+ mask: '[IP REDACTED]',
46
+ },
47
+ {
48
+ name: 'APIKey',
49
+ pattern: /\b(?:sk-|pk-|api[_-]?key[_-]?)[a-zA-Z0-9]{20,}\b/gi,
50
+ mask: '[API_KEY REDACTED]',
51
+ },
52
+ {
53
+ name: 'AWSAccessKey',
54
+ pattern: /\bAKIA[0-9A-Z]{16}\b/g,
55
+ mask: '[AWS_KEY REDACTED]',
56
+ },
57
+ {
58
+ name: 'AWSSecretKey',
59
+ pattern: /\b[A-Za-z0-9/+=]{40}\b/g,
60
+ mask: '[SECRET REDACTED]',
61
+ context: /aws|secret|key/i, // Only match if context suggests it's a secret
62
+ },
63
+ {
64
+ name: 'JWTToken',
65
+ pattern: /\beyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g,
66
+ mask: '[JWT REDACTED]',
67
+ },
68
+ {
69
+ name: 'Password',
70
+ pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*['"]?[^\s'"]+['"]?/gi,
71
+ mask: '[PASSWORD REDACTED]',
72
+ },
73
+ {
74
+ name: 'PrivateKey',
75
+ pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g,
76
+ mask: '[PRIVATE_KEY REDACTED]',
77
+ },
78
+ {
79
+ name: 'ConnectionString',
80
+ pattern: /(?:mongodb|postgres|mysql|redis|amqp):\/\/[^\s]+/gi,
81
+ mask: '[CONNECTION_STRING REDACTED]',
82
+ },
83
+ ];
84
+
85
+ // Harmful content patterns
86
+ this.harmfulPatterns = [
87
+ {
88
+ name: 'MaliciousCode',
89
+ pattern: /(?:rm\s+-rf|format\s+c:|del\s+\/[fqs]|shutdown|reboot|kill\s+-9)/gi,
90
+ action: 'block',
91
+ message: 'Potentially harmful command detected',
92
+ },
93
+ {
94
+ name: 'SQLInjection',
95
+ pattern: /(?:UNION\s+SELECT|DROP\s+TABLE|DELETE\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET)/gi,
96
+ action: 'warn',
97
+ message: 'SQL injection pattern detected',
98
+ },
99
+ ];
100
+
101
+ // Custom redaction patterns from config
102
+ this.customPatterns = config.sensitiveDataPatterns || [];
103
+ }
104
+
105
+ /**
106
+ * Filter response content
107
+ * @param {Object|string} response - The response to filter
108
+ * @param {Object} context - Additional context
109
+ * @returns {Object} - Filter result
110
+ */
111
+ async filter(response, context = {}) {
112
+ const redactions = [];
113
+ let modified = false;
114
+
115
+ // Extract and process text content
116
+ let filteredResponse;
117
+ if (typeof response === 'string') {
118
+ const result = this.filterText(response);
119
+ filteredResponse = result.text;
120
+ redactions.push(...result.redactions);
121
+ modified = result.modified;
122
+ } else {
123
+ filteredResponse = await this.filterObject(response, redactions);
124
+ modified = redactions.length > 0;
125
+ }
126
+
127
+ // Check for harmful content
128
+ const textContent = typeof filteredResponse === 'string'
129
+ ? filteredResponse
130
+ : JSON.stringify(filteredResponse);
131
+
132
+ const harmfulCheck = this.checkHarmfulContent(textContent);
133
+ if (harmfulCheck.found) {
134
+ redactions.push(...harmfulCheck.findings.map(f => ({
135
+ type: 'HARMFUL_CONTENT',
136
+ name: f.name,
137
+ action: f.action,
138
+ message: f.message,
139
+ })));
140
+
141
+ // Block if any harmful pattern has action='block'
142
+ if (harmfulCheck.findings.some(f => f.action === 'block')) {
143
+ return {
144
+ response: null,
145
+ modified: true,
146
+ blocked: true,
147
+ redactions,
148
+ message: 'Response blocked due to harmful content',
149
+ };
150
+ }
151
+ }
152
+
153
+ return {
154
+ response: filteredResponse,
155
+ modified,
156
+ blocked: false,
157
+ redactions,
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Filter text content
163
+ */
164
+ filterText(text) {
165
+ let filtered = text;
166
+ const redactions = [];
167
+ let modified = false;
168
+
169
+ // Apply sensitive data patterns
170
+ for (const pattern of [...this.sensitivePatterns, ...this.customPatterns]) {
171
+ const matches = filtered.match(pattern.pattern);
172
+ if (matches) {
173
+ for (const match of matches) {
174
+ // Check context if specified
175
+ if (pattern.context && !pattern.context.test(filtered)) {
176
+ continue;
177
+ }
178
+
179
+ const replacement = this.config.partialRedaction && pattern.partialMask
180
+ ? pattern.partialMask(match)
181
+ : pattern.mask;
182
+
183
+ filtered = filtered.replace(match, replacement);
184
+ redactions.push({
185
+ type: 'SENSITIVE_DATA',
186
+ name: pattern.name,
187
+ originalLength: match.length,
188
+ });
189
+ modified = true;
190
+ }
191
+ }
192
+ }
193
+
194
+ return { text: filtered, redactions, modified };
195
+ }
196
+
197
+ /**
198
+ * Filter object recursively
199
+ */
200
+ async filterObject(obj, redactions = []) {
201
+ if (obj === null || obj === undefined) {
202
+ return obj;
203
+ }
204
+
205
+ if (typeof obj === 'string') {
206
+ const result = this.filterText(obj);
207
+ redactions.push(...result.redactions);
208
+ return result.text;
209
+ }
210
+
211
+ if (Array.isArray(obj)) {
212
+ return Promise.all(obj.map(item => this.filterObject(item, redactions)));
213
+ }
214
+
215
+ if (typeof obj === 'object') {
216
+ const filtered = {};
217
+ for (const [key, value] of Object.entries(obj)) {
218
+ // Check if key itself is sensitive
219
+ const sensitiveKeys = ['password', 'secret', 'apiKey', 'token', 'credential', 'auth'];
220
+ const isSensitiveKey = sensitiveKeys.some(k =>
221
+ key.toLowerCase().includes(k.toLowerCase())
222
+ );
223
+
224
+ if (isSensitiveKey && typeof value === 'string') {
225
+ filtered[key] = '[REDACTED]';
226
+ redactions.push({
227
+ type: 'SENSITIVE_KEY',
228
+ name: key,
229
+ });
230
+ } else {
231
+ filtered[key] = await this.filterObject(value, redactions);
232
+ }
233
+ }
234
+ return filtered;
235
+ }
236
+
237
+ return obj;
238
+ }
239
+
240
+ /**
241
+ * Check for harmful content
242
+ */
243
+ checkHarmfulContent(text) {
244
+ const findings = [];
245
+
246
+ for (const pattern of this.harmfulPatterns) {
247
+ if (pattern.pattern.test(text)) {
248
+ findings.push({
249
+ name: pattern.name,
250
+ action: pattern.action,
251
+ message: pattern.message,
252
+ });
253
+ }
254
+ }
255
+
256
+ return {
257
+ found: findings.length > 0,
258
+ findings,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Update configuration
264
+ */
265
+ updateConfig(config) {
266
+ this.config = { ...this.config, ...config };
267
+ if (config.sensitiveDataPatterns) {
268
+ this.customPatterns = config.sensitiveDataPatterns;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Add a custom sensitive pattern
274
+ */
275
+ addSensitivePattern(name, pattern, mask) {
276
+ this.sensitivePatterns.push({
277
+ name,
278
+ pattern: typeof pattern === 'string' ? new RegExp(pattern, 'g') : pattern,
279
+ mask,
280
+ });
281
+ }
282
+
283
+ /**
284
+ * Add a custom harmful pattern
285
+ */
286
+ addHarmfulPattern(name, pattern, action = 'warn', message = '') {
287
+ this.harmfulPatterns.push({
288
+ name,
289
+ pattern: typeof pattern === 'string' ? new RegExp(pattern, 'gi') : pattern,
290
+ action,
291
+ message: message || `${name} pattern detected`,
292
+ });
293
+ }
294
+ }
295
+
296
+ export default OutputFilter;
@@ -0,0 +1,337 @@
1
+ /**
2
+ * PolicyEngine - Enforces security and usage policies
3
+ *
4
+ * Provides:
5
+ * - Role-based access control
6
+ * - Resource access policies
7
+ * - Time-based restrictions
8
+ * - Quota management
9
+ * - Custom rule evaluation
10
+ */
11
+
12
+ export class PolicyEngine {
13
+ constructor(config = {}) {
14
+ this.config = config;
15
+ this.policies = new Map();
16
+ this.quotas = new Map();
17
+
18
+ // Initialize default policies
19
+ this.initializeDefaultPolicies();
20
+ }
21
+
22
+ /**
23
+ * Initialize default security policies
24
+ */
25
+ initializeDefaultPolicies() {
26
+ // Default: Block file system access to sensitive paths
27
+ this.addPolicy({
28
+ id: 'block-sensitive-paths',
29
+ name: 'Block Sensitive File Paths',
30
+ description: 'Prevents access to sensitive system paths',
31
+ enabled: true,
32
+ priority: 100,
33
+ condition: (request, context) => {
34
+ const text = JSON.stringify(request).toLowerCase();
35
+ const sensitivePaths = [
36
+ '/etc/passwd', '/etc/shadow', '/etc/hosts',
37
+ '.ssh/', '.aws/', '.env',
38
+ 'id_rsa', 'credentials',
39
+ '/var/log/', '/proc/', '/sys/',
40
+ ];
41
+ return sensitivePaths.some(path => text.includes(path));
42
+ },
43
+ action: 'deny',
44
+ message: 'Access to sensitive system paths is not allowed',
45
+ });
46
+
47
+ // Default: Block potentially dangerous tool calls
48
+ this.addPolicy({
49
+ id: 'block-dangerous-tools',
50
+ name: 'Block Dangerous Tool Calls',
51
+ description: 'Prevents execution of potentially dangerous operations',
52
+ enabled: true,
53
+ priority: 90,
54
+ condition: (request, context) => {
55
+ const toolName = request.name || request.tool || '';
56
+ const dangerousTools = [
57
+ 'shell', 'exec', 'system', 'spawn',
58
+ 'eval', 'run_command', 'execute',
59
+ ];
60
+ return dangerousTools.some(t =>
61
+ toolName.toLowerCase().includes(t)
62
+ );
63
+ },
64
+ action: 'deny',
65
+ message: 'Execution of shell commands requires explicit authorization',
66
+ });
67
+
68
+ // Default: Require authentication for sensitive operations
69
+ this.addPolicy({
70
+ id: 'require-auth-sensitive',
71
+ name: 'Require Auth for Sensitive Operations',
72
+ description: 'Requires authentication for sensitive operations',
73
+ enabled: true,
74
+ priority: 80,
75
+ condition: (request, context) => {
76
+ const sensitiveOps = ['delete', 'remove', 'drop', 'truncate', 'destroy'];
77
+ const text = JSON.stringify(request).toLowerCase();
78
+ const isSensitive = sensitiveOps.some(op => text.includes(op));
79
+ return isSensitive && !context.authenticated;
80
+ },
81
+ action: 'deny',
82
+ message: 'Authentication required for sensitive operations',
83
+ });
84
+
85
+ // Default: Rate limit by user
86
+ this.addPolicy({
87
+ id: 'user-quota',
88
+ name: 'User Request Quota',
89
+ description: 'Enforces per-user request quotas',
90
+ enabled: true,
91
+ priority: 70,
92
+ condition: (request, context) => {
93
+ if (!context.userId) return false;
94
+ const quota = this.getQuota(context.userId);
95
+ return quota.used >= quota.limit;
96
+ },
97
+ action: 'deny',
98
+ message: 'User quota exceeded',
99
+ });
100
+
101
+ // Default: Block during maintenance windows
102
+ this.addPolicy({
103
+ id: 'maintenance-window',
104
+ name: 'Maintenance Window',
105
+ description: 'Blocks requests during maintenance windows',
106
+ enabled: false, // Disabled by default
107
+ priority: 200,
108
+ condition: (request, context) => {
109
+ const maintenanceWindows = this.config.maintenanceWindows || [];
110
+ const now = new Date();
111
+ return maintenanceWindows.some(window => {
112
+ const start = new Date(window.start);
113
+ const end = new Date(window.end);
114
+ return now >= start && now <= end;
115
+ });
116
+ },
117
+ action: 'deny',
118
+ message: 'System is under maintenance',
119
+ });
120
+
121
+ // Default: Allow-list for external URLs
122
+ this.addPolicy({
123
+ id: 'url-allowlist',
124
+ name: 'URL Allow List',
125
+ description: 'Only allows access to approved external URLs',
126
+ enabled: false, // Disabled by default
127
+ priority: 60,
128
+ condition: (request, context) => {
129
+ const allowedDomains = this.config.allowedDomains || [];
130
+ if (allowedDomains.length === 0) return false;
131
+
132
+ const urlPattern = /https?:\/\/([^\/\s]+)/gi;
133
+ const text = JSON.stringify(request);
134
+ const matches = text.matchAll(urlPattern);
135
+
136
+ for (const match of matches) {
137
+ const domain = match[1].toLowerCase();
138
+ const isAllowed = allowedDomains.some(allowed =>
139
+ domain === allowed || domain.endsWith('.' + allowed)
140
+ );
141
+ if (!isAllowed) return true; // Block if not in allowlist
142
+ }
143
+ return false;
144
+ },
145
+ action: 'deny',
146
+ message: 'External URL not in allowed list',
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Evaluate all policies against a request
152
+ * @param {Object} request - The request to evaluate
153
+ * @param {Object} context - Additional context
154
+ * @returns {Object} - Evaluation result
155
+ */
156
+ async evaluate(request, context = {}) {
157
+ // Get enabled policies sorted by priority (higher first)
158
+ const enabledPolicies = Array.from(this.policies.values())
159
+ .filter(p => p.enabled)
160
+ .sort((a, b) => b.priority - a.priority);
161
+
162
+ for (const policy of enabledPolicies) {
163
+ try {
164
+ const conditionResult = await Promise.resolve(
165
+ policy.condition(request, context)
166
+ );
167
+
168
+ if (conditionResult) {
169
+ // Policy condition matched
170
+ if (policy.action === 'deny') {
171
+ return {
172
+ allowed: false,
173
+ reason: policy.message,
174
+ violatedPolicy: {
175
+ id: policy.id,
176
+ name: policy.name,
177
+ },
178
+ };
179
+ } else if (policy.action === 'warn') {
180
+ // Log warning but allow
181
+ console.warn(`Policy warning [${policy.id}]: ${policy.message}`);
182
+ } else if (policy.action === 'modify') {
183
+ // Allow policy to modify request
184
+ if (policy.modify) {
185
+ request = await Promise.resolve(policy.modify(request, context));
186
+ }
187
+ }
188
+ }
189
+ } catch (error) {
190
+ console.error(`Policy evaluation error [${policy.id}]:`, error.message);
191
+ // Fail closed on policy evaluation errors
192
+ if (this.config.failClosed !== false) {
193
+ return {
194
+ allowed: false,
195
+ reason: 'Policy evaluation error',
196
+ error: error.message,
197
+ };
198
+ }
199
+ }
200
+ }
201
+
202
+ return { allowed: true };
203
+ }
204
+
205
+ /**
206
+ * Add a policy
207
+ */
208
+ addPolicy(policy) {
209
+ if (!policy.id) {
210
+ policy.id = `policy_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
211
+ }
212
+ this.policies.set(policy.id, {
213
+ enabled: true,
214
+ priority: 50,
215
+ action: 'deny',
216
+ ...policy,
217
+ });
218
+ return policy.id;
219
+ }
220
+
221
+ /**
222
+ * Remove a policy
223
+ */
224
+ removePolicy(policyId) {
225
+ return this.policies.delete(policyId);
226
+ }
227
+
228
+ /**
229
+ * Enable/disable a policy
230
+ */
231
+ setEnabled(policyId, enabled) {
232
+ const policy = this.policies.get(policyId);
233
+ if (policy) {
234
+ policy.enabled = enabled;
235
+ return true;
236
+ }
237
+ return false;
238
+ }
239
+
240
+ /**
241
+ * Get all policies
242
+ */
243
+ getPolicies() {
244
+ return Array.from(this.policies.values());
245
+ }
246
+
247
+ /**
248
+ * Get policy by ID
249
+ */
250
+ getPolicy(policyId) {
251
+ return this.policies.get(policyId);
252
+ }
253
+
254
+ /**
255
+ * Update configuration
256
+ */
257
+ updateConfig(config) {
258
+ this.config = { ...this.config, ...config };
259
+ }
260
+
261
+ /**
262
+ * Set quota for a user
263
+ */
264
+ setQuota(userId, limit) {
265
+ this.quotas.set(userId, { limit, used: 0, resetAt: Date.now() + 86400000 });
266
+ }
267
+
268
+ /**
269
+ * Get quota for a user
270
+ */
271
+ getQuota(userId) {
272
+ if (!this.quotas.has(userId)) {
273
+ this.setQuota(userId, this.config.defaultQuota || 1000);
274
+ }
275
+
276
+ const quota = this.quotas.get(userId);
277
+
278
+ // Reset quota if period expired
279
+ if (Date.now() > quota.resetAt) {
280
+ quota.used = 0;
281
+ quota.resetAt = Date.now() + 86400000;
282
+ }
283
+
284
+ return quota;
285
+ }
286
+
287
+ /**
288
+ * Increment quota usage
289
+ */
290
+ incrementQuota(userId, amount = 1) {
291
+ const quota = this.getQuota(userId);
292
+ quota.used += amount;
293
+ }
294
+
295
+ /**
296
+ * Create a policy from a simple rule definition
297
+ */
298
+ createSimplePolicy(rule) {
299
+ const { id, name, description, pattern, action = 'deny', message, field = 'all' } = rule;
300
+
301
+ return this.addPolicy({
302
+ id,
303
+ name,
304
+ description,
305
+ enabled: true,
306
+ priority: 50,
307
+ action,
308
+ message: message || `Rule ${name} violated`,
309
+ condition: (request, context) => {
310
+ let text;
311
+ if (field === 'all') {
312
+ text = JSON.stringify(request);
313
+ } else {
314
+ text = request[field] || '';
315
+ }
316
+
317
+ const regex = typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern;
318
+ return regex.test(text);
319
+ },
320
+ });
321
+ }
322
+
323
+ /**
324
+ * Load policies from JSON configuration
325
+ */
326
+ loadPolicies(policiesConfig) {
327
+ for (const config of policiesConfig) {
328
+ if (config.type === 'simple') {
329
+ this.createSimplePolicy(config);
330
+ } else {
331
+ this.addPolicy(config);
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ export default PolicyEngine;