@rigour-labs/cli 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.
- package/dist/commands/hooks.d.ts +6 -0
- package/dist/commands/hooks.js +158 -32
- package/dist/commands/hooks.test.js +45 -0
- package/dist/commands/init.js +2 -1
- package/dist/utils/cli-version.js +3 -1
- package/dist/utils/cli-version.test.d.ts +1 -0
- package/dist/utils/cli-version.test.js +9 -0
- package/package.json +2 -2
package/dist/commands/hooks.d.ts
CHANGED
|
@@ -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>;
|
package/dist/commands/hooks.js
CHANGED
|
@@ -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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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:
|
|
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
|
|
123
|
-
|
|
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:
|
|
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
|
|
134
|
-
return [{
|
|
148
|
+
function generateClineHooks(checker, block, dlp = true) {
|
|
149
|
+
const files = [{
|
|
135
150
|
path: '.clinerules/hooks/PostToolUse',
|
|
136
|
-
content:
|
|
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
|
|
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
|
|
202
|
-
|
|
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:
|
|
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
|
-
//
|
|
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(() => {
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
47
|
+
"@rigour-labs/core": "4.2.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/fs-extra": "^11.0.4",
|