@rigour-labs/cli 4.1.1 → 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.
@@ -18,12 +18,18 @@ export interface HooksOptions {
18
18
  dryRun?: boolean;
19
19
  force?: boolean;
20
20
  block?: boolean;
21
+ /** Also generate DLP (pre-input credential interception) hooks */
22
+ dlp?: boolean;
21
23
  }
22
24
  export interface HooksCheckOptions {
23
25
  files?: string;
24
26
  stdin?: boolean;
25
27
  block?: boolean;
26
28
  timeout?: string;
29
+ /** Run in DLP mode: scan text for credentials instead of checking files */
30
+ mode?: 'check' | 'dlp';
31
+ /** Agent name for audit trail (DLP mode) */
32
+ agent?: string;
27
33
  }
28
34
  export declare function hooksInitCommand(cwd: string, options?: HooksOptions): Promise<void>;
29
35
  export declare function hooksCheckCommand(cwd: string, options?: HooksCheckOptions): Promise<void>;
@@ -17,7 +17,7 @@ import fs from 'fs-extra';
17
17
  import path from 'path';
18
18
  import chalk from 'chalk';
19
19
  import { randomUUID } from 'crypto';
20
- import { runHookChecker } from '@rigour-labs/core';
20
+ import { runHookChecker, scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from '@rigour-labs/core';
21
21
  // ── Studio event logging ─────────────────────────────────────────────
22
22
  async function logStudioEvent(cwd, event) {
23
23
  try {
@@ -96,47 +96,71 @@ function resolveTools(cwd, toolFlag) {
96
96
  return detected;
97
97
  }
98
98
  // ── Per-tool hook generators ─────────────────────────────────────────
99
- function generateClaudeHooks(checker, block) {
99
+ function generateClaudeHooks(checker, block, dlp = true) {
100
100
  const blockFlag = block ? ' --block' : '';
101
101
  const checkerCommand = checkerToShellCommand(checker);
102
- const settings = {
103
- hooks: {
104
- PostToolUse: [{
105
- matcher: "Write|Edit|MultiEdit",
106
- hooks: [{
107
- type: "command",
108
- command: `${checkerCommand} --files "$TOOL_INPUT_file_path"${blockFlag}`,
109
- }]
110
- }]
111
- }
102
+ const hooks = {
103
+ PostToolUse: [{
104
+ matcher: "Write|Edit|MultiEdit",
105
+ hooks: [{
106
+ type: "command",
107
+ command: `${checkerCommand} --files "$TOOL_INPUT_file_path"${blockFlag}`,
108
+ }]
109
+ }],
112
110
  };
111
+ // DLP: Add PreToolUse hook for credential interception
112
+ if (dlp) {
113
+ hooks.PreToolUse = [{
114
+ matcher: ".*",
115
+ hooks: [{
116
+ type: "command",
117
+ command: `${checkerCommand} --mode dlp --stdin`,
118
+ }]
119
+ }];
120
+ }
121
+ const settings = { hooks };
113
122
  return [{
114
123
  path: '.claude/settings.json',
115
124
  content: JSON.stringify(settings, null, 4),
116
- description: 'Claude Code PostToolUse hook',
125
+ description: dlp
126
+ ? 'Claude Code hooks — PostToolUse quality checks + PreToolUse DLP credential interception'
127
+ : 'Claude Code PostToolUse hook',
117
128
  }];
118
129
  }
119
- function generateCursorHooks(checker, block) {
130
+ function generateCursorHooks(checker, block, dlp = true) {
120
131
  const blockFlag = block ? ' --block' : '';
121
132
  const checkerCommand = checkerToShellCommand(checker);
122
- const hooks = {
123
- version: 1,
124
- hooks: { afterFileEdit: [{ command: `${checkerCommand} --stdin${blockFlag}` }] }
133
+ const hookEntries = {
134
+ afterFileEdit: [{ command: `${checkerCommand} --stdin${blockFlag}` }],
125
135
  };
136
+ if (dlp) {
137
+ hookEntries.beforeFileEdit = [{ command: `${checkerCommand} --mode dlp --stdin` }];
138
+ }
139
+ const hooks = { version: 1, hooks: hookEntries };
126
140
  return [{
127
141
  path: '.cursor/hooks.json',
128
142
  content: JSON.stringify(hooks, null, 4),
129
- description: 'Cursor afterFileEdit hook config',
143
+ description: dlp
144
+ ? 'Cursor hooks — afterFileEdit quality checks + beforeFileEdit DLP credential interception'
145
+ : 'Cursor afterFileEdit hook config',
130
146
  }];
131
147
  }
132
- function generateClineHooks(checker, block) {
133
- const script = buildClineScript(checker, block);
134
- return [{
148
+ function generateClineHooks(checker, block, dlp = true) {
149
+ const files = [{
135
150
  path: '.clinerules/hooks/PostToolUse',
136
- content: script,
151
+ content: buildClineScript(checker, block),
137
152
  executable: true,
138
- description: 'Cline PostToolUse executable hook',
153
+ description: 'Cline PostToolUse executable hook — quality checks after file writes',
139
154
  }];
155
+ if (dlp) {
156
+ files.push({
157
+ path: '.clinerules/hooks/PreToolUse',
158
+ content: buildClineDLPScript(checker),
159
+ executable: true,
160
+ description: 'Cline PreToolUse DLP hook — credential interception before agent execution',
161
+ });
162
+ }
163
+ return files;
140
164
  }
141
165
  function buildClineScript(checker, block) {
142
166
  const blockArgLiteral = block ? `, '--block'` : '';
@@ -195,17 +219,79 @@ process.stdin.on('end', async () => {
195
219
  });
196
220
  `;
197
221
  }
198
- function generateWindsurfHooks(checker, block) {
222
+ function buildClineDLPScript(checker) {
223
+ return `#!/usr/bin/env node
224
+ /**
225
+ * Cline PreToolUse DLP hook for Rigour.
226
+ * Scans tool input for credentials BEFORE agent execution.
227
+ */
228
+ let data = '';
229
+ process.stdin.on('data', chunk => { data += chunk; });
230
+ process.stdin.on('end', async () => {
231
+ try {
232
+ const payload = JSON.parse(data);
233
+ const textsToScan = [];
234
+ if (payload.toolInput) {
235
+ for (const [key, value] of Object.entries(payload.toolInput)) {
236
+ if (typeof value === 'string' && value.length > 5) {
237
+ textsToScan.push(value);
238
+ }
239
+ }
240
+ }
241
+ if (textsToScan.length === 0) {
242
+ process.stdout.write(JSON.stringify({}));
243
+ return;
244
+ }
245
+
246
+ const { spawnSync } = require('child_process');
247
+ const command = ${JSON.stringify(checker.command)};
248
+ const baseArgs = ${JSON.stringify(checker.args)};
249
+ const proc = spawnSync(
250
+ command,
251
+ [...baseArgs, '--mode', 'dlp', '--stdin'],
252
+ { input: textsToScan.join('\\n'), encoding: 'utf-8', timeout: 3000 }
253
+ );
254
+ if (proc.error) throw proc.error;
255
+ const raw = (proc.stdout || '').trim();
256
+ if (!raw) {
257
+ process.stdout.write(JSON.stringify({}));
258
+ return;
259
+ }
260
+ const result = JSON.parse(raw);
261
+ if (result.status === 'blocked') {
262
+ const msgs = result.detections
263
+ .map(d => \`[rigour/dlp/\${d.type}] \${d.description} → \${d.recommendation}\`)
264
+ .join('\\n');
265
+ process.stdout.write(JSON.stringify({
266
+ contextModification: \`\\n🛑 [Rigour DLP] \${result.detections.length} credential(s) BLOCKED:\\n\${msgs}\\nReplace with environment variable references.\`,
267
+ }));
268
+ process.exit(2);
269
+ } else {
270
+ process.stdout.write(JSON.stringify({}));
271
+ }
272
+ } catch (err) {
273
+ process.stderr.write(\`Rigour DLP hook error: \${err.message}\\n\`);
274
+ process.stdout.write(JSON.stringify({}));
275
+ }
276
+ });
277
+ `;
278
+ }
279
+ function generateWindsurfHooks(checker, block, dlp = true) {
199
280
  const blockFlag = block ? ' --block' : '';
200
281
  const checkerCommand = checkerToShellCommand(checker);
201
- const hooks = {
202
- version: 1,
203
- hooks: { post_write_code: [{ command: `${checkerCommand} --stdin${blockFlag}` }] }
282
+ const hookEntries = {
283
+ post_write_code: [{ command: `${checkerCommand} --stdin${blockFlag}` }],
204
284
  };
285
+ if (dlp) {
286
+ hookEntries.pre_write_code = [{ command: `${checkerCommand} --mode dlp --stdin` }];
287
+ }
288
+ const hooks = { version: 1, hooks: hookEntries };
205
289
  return [{
206
290
  path: '.windsurf/hooks.json',
207
291
  content: JSON.stringify(hooks, null, 4),
208
- description: 'Windsurf post_write_code hook config',
292
+ description: dlp
293
+ ? 'Windsurf hooks — post_write_code quality checks + pre_write_code DLP credential interception'
294
+ : 'Windsurf post_write_code hook config',
209
295
  }];
210
296
  }
211
297
  const GENERATORS = {
@@ -268,15 +354,17 @@ export async function hooksInitCommand(cwd, options = {}) {
268
354
  await logStudioEvent(cwd, {
269
355
  type: 'tool_call',
270
356
  tool: 'rigour_hooks_init',
271
- arguments: { tool: options.tool, dryRun: options.dryRun },
357
+ arguments: { tool: options.tool, dryRun: options.dryRun, dlp: options.dlp },
272
358
  });
273
359
  const tools = resolveTools(cwd, options.tool);
274
360
  const checker = resolveCheckerCommand(cwd);
275
361
  const block = !!options.block;
276
- // Collect generated files from all tools
362
+ // DLP is ON by default — user must explicitly pass --no-dlp to disable
363
+ const dlp = options.dlp !== false;
364
+ // Collect generated files — each generator includes DLP hooks in the SAME config file
277
365
  const allFiles = [];
278
366
  for (const tool of tools) {
279
- allFiles.push(...GENERATORS[tool](checker, block));
367
+ allFiles.push(...GENERATORS[tool](checker, block, dlp));
280
368
  }
281
369
  if (options.dryRun) {
282
370
  printDryRun(allFiles);
@@ -291,11 +379,16 @@ export async function hooksInitCommand(cwd, options = {}) {
291
379
  console.log(chalk.yellow(`Skipped ${skipped} existing file(s).`));
292
380
  }
293
381
  printNextSteps(tools);
382
+ if (dlp) {
383
+ console.log(chalk.red.bold(' 🛑 DLP Protection ACTIVE'));
384
+ console.log(chalk.dim(' Credentials will be intercepted BEFORE reaching AI agents.'));
385
+ console.log(chalk.dim(' Coverage: AWS keys, API tokens, database URLs, private keys, JWTs, passwords.\n'));
386
+ }
294
387
  await logStudioEvent(cwd, {
295
388
  type: 'tool_response',
296
389
  tool: 'rigour_hooks_init',
297
390
  status: 'success',
298
- content: [{ type: 'text', text: `Generated hooks for: ${tools.join(', ')}` }],
391
+ content: [{ type: 'text', text: `Generated hooks for: ${tools.join(', ')}${options.dlp ? ' (+ DLP)' : ''}` }],
299
392
  });
300
393
  }
301
394
  async function readStdin() {
@@ -330,6 +423,39 @@ function parseStdinFiles(input) {
330
423
  }
331
424
  }
332
425
  export async function hooksCheckCommand(cwd, options = {}) {
426
+ // ── DLP Mode: Scan text for credentials ──────────────────
427
+ if (options.mode === 'dlp') {
428
+ const input = options.stdin
429
+ ? await readStdin()
430
+ : (options.files ?? ''); // Reuse files param as text in DLP mode
431
+ if (!input) {
432
+ process.stdout.write(JSON.stringify({ status: 'clean', detections: [], duration_ms: 0, scanned_length: 0 }));
433
+ return;
434
+ }
435
+ const result = scanInputForCredentials(input, {
436
+ enabled: true,
437
+ block_on_detection: options.block ?? true,
438
+ });
439
+ process.stdout.write(JSON.stringify(result));
440
+ if (result.status !== 'clean') {
441
+ process.stderr.write('\n' + formatDLPAlert(result) + '\n');
442
+ // Audit trail
443
+ try {
444
+ const auditEntry = createDLPAuditEntry(result, {
445
+ agent: options.agent ?? 'hook',
446
+ });
447
+ await logStudioEvent(cwd, auditEntry);
448
+ }
449
+ catch {
450
+ // Silent
451
+ }
452
+ if (result.status === 'blocked') {
453
+ process.exitCode = 2;
454
+ }
455
+ }
456
+ return;
457
+ }
458
+ // ── Standard Mode: Check files ───────────────────────────
333
459
  const timeout = options.timeout ? Number(options.timeout) : 5000;
334
460
  const files = options.stdin
335
461
  ? parseStdinFiles(await readStdin())
@@ -87,6 +87,51 @@ describe('hooksInitCommand', () => {
87
87
  expect(clineScript).toContain('--block');
88
88
  });
89
89
  });
90
+ describe('hooksInitCommand — DLP integration', () => {
91
+ let testDir;
92
+ beforeEach(() => {
93
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-dlp-test-'));
94
+ fs.writeFileSync(path.join(testDir, 'rigour.yml'), yaml.stringify({
95
+ version: 1,
96
+ gates: { max_file_lines: 500 },
97
+ }));
98
+ vi.spyOn(console, 'log').mockImplementation(() => { });
99
+ vi.spyOn(console, 'error').mockImplementation(() => { });
100
+ });
101
+ afterEach(() => {
102
+ fs.rmSync(testDir, { recursive: true, force: true });
103
+ vi.restoreAllMocks();
104
+ });
105
+ it('should generate Claude hooks with DLP (PreToolUse) by default', async () => {
106
+ await hooksInitCommand(testDir, { tool: 'claude', force: true });
107
+ const settingsPath = path.join(testDir, '.claude', 'settings.json');
108
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
109
+ expect(settings.hooks.PostToolUse).toBeDefined();
110
+ expect(settings.hooks.PreToolUse).toBeDefined();
111
+ expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain('--mode dlp');
112
+ });
113
+ it('should generate Cursor hooks with DLP (beforeFileEdit) by default', async () => {
114
+ await hooksInitCommand(testDir, { tool: 'cursor', force: true });
115
+ const hooksPath = path.join(testDir, '.cursor', 'hooks.json');
116
+ const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
117
+ expect(hooks.hooks.afterFileEdit).toBeDefined();
118
+ expect(hooks.hooks.beforeFileEdit).toBeDefined();
119
+ });
120
+ it('should generate Windsurf hooks with DLP by default', async () => {
121
+ await hooksInitCommand(testDir, { tool: 'windsurf', force: true });
122
+ const hooksPath = path.join(testDir, '.windsurf', 'hooks.json');
123
+ const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
124
+ expect(hooks.hooks.post_write_code).toBeDefined();
125
+ expect(hooks.hooks.pre_write_code).toBeDefined();
126
+ });
127
+ it('should skip DLP hooks when dlp: false', async () => {
128
+ await hooksInitCommand(testDir, { tool: 'claude', force: true, dlp: false });
129
+ const settingsPath = path.join(testDir, '.claude', 'settings.json');
130
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
131
+ expect(settings.hooks.PostToolUse).toBeDefined();
132
+ expect(settings.hooks.PreToolUse).toBeUndefined();
133
+ });
134
+ });
90
135
  describe('hooksCheckCommand', () => {
91
136
  let testDir;
92
137
  beforeEach(() => {
@@ -467,7 +467,8 @@ async function initHooksForDetectedTools(cwd, detectedIDE) {
467
467
  }
468
468
  try {
469
469
  console.log(chalk.dim(`\n Setting up real-time hooks for ${detectedIDE}...`));
470
- await hooksInitCommand(cwd, { tool: hookTool });
470
+ await hooksInitCommand(cwd, { tool: hookTool, dlp: true });
471
+ console.log(chalk.dim(` 🛑 DLP protection active — credentials intercepted before reaching agents`));
471
472
  }
472
473
  catch {
473
474
  // Non-fatal — hooks are a bonus, not a requirement
@@ -1,12 +1,14 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
3
4
  /**
4
5
  * Resolve CLI version from local package.json at runtime.
5
6
  * Works for source and built dist paths.
6
7
  */
7
8
  export function getCliVersion(fallback = '0.0.0') {
8
9
  try {
9
- const pkgPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../package.json');
10
+ const modulePath = fileURLToPath(import.meta.url);
11
+ const pkgPath = path.resolve(path.dirname(modulePath), '../../package.json');
10
12
  if (fs.existsSync(pkgPath)) {
11
13
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
12
14
  if (pkg.version && pkg.version.trim().length > 0) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getCliVersion } from './cli-version.js';
3
+ describe('getCliVersion', () => {
4
+ it('resolves package version at runtime', () => {
5
+ const version = getCliVersion();
6
+ expect(version).toMatch(/^\d+\.\d+\.\d+/);
7
+ expect(version).not.toBe('0.0.0');
8
+ });
9
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "4.1.1",
3
+ "version": "4.2.0",
4
4
  "description": "CLI quality gates for AI-generated code. Forces AI agents (Claude, Cursor, Copilot) to meet strict engineering standards with PASS/FAIL enforcement.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -44,7 +44,7 @@
44
44
  "inquirer": "9.2.16",
45
45
  "ora": "^8.0.1",
46
46
  "yaml": "^2.8.2",
47
- "@rigour-labs/core": "4.1.1"
47
+ "@rigour-labs/core": "4.2.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/fs-extra": "^11.0.4",