@rigour-labs/mcp 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.
- package/dist/index.js +3 -3
- package/dist/tools/definitions.d.ts +12 -0
- package/dist/tools/definitions.js +9 -6
- package/dist/tools/dlp-handler.d.ts +36 -0
- package/dist/tools/dlp-handler.js +183 -0
- package/dist/tools/hooks-handler.d.ts +8 -2
- package/dist/tools/hooks-handler.js +62 -5
- package/dist/tools/memory-handlers.js +114 -9
- package/dist/utils/config.d.ts +18 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -125,12 +125,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
125
125
|
case "rigour_handoff_accept":
|
|
126
126
|
result = await handleHandoffAccept(cwd, args.handoffId, args.agentId, requestId);
|
|
127
127
|
break;
|
|
128
|
-
// Real-time hooks (v3.0)
|
|
128
|
+
// Real-time hooks + DLP (v3.0 / v4.2)
|
|
129
129
|
case "rigour_hooks_check":
|
|
130
|
-
result = await handleHooksCheck(cwd, args.files, args.timeout);
|
|
130
|
+
result = await handleHooksCheck(cwd, args.files ?? [], args.timeout, args.text, args.agent);
|
|
131
131
|
break;
|
|
132
132
|
case "rigour_hooks_init":
|
|
133
|
-
result = await handleHooksInit(cwd, args.tool, args.force, args.dryRun);
|
|
133
|
+
result = await handleHooksInit(cwd, args.tool, args.force, args.dryRun, args.dlp ?? true);
|
|
134
134
|
break;
|
|
135
135
|
// Deep analysis (v4.0+)
|
|
136
136
|
case "rigour_check_deep": {
|
|
@@ -438,6 +438,14 @@ export declare const TOOL_DEFINITIONS: ({
|
|
|
438
438
|
type: string;
|
|
439
439
|
description: string;
|
|
440
440
|
};
|
|
441
|
+
text: {
|
|
442
|
+
type: string;
|
|
443
|
+
description: string;
|
|
444
|
+
};
|
|
445
|
+
agent: {
|
|
446
|
+
type: string;
|
|
447
|
+
description: string;
|
|
448
|
+
};
|
|
441
449
|
cwd: {
|
|
442
450
|
type: "string";
|
|
443
451
|
description: string;
|
|
@@ -470,6 +478,10 @@ export declare const TOOL_DEFINITIONS: ({
|
|
|
470
478
|
type: string;
|
|
471
479
|
description: string;
|
|
472
480
|
};
|
|
481
|
+
dlp: {
|
|
482
|
+
type: string;
|
|
483
|
+
description: string;
|
|
484
|
+
};
|
|
473
485
|
cwd: {
|
|
474
486
|
type: "string";
|
|
475
487
|
description: string;
|
|
@@ -413,21 +413,23 @@ export const TOOL_DEFINITIONS = [
|
|
|
413
413
|
openWorldHint: false,
|
|
414
414
|
},
|
|
415
415
|
},
|
|
416
|
-
// ─── Real-Time Hooks (v3.0)
|
|
416
|
+
// ─── Real-Time Hooks + DLP (v3.0 / v4.2) ───────────────
|
|
417
417
|
{
|
|
418
418
|
name: "rigour_hooks_check",
|
|
419
|
-
description: "Run the fast hook checker on specific files. Same checks that run inside IDE hooks (Claude, Cursor, Cline, Windsurf). Catches: hardcoded secrets, hallucinated imports, command injection, file size. Completes in <100ms.",
|
|
419
|
+
description: "Run the fast hook checker on specific files. Same checks that run inside IDE hooks (Claude, Cursor, Cline, Windsurf). Catches: hardcoded secrets, hallucinated imports, command injection, file size. Completes in <100ms. NEW: Pass 'text' param for DLP mode — scans user input for credentials (AWS keys, API tokens, database URLs, private keys, JWTs) before agent processing.",
|
|
420
420
|
inputSchema: {
|
|
421
421
|
type: "object",
|
|
422
422
|
properties: {
|
|
423
423
|
...cwdParam(),
|
|
424
424
|
files: { type: "array", items: { type: "string" }, description: "List of file paths (relative to cwd) to check." },
|
|
425
425
|
timeout: { type: "number", description: "Optional timeout in milliseconds (default: 5000)." },
|
|
426
|
+
text: { type: "string", description: "Text to scan for credentials (DLP mode). When provided, scans text instead of files. Use this to validate user input before agent processing." },
|
|
427
|
+
agent: { type: "string", description: "Agent name for DLP audit trail (e.g., 'claude', 'cursor'). Only used in DLP mode." },
|
|
426
428
|
},
|
|
427
|
-
required: ["cwd"
|
|
429
|
+
required: ["cwd"],
|
|
428
430
|
},
|
|
429
431
|
annotations: {
|
|
430
|
-
title: "Fast Hook Check",
|
|
432
|
+
title: "Fast Hook Check + DLP",
|
|
431
433
|
readOnlyHint: true,
|
|
432
434
|
destructiveHint: false,
|
|
433
435
|
idempotentHint: true,
|
|
@@ -436,7 +438,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
436
438
|
},
|
|
437
439
|
{
|
|
438
440
|
name: "rigour_hooks_init",
|
|
439
|
-
description: "Generate hook configs for AI coding tools (Claude, Cursor, Cline, Windsurf). Installs real-time quality checks
|
|
441
|
+
description: "Generate hook configs for AI coding tools (Claude, Cursor, Cline, Windsurf). Installs real-time quality checks AND DLP (credential interception) hooks by default. DLP hooks intercept credentials in user input BEFORE they reach the AI agent. Pass dlp=false to disable DLP hooks only.",
|
|
440
442
|
inputSchema: {
|
|
441
443
|
type: "object",
|
|
442
444
|
properties: {
|
|
@@ -444,11 +446,12 @@ export const TOOL_DEFINITIONS = [
|
|
|
444
446
|
tool: { type: "string", description: "Target tool: 'claude', 'cursor', 'cline', or 'windsurf'." },
|
|
445
447
|
force: { type: "boolean", description: "Overwrite existing hook files (default: false)." },
|
|
446
448
|
dryRun: { type: "boolean", description: "Preview changes without writing files (default: false)." },
|
|
449
|
+
dlp: { type: "boolean", description: "Generate DLP (Data Loss Prevention) hooks for pre-input credential interception (default: true). Set false to skip DLP hooks." },
|
|
447
450
|
},
|
|
448
451
|
required: ["cwd", "tool"],
|
|
449
452
|
},
|
|
450
453
|
annotations: {
|
|
451
|
-
title: "Install IDE Hooks",
|
|
454
|
+
title: "Install IDE Hooks + DLP",
|
|
452
455
|
readOnlyHint: false,
|
|
453
456
|
destructiveHint: false,
|
|
454
457
|
idempotentHint: true,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DLP Tool Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handlers for: rigour_input_scan, rigour_dlp_init
|
|
5
|
+
*
|
|
6
|
+
* AI Agent Data Loss Prevention — scans user input for credentials
|
|
7
|
+
* before they reach AI agents.
|
|
8
|
+
*
|
|
9
|
+
* @since v4.2.0 — AI Agent DLP layer
|
|
10
|
+
*/
|
|
11
|
+
type ToolResult = {
|
|
12
|
+
content: {
|
|
13
|
+
type: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}[];
|
|
16
|
+
isError?: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* rigour_input_scan — Scan text for credentials before agent processing.
|
|
20
|
+
*
|
|
21
|
+
* This is the core DLP tool. Agents can call this to validate user input
|
|
22
|
+
* contains no secrets before processing it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function handleInputScan(cwd: string, text: string, agent?: string, block?: boolean): Promise<ToolResult>;
|
|
25
|
+
/**
|
|
26
|
+
* rigour_dlp_init — Generate DLP hook configs for AI coding tools.
|
|
27
|
+
*
|
|
28
|
+
* Creates pre-input hooks that intercept credentials before they
|
|
29
|
+
* reach the AI agent.
|
|
30
|
+
*/
|
|
31
|
+
export declare function handleDLPInit(cwd: string, tool: string, force?: boolean, dryRun?: boolean): Promise<ToolResult>;
|
|
32
|
+
/**
|
|
33
|
+
* rigour_dlp_audit — Query the DLP audit log for recent credential interceptions.
|
|
34
|
+
*/
|
|
35
|
+
export declare function handleDLPAudit(cwd: string, limit?: number): Promise<ToolResult>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DLP Tool Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handlers for: rigour_input_scan, rigour_dlp_init
|
|
5
|
+
*
|
|
6
|
+
* AI Agent Data Loss Prevention — scans user input for credentials
|
|
7
|
+
* before they reach AI agents.
|
|
8
|
+
*
|
|
9
|
+
* @since v4.2.0 — AI Agent DLP layer
|
|
10
|
+
*/
|
|
11
|
+
import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, generateDLPHookFiles } from '@rigour-labs/core';
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
/**
|
|
15
|
+
* rigour_input_scan — Scan text for credentials before agent processing.
|
|
16
|
+
*
|
|
17
|
+
* This is the core DLP tool. Agents can call this to validate user input
|
|
18
|
+
* contains no secrets before processing it.
|
|
19
|
+
*/
|
|
20
|
+
export async function handleInputScan(cwd, text, agent, block) {
|
|
21
|
+
if (!text || text.trim().length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: '✓ No input to scan.',
|
|
26
|
+
}],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const result = scanInputForCredentials(text, {
|
|
30
|
+
enabled: true,
|
|
31
|
+
block_on_detection: block ?? true,
|
|
32
|
+
audit_log: true,
|
|
33
|
+
});
|
|
34
|
+
// Log to audit trail
|
|
35
|
+
if (result.status !== 'clean') {
|
|
36
|
+
try {
|
|
37
|
+
const rigourDir = path.join(cwd, '.rigour');
|
|
38
|
+
await fs.ensureDir(rigourDir);
|
|
39
|
+
const eventsPath = path.join(rigourDir, 'events.jsonl');
|
|
40
|
+
const auditEntry = createDLPAuditEntry(result, {
|
|
41
|
+
agent: agent ?? 'mcp',
|
|
42
|
+
});
|
|
43
|
+
await fs.appendFile(eventsPath, JSON.stringify(auditEntry) + '\n');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Silent
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (result.status === 'clean') {
|
|
50
|
+
return {
|
|
51
|
+
content: [{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: `✓ CLEAN — No credentials detected in input.\nScanned: ${result.scanned_length} chars | Duration: ${result.duration_ms}ms`,
|
|
54
|
+
}],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const alert = formatDLPAlert(result);
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: alert,
|
|
62
|
+
}],
|
|
63
|
+
isError: result.status === 'blocked',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* rigour_dlp_init — Generate DLP hook configs for AI coding tools.
|
|
68
|
+
*
|
|
69
|
+
* Creates pre-input hooks that intercept credentials before they
|
|
70
|
+
* reach the AI agent.
|
|
71
|
+
*/
|
|
72
|
+
export async function handleDLPInit(cwd, tool, force = false, dryRun = false) {
|
|
73
|
+
try {
|
|
74
|
+
const hookTool = tool;
|
|
75
|
+
const checkerCommand = 'rigour hooks check';
|
|
76
|
+
const files = generateDLPHookFiles(hookTool, checkerCommand);
|
|
77
|
+
if (dryRun) {
|
|
78
|
+
const preview = files.map(f => `${f.path}:\n${f.content.slice(0, 200)}...`).join('\n\n');
|
|
79
|
+
return {
|
|
80
|
+
content: [{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: `[DRY RUN] Would generate ${files.length} DLP hook file(s) for '${tool}':\n\n${preview}`,
|
|
83
|
+
}],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const written = [];
|
|
87
|
+
const skipped = [];
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const fullPath = path.join(cwd, file.path);
|
|
90
|
+
if (!force && await fs.pathExists(fullPath)) {
|
|
91
|
+
skipped.push(file.path);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
await fs.ensureDir(path.dirname(fullPath));
|
|
95
|
+
await fs.writeFile(fullPath, file.content);
|
|
96
|
+
if (file.executable) {
|
|
97
|
+
await fs.chmod(fullPath, 0o755);
|
|
98
|
+
}
|
|
99
|
+
written.push(file.path);
|
|
100
|
+
}
|
|
101
|
+
const parts = [];
|
|
102
|
+
if (written.length > 0)
|
|
103
|
+
parts.push(`✓ Created: ${written.join(', ')}`);
|
|
104
|
+
if (skipped.length > 0)
|
|
105
|
+
parts.push(`⊘ Skipped (exists): ${skipped.join(', ')}. Use force=true to overwrite.`);
|
|
106
|
+
parts.push(`Tool: ${tool}`);
|
|
107
|
+
parts.push('DLP Protection: AWS keys, API tokens, database URLs, private keys, JWTs, passwords');
|
|
108
|
+
parts.push('');
|
|
109
|
+
parts.push('🛑 Credentials will now be intercepted BEFORE reaching the AI agent.');
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: 'text', text: parts.join('\n') }],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
content: [{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: `DLP hook init failed: ${error.message}`,
|
|
119
|
+
}],
|
|
120
|
+
isError: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* rigour_dlp_audit — Query the DLP audit log for recent credential interceptions.
|
|
126
|
+
*/
|
|
127
|
+
export async function handleDLPAudit(cwd, limit = 20) {
|
|
128
|
+
try {
|
|
129
|
+
const eventsPath = path.join(cwd, '.rigour', 'events.jsonl');
|
|
130
|
+
if (!await fs.pathExists(eventsPath)) {
|
|
131
|
+
return {
|
|
132
|
+
content: [{
|
|
133
|
+
type: 'text',
|
|
134
|
+
text: 'No DLP events found. The audit trail is empty.',
|
|
135
|
+
}],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const content = await fs.readFile(eventsPath, 'utf-8');
|
|
139
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
140
|
+
// Filter for DLP events only
|
|
141
|
+
const dlpEvents = lines
|
|
142
|
+
.map(line => {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(line);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
.filter(event => event?.type === 'dlp_event')
|
|
151
|
+
.slice(-limit);
|
|
152
|
+
if (dlpEvents.length === 0) {
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: 'No DLP interceptions found in the audit trail.',
|
|
157
|
+
}],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const summary = dlpEvents.map((event) => {
|
|
161
|
+
const types = (event.detections || []).map((d) => d.type).join(', ');
|
|
162
|
+
const severities = (event.detections || []).map((d) => d.severity).join(', ');
|
|
163
|
+
return `[${event.timestamp}] ${event.status.toUpperCase()} | Agent: ${event.agent} | Types: ${types} | Severity: ${severities}`;
|
|
164
|
+
}).join('\n');
|
|
165
|
+
const totalBlocked = dlpEvents.filter((e) => e.status === 'blocked').length;
|
|
166
|
+
const totalWarnings = dlpEvents.filter((e) => e.status === 'warning').length;
|
|
167
|
+
return {
|
|
168
|
+
content: [{
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: `DLP Audit Trail (last ${dlpEvents.length} events)\n\nBlocked: ${totalBlocked} | Warnings: ${totalWarnings}\n\n${summary}`,
|
|
171
|
+
}],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
content: [{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: `DLP audit query failed: ${error.message}`,
|
|
179
|
+
}],
|
|
180
|
+
isError: true,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -9,10 +9,16 @@ type ToolResult = {
|
|
|
9
9
|
* rigour_hooks_check — Run the fast hook checker on specific files.
|
|
10
10
|
* This is the same check that runs inside IDE hooks (Claude, Cursor, Cline, Windsurf).
|
|
11
11
|
* Catches: hardcoded secrets, hallucinated imports, command injection, file size.
|
|
12
|
+
*
|
|
13
|
+
* NEW in v4.2.0: When `text` param is provided, runs in DLP mode —
|
|
14
|
+
* scans text for credentials instead of checking files.
|
|
12
15
|
*/
|
|
13
|
-
export declare function handleHooksCheck(cwd: string, files: string[], timeout?: number): Promise<ToolResult>;
|
|
16
|
+
export declare function handleHooksCheck(cwd: string, files: string[], timeout?: number, text?: string, agent?: string): Promise<ToolResult>;
|
|
14
17
|
/**
|
|
15
18
|
* rigour_hooks_init — Generate hook configs for AI coding tools.
|
|
19
|
+
*
|
|
20
|
+
* NEW in v4.2.0: When `dlp` param is true, also generates pre-input
|
|
21
|
+
* DLP hooks that intercept credentials before agent processing.
|
|
16
22
|
*/
|
|
17
|
-
export declare function handleHooksInit(cwd: string, tool: string, force?: boolean, dryRun?: boolean): Promise<ToolResult>;
|
|
23
|
+
export declare function handleHooksInit(cwd: string, tool: string, force?: boolean, dryRun?: boolean, dlp?: boolean): Promise<ToolResult>;
|
|
18
24
|
export {};
|
|
@@ -2,18 +2,63 @@
|
|
|
2
2
|
* Hooks Tool Handlers
|
|
3
3
|
*
|
|
4
4
|
* Handlers for: rigour_hooks_check, rigour_hooks_init
|
|
5
|
+
* Now includes DLP (Data Loss Prevention) capabilities.
|
|
5
6
|
*
|
|
6
7
|
* @since v3.0.0 — real-time hooks for AI coding tools
|
|
8
|
+
* @since v4.2.0 — AI Agent DLP layer (credential interception)
|
|
7
9
|
*/
|
|
8
10
|
import { runHookChecker, generateHookFiles } from "@rigour-labs/core";
|
|
11
|
+
import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, generateDLPHookFiles } from "@rigour-labs/core";
|
|
9
12
|
import fs from "fs-extra";
|
|
10
13
|
import path from "path";
|
|
11
14
|
/**
|
|
12
15
|
* rigour_hooks_check — Run the fast hook checker on specific files.
|
|
13
16
|
* This is the same check that runs inside IDE hooks (Claude, Cursor, Cline, Windsurf).
|
|
14
17
|
* Catches: hardcoded secrets, hallucinated imports, command injection, file size.
|
|
18
|
+
*
|
|
19
|
+
* NEW in v4.2.0: When `text` param is provided, runs in DLP mode —
|
|
20
|
+
* scans text for credentials instead of checking files.
|
|
15
21
|
*/
|
|
16
|
-
export async function handleHooksCheck(cwd, files, timeout) {
|
|
22
|
+
export async function handleHooksCheck(cwd, files, timeout, text, agent) {
|
|
23
|
+
// ── DLP Mode: Scan text for credentials ──────────────────
|
|
24
|
+
if (text) {
|
|
25
|
+
const result = scanInputForCredentials(text, {
|
|
26
|
+
enabled: true,
|
|
27
|
+
block_on_detection: true,
|
|
28
|
+
audit_log: true,
|
|
29
|
+
});
|
|
30
|
+
// Log to audit trail
|
|
31
|
+
if (result.status !== 'clean') {
|
|
32
|
+
try {
|
|
33
|
+
const rigourDir = path.join(cwd, '.rigour');
|
|
34
|
+
await fs.ensureDir(rigourDir);
|
|
35
|
+
const eventsPath = path.join(rigourDir, 'events.jsonl');
|
|
36
|
+
const auditEntry = createDLPAuditEntry(result, {
|
|
37
|
+
agent: agent ?? 'mcp',
|
|
38
|
+
});
|
|
39
|
+
await fs.appendFile(eventsPath, JSON.stringify(auditEntry) + '\n');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Silent
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (result.status === 'clean') {
|
|
46
|
+
return {
|
|
47
|
+
content: [{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `✓ CLEAN — No credentials detected in input.\nScanned: ${result.scanned_length} chars | Duration: ${result.duration_ms}ms`,
|
|
50
|
+
}],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
content: [{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: formatDLPAlert(result),
|
|
57
|
+
}],
|
|
58
|
+
isError: result.status === 'blocked',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// ── Standard Mode: Check files ───────────────────────────
|
|
17
62
|
const input = { cwd, files };
|
|
18
63
|
if (timeout)
|
|
19
64
|
input.timeout_ms = timeout;
|
|
@@ -36,24 +81,31 @@ export async function handleHooksCheck(cwd, files, timeout) {
|
|
|
36
81
|
}
|
|
37
82
|
/**
|
|
38
83
|
* rigour_hooks_init — Generate hook configs for AI coding tools.
|
|
84
|
+
*
|
|
85
|
+
* NEW in v4.2.0: When `dlp` param is true, also generates pre-input
|
|
86
|
+
* DLP hooks that intercept credentials before agent processing.
|
|
39
87
|
*/
|
|
40
|
-
export async function handleHooksInit(cwd, tool, force = false, dryRun = false) {
|
|
88
|
+
export async function handleHooksInit(cwd, tool, force = false, dryRun = false, dlp = true) {
|
|
41
89
|
try {
|
|
42
90
|
const hookTool = tool;
|
|
43
91
|
const checkerCommand = 'rigour hooks check';
|
|
92
|
+
// Generate post-output hooks (existing)
|
|
44
93
|
const files = generateHookFiles(hookTool, checkerCommand);
|
|
94
|
+
// Generate DLP pre-input hooks (new)
|
|
95
|
+
const dlpFiles = dlp ? generateDLPHookFiles(hookTool, checkerCommand) : [];
|
|
96
|
+
const allFiles = [...files, ...dlpFiles];
|
|
45
97
|
if (dryRun) {
|
|
46
|
-
const preview =
|
|
98
|
+
const preview = allFiles.map(f => `${f.path}:\n${f.content.slice(0, 300)}...`).join('\n\n');
|
|
47
99
|
return {
|
|
48
100
|
content: [{
|
|
49
101
|
type: "text",
|
|
50
|
-
text: `[DRY RUN] Would generate ${
|
|
102
|
+
text: `[DRY RUN] Would generate ${allFiles.length} hook file(s) for '${tool}'${dlp ? ' (+ DLP)' : ''}:\n\n${preview}`,
|
|
51
103
|
}],
|
|
52
104
|
};
|
|
53
105
|
}
|
|
54
106
|
const written = [];
|
|
55
107
|
const skipped = [];
|
|
56
|
-
for (const file of
|
|
108
|
+
for (const file of allFiles) {
|
|
57
109
|
const fullPath = path.join(cwd, file.path);
|
|
58
110
|
if (!force && await fs.pathExists(fullPath)) {
|
|
59
111
|
skipped.push(file.path);
|
|
@@ -73,6 +125,11 @@ export async function handleHooksInit(cwd, tool, force = false, dryRun = false)
|
|
|
73
125
|
parts.push(`⊘ Skipped (exists): ${skipped.join(', ')}. Use force=true to overwrite.`);
|
|
74
126
|
parts.push(`Tool: ${tool}`);
|
|
75
127
|
parts.push('Checks: file-size, security-patterns, hallucinated-imports, command-injection');
|
|
128
|
+
if (dlp) {
|
|
129
|
+
parts.push('');
|
|
130
|
+
parts.push('🛑 DLP Protection: AWS keys, API tokens, database URLs, private keys, JWTs, passwords');
|
|
131
|
+
parts.push('Credentials will be intercepted BEFORE reaching the AI agent.');
|
|
132
|
+
}
|
|
76
133
|
return {
|
|
77
134
|
content: [{ type: "text", text: parts.join('\n') }],
|
|
78
135
|
};
|
|
@@ -3,13 +3,85 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handlers for: rigour_remember, rigour_recall, rigour_forget
|
|
5
5
|
*
|
|
6
|
+
* DLP enforcement: rigour_remember scans BOTH key and value for
|
|
7
|
+
* credentials before persisting. Blocked if secrets detected.
|
|
8
|
+
*
|
|
6
9
|
* @since v2.17.0 — extracted from monolithic index.ts
|
|
10
|
+
* @since v4.2.0 — DLP gate on memory persistence
|
|
7
11
|
*/
|
|
8
12
|
import { loadMemory, saveMemory } from '../utils/config.js';
|
|
13
|
+
import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from '@rigour-labs/core';
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
/**
|
|
17
|
+
* Append a DLP audit event to .rigour/events.jsonl
|
|
18
|
+
*/
|
|
19
|
+
async function appendDLPAudit(cwd, entry) {
|
|
20
|
+
try {
|
|
21
|
+
const eventsPath = path.join(cwd, '.rigour', 'events.jsonl');
|
|
22
|
+
await fs.ensureDir(path.dirname(eventsPath));
|
|
23
|
+
await fs.appendFile(eventsPath, JSON.stringify(entry) + '\n');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Audit logging is best-effort, never block on failure
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Deep-scan a value: if JSON, extract all nested string values and scan each.
|
|
31
|
+
* Catches agents serializing credentials as JSON to bypass flat-text scanning.
|
|
32
|
+
*/
|
|
33
|
+
function deepScanValue(key, value) {
|
|
34
|
+
const texts = [key, value];
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(value);
|
|
37
|
+
extractStrings(parsed, texts);
|
|
38
|
+
}
|
|
39
|
+
catch { /* not JSON — flat scan is sufficient */ }
|
|
40
|
+
return texts.join('\n');
|
|
41
|
+
}
|
|
42
|
+
function extractStrings(obj, out) {
|
|
43
|
+
if (typeof obj === 'string' && obj.length >= 8)
|
|
44
|
+
out.push(obj);
|
|
45
|
+
else if (Array.isArray(obj))
|
|
46
|
+
obj.forEach(v => extractStrings(v, out));
|
|
47
|
+
else if (obj && typeof obj === 'object') {
|
|
48
|
+
for (const v of Object.values(obj))
|
|
49
|
+
extractStrings(v, out);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
9
52
|
export async function handleRemember(cwd, key, value) {
|
|
53
|
+
// ── DLP Gate: deep-scan key + value (including JSON interiors) ──
|
|
54
|
+
const textToScan = deepScanValue(key, value);
|
|
55
|
+
const dlpResult = scanInputForCredentials(textToScan);
|
|
56
|
+
if (dlpResult.status === 'blocked') {
|
|
57
|
+
// Log the blocked attempt
|
|
58
|
+
const auditEntry = createDLPAuditEntry(dlpResult, {
|
|
59
|
+
agent: 'rigour_remember',
|
|
60
|
+
userId: key,
|
|
61
|
+
});
|
|
62
|
+
await appendDLPAudit(cwd, { ...auditEntry, memory_key: key });
|
|
63
|
+
const alert = formatDLPAlert(dlpResult);
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: `🛑 MEMORY BLOCKED — credentials detected in value for key "${key}".\n\n${alert}\n\nRigour prevented storing secrets in persistent memory. Use environment variables instead.`,
|
|
68
|
+
}],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Clean — proceed with storage
|
|
10
72
|
const store = await loadMemory(cwd);
|
|
11
73
|
store.memories[key] = { value, timestamp: new Date().toISOString() };
|
|
12
74
|
await saveMemory(cwd, store);
|
|
75
|
+
// If there were warnings (non-blocking), include them
|
|
76
|
+
if (dlpResult.status === 'warning') {
|
|
77
|
+
const alert = formatDLPAlert(dlpResult);
|
|
78
|
+
return {
|
|
79
|
+
content: [{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: `MEMORY STORED: "${key}" has been saved (with warnings).\n\n${alert}\n\nStored value: ${value}`,
|
|
82
|
+
}],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
13
85
|
return {
|
|
14
86
|
content: [{
|
|
15
87
|
type: "text",
|
|
@@ -24,22 +96,55 @@ export async function handleRecall(cwd, key) {
|
|
|
24
96
|
if (!memory) {
|
|
25
97
|
return { content: [{ type: "text", text: `NO MEMORY FOUND for key "${key}". Use rigour_remember to store instructions.` }] };
|
|
26
98
|
}
|
|
99
|
+
// ── DLP Gate on recall: catch credentials stored before DLP existed ──
|
|
100
|
+
const dlpResult = scanInputForCredentials(memory.value);
|
|
101
|
+
if (dlpResult.status === 'blocked') {
|
|
102
|
+
const alert = formatDLPAlert(dlpResult);
|
|
103
|
+
await appendDLPAudit(cwd, {
|
|
104
|
+
...createDLPAuditEntry(dlpResult, { agent: 'rigour_recall' }),
|
|
105
|
+
memory_key: key,
|
|
106
|
+
action: 'recall_blocked',
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
content: [{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `🛑 RECALL BLOCKED — memory "${key}" contains ${dlpResult.detections.length} credential(s).\n\n${alert}\n\nThis memory was stored before DLP was active. Remove it with rigour_forget("${key}") and use environment variables instead.`,
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
27
115
|
return { content: [{ type: "text", text: `RECALLED MEMORY [${key}]:\n${memory.value}\n\n(Stored: ${memory.timestamp})` }] };
|
|
28
116
|
}
|
|
29
117
|
const keys = Object.keys(store.memories);
|
|
30
118
|
if (keys.length === 0) {
|
|
31
119
|
return { content: [{ type: "text", text: "NO MEMORIES STORED. Use rigour_remember to persist important instructions." }] };
|
|
32
120
|
}
|
|
33
|
-
|
|
121
|
+
// ── DLP scan all memories on bulk recall ──
|
|
122
|
+
const cleanMemories = [];
|
|
123
|
+
const taintedKeys = [];
|
|
124
|
+
for (const k of keys) {
|
|
34
125
|
const mem = store.memories[k];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
126
|
+
const dlpResult = scanInputForCredentials(mem.value);
|
|
127
|
+
if (dlpResult.status === 'blocked') {
|
|
128
|
+
taintedKeys.push(k);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
cleanMemories.push(`## ${k}\n${mem.value}\n(Stored: ${mem.timestamp})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
let text = '';
|
|
135
|
+
if (taintedKeys.length > 0) {
|
|
136
|
+
text += `🛑 ${taintedKeys.length} memory(ies) BLOCKED — contain credentials: ${taintedKeys.join(', ')}\nUse rigour_forget to remove them.\n\n---\n\n`;
|
|
137
|
+
}
|
|
138
|
+
if (cleanMemories.length > 0) {
|
|
139
|
+
text += `RECALLED ${cleanMemories.length} CLEAN MEMORIES:\n\n${cleanMemories.join('\n\n---\n\n')}\n\n---\nIMPORTANT: Follow these stored instructions throughout this session.`;
|
|
140
|
+
}
|
|
141
|
+
else if (taintedKeys.length > 0) {
|
|
142
|
+
text += `No clean memories to recall. All stored memories contain credentials.`;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
text = "NO MEMORIES STORED. Use rigour_remember to persist important instructions.";
|
|
146
|
+
}
|
|
147
|
+
return { content: [{ type: "text", text }] };
|
|
43
148
|
}
|
|
44
149
|
export async function handleForget(cwd, key) {
|
|
45
150
|
const store = await loadMemory(cwd);
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -178,6 +178,23 @@ export declare function loadConfig(cwd: string): Promise<{
|
|
|
178
178
|
check_assertion_free_async: boolean;
|
|
179
179
|
max_mocks_per_test: number;
|
|
180
180
|
};
|
|
181
|
+
governance: {
|
|
182
|
+
enabled: boolean;
|
|
183
|
+
enforce_memory: boolean;
|
|
184
|
+
enforce_skills: boolean;
|
|
185
|
+
block_native_memory: boolean;
|
|
186
|
+
protected_memory_paths: string[];
|
|
187
|
+
protected_skills_paths: string[];
|
|
188
|
+
exempt_paths: string[];
|
|
189
|
+
};
|
|
190
|
+
input_validation: {
|
|
191
|
+
enabled: boolean;
|
|
192
|
+
ignore_patterns: string[];
|
|
193
|
+
block_on_detection: boolean;
|
|
194
|
+
min_secret_length: number;
|
|
195
|
+
custom_patterns: string[];
|
|
196
|
+
audit_log: boolean;
|
|
197
|
+
};
|
|
181
198
|
};
|
|
182
199
|
hooks: {
|
|
183
200
|
enabled: boolean;
|
|
@@ -185,6 +202,7 @@ export declare function loadConfig(cwd: string): Promise<{
|
|
|
185
202
|
timeout_ms: number;
|
|
186
203
|
fast_gates: string[];
|
|
187
204
|
block_on_failure: boolean;
|
|
205
|
+
dlp: boolean;
|
|
188
206
|
};
|
|
189
207
|
output: {
|
|
190
208
|
report_path: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "MCP server for AI code governance — OWASP LLM Top 10 (10/10), real-time hooks, 25+ security patterns, hallucinated import detection, multi-agent governance. Works with Claude, Cursor, Cline, Windsurf, Gemini. Industry presets for HIPAA, SOC2, FedRAMP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"execa": "^8.0.1",
|
|
49
49
|
"fs-extra": "^11.2.0",
|
|
50
50
|
"yaml": "^2.8.2",
|
|
51
|
-
"@rigour-labs/core": "4.
|
|
51
|
+
"@rigour-labs/core": "4.2.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^25.0.3",
|