@rigour-labs/core 4.1.1 → 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.
@@ -157,6 +157,47 @@ export class PromiseSafetyGate extends Gate {
157
157
  const funcName = match[1];
158
158
  const body = extractIndentedBody(content, match.index + match[0].length);
159
159
  if (body && !/\bawait\b/.test(body) && body.trim().split('\n').length > 2) {
160
+ // Check for framework decorators that require async def without await.
161
+ // FastAPI, Starlette, Django, pytest, and other frameworks use async handlers
162
+ // that legitimately don't need await (e.g. exception handlers, simple endpoints).
163
+ const linesBefore = content.substring(0, match.index);
164
+ const precedingLines = linesBefore.split('\n');
165
+ let hasFrameworkDecorator = false;
166
+ // Walk backwards to find decorators (immediately preceding lines starting with @)
167
+ for (let j = precedingLines.length - 1; j >= 0 && j >= precedingLines.length - 5; j--) {
168
+ const trimmedLine = precedingLines[j].trim();
169
+ if (!trimmedLine || trimmedLine.startsWith('#'))
170
+ continue;
171
+ if (!trimmedLine.startsWith('@'))
172
+ break; // Non-decorator, non-empty line — stop
173
+ // FastAPI/Starlette: @app.*, @router.*, @exception_handler
174
+ if (/^@(?:app|router)\.\w+/.test(trimmedLine)) {
175
+ hasFrameworkDecorator = true;
176
+ break;
177
+ }
178
+ // pytest: @pytest.fixture, @pytest.mark.asyncio
179
+ if (/^@pytest\.\w+/.test(trimmedLine)) {
180
+ hasFrameworkDecorator = true;
181
+ break;
182
+ }
183
+ // Django: @api_view, @action, @admin.*
184
+ if (/^@(?:api_view|action|admin\.)/.test(trimmedLine)) {
185
+ hasFrameworkDecorator = true;
186
+ break;
187
+ }
188
+ // Generic: @override, @abstractmethod, @staticmethod, @classmethod, @property
189
+ if (/^@(?:override|abstractmethod|staticmethod|classmethod|property)\b/.test(trimmedLine)) {
190
+ hasFrameworkDecorator = true;
191
+ break;
192
+ }
193
+ // Any decorator with 'handler', 'endpoint', 'route', 'hook', 'middleware', 'listener' in name
194
+ if (/^@\w*(?:handler|endpoint|route|hook|middleware|listener|callback|event)/i.test(trimmedLine)) {
195
+ hasFrameworkDecorator = true;
196
+ break;
197
+ }
198
+ }
199
+ if (hasFrameworkDecorator)
200
+ continue;
160
201
  const lineNum = content.substring(0, match.index).split('\n').length;
161
202
  violations.push({ file, line: lineNum, type: 'async-no-await', code: `async def ${funcName}()`, reason: `async def never uses await` });
162
203
  }
@@ -212,9 +253,26 @@ export class PromiseSafetyGate extends Gate {
212
253
  }
213
254
  }
214
255
  detectIgnoredErrorsGo(lines, file, violations) {
256
+ // Functions where ignoring the error is idiomatic/safe in Go:
257
+ // - json.Marshal/MarshalIndent on simple types (cannot fail on []string, map[string]string, etc.)
258
+ // - fmt.Sprintf/Fprintf/Fprint (format functions almost never fail)
259
+ // - strconv.Itoa (infallible)
260
+ // - strings.* functions (pure string operations)
261
+ const safeToIgnorePatterns = [
262
+ /\bjson\.Marshal\s*\(/, // json.Marshal on simple types
263
+ /\bjson\.MarshalIndent\s*\(/, // json.MarshalIndent on simple types
264
+ /\bfmt\.(?:Sprintf|Fprintf|Fprint|Sprint|Sprintln|Fprintln)\s*\(/,
265
+ /\bstrconv\.(?:Itoa|FormatBool|FormatInt|FormatUint|FormatFloat)\s*\(/,
266
+ /\bstrings\.(?:Join|Replace|ToLower|ToUpper|TrimSpace|Trim|Split)\s*\(/,
267
+ /\bbytes\.(?:Join|Replace)\s*\(/,
268
+ ];
215
269
  for (let i = 0; i < lines.length; i++) {
216
270
  const match = lines[i].match(/(\w+)\s*,\s*_\s*(?::=|=)\s*(\w+)\./);
217
271
  if (match && /\b(?:os|io|ioutil|bufio|sql|net|http|json|xml|yaml|strconv)\./.test(lines[i].trim())) {
272
+ // Check if this is an infallible operation where _ is idiomatic
273
+ const isSafeToIgnore = safeToIgnorePatterns.some(p => p.test(lines[i]));
274
+ if (isSafeToIgnore)
275
+ continue;
218
276
  violations.push({ file, line: i + 1, type: 'ignored-error', code: lines[i].trim().substring(0, 80), reason: `Error return ignored with _` });
219
277
  }
220
278
  }
@@ -310,7 +368,9 @@ export class PromiseSafetyGate extends Gate {
310
368
  const normalized = file.replace(/\\/g, '/');
311
369
  return (normalized.includes('/examples/') ||
312
370
  /\/commands\/demo(?:-|\/)/.test(`/${normalized}`) ||
313
- /\/gates\/(?:promise-safety|deprecated-apis-rules(?:-node|-lang)?)\.ts$/i.test(normalized));
371
+ /\/gates\/(?:promise-safety|deprecated-apis-rules(?:-node|-lang)?)\.ts$/i.test(normalized) ||
372
+ // Skip conftest.py (pytest fixtures, not application code)
373
+ /(?:^|\/)conftest\.py$/i.test(normalized));
314
374
  }
315
375
  sanitizeLine(line) {
316
376
  // Remove obvious comments and quoted literals to avoid matching detector text/examples.
@@ -47,6 +47,11 @@ export declare class SecurityPatternsGate extends Gate {
47
47
  run(context: GateContext): Promise<Failure[]>;
48
48
  private shouldSkipSecurityFile;
49
49
  private scanFileForVulnerabilities;
50
+ /**
51
+ * Check if a hardcoded secret match is actually a dummy/placeholder value.
52
+ * Filters out test values, placeholder defaults, and env-var-name assignments.
53
+ */
54
+ private isDummySecretValue;
50
55
  }
51
56
  /**
52
57
  * Quick helper to check a single file for security issues
@@ -300,16 +300,35 @@ export class SecurityPatternsGate extends Gate {
300
300
  }
301
301
  shouldSkipSecurityFile(file) {
302
302
  const normalized = file.replace(/\\/g, '/');
303
+ // Skip common non-source directories
303
304
  if (/\/(?:examples|studio-dist|dist|build|coverage|target|out)\//.test(`/${normalized}`))
304
305
  return true;
305
- if (/\/__tests__\//.test(`/${normalized}`))
306
+ // Skip test directories: __tests__/, tests/, test/, __test__/, e2e/, fixtures/, mocks/
307
+ if (/\/(?:__tests__|tests|test|__test__|e2e|fixtures|mocks)\//.test(`/${normalized}`))
306
308
  return true;
307
309
  if (/\/commands\/demo(?:-|\/)/.test(`/${normalized}`))
308
310
  return true;
309
311
  if (/\/gates\/deprecated-apis-rules(?:-node|-lang)?\.ts$/i.test(normalized))
310
312
  return true;
313
+ // Skip test files: *.test.ts, *.spec.ts (JS/TS/Java)
311
314
  if (/\.(test|spec)\.(?:ts|tsx|js|jsx|py|java|go)$/i.test(normalized))
312
315
  return true;
316
+ // Skip Go test files: *_test.go
317
+ if (/_test\.go$/i.test(normalized))
318
+ return true;
319
+ // Skip Python test files: test_*.py, *_test.py, conftest.py
320
+ if (/(?:^|\/)test_[^/]+\.py$/i.test(normalized))
321
+ return true;
322
+ if (/_test\.py$/i.test(normalized))
323
+ return true;
324
+ if (/(?:^|\/)conftest\.py$/i.test(normalized))
325
+ return true;
326
+ // Skip Java/Kotlin test files in src/test/ directories
327
+ if (/\/src\/test\//.test(`/${normalized}`))
328
+ return true;
329
+ // Skip E2E test files by naming convention
330
+ if (/[._-]e2e[._-]/i.test(normalized) || /E2E/i.test(path.basename(normalized)))
331
+ return true;
313
332
  return false;
314
333
  }
315
334
  scanFileForVulnerabilities(content, file, ext, vulnerabilities) {
@@ -323,6 +342,10 @@ export class SecurityPatternsGate extends Gate {
323
342
  pattern.regex.lastIndex = 0;
324
343
  let match;
325
344
  while ((match = pattern.regex.exec(content)) !== null) {
345
+ // For hardcoded_secrets: filter out placeholder/dummy values and env var names
346
+ if (pattern.type === 'hardcoded_secrets' && this.isDummySecretValue(match[0])) {
347
+ continue;
348
+ }
326
349
  // Find line number
327
350
  const beforeMatch = content.slice(0, match.index);
328
351
  const lineNumber = beforeMatch.split('\n').length;
@@ -338,6 +361,33 @@ export class SecurityPatternsGate extends Gate {
338
361
  }
339
362
  }
340
363
  }
364
+ /**
365
+ * Check if a hardcoded secret match is actually a dummy/placeholder value.
366
+ * Filters out test values, placeholder defaults, and env-var-name assignments.
367
+ */
368
+ isDummySecretValue(matchText) {
369
+ // Extract the quoted value from the match (e.g., api_key="test-api-key" → test-api-key)
370
+ const valueMatch = matchText.match(/[:=]\s*['"]([^'"]+)['"]/);
371
+ if (!valueMatch)
372
+ return false;
373
+ const value = valueMatch[1];
374
+ // Placeholder/example patterns
375
+ if (/^(?:your[_-]|my[_-]|example[_-]|placeholder|changeme|replace[_-]me|xxx+|dummy|fake|sample)/i.test(value))
376
+ return true;
377
+ // Test-specific dummy values
378
+ if (/^(?:test[_-]|e2e[_-]|mock[_-]|stub[_-]|dev[_-])/i.test(value))
379
+ return true;
380
+ if (/^testpass(?:word)?$/i.test(value))
381
+ return true;
382
+ // All-caps with underscores = env var names, not actual secrets
383
+ // e.g., API_KEY = "OPEN_SANDBOX_API_KEY" is referencing a var name, not a real key
384
+ if (/^[A-Z][A-Z0-9_]{7,}$/.test(value))
385
+ return true;
386
+ // Common documentation/tutorial dummy values
387
+ if (/^(?:sk_test_|pk_test_|sk_live_xxx|password123|secret123|abcdef|abc123)/i.test(value))
388
+ return true;
389
+ return false;
390
+ }
341
391
  }
342
392
  /**
343
393
  * Quick helper to check a single file for security issues
@@ -236,18 +236,38 @@ export class TestQualityGate extends Gate {
236
236
  }
237
237
  checkPythonTestQuality(content, file, issues) {
238
238
  const lines = content.split('\n');
239
+ const basename = path.basename(file);
240
+ // Skip conftest.py entirely — these contain fixtures, not tests
241
+ if (basename === 'conftest.py')
242
+ return;
239
243
  let inTestFunc = false;
240
244
  let testStartLine = 0;
241
245
  let testIndent = 0;
242
246
  let hasAssertion = false;
243
247
  let mockCount = 0;
244
248
  let testContent = '';
249
+ let hasFixtureDecorator = false;
245
250
  for (let i = 0; i < lines.length; i++) {
246
251
  const line = lines[i];
247
252
  const trimmed = line.trim();
248
253
  // Detect test function start
249
254
  const testFuncMatch = line.match(/^(\s*)(?:def|async\s+def)\s+(test_\w+)\s*\(/);
250
255
  if (testFuncMatch) {
256
+ // Check if this function has a @pytest.fixture decorator (not a real test)
257
+ hasFixtureDecorator = false;
258
+ for (let j = i - 1; j >= 0 && j >= i - 5; j--) {
259
+ const prevLine = lines[j].trim();
260
+ if (!prevLine || prevLine.startsWith('#'))
261
+ continue;
262
+ if (!prevLine.startsWith('@'))
263
+ break;
264
+ if (/^@pytest\.fixture/.test(prevLine)) {
265
+ hasFixtureDecorator = true;
266
+ break;
267
+ }
268
+ }
269
+ if (hasFixtureDecorator)
270
+ continue; // Skip — this is a fixture, not a test
251
271
  // If we were in a previous test, analyze it
252
272
  if (inTestFunc) {
253
273
  this.analyzePythonTestBlock(testContent, file, testStartLine, hasAssertion, mockCount, issues);
@@ -12,6 +12,7 @@ import fs from 'fs-extra';
12
12
  import path from 'path';
13
13
  import yaml from 'yaml';
14
14
  import { ConfigSchema } from '../types/index.js';
15
+ import { scanInputForCredentials } from './input-validator.js';
15
16
  const JS_TS_PATTERN = /\.(ts|tsx|js|jsx|mts|mjs)$/;
16
17
  /**
17
18
  * Load rigour config from cwd, falling back to defaults.
@@ -42,6 +43,8 @@ async function resolveFile(filePath, cwd) {
42
43
  function checkFile(content, relPath, cwd, config) {
43
44
  const failures = [];
44
45
  const lines = content.split('\n');
46
+ // Gate 0: Memory & Skills Governance — block writes to agent-native memory paths
47
+ checkGovernance(content, relPath, config, failures);
45
48
  // Gate 1: File size
46
49
  const maxLines = config.gates.max_file_lines ?? 500;
47
50
  if (lines.length > maxLines) {
@@ -220,3 +223,89 @@ function checkCommandInjection(line, i, relPath, failures) {
220
223
  });
221
224
  }
222
225
  }
226
+ // ── Memory & Skills Governance (v4.2+) ────────────────────────────
227
+ /**
228
+ * Simple glob matcher — handles exact paths, `*` (single segment),
229
+ * and `**` (any depth). No external dependencies.
230
+ */
231
+ function simpleGlob(filePath, pattern) {
232
+ // Exact match
233
+ if (filePath === pattern)
234
+ return true;
235
+ // Convert glob to regex: ** → any path, * → single segment
236
+ const regexStr = pattern
237
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex specials (except * and ?)
238
+ .replace(/\*\*/g, '<<<DOUBLESTAR>>>')
239
+ .replace(/\*/g, '[^/]*')
240
+ .replace(/<<<DOUBLESTAR>>>/g, '.*');
241
+ return new RegExp(`^${regexStr}$`).test(filePath);
242
+ }
243
+ /**
244
+ * Intercept writes to agent-native memory AND skills files.
245
+ *
246
+ * Two separate enforcement layers:
247
+ * 1. enforce_memory — blocks writes to CLAUDE.md, .clinerules, .windsurf/memories/
248
+ * → tells agent: "use rigour_remember instead"
249
+ * 2. enforce_skills — blocks writes to .claude/skills/, .cursor/rules/, etc.
250
+ * → tells agent: "use rigour skills system instead"
251
+ *
252
+ * Both layers DLP-scan content for credentials regardless of blocking.
253
+ *
254
+ * Users can disable via rigour.yml:
255
+ * gates:
256
+ * governance:
257
+ * enabled: false # disable everything
258
+ * enforce_memory: false # allow native memory, still enforce skills
259
+ * enforce_skills: false # allow native skills, still enforce memory
260
+ *
261
+ * @since v4.2.0
262
+ */
263
+ function checkGovernance(content, relPath, config, failures) {
264
+ const gov = config.gates.governance;
265
+ if (!gov?.enabled)
266
+ return;
267
+ const exemptPaths = gov.exempt_paths ?? [];
268
+ const normalizedPath = relPath.replace(/\\/g, '/');
269
+ // Check exemptions first (Rigour's own hook configs)
270
+ const isExempt = exemptPaths.some(pattern => simpleGlob(normalizedPath, pattern));
271
+ if (isExempt)
272
+ return;
273
+ // ── Check memory paths ──
274
+ const memoryPaths = gov.protected_memory_paths ?? [];
275
+ const isMemoryPath = memoryPaths.some(pattern => simpleGlob(normalizedPath, pattern));
276
+ // ── Check skills paths ──
277
+ const skillsPaths = gov.protected_skills_paths ?? [];
278
+ const isSkillsPath = skillsPaths.some((pattern) => simpleGlob(normalizedPath, pattern));
279
+ if (!isMemoryPath && !isSkillsPath)
280
+ return;
281
+ // ── Enforcement: block memory writes ──
282
+ if (isMemoryPath && gov.enforce_memory && gov.block_native_memory) {
283
+ failures.push({
284
+ gate: 'governance',
285
+ file: relPath,
286
+ message: `BLOCKED: Agent writing to native memory path "${relPath}". Use rigour_remember instead — it DLP-scans and persists safely to .rigour/memory.json`,
287
+ severity: 'critical',
288
+ });
289
+ }
290
+ // ── Enforcement: block skills writes ──
291
+ if (isSkillsPath && gov.enforce_skills) {
292
+ failures.push({
293
+ gate: 'governance-skills',
294
+ file: relPath,
295
+ message: `BLOCKED: Agent writing to native skills/rules path "${relPath}". Use Rigour skills system instead — governed, DLP-scanned, and auditable`,
296
+ severity: 'critical',
297
+ });
298
+ }
299
+ // ── DLP scan the content being written (always, regardless of block settings) ──
300
+ const dlpResult = scanInputForCredentials(content);
301
+ if (dlpResult.status !== 'clean') {
302
+ for (const detection of dlpResult.detections) {
303
+ failures.push({
304
+ gate: 'governance-dlp',
305
+ file: relPath,
306
+ message: `${detection.description} found in agent ${isMemoryPath ? 'memory' : 'skills'} file. ${detection.recommendation}`,
307
+ severity: detection.severity === 'critical' ? 'critical' : 'high',
308
+ });
309
+ }
310
+ }
311
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * DLP Hook Templates — Pre-Input Credential Interception
3
+ *
4
+ * Generates tool-native hook configs that intercept user input
5
+ * BEFORE it reaches the AI agent. Complements the existing
6
+ * post-output templates in templates.ts.
7
+ *
8
+ * Hook events:
9
+ * - Claude Code: PreToolUse matcher (all tools)
10
+ * - Cursor: beforeFileEdit event
11
+ * - Cline: PreToolUse executable script
12
+ * - Windsurf: pre_write_code event
13
+ *
14
+ * @since v4.2.0 — AI Agent DLP layer
15
+ */
16
+ import type { HookTool } from './types.js';
17
+ export interface GeneratedDLPHookFile {
18
+ path: string;
19
+ content: string;
20
+ executable?: boolean;
21
+ description: string;
22
+ }
23
+ /**
24
+ * Generate DLP (pre-input) hook config files for a specific tool.
25
+ */
26
+ export declare function generateDLPHookFiles(tool: HookTool, checkerCommand: string): GeneratedDLPHookFile[];
@@ -0,0 +1,281 @@
1
+ /**
2
+ * DLP Hook Templates — Pre-Input Credential Interception
3
+ *
4
+ * Generates tool-native hook configs that intercept user input
5
+ * BEFORE it reaches the AI agent. Complements the existing
6
+ * post-output templates in templates.ts.
7
+ *
8
+ * Hook events:
9
+ * - Claude Code: PreToolUse matcher (all tools)
10
+ * - Cursor: beforeFileEdit event
11
+ * - Cline: PreToolUse executable script
12
+ * - Windsurf: pre_write_code event
13
+ *
14
+ * @since v4.2.0 — AI Agent DLP layer
15
+ */
16
+ /**
17
+ * Generate DLP (pre-input) hook config files for a specific tool.
18
+ */
19
+ export function generateDLPHookFiles(tool, checkerCommand) {
20
+ switch (tool) {
21
+ case 'claude':
22
+ return generateClaudeDLPHooks(checkerCommand);
23
+ case 'cursor':
24
+ return generateCursorDLPHooks(checkerCommand);
25
+ case 'cline':
26
+ return generateClineDLPHooks(checkerCommand);
27
+ case 'windsurf':
28
+ return generateWindsurfDLPHooks(checkerCommand);
29
+ default:
30
+ return [];
31
+ }
32
+ }
33
+ // ── Claude Code DLP Hook ──────────────────────────────────────────
34
+ function generateClaudeDLPHooks(checkerCommand) {
35
+ // Claude Code supports PreToolUse hooks that fire BEFORE tool execution.
36
+ // We intercept all tool uses to scan user input for credentials.
37
+ const settings = {
38
+ hooks: {
39
+ PreToolUse: [
40
+ {
41
+ matcher: ".*",
42
+ hooks: [
43
+ {
44
+ type: "command",
45
+ command: `${checkerCommand} --mode dlp --stdin`,
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ };
52
+ return [
53
+ {
54
+ path: '.claude/dlp-settings.json',
55
+ content: JSON.stringify(settings, null, 4),
56
+ description: 'Claude Code PreToolUse DLP hook — scans input for credentials before agent processing',
57
+ },
58
+ ];
59
+ }
60
+ // ── Cursor DLP Hook ───────────────────────────────────────────────
61
+ function generateCursorDLPHooks(checkerCommand) {
62
+ const hooks = {
63
+ version: 1,
64
+ hooks: {
65
+ beforeFileEdit: [
66
+ {
67
+ command: `${checkerCommand} --mode dlp --stdin`,
68
+ }
69
+ ]
70
+ }
71
+ };
72
+ const wrapper = `#!/usr/bin/env node
73
+ /**
74
+ * Cursor DLP hook — scans input for credentials before agent processing.
75
+ * Receives { file_path, new_content } on stdin.
76
+ * Runs Rigour credential scanner on the content.
77
+ *
78
+ * @since v4.2.0 — AI Agent DLP
79
+ */
80
+
81
+ let data = '';
82
+ process.stdin.on('data', chunk => { data += chunk; });
83
+ process.stdin.on('end', async () => {
84
+ try {
85
+ const payload = JSON.parse(data);
86
+ const textToScan = payload.new_content || payload.content || '';
87
+
88
+ if (!textToScan) {
89
+ process.stdout.write(JSON.stringify({ status: 'ok' }));
90
+ return;
91
+ }
92
+
93
+ const { spawnSync } = require('child_process');
94
+ const proc = spawnSync(
95
+ 'node',
96
+ [require.resolve('@rigour-labs/core/dist/hooks/standalone-dlp-checker.js')],
97
+ {
98
+ input: textToScan,
99
+ encoding: 'utf-8',
100
+ timeout: 3000,
101
+ }
102
+ );
103
+
104
+ if (proc.error) throw proc.error;
105
+
106
+ const result = JSON.parse((proc.stdout || '').trim());
107
+ if (result.status === 'blocked') {
108
+ process.stderr.write('[rigour/dlp] ' + result.detections.length + ' credential(s) BLOCKED\\n');
109
+ for (const d of result.detections) {
110
+ process.stderr.write(' [' + d.severity.toUpperCase() + '] ' + d.description + '\\n');
111
+ process.stderr.write(' → ' + d.recommendation + '\\n');
112
+ }
113
+ process.exit(2); // Block the operation
114
+ }
115
+
116
+ process.stdout.write(JSON.stringify({ status: 'ok' }));
117
+ } catch (err) {
118
+ process.stderr.write('Rigour DLP hook error: ' + err.message + '\\n');
119
+ process.stdout.write(JSON.stringify({ status: 'ok' }));
120
+ }
121
+ });
122
+ `;
123
+ return [
124
+ {
125
+ path: '.cursor/dlp-hooks.json',
126
+ content: JSON.stringify(hooks, null, 4),
127
+ description: 'Cursor DLP hook config — pre-input credential scanning',
128
+ },
129
+ {
130
+ path: '.cursor/rigour-dlp-hook.js',
131
+ content: wrapper,
132
+ executable: true,
133
+ description: 'Cursor DLP hook wrapper — credential interception',
134
+ },
135
+ ];
136
+ }
137
+ // ── Cline DLP Hook ────────────────────────────────────────────────
138
+ function generateClineDLPHooks(checkerCommand) {
139
+ const script = `#!/usr/bin/env node
140
+ /**
141
+ * Cline PreToolUse DLP hook for Rigour.
142
+ * Receives JSON on stdin with { toolName, toolInput }.
143
+ * Scans tool input for credentials BEFORE execution.
144
+ *
145
+ * @since v4.2.0 — AI Agent DLP
146
+ */
147
+
148
+ let data = '';
149
+ process.stdin.on('data', chunk => { data += chunk; });
150
+ process.stdin.on('end', async () => {
151
+ try {
152
+ const payload = JSON.parse(data);
153
+
154
+ // Extract all string values from toolInput to scan
155
+ const textsToScan = [];
156
+ if (payload.toolInput) {
157
+ for (const [key, value] of Object.entries(payload.toolInput)) {
158
+ if (typeof value === 'string' && value.length > 5) {
159
+ textsToScan.push(value);
160
+ }
161
+ }
162
+ }
163
+
164
+ if (textsToScan.length === 0) {
165
+ process.stdout.write(JSON.stringify({}));
166
+ return;
167
+ }
168
+
169
+ const combined = textsToScan.join('\\n');
170
+ const { spawnSync } = require('child_process');
171
+ const proc = spawnSync(
172
+ 'node',
173
+ [require.resolve('@rigour-labs/core/dist/hooks/standalone-dlp-checker.js')],
174
+ {
175
+ input: combined,
176
+ encoding: 'utf-8',
177
+ timeout: 3000,
178
+ }
179
+ );
180
+
181
+ if (proc.error) throw proc.error;
182
+
183
+ const result = JSON.parse((proc.stdout || '').trim());
184
+ if (result.status === 'blocked') {
185
+ const msgs = result.detections
186
+ .map(d => '[rigour/dlp/' + d.type + '] ' + d.description + ' → ' + d.recommendation)
187
+ .join('\\n');
188
+
189
+ process.stdout.write(JSON.stringify({
190
+ contextModification: '\\n🛑 [Rigour DLP] ' + result.detections.length + ' credential(s) BLOCKED before agent processing:\\n' + msgs + '\\nReplace with environment variable references.',
191
+ }));
192
+ process.exit(2);
193
+ } else {
194
+ process.stdout.write(JSON.stringify({}));
195
+ }
196
+ } catch (err) {
197
+ process.stderr.write('Rigour DLP hook error: ' + err.message + '\\n');
198
+ process.stdout.write(JSON.stringify({}));
199
+ }
200
+ });
201
+ `;
202
+ return [
203
+ {
204
+ path: '.clinerules/hooks/PreToolUse',
205
+ content: script,
206
+ executable: true,
207
+ description: 'Cline PreToolUse DLP hook — credential interception before agent execution',
208
+ },
209
+ ];
210
+ }
211
+ // ── Windsurf DLP Hook ─────────────────────────────────────────────
212
+ function generateWindsurfDLPHooks(checkerCommand) {
213
+ const hooks = {
214
+ version: 1,
215
+ hooks: {
216
+ pre_write_code: [
217
+ {
218
+ command: `${checkerCommand} --mode dlp --stdin`,
219
+ }
220
+ ]
221
+ }
222
+ };
223
+ const wrapper = `#!/usr/bin/env node
224
+ /**
225
+ * Windsurf DLP hook — scans input for credentials before Cascade agent processing.
226
+ * Receives { file_path, content } on stdin.
227
+ *
228
+ * @since v4.2.0 — AI Agent DLP
229
+ */
230
+
231
+ let data = '';
232
+ process.stdin.on('data', chunk => { data += chunk; });
233
+ process.stdin.on('end', async () => {
234
+ try {
235
+ const payload = JSON.parse(data);
236
+ const textToScan = payload.content || '';
237
+
238
+ if (!textToScan) {
239
+ return;
240
+ }
241
+
242
+ const { spawnSync } = require('child_process');
243
+ const proc = spawnSync(
244
+ 'node',
245
+ [require.resolve('@rigour-labs/core/dist/hooks/standalone-dlp-checker.js')],
246
+ {
247
+ input: textToScan,
248
+ encoding: 'utf-8',
249
+ timeout: 3000,
250
+ }
251
+ );
252
+
253
+ if (proc.error) throw proc.error;
254
+
255
+ const result = JSON.parse((proc.stdout || '').trim());
256
+ if (result.status === 'blocked') {
257
+ for (const d of result.detections) {
258
+ process.stderr.write('[rigour/dlp] 🛑 ' + d.severity.toUpperCase() + ': ' + d.description + '\\n');
259
+ process.stderr.write(' → ' + d.recommendation + '\\n');
260
+ }
261
+ process.exit(2); // Block
262
+ }
263
+ } catch (err) {
264
+ process.stderr.write('Rigour DLP hook error: ' + err.message + '\\n');
265
+ }
266
+ });
267
+ `;
268
+ return [
269
+ {
270
+ path: '.windsurf/dlp-hooks.json',
271
+ content: JSON.stringify(hooks, null, 4),
272
+ description: 'Windsurf DLP hook config — pre-input credential scanning',
273
+ },
274
+ {
275
+ path: '.windsurf/rigour-dlp-hook.js',
276
+ content: wrapper,
277
+ executable: true,
278
+ description: 'Windsurf DLP hook wrapper — credential interception before Cascade',
279
+ },
280
+ ];
281
+ }
@@ -2,8 +2,13 @@
2
2
  * Hooks module — multi-tool hook integration for Rigour.
3
3
  *
4
4
  * @since v3.0.0
5
+ * @since v4.2.0 — AI Agent DLP (Data Loss Prevention)
5
6
  */
6
7
  export { runHookChecker } from './checker.js';
7
8
  export { generateHookFiles } from './templates.js';
8
9
  export type { HookTool, HookConfig, HookCheckerResult } from './types.js';
9
10
  export { DEFAULT_HOOK_CONFIG, FAST_GATE_IDS } from './types.js';
11
+ export { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from './input-validator.js';
12
+ export type { CredentialDetection, InputValidationResult, InputValidationConfig } from './input-validator.js';
13
+ export { generateDLPHookFiles } from './dlp-templates.js';
14
+ export type { GeneratedDLPHookFile } from './dlp-templates.js';
@@ -2,7 +2,11 @@
2
2
  * Hooks module — multi-tool hook integration for Rigour.
3
3
  *
4
4
  * @since v3.0.0
5
+ * @since v4.2.0 — AI Agent DLP (Data Loss Prevention)
5
6
  */
6
7
  export { runHookChecker } from './checker.js';
7
8
  export { generateHookFiles } from './templates.js';
8
9
  export { DEFAULT_HOOK_CONFIG, FAST_GATE_IDS } from './types.js';
10
+ // DLP (Data Loss Prevention) — v4.2.0
11
+ export { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from './input-validator.js';
12
+ export { generateDLPHookFiles } from './dlp-templates.js';