@rigour-labs/core 4.1.0 → 4.2.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,272 @@
1
+ /**
2
+ * Tests for Input Validation Gate — AI Agent DLP (Data Loss Prevention)
3
+ *
4
+ * @since v4.2.0
5
+ */
6
+ import { describe, it, expect } from 'vitest';
7
+ import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, } from './input-validator.js';
8
+ // ── Cloud Provider Keys ──────────────────────────────────────────
9
+ describe('scanInputForCredentials — AWS', () => {
10
+ it('detects AWS Access Key IDs', () => {
11
+ const result = scanInputForCredentials('Here is my key: AKIAIOSFODNN7EXAMPLE');
12
+ expect(result.status).toBe('blocked');
13
+ expect(result.detections).toHaveLength(1);
14
+ expect(result.detections[0].type).toBe('aws_access_key');
15
+ expect(result.detections[0].severity).toBe('critical');
16
+ });
17
+ it('detects AWS Secret Key assignments', () => {
18
+ const result = scanInputForCredentials('aws_secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"');
19
+ expect(result.status).toBe('blocked');
20
+ expect(result.detections.some(d => d.type === 'aws_secret_key')).toBe(true);
21
+ });
22
+ });
23
+ describe('scanInputForCredentials — GCP', () => {
24
+ it('detects GCP service account JSON', () => {
25
+ const input = '{"type": "service_account", "project_id": "my-proj", "private_key": "-----BEGIN RSA PRIVATE KEY-----"}';
26
+ const result = scanInputForCredentials(input);
27
+ expect(result.status).toBe('blocked');
28
+ // May detect as gcp_service_account and/or private_key
29
+ expect(result.detections.length).toBeGreaterThanOrEqual(1);
30
+ });
31
+ });
32
+ describe('scanInputForCredentials — Azure', () => {
33
+ it('detects Azure storage key', () => {
34
+ const result = scanInputForCredentials('AccountKey=dGhpcyBpcyBhIGJhc2U2NCBzdHJpbmcgdGhhdCBpcyBsb25nIGVub3VnaCB0byBtYXRjaA==');
35
+ expect(result.status).toBe('blocked');
36
+ expect(result.detections.some(d => d.type === 'azure_key')).toBe(true);
37
+ });
38
+ });
39
+ // ── API Keys (Provider-Specific) ─────────────────────────────────
40
+ describe('scanInputForCredentials — API keys', () => {
41
+ it('detects OpenAI key', () => {
42
+ const result = scanInputForCredentials('sk-proj-abc1234567890ABCDEFGH');
43
+ expect(result.status).toBe('blocked');
44
+ expect(result.detections[0].type).toBe('openai_key');
45
+ });
46
+ it('detects Anthropic key', () => {
47
+ const result = scanInputForCredentials('sk-ant-api03-abcdefghijklmnop123456');
48
+ expect(result.status).toBe('blocked');
49
+ expect(result.detections[0].type).toBe('anthropic_key');
50
+ });
51
+ it('detects GitHub PAT', () => {
52
+ const result = scanInputForCredentials('ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh12');
53
+ expect(result.status).toBe('blocked');
54
+ expect(result.detections[0].type).toBe('github_token');
55
+ });
56
+ it('detects Stripe live key', () => {
57
+ const result = scanInputForCredentials('sk_live_51HxAbCdEfGhIjKlMnOpQrStU');
58
+ expect(result.status).toBe('blocked');
59
+ expect(result.detections[0].type).toBe('stripe_key');
60
+ });
61
+ it('detects SendGrid key', () => {
62
+ const result = scanInputForCredentials('SG.abcdefghijklmnopqrstuv.1234567890abcdefghijklmnopqrstuvwxyz1234567');
63
+ expect(result.status).toBe('blocked');
64
+ expect(result.detections[0].type).toBe('sendgrid_key');
65
+ });
66
+ });
67
+ // ── Private Keys ─────────────────────────────────────────────────
68
+ describe('scanInputForCredentials — Private keys', () => {
69
+ it('detects RSA private key header', () => {
70
+ const result = scanInputForCredentials('-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQ...');
71
+ expect(result.status).toBe('blocked');
72
+ expect(result.detections.some(d => d.type === 'private_key')).toBe(true);
73
+ });
74
+ it('detects EC private key header', () => {
75
+ const result = scanInputForCredentials('-----BEGIN EC PRIVATE KEY-----');
76
+ expect(result.status).toBe('blocked');
77
+ });
78
+ it('detects OPENSSH private key header', () => {
79
+ const result = scanInputForCredentials('-----BEGIN OPENSSH PRIVATE KEY-----');
80
+ expect(result.status).toBe('blocked');
81
+ });
82
+ });
83
+ // ── Database Connection Strings ──────────────────────────────────
84
+ describe('scanInputForCredentials — Database URLs', () => {
85
+ it('detects PostgreSQL connection string', () => {
86
+ const result = scanInputForCredentials('postgresql://user:password@prod-server:5432/mydb');
87
+ expect(result.status).toBe('blocked');
88
+ const dbDetection = result.detections.find(d => d.type === 'database_url');
89
+ expect(dbDetection).toBeDefined();
90
+ });
91
+ it('detects MongoDB connection string', () => {
92
+ const result = scanInputForCredentials('mongodb+srv://admin:s3cret@cluster0.abc123.mongodb.net/production');
93
+ expect(result.status).toBe('blocked');
94
+ });
95
+ it('detects Redis connection string', () => {
96
+ const result = scanInputForCredentials('redis://default:mypassword@redis-host:6379');
97
+ expect(result.status).toBe('blocked');
98
+ });
99
+ });
100
+ // ── Bearer Tokens & JWTs ─────────────────────────────────────────
101
+ describe('scanInputForCredentials — Tokens', () => {
102
+ it('detects JWT token', () => {
103
+ const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
104
+ const result = scanInputForCredentials(jwt);
105
+ expect(result.status).toBe('blocked');
106
+ expect(result.detections.some(d => d.type === 'jwt_token')).toBe(true);
107
+ });
108
+ });
109
+ // ── Generic Patterns ─────────────────────────────────────────────
110
+ describe('scanInputForCredentials — Generic patterns', () => {
111
+ it('detects password assignment', () => {
112
+ const result = scanInputForCredentials("password = 'SuperSecret123!'");
113
+ expect(result.status).toBe('blocked');
114
+ expect(result.detections[0].type).toBe('password_assignment');
115
+ });
116
+ it('detects api_key assignment', () => {
117
+ const result = scanInputForCredentials('api_key: "abcdefghijklmnopqrstuvwxyz"');
118
+ expect(result.status).toBe('blocked');
119
+ });
120
+ it('detects .env format', () => {
121
+ const result = scanInputForCredentials('DATABASE_PASSWORD=myS3cr3tP@ssw0rd!');
122
+ expect(result.status).toBe('blocked');
123
+ expect(result.detections.some(d => d.type === 'env_variable')).toBe(true);
124
+ });
125
+ it('detects URL with embedded credentials', () => {
126
+ const result = scanInputForCredentials('http://admin:password123@internal-api.company.com/v1');
127
+ expect(result.status).toBe('blocked');
128
+ expect(result.detections.some(d => d.type === 'credentials_in_url')).toBe(true);
129
+ });
130
+ });
131
+ // ── Clean Input ──────────────────────────────────────────────────
132
+ describe('scanInputForCredentials — Clean input', () => {
133
+ it('returns clean for normal code', () => {
134
+ const result = scanInputForCredentials('function hello() { return "world"; }');
135
+ expect(result.status).toBe('clean');
136
+ expect(result.detections).toHaveLength(0);
137
+ });
138
+ it('returns clean for env var references (not values)', () => {
139
+ const result = scanInputForCredentials('const key = process.env.API_KEY;');
140
+ expect(result.status).toBe('clean');
141
+ });
142
+ it('returns clean for placeholder values', () => {
143
+ const result = scanInputForCredentials('password = "xxx"');
144
+ expect(result.status).toBe('clean'); // too short
145
+ });
146
+ });
147
+ // ── Config Options ───────────────────────────────────────────────
148
+ describe('scanInputForCredentials — Config', () => {
149
+ it('respects enabled: false', () => {
150
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE', { enabled: false });
151
+ expect(result.status).toBe('clean');
152
+ });
153
+ it('returns warning instead of blocked when block_on_detection is false', () => {
154
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE', { block_on_detection: false });
155
+ expect(result.status).toBe('warning');
156
+ expect(result.detections).toHaveLength(1);
157
+ });
158
+ it('respects custom min_secret_length', () => {
159
+ const result = scanInputForCredentials('password = "short"', { min_secret_length: 20 });
160
+ // "short" is only 5 chars, below default 8, so it would be skipped anyway
161
+ expect(result.status).toBe('clean');
162
+ });
163
+ it('applies custom patterns', () => {
164
+ const result = scanInputForCredentials('internal-token-XYZ123456', {
165
+ custom_patterns: ['internal-token-[A-Z0-9]+'],
166
+ });
167
+ expect(result.status).toBe('blocked');
168
+ expect(result.detections[0].type).toBe('custom_pattern');
169
+ });
170
+ it('respects ignore patterns', () => {
171
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE', {
172
+ ignore_patterns: ['AKIAIOSFODNN7EXAMPLE'],
173
+ });
174
+ expect(result.status).toBe('clean');
175
+ });
176
+ });
177
+ // ── Performance ──────────────────────────────────────────────────
178
+ describe('scanInputForCredentials — Performance', () => {
179
+ it('completes in under 50ms for typical input', () => {
180
+ const input = 'A'.repeat(10000); // 10KB of text
181
+ const result = scanInputForCredentials(input);
182
+ expect(result.duration_ms).toBeLessThan(50);
183
+ });
184
+ it('tracks scanned_length correctly', () => {
185
+ const input = 'test input here';
186
+ const result = scanInputForCredentials(input);
187
+ expect(result.scanned_length).toBe(input.length);
188
+ });
189
+ });
190
+ // ── Deduplication ────────────────────────────────────────────────
191
+ describe('scanInputForCredentials — Deduplication', () => {
192
+ it('deduplicates overlapping detections and keeps higher severity', () => {
193
+ // Input that might trigger both generic password_assignment and a more specific pattern
194
+ const input = 'api_key = "sk-proj-abc1234567890ABCDEFGH"';
195
+ const result = scanInputForCredentials(input);
196
+ // Should not have duplicate detections for the same match region
197
+ const positions = result.detections.map(d => d.position?.start);
198
+ const uniquePositions = new Set(positions);
199
+ // The exact count depends on pattern overlaps, but there should be some deduplication
200
+ expect(result.detections.length).toBeLessThanOrEqual(positions.length);
201
+ });
202
+ });
203
+ // ── Redaction ────────────────────────────────────────────────────
204
+ describe('scanInputForCredentials — Redaction', () => {
205
+ it('redacts matched credentials', () => {
206
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE');
207
+ expect(result.detections[0].redacted).toContain('****');
208
+ expect(result.detections[0].redacted).not.toBe('AKIAIOSFODNN7EXAMPLE');
209
+ });
210
+ });
211
+ // ── Compliance ───────────────────────────────────────────────────
212
+ describe('scanInputForCredentials — Compliance', () => {
213
+ it('includes compliance tags for AWS keys', () => {
214
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE');
215
+ expect(result.detections[0].compliance).toContain('SOC2-CC6.1');
216
+ expect(result.detections[0].compliance).toContain('HIPAA-164.312');
217
+ expect(result.detections[0].compliance).toContain('PCI-DSS-3.4');
218
+ });
219
+ });
220
+ // ── formatDLPAlert ───────────────────────────────────────────────
221
+ describe('formatDLPAlert', () => {
222
+ it('returns clean message for clean input', () => {
223
+ const result = scanInputForCredentials('just normal code');
224
+ const alert = formatDLPAlert(result);
225
+ expect(alert).toContain('clean');
226
+ });
227
+ it('shows BLOCKED header when credentials found', () => {
228
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE');
229
+ const alert = formatDLPAlert(result);
230
+ expect(alert).toContain('BLOCKED');
231
+ expect(alert).toContain('credential');
232
+ });
233
+ it('shows WARNING header when block_on_detection is false', () => {
234
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE', { block_on_detection: false });
235
+ const alert = formatDLPAlert(result);
236
+ expect(alert).toContain('WARNING');
237
+ });
238
+ it('includes severity, redacted value, and recommendation', () => {
239
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE');
240
+ const alert = formatDLPAlert(result);
241
+ expect(alert).toContain('CRITICAL');
242
+ expect(alert).toContain('****');
243
+ expect(alert).toContain('process.env');
244
+ });
245
+ });
246
+ // ── createDLPAuditEntry ──────────────────────────────────────────
247
+ describe('createDLPAuditEntry', () => {
248
+ it('creates structured audit entry', () => {
249
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE');
250
+ const entry = createDLPAuditEntry(result, { agent: 'claude', userId: 'test-user' });
251
+ expect(entry.type).toBe('dlp_event');
252
+ expect(entry.agent).toBe('claude');
253
+ expect(entry.userId).toBe('test-user');
254
+ expect(entry.status).toBe('blocked');
255
+ expect(entry.timestamp).toBeDefined();
256
+ expect(Array.isArray(entry.detections)).toBe(true);
257
+ });
258
+ it('uses provided timestamp if given', () => {
259
+ const result = scanInputForCredentials('just code');
260
+ const ts = '2025-01-01T00:00:00.000Z';
261
+ const entry = createDLPAuditEntry(result, { agent: 'cursor', timestamp: ts });
262
+ expect(entry.timestamp).toBe(ts);
263
+ });
264
+ it('redacts credentials in audit log (no raw match)', () => {
265
+ const result = scanInputForCredentials('AKIAIOSFODNN7EXAMPLE');
266
+ const entry = createDLPAuditEntry(result, { agent: 'claude' });
267
+ const detections = entry.detections;
268
+ // Audit entry should have redacted field but NOT the raw match
269
+ expect(detections[0].redacted).toBeDefined();
270
+ expect(detections[0].match).toBeUndefined();
271
+ });
272
+ });
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone DLP Checker — invoked directly by IDE hooks.
4
+ *
5
+ * Reads text from stdin, scans for credentials using the
6
+ * InputValidationGate, and outputs JSON result to stdout.
7
+ *
8
+ * Exit codes:
9
+ * 0 — clean (no credentials found)
10
+ * 2 — blocked (credentials detected, transmission prevented)
11
+ *
12
+ * Usage:
13
+ * echo "my api_key = sk-abc123..." | node standalone-dlp-checker.js
14
+ * echo '{"content":"..."}' | node standalone-dlp-checker.js --json
15
+ *
16
+ * @since v4.2.0 — AI Agent DLP
17
+ */
18
+ export {};
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone DLP Checker — invoked directly by IDE hooks.
4
+ *
5
+ * Reads text from stdin, scans for credentials using the
6
+ * InputValidationGate, and outputs JSON result to stdout.
7
+ *
8
+ * Exit codes:
9
+ * 0 — clean (no credentials found)
10
+ * 2 — blocked (credentials detected, transmission prevented)
11
+ *
12
+ * Usage:
13
+ * echo "my api_key = sk-abc123..." | node standalone-dlp-checker.js
14
+ * echo '{"content":"..."}' | node standalone-dlp-checker.js --json
15
+ *
16
+ * @since v4.2.0 — AI Agent DLP
17
+ */
18
+ import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from './input-validator.js';
19
+ import fs from 'fs-extra';
20
+ import path from 'path';
21
+ async function main() {
22
+ const args = process.argv.slice(2);
23
+ const isJson = args.includes('--json');
24
+ const block = !args.includes('--warn-only');
25
+ const agent = args.find(a => a.startsWith('--agent='))?.split('=')[1] || 'unknown';
26
+ // Read stdin
27
+ const chunks = [];
28
+ for await (const chunk of process.stdin) {
29
+ chunks.push(chunk);
30
+ }
31
+ const input = Buffer.concat(chunks).toString('utf-8').trim();
32
+ if (!input) {
33
+ process.stdout.write(JSON.stringify({ status: 'clean', detections: [], duration_ms: 0 }));
34
+ return;
35
+ }
36
+ // Parse JSON input if flag set, otherwise scan raw text
37
+ let textToScan = input;
38
+ if (isJson) {
39
+ try {
40
+ const payload = JSON.parse(input);
41
+ // Scan all string values in the payload
42
+ const texts = [];
43
+ function extractStrings(obj) {
44
+ if (typeof obj === 'string' && obj.length > 5) {
45
+ texts.push(obj);
46
+ }
47
+ else if (Array.isArray(obj)) {
48
+ obj.forEach(extractStrings);
49
+ }
50
+ else if (obj && typeof obj === 'object') {
51
+ Object.values(obj).forEach(extractStrings);
52
+ }
53
+ }
54
+ extractStrings(payload);
55
+ textToScan = texts.join('\n');
56
+ }
57
+ catch {
58
+ // If JSON parse fails, scan as raw text
59
+ }
60
+ }
61
+ const result = scanInputForCredentials(textToScan, {
62
+ enabled: true,
63
+ block_on_detection: block,
64
+ });
65
+ // Output JSON result to stdout
66
+ process.stdout.write(JSON.stringify(result));
67
+ // Log to stderr for visibility in IDE panels
68
+ if (result.status !== 'clean') {
69
+ process.stderr.write(formatDLPAlert(result) + '\n');
70
+ // Append to audit trail
71
+ try {
72
+ const cwd = process.cwd();
73
+ const rigourDir = path.join(cwd, '.rigour');
74
+ await fs.ensureDir(rigourDir);
75
+ const eventsPath = path.join(rigourDir, 'events.jsonl');
76
+ const auditEntry = createDLPAuditEntry(result, { agent });
77
+ await fs.appendFile(eventsPath, JSON.stringify(auditEntry) + '\n');
78
+ }
79
+ catch {
80
+ // Silent fail on audit logging
81
+ }
82
+ }
83
+ // Exit code: 2 = blocked, 0 = clean/warning
84
+ if (result.status === 'blocked') {
85
+ process.exitCode = 2;
86
+ }
87
+ }
88
+ main().catch(err => {
89
+ process.stderr.write(`Rigour DLP checker error: ${err.message}\n`);
90
+ process.stdout.write(JSON.stringify({ status: 'clean', detections: [], duration_ms: 0 }));
91
+ });
@@ -155,6 +155,38 @@ export const UNIVERSAL_CONFIG = {
155
155
  max_mocks_per_test: 5,
156
156
  ignore_patterns: [],
157
157
  },
158
+ governance: {
159
+ enabled: true,
160
+ enforce_memory: true,
161
+ enforce_skills: true,
162
+ block_native_memory: true,
163
+ protected_memory_paths: [
164
+ 'CLAUDE.md', '.claude/CLAUDE.md',
165
+ '.clinerules', '.clinerules/**',
166
+ '.windsurf/memories/**',
167
+ '.github/copilot-instructions.md',
168
+ ],
169
+ protected_skills_paths: [
170
+ '.claude/skills/**', '.claude/rules/**', '.claude/commands/**',
171
+ '.cursorrules', '.cursor/rules/**', '.cursor/prompts/**',
172
+ '.cline/rules/**',
173
+ '.windsurf/rules/**', '.windsurfrules',
174
+ '.github/instructions/**', 'copilot-instructions.md',
175
+ ],
176
+ exempt_paths: [
177
+ '.claude/settings.json',
178
+ '.cursor/hooks.json',
179
+ '.windsurf/hooks.json',
180
+ ],
181
+ },
182
+ input_validation: {
183
+ enabled: true,
184
+ block_on_detection: true,
185
+ min_secret_length: 8,
186
+ custom_patterns: [],
187
+ ignore_patterns: [],
188
+ audit_log: true,
189
+ },
158
190
  deep: {
159
191
  enabled: false,
160
192
  pro: false,
@@ -181,6 +213,7 @@ export const UNIVERSAL_CONFIG = {
181
213
  fast_gates: ['hallucinated-imports', 'phantom-apis', 'deprecated-apis', 'promise-safety', 'security-patterns', 'file-size'],
182
214
  timeout_ms: 5000,
183
215
  block_on_failure: false,
216
+ dlp: true,
184
217
  },
185
218
  output: {
186
219
  report_path: 'rigour-report.json',