@rigour-labs/cli 5.4.0 → 5.5.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.
- package/README.md +2 -2
- package/dist/cli.js +4 -4
- package/dist/commands/export-audit.js +4 -1
- package/dist/commands/guide.js +2 -2
- package/dist/commands/hooks.d.ts +2 -2
- package/dist/commands/hooks.js +93 -53
- package/dist/commands/hooks.test.js +57 -0
- package/dist/commands/init.js +10 -2
- package/dist/commands/studio.js +792 -496
- package/package.json +2 -2
- package/studio-dist/assets/index-BvR6Si_S.js +362 -0
- package/studio-dist/assets/index-jC3k12bf.css +1 -0
- package/studio-dist/index.html +3 -3
- package/studio-dist/assets/index-BLGQnkLS.js +0 -322
- package/studio-dist/assets/index-hcvdpk3Y.css +0 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ brew install rigour
|
|
|
29
29
|
|
|
30
30
|
AI agents are powerful but ungoverned. They claim success based on narrative, not execution. Credentials get cached in agent memory. Imports get hallucinated. Code quality drifts across sessions.
|
|
31
31
|
|
|
32
|
-
**Rigour breaks this cycle** with deterministic PASS/FAIL gates, credential
|
|
32
|
+
**Rigour breaks this cycle** with deterministic PASS/FAIL gates, credential warnings, and memory governance — all local-first.
|
|
33
33
|
|
|
34
34
|
## 🔄 How It Works
|
|
35
35
|
|
|
@@ -80,7 +80,7 @@ Every failure carries a provenance tag (`ai-drift`, `traditional`, `security`, `
|
|
|
80
80
|
|
|
81
81
|
## 🔒 AI Agent DLP (Data Loss Prevention)
|
|
82
82
|
|
|
83
|
-
Real-time credential
|
|
83
|
+
Real-time credential detection via PreToolUse hooks — warns by default before agent actions.
|
|
84
84
|
|
|
85
85
|
- **29 credential patterns**: AWS, GCP, Azure, OpenAI, Anthropic, GitHub, Stripe, private keys, database URLs, JWTs, CI/CD tokens
|
|
86
86
|
- **Anti-evasion**: Unicode normalization, zero-width char removal, bidi control stripping, Shannon entropy detection (>4.5 bits)
|
package/dist/cli.js
CHANGED
|
@@ -267,7 +267,7 @@ const hooksCmd = program
|
|
|
267
267
|
.description('Manage AI coding tool hook integrations (file checks + DLP credential scanning)')
|
|
268
268
|
.addHelpText('after', `
|
|
269
269
|
DLP false-positive learning:
|
|
270
|
-
When a hook
|
|
270
|
+
When a hook warns about your prompt incorrectly, teach Rigour once:
|
|
271
271
|
$ rigour hooks check --dlp-allow-last
|
|
272
272
|
|
|
273
273
|
Learned patterns are stored per-project in .rigour/dlp-feedback.json.
|
|
@@ -305,7 +305,7 @@ hooksCmd
|
|
|
305
305
|
.option('--timeout <ms>', 'Timeout in milliseconds (default: 5000)')
|
|
306
306
|
.option('--mode <mode>', 'Check mode: "check" (default) or "dlp" (credential scanning)')
|
|
307
307
|
.option('--agent <name>', 'Agent name for DLP audit trail (e.g., cursor, claude)')
|
|
308
|
-
.option('--dlp-allow-last', 'Record last DLP
|
|
308
|
+
.option('--dlp-allow-last', 'Record the last DLP warning as learned false positives (hook feedback)')
|
|
309
309
|
.addHelpText('after', `
|
|
310
310
|
Examples:
|
|
311
311
|
$ rigour hooks check --files src/app.ts
|
|
@@ -315,8 +315,8 @@ Examples:
|
|
|
315
315
|
$ rigour hooks check --dlp-allow-last
|
|
316
316
|
|
|
317
317
|
DLP learning:
|
|
318
|
-
After a false-positive
|
|
319
|
-
in .rigour/dlp-feedback.json. Future scans allow matching patterns
|
|
318
|
+
After a false-positive warning, run --dlp-allow-last to store a safe fingerprint
|
|
319
|
+
in .rigour/dlp-feedback.json. Future scans allow matching generic patterns.
|
|
320
320
|
Provider keys and high-confidence secrets are never learned away.
|
|
321
321
|
`)
|
|
322
322
|
.action(async (options) => {
|
|
@@ -135,6 +135,8 @@ function buildAuditPackage(cwd, report, config) {
|
|
|
135
135
|
score_trend: trend ? {
|
|
136
136
|
direction: trend.direction,
|
|
137
137
|
delta: trend.delta,
|
|
138
|
+
visible_delta: trend.visibleDelta
|
|
139
|
+
?? trend.recentScores[trend.recentScores.length - 1] - trend.recentScores[0],
|
|
138
140
|
recent_average: trend.recentAvg,
|
|
139
141
|
previous_average: trend.previousAvg,
|
|
140
142
|
last_scores: trend.recentScores,
|
|
@@ -234,7 +236,8 @@ function renderMarkdown(audit) {
|
|
|
234
236
|
lines.push(`**Direction:** ${audit.score_trend.direction} ${arrow}`);
|
|
235
237
|
lines.push(`**Recent Average:** ${audit.score_trend.recent_average}/100`);
|
|
236
238
|
lines.push(`**Previous Average:** ${audit.score_trend.previous_average}/100`);
|
|
237
|
-
lines.push(`**Delta:** ${audit.score_trend.
|
|
239
|
+
lines.push(`**Visible Delta:** ${audit.score_trend.visible_delta > 0 ? '+' : ''}${audit.score_trend.visible_delta}`);
|
|
240
|
+
lines.push(`**Window Average Delta:** ${audit.score_trend.delta > 0 ? '+' : ''}${audit.score_trend.delta}`);
|
|
238
241
|
lines.push(`**Recent Scores:** ${audit.score_trend.last_scores.join(' → ')}`);
|
|
239
242
|
lines.push('');
|
|
240
243
|
}
|
package/dist/commands/guide.js
CHANGED
|
@@ -13,9 +13,9 @@ export function guideCommand() {
|
|
|
13
13
|
console.log(chalk.yellow(' • Strategic Guardians') + chalk.dim(': Dependency and Architectural boundary enforcement.\n'));
|
|
14
14
|
console.log(chalk.bold('Hooks & DLP Learning:'));
|
|
15
15
|
console.log(chalk.dim(' 1. Run ') + chalk.cyan('rigour hooks init') + chalk.dim(' to wire Cursor/Claude/Cline/Windsurf hooks.'));
|
|
16
|
-
console.log(chalk.dim(' 2. When DLP
|
|
16
|
+
console.log(chalk.dim(' 2. When DLP warns about a prompt falsely, run ') + chalk.cyan('rigour hooks check --dlp-allow-last'));
|
|
17
17
|
console.log(chalk.dim(' to teach Rigour that pattern is safe (stored in ') + chalk.cyan('.rigour/dlp-feedback.json') + chalk.dim(').'));
|
|
18
|
-
console.log(chalk.dim(' 3. Real provider keys (OpenAI, AWS, etc.)
|
|
18
|
+
console.log(chalk.dim(' 3. Real provider keys (OpenAI, AWS, etc.) cannot be learned away.\n'));
|
|
19
19
|
console.log(chalk.bold('Workflow Integration:'));
|
|
20
20
|
console.log(chalk.green(' • Cursor') + chalk.dim(': Add the MCP server or use the ') + chalk.cyan('.cursor/rules/rigour.mdc') + chalk.dim(' handshake.'));
|
|
21
21
|
console.log(chalk.green(' • CI/CD') + chalk.dim(': Use ') + chalk.cyan('rigour check --ci') + chalk.dim(' to fail PRs that violate quality gates.'));
|
package/dist/commands/hooks.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface HooksOptions {
|
|
|
18
18
|
dryRun?: boolean;
|
|
19
19
|
force?: boolean;
|
|
20
20
|
block?: boolean;
|
|
21
|
-
/** Also generate DLP
|
|
21
|
+
/** Also generate DLP pre-input warning hooks */
|
|
22
22
|
dlp?: boolean;
|
|
23
23
|
}
|
|
24
24
|
export interface HooksCheckOptions {
|
|
@@ -30,7 +30,7 @@ export interface HooksCheckOptions {
|
|
|
30
30
|
mode?: 'check' | 'dlp';
|
|
31
31
|
/** Agent name for audit trail (DLP mode) */
|
|
32
32
|
agent?: string;
|
|
33
|
-
/** Record last DLP
|
|
33
|
+
/** Record last DLP warning detections as learned false positives (hook feedback) */
|
|
34
34
|
dlpAllowLast?: boolean;
|
|
35
35
|
}
|
|
36
36
|
export declare function hooksInitCommand(cwd: string, options?: HooksOptions): Promise<void>;
|
package/dist/commands/hooks.js
CHANGED
|
@@ -17,7 +17,18 @@ 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 { fileURLToPath } from 'url';
|
|
20
21
|
import { runHookChecker, scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, writeDLPBlockManifest, allowLastDLPBlock } from '@rigour-labs/core';
|
|
22
|
+
function getHookCliVersion() {
|
|
23
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const packagePath = path.resolve(thisDir, '../../package.json');
|
|
25
|
+
const pkg = fs.readJsonSync(packagePath);
|
|
26
|
+
const version = pkg.version?.trim();
|
|
27
|
+
if (!version || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
28
|
+
throw new Error('Unable to resolve the installed Rigour CLI version');
|
|
29
|
+
}
|
|
30
|
+
return version;
|
|
31
|
+
}
|
|
21
32
|
// ── Studio event logging ─────────────────────────────────────────────
|
|
22
33
|
const MAX_EVENT_LOG_LINES = 2000;
|
|
23
34
|
async function logStudioEvent(cwd, event) {
|
|
@@ -74,20 +85,11 @@ function detectTools(cwd) {
|
|
|
74
85
|
}
|
|
75
86
|
return detected;
|
|
76
87
|
}
|
|
77
|
-
function resolveCheckerCommand(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
// 2. Try dev checkout: ESM has no __dirname, derive from import.meta.url
|
|
84
|
-
const thisDir = path.dirname(new URL(import.meta.url).pathname);
|
|
85
|
-
const localCli = path.resolve(thisDir, '../cli.js');
|
|
86
|
-
if (fs.existsSync(localCli)) {
|
|
87
|
-
return { command: 'node', args: [localCli, 'hooks', 'check'] };
|
|
88
|
-
}
|
|
89
|
-
// 3. Fallback: assume globally installed or aliased
|
|
90
|
-
return { command: 'npx', args: ['@rigour-labs/cli', 'hooks', 'check'] };
|
|
88
|
+
function resolveCheckerCommand() {
|
|
89
|
+
return {
|
|
90
|
+
command: 'npx',
|
|
91
|
+
args: ['--yes', `@rigour-labs/cli@${getHookCliVersion()}`, 'hooks', 'check'],
|
|
92
|
+
};
|
|
91
93
|
}
|
|
92
94
|
function shellEscape(arg) {
|
|
93
95
|
if (/^[A-Za-z0-9_/@%+=:,.-]+$/.test(arg)) {
|
|
@@ -136,7 +138,7 @@ function generateClaudeHooks(checker, block, dlp = true) {
|
|
|
136
138
|
}]
|
|
137
139
|
}],
|
|
138
140
|
};
|
|
139
|
-
// DLP: Add PreToolUse hook for credential
|
|
141
|
+
// DLP: Add PreToolUse hook for credential warnings
|
|
140
142
|
if (dlp) {
|
|
141
143
|
hooks.PreToolUse = [{
|
|
142
144
|
matcher: ".*",
|
|
@@ -151,7 +153,7 @@ function generateClaudeHooks(checker, block, dlp = true) {
|
|
|
151
153
|
path: '.claude/settings.json',
|
|
152
154
|
content: JSON.stringify(settings, null, 4),
|
|
153
155
|
description: dlp
|
|
154
|
-
? 'Claude Code hooks — PostToolUse quality checks + PreToolUse DLP credential
|
|
156
|
+
? 'Claude Code hooks — PostToolUse quality checks + PreToolUse DLP credential warnings'
|
|
155
157
|
: 'Claude Code PostToolUse hook',
|
|
156
158
|
}];
|
|
157
159
|
}
|
|
@@ -169,7 +171,7 @@ function generateCursorHooks(checker, block, dlp = true) {
|
|
|
169
171
|
path: '.cursor/hooks.json',
|
|
170
172
|
content: JSON.stringify(hooks, null, 4),
|
|
171
173
|
description: dlp
|
|
172
|
-
? 'Cursor hooks — afterFileEdit quality checks + beforeSubmitPrompt DLP
|
|
174
|
+
? 'Cursor hooks — afterFileEdit quality checks + beforeSubmitPrompt DLP warnings'
|
|
173
175
|
: 'Cursor afterFileEdit hook config',
|
|
174
176
|
}];
|
|
175
177
|
}
|
|
@@ -185,7 +187,7 @@ function generateClineHooks(checker, block, dlp = true) {
|
|
|
185
187
|
path: '.clinerules/hooks/PreToolUse',
|
|
186
188
|
content: buildClineDLPScript(checker),
|
|
187
189
|
executable: true,
|
|
188
|
-
description: 'Cline PreToolUse DLP hook — credential
|
|
190
|
+
description: 'Cline PreToolUse DLP hook — credential warnings before agent execution',
|
|
189
191
|
});
|
|
190
192
|
}
|
|
191
193
|
return files;
|
|
@@ -251,7 +253,7 @@ function buildClineDLPScript(checker) {
|
|
|
251
253
|
return `#!/usr/bin/env node
|
|
252
254
|
/**
|
|
253
255
|
* Cline PreToolUse DLP hook for Rigour.
|
|
254
|
-
*
|
|
256
|
+
* Warns about possible credentials before agent execution.
|
|
255
257
|
*/
|
|
256
258
|
let data = '';
|
|
257
259
|
process.stdin.on('data', chunk => { data += chunk; });
|
|
@@ -288,14 +290,15 @@ process.stdin.on('end', async () => {
|
|
|
288
290
|
return;
|
|
289
291
|
}
|
|
290
292
|
const result = JSON.parse(raw);
|
|
291
|
-
if (result.status
|
|
293
|
+
if (result.status !== 'clean') {
|
|
292
294
|
const msgs = result.detections
|
|
293
295
|
.map(d => \`[rigour/dlp/\${d.type}] \${d.description} → \${d.recommendation}\`)
|
|
294
296
|
.join('\\n');
|
|
297
|
+
const label = result.status === 'blocked' ? 'BLOCKED' : 'warning';
|
|
295
298
|
process.stdout.write(JSON.stringify({
|
|
296
|
-
contextModification: \`\\n
|
|
299
|
+
contextModification: \`\\n⚠️ [Rigour DLP] \${result.detections.length} possible credential(s) \${label}:\\n\${msgs}\`,
|
|
297
300
|
}));
|
|
298
|
-
process.exit(2);
|
|
301
|
+
if (result.status === 'blocked') process.exit(2);
|
|
299
302
|
} else {
|
|
300
303
|
process.stdout.write(JSON.stringify({}));
|
|
301
304
|
}
|
|
@@ -320,7 +323,7 @@ function generateWindsurfHooks(checker, block, dlp = true) {
|
|
|
320
323
|
path: '.windsurf/hooks.json',
|
|
321
324
|
content: JSON.stringify(hooks, null, 4),
|
|
322
325
|
description: dlp
|
|
323
|
-
? 'Windsurf hooks — post_write_code quality checks + pre_write_code DLP
|
|
326
|
+
? 'Windsurf hooks — post_write_code quality checks + pre_write_code DLP warnings'
|
|
324
327
|
: 'Windsurf post_write_code hook config',
|
|
325
328
|
}];
|
|
326
329
|
}
|
|
@@ -345,6 +348,7 @@ function printDryRun(files) {
|
|
|
345
348
|
async function writeHookFiles(cwd, files, force) {
|
|
346
349
|
let written = 0;
|
|
347
350
|
let skipped = 0;
|
|
351
|
+
const failedPaths = new Set();
|
|
348
352
|
for (const file of files) {
|
|
349
353
|
const fullPath = path.join(cwd, file.path);
|
|
350
354
|
const exists = await fs.pathExists(fullPath);
|
|
@@ -353,16 +357,28 @@ async function writeHookFiles(cwd, files, force) {
|
|
|
353
357
|
skipped++;
|
|
354
358
|
continue;
|
|
355
359
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
360
|
+
try {
|
|
361
|
+
await fs.ensureDir(path.dirname(fullPath));
|
|
362
|
+
await fs.writeFile(fullPath, file.content, 'utf-8');
|
|
363
|
+
if (file.executable) {
|
|
364
|
+
await fs.chmod(fullPath, 0o755);
|
|
365
|
+
}
|
|
366
|
+
console.log(chalk.green(` CREATE ${file.path}`));
|
|
367
|
+
console.log(chalk.dim(` ${file.description}`));
|
|
368
|
+
written++;
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
const code = error instanceof Error && 'code' in error
|
|
372
|
+
? String(error.code)
|
|
373
|
+
: 'UNKNOWN';
|
|
374
|
+
const reason = code === 'ENOTDIR'
|
|
375
|
+
? 'a parent path is a file; keep the existing config and configure this tool manually'
|
|
376
|
+
: error instanceof Error ? error.message : String(error);
|
|
377
|
+
console.error(chalk.yellow(` SKIP ${file.path} (${reason})`));
|
|
378
|
+
failedPaths.add(file.path);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { written, skipped, failedPaths };
|
|
366
382
|
}
|
|
367
383
|
// ── Next-steps guidance ──────────────────────────────────────────────
|
|
368
384
|
const NEXT_STEPS = {
|
|
@@ -371,10 +387,15 @@ const NEXT_STEPS = {
|
|
|
371
387
|
cline: 'Cline: Hook is active. Quality feedback appears in agent context on violations.',
|
|
372
388
|
windsurf: 'Windsurf: Reload editor. Check terminal for Rigour output after Cascade writes.',
|
|
373
389
|
};
|
|
374
|
-
function printNextSteps(tools) {
|
|
390
|
+
function printNextSteps(tools, unavailableTools) {
|
|
375
391
|
console.log(chalk.cyan('\nNext steps:'));
|
|
376
392
|
for (const tool of tools) {
|
|
377
|
-
|
|
393
|
+
if (unavailableTools.has(tool)) {
|
|
394
|
+
console.log(chalk.yellow(` ${tool[0].toUpperCase() + tool.slice(1)}: Not configured; resolve the path conflict or configure manually.`));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(chalk.dim(` ${NEXT_STEPS[tool]}`));
|
|
398
|
+
}
|
|
378
399
|
}
|
|
379
400
|
console.log('');
|
|
380
401
|
}
|
|
@@ -387,7 +408,7 @@ export async function hooksInitCommand(cwd, options = {}) {
|
|
|
387
408
|
arguments: { tool: options.tool, dryRun: options.dryRun, dlp: options.dlp },
|
|
388
409
|
});
|
|
389
410
|
const tools = resolveTools(cwd, options.tool);
|
|
390
|
-
const checker = resolveCheckerCommand(
|
|
411
|
+
const checker = resolveCheckerCommand();
|
|
391
412
|
const block = !!options.block;
|
|
392
413
|
// DLP is ON by default — user must explicitly pass --no-dlp to disable
|
|
393
414
|
const dlp = options.dlp !== false;
|
|
@@ -400,7 +421,12 @@ export async function hooksInitCommand(cwd, options = {}) {
|
|
|
400
421
|
printDryRun(allFiles);
|
|
401
422
|
return;
|
|
402
423
|
}
|
|
403
|
-
const { written, skipped } = await writeHookFiles(cwd, allFiles, !!options.force);
|
|
424
|
+
const { written, skipped, failedPaths } = await writeHookFiles(cwd, allFiles, !!options.force);
|
|
425
|
+
const failed = failedPaths.size;
|
|
426
|
+
const unavailableTools = new Set(tools.filter(tool => {
|
|
427
|
+
const generatedPaths = GENERATORS[tool](checker, block, dlp).map(file => file.path);
|
|
428
|
+
return generatedPaths.every(filePath => failedPaths.has(filePath));
|
|
429
|
+
}));
|
|
404
430
|
console.log('');
|
|
405
431
|
if (written > 0) {
|
|
406
432
|
console.log(chalk.green.bold(`Created ${written} hook file(s).`));
|
|
@@ -408,17 +434,24 @@ export async function hooksInitCommand(cwd, options = {}) {
|
|
|
408
434
|
if (skipped > 0) {
|
|
409
435
|
console.log(chalk.yellow(`Skipped ${skipped} existing file(s).`));
|
|
410
436
|
}
|
|
411
|
-
|
|
437
|
+
if (failed > 0) {
|
|
438
|
+
console.log(chalk.yellow(`Skipped ${failed} incompatible hook file(s); other tools were configured.`));
|
|
439
|
+
}
|
|
440
|
+
printNextSteps(tools, unavailableTools);
|
|
412
441
|
if (dlp) {
|
|
413
|
-
console.log(chalk.
|
|
414
|
-
console.log(chalk.dim('
|
|
442
|
+
console.log(chalk.yellow.bold(' ⚠ DLP warnings ACTIVE'));
|
|
443
|
+
console.log(chalk.dim(' Possible credentials will be reported before agent actions.'));
|
|
444
|
+
console.log(chalk.dim(' Use --block only when every input path is covered by the same policy.'));
|
|
415
445
|
console.log(chalk.dim(' Coverage: AWS keys, API tokens, database URLs, private keys, JWTs, passwords.\n'));
|
|
416
446
|
}
|
|
417
447
|
await logStudioEvent(cwd, {
|
|
418
448
|
type: 'tool_response',
|
|
419
449
|
tool: 'rigour_hooks_init',
|
|
420
|
-
status: 'success',
|
|
421
|
-
content: [{
|
|
450
|
+
status: failed > 0 ? 'partial' : 'success',
|
|
451
|
+
content: [{
|
|
452
|
+
type: 'text',
|
|
453
|
+
text: `Generated hooks for: ${tools.join(', ')}; ${written} written, ${skipped} existing, ${failed} incompatible`,
|
|
454
|
+
}],
|
|
422
455
|
});
|
|
423
456
|
}
|
|
424
457
|
async function readStdin() {
|
|
@@ -533,19 +566,26 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
533
566
|
}
|
|
534
567
|
const result = scanInputForCredentials(textToScan, {
|
|
535
568
|
enabled: true,
|
|
536
|
-
block_on_detection: options.block ??
|
|
569
|
+
block_on_detection: options.block ?? false,
|
|
537
570
|
cwd,
|
|
538
571
|
use_learned_feedback: true,
|
|
539
572
|
});
|
|
573
|
+
const messages = result.detections
|
|
574
|
+
.map((d) => `[${d.type}] ${d.description} → ${d.recommendation}`)
|
|
575
|
+
.join('\n');
|
|
576
|
+
const allowCommand = `npx --yes @rigour-labs/cli@${getHookCliVersion()} hooks check --dlp-allow-last`;
|
|
540
577
|
// Return Cursor-compatible format if detected as Cursor hook
|
|
541
578
|
if (cursorMode) {
|
|
542
579
|
if (result.status === 'blocked') {
|
|
543
|
-
const messages = result.detections
|
|
544
|
-
.map((d) => `[${d.type}] ${d.description} → ${d.recommendation}`)
|
|
545
|
-
.join('\n');
|
|
546
580
|
process.stdout.write(JSON.stringify({
|
|
547
581
|
continue: false,
|
|
548
|
-
user_message: `🛑 Rigour DLP: ${result.detections.length} credential(s) detected in your prompt:\n${messages}\n\nReplace with environment variable references before submitting.\n\nIf this is a false positive, run:
|
|
582
|
+
user_message: `🛑 Rigour DLP: ${result.detections.length} credential(s) detected in your prompt:\n${messages}\n\nReplace with environment variable references before submitting.\n\nIf this is a false positive, run: ${allowCommand}`,
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
else if (result.status === 'warning') {
|
|
586
|
+
process.stdout.write(JSON.stringify({
|
|
587
|
+
continue: true,
|
|
588
|
+
user_message: `⚠️ Rigour DLP warning: ${result.detections.length} possible credential(s) detected:\n${messages}`,
|
|
549
589
|
}));
|
|
550
590
|
}
|
|
551
591
|
else {
|
|
@@ -567,13 +607,13 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
567
607
|
catch {
|
|
568
608
|
// Silent
|
|
569
609
|
}
|
|
610
|
+
try {
|
|
611
|
+
await writeDLPBlockManifest(cwd, result.detections, textToScan);
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
// best-effort
|
|
615
|
+
}
|
|
570
616
|
if (result.status === 'blocked') {
|
|
571
|
-
try {
|
|
572
|
-
await writeDLPBlockManifest(cwd, result.detections, textToScan);
|
|
573
|
-
}
|
|
574
|
-
catch {
|
|
575
|
-
// best-effort
|
|
576
|
-
}
|
|
577
617
|
process.exitCode = 2;
|
|
578
618
|
}
|
|
579
619
|
}
|
|
@@ -44,6 +44,17 @@ describe('hooksInitCommand', () => {
|
|
|
44
44
|
const hookPath = path.join(testDir, '.clinerules', 'hooks', 'PostToolUse');
|
|
45
45
|
expect(fs.existsSync(hookPath)).toBe(true);
|
|
46
46
|
});
|
|
47
|
+
it('should skip a legacy .clinerules file and continue other tools', async () => {
|
|
48
|
+
fs.writeFileSync(path.join(testDir, '.clinerules'), 'legacy Cline rules');
|
|
49
|
+
await expect(hooksInitCommand(testDir, {
|
|
50
|
+
tool: 'cline,windsurf',
|
|
51
|
+
})).resolves.toBeUndefined();
|
|
52
|
+
expect(fs.readFileSync(path.join(testDir, '.clinerules'), 'utf-8'))
|
|
53
|
+
.toBe('legacy Cline rules');
|
|
54
|
+
expect(fs.existsSync(path.join(testDir, '.windsurf', 'hooks.json'))).toBe(true);
|
|
55
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('SKIP .clinerules/hooks/PostToolUse'));
|
|
56
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Cline: Not configured'));
|
|
57
|
+
});
|
|
47
58
|
it('should generate Windsurf hooks', async () => {
|
|
48
59
|
await hooksInitCommand(testDir, { tool: 'windsurf' });
|
|
49
60
|
const hooksPath = path.join(testDir, '.windsurf', 'hooks.json');
|
|
@@ -109,6 +120,8 @@ describe('hooksInitCommand — DLP integration', () => {
|
|
|
109
120
|
expect(settings.hooks.PostToolUse).toBeDefined();
|
|
110
121
|
expect(settings.hooks.PreToolUse).toBeDefined();
|
|
111
122
|
expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain('--mode dlp');
|
|
123
|
+
expect(settings.hooks.PreToolUse[0].hooks[0].command)
|
|
124
|
+
.toMatch(/npx --yes @rigour-labs\/cli@\d+\.\d+\.\d+/);
|
|
112
125
|
});
|
|
113
126
|
it('should generate Cursor hooks with DLP (beforeFileEdit) by default', async () => {
|
|
114
127
|
await hooksInitCommand(testDir, { tool: 'cursor', force: true });
|
|
@@ -131,6 +144,22 @@ describe('hooksInitCommand — DLP integration', () => {
|
|
|
131
144
|
expect(settings.hooks.PostToolUse).toBeDefined();
|
|
132
145
|
expect(settings.hooks.PreToolUse).toBeUndefined();
|
|
133
146
|
});
|
|
147
|
+
it('should generate Cline DLP warnings without default blocking', async () => {
|
|
148
|
+
await hooksInitCommand(testDir, { tool: 'cline', force: true });
|
|
149
|
+
const script = fs.readFileSync(path.join(testDir, '.clinerules', 'hooks', 'PreToolUse'), 'utf-8');
|
|
150
|
+
expect(script).toContain("result.status !== 'clean'");
|
|
151
|
+
expect(script).toContain("if (result.status === 'blocked') process.exit(2)");
|
|
152
|
+
});
|
|
153
|
+
it('should not route DLP hooks through the file-only core checker', async () => {
|
|
154
|
+
const coreChecker = path.join(testDir, 'node_modules/@rigour-labs/core/dist/hooks/standalone-checker.js');
|
|
155
|
+
fs.mkdirSync(path.dirname(coreChecker), { recursive: true });
|
|
156
|
+
fs.writeFileSync(coreChecker, '');
|
|
157
|
+
await hooksInitCommand(testDir, { tool: 'cursor', force: true });
|
|
158
|
+
const hooks = JSON.parse(fs.readFileSync(path.join(testDir, '.cursor', 'hooks.json'), 'utf-8'));
|
|
159
|
+
const command = hooks.hooks.beforeSubmitPrompt[0].command;
|
|
160
|
+
expect(command).toMatch(/npx --yes @rigour-labs\/cli@\d+\.\d+\.\d+/);
|
|
161
|
+
expect(command).not.toContain('standalone-checker');
|
|
162
|
+
});
|
|
134
163
|
});
|
|
135
164
|
describe('hooksCheckCommand', () => {
|
|
136
165
|
let testDir;
|
|
@@ -164,4 +193,32 @@ describe('hooksCheckCommand', () => {
|
|
|
164
193
|
expect(process.exitCode).toBe(2);
|
|
165
194
|
process.exitCode = originalExitCode;
|
|
166
195
|
});
|
|
196
|
+
it('should warn and continue for DLP detections by default', async () => {
|
|
197
|
+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
198
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
199
|
+
const originalExitCode = process.exitCode;
|
|
200
|
+
await hooksCheckCommand(testDir, {
|
|
201
|
+
mode: 'dlp',
|
|
202
|
+
files: 'AKIAZ9Y8X7W6V5U4T3Q2',
|
|
203
|
+
});
|
|
204
|
+
const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
|
|
205
|
+
expect(output).toContain('"status":"warning"');
|
|
206
|
+
expect(output).not.toContain('AKIAZ9Y8X7W6V5U4T3Q2');
|
|
207
|
+
expect(stderrSpy).toHaveBeenCalled();
|
|
208
|
+
expect(process.exitCode).toBe(originalExitCode);
|
|
209
|
+
});
|
|
210
|
+
it('should block DLP detections only with --block', async () => {
|
|
211
|
+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
212
|
+
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
213
|
+
const originalExitCode = process.exitCode;
|
|
214
|
+
await hooksCheckCommand(testDir, {
|
|
215
|
+
mode: 'dlp',
|
|
216
|
+
files: 'AKIAZ9Y8X7W6V5U4T3Q2',
|
|
217
|
+
block: true,
|
|
218
|
+
});
|
|
219
|
+
const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
|
|
220
|
+
expect(output).toContain('"status":"blocked"');
|
|
221
|
+
expect(process.exitCode).toBe(2);
|
|
222
|
+
process.exitCode = originalExitCode;
|
|
223
|
+
});
|
|
167
224
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -472,6 +472,12 @@ const IDE_TO_HOOK_TOOL = {
|
|
|
472
472
|
cline: 'cline',
|
|
473
473
|
windsurf: 'windsurf',
|
|
474
474
|
};
|
|
475
|
+
const PRIMARY_HOOK_PATH = {
|
|
476
|
+
claude: '.claude/settings.json',
|
|
477
|
+
cursor: '.cursor/hooks.json',
|
|
478
|
+
cline: '.clinerules/hooks/PostToolUse',
|
|
479
|
+
windsurf: '.windsurf/hooks.json',
|
|
480
|
+
};
|
|
475
481
|
/**
|
|
476
482
|
* Build the pattern index so rigour_check_pattern can detect duplicates.
|
|
477
483
|
* Uses semantic embeddings by default for fuzzy matching.
|
|
@@ -511,14 +517,16 @@ async function initHooksForAllDetectedTools(cwd, detectedIDEs) {
|
|
|
511
517
|
try {
|
|
512
518
|
console.log(chalk.dim(`\n Setting up real-time hooks for ${ide}...`));
|
|
513
519
|
await hooksInitCommand(cwd, { tool: hookTool, dlp: true, force: true, block: true });
|
|
514
|
-
|
|
520
|
+
if (await fs.pathExists(path.join(cwd, PRIMARY_HOOK_PATH[hookTool]))) {
|
|
521
|
+
enabledTools.push(hookTool);
|
|
522
|
+
}
|
|
515
523
|
}
|
|
516
524
|
catch (err) {
|
|
517
525
|
console.log(chalk.dim(` (Hooks setup for ${ide} failed: ${err?.message || err})`));
|
|
518
526
|
}
|
|
519
527
|
}
|
|
520
528
|
if (enabledTools.length > 0) {
|
|
521
|
-
console.log(chalk.dim(`
|
|
529
|
+
console.log(chalk.dim(` ⚠ DLP warnings active for: ${enabledTools.join(', ')}`));
|
|
522
530
|
}
|
|
523
531
|
return enabledTools;
|
|
524
532
|
}
|