@securityreviewai/securityreview-kit 0.1.46 → 0.1.48
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/package.json +1 -1
- package/src/generators/mcp/claude.js +12 -0
- package/src/generators/mcp/claude.test.js +5 -0
- package/src/generators/mcp/codex.js +1 -0
- package/src/generators/mcp/codex.test.js +1 -0
- package/src/generators/mcp/cursor.js +2 -0
- package/src/generators/mcp/cursor.test.js +50 -0
- package/src/generators/rules/cursor.js +6 -32
- package/src/utils/cursor-cli-permissions.js +28 -0
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { readJson, writeJson } from '../../utils/fs-helpers.js';
|
|
3
3
|
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
4
|
|
|
5
|
+
const CLAUDE_MCP_PERMISSION = `mcp__${MCP_SERVER_NAME}`;
|
|
6
|
+
|
|
5
7
|
function getClaudeSessionStartHooks() {
|
|
6
8
|
const prompt = [
|
|
7
9
|
'MANDATORY SECURITY GATE (Claude Code Session Policy)',
|
|
@@ -59,6 +61,16 @@ export function generate(cwd, envVars) {
|
|
|
59
61
|
existing.enabledMcpjsonServers = [...enabledServers, MCP_SERVER_NAME];
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
if (!existing.permissions || typeof existing.permissions !== 'object' || Array.isArray(existing.permissions)) {
|
|
65
|
+
existing.permissions = {};
|
|
66
|
+
}
|
|
67
|
+
const allowedTools = Array.isArray(existing.permissions.allow)
|
|
68
|
+
? existing.permissions.allow.filter((entry) => typeof entry === 'string' && entry.trim())
|
|
69
|
+
: [];
|
|
70
|
+
if (!allowedTools.includes(CLAUDE_MCP_PERMISSION)) {
|
|
71
|
+
existing.permissions.allow = [...allowedTools, CLAUDE_MCP_PERMISSION];
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
const existingSessionStart = Array.isArray(existing.SessionStart) ? existing.SessionStart : [];
|
|
63
75
|
const marker = 'MANDATORY SECURITY GATE (Claude Code Session Policy)';
|
|
64
76
|
const ours = getClaudeSessionStartHooks();
|
|
@@ -17,6 +17,7 @@ test('Claude MCP generator writes .mcp.json, enables the project MCP server, and
|
|
|
17
17
|
const settings = JSON.parse(readFileSync(join(cwd, '.claude', 'settings.json'), 'utf8'));
|
|
18
18
|
assert.equal(mcpConfig.mcpServers['security-review-mcp'].command, 'npx');
|
|
19
19
|
assert.deepEqual(settings.enabledMcpjsonServers, ['security-review-mcp']);
|
|
20
|
+
assert.deepEqual(settings.permissions.allow, ['mcp__security-review-mcp']);
|
|
20
21
|
assert.equal(Array.isArray(settings.SessionStart), true);
|
|
21
22
|
assert.match(settings.SessionStart[0].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
22
23
|
assert.match(settings.SessionStart[0].hooks[0].prompt, /vibereview\//);
|
|
@@ -39,6 +40,9 @@ test('Claude MCP generator preserves existing SessionStart hooks', () => {
|
|
|
39
40
|
hooks: [{ type: 'prompt', prompt: 'Existing hook' }],
|
|
40
41
|
},
|
|
41
42
|
],
|
|
43
|
+
permissions: {
|
|
44
|
+
allow: ['Bash(npm run test:*)'],
|
|
45
|
+
},
|
|
42
46
|
},
|
|
43
47
|
null,
|
|
44
48
|
2,
|
|
@@ -56,4 +60,5 @@ test('Claude MCP generator preserves existing SessionStart hooks', () => {
|
|
|
56
60
|
assert.match(config.SessionStart[0].hooks[0].prompt, /Existing hook/);
|
|
57
61
|
assert.match(config.SessionStart[1].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
58
62
|
assert.deepEqual(config.enabledMcpjsonServers, ['security-review-mcp']);
|
|
63
|
+
assert.deepEqual(config.permissions.allow, ['Bash(npm run test:*)', 'mcp__security-review-mcp']);
|
|
59
64
|
});
|
|
@@ -38,6 +38,7 @@ export function generate(cwd, envVars) {
|
|
|
38
38
|
[mcp_servers.${MCP_SERVER_NAME}]
|
|
39
39
|
command = "npx"
|
|
40
40
|
args = ["-y", "${MCP_SERVER_PACKAGE}@latest"]
|
|
41
|
+
default_tools_approval_mode = "approve"
|
|
41
42
|
|
|
42
43
|
[mcp_servers.${MCP_SERVER_NAME}.env]
|
|
43
44
|
SECURITY_REVIEW_API_URL = "${envVars.apiUrl}"
|
|
@@ -16,6 +16,7 @@ test('Codex MCP generator enables hooks and writes MCP server block', () => {
|
|
|
16
16
|
const config = readFileSync(join(cwd, '.codex/config.toml'), 'utf8');
|
|
17
17
|
assert.match(config, /\[features\]\ncodex_hooks = true/);
|
|
18
18
|
assert.match(config, /\[mcp_servers\.security-review-mcp\]/);
|
|
19
|
+
assert.match(config, /default_tools_approval_mode = "approve"/);
|
|
19
20
|
assert.match(config, /SECURITY_REVIEW_API_URL = "https:\/\/example\.test"/);
|
|
20
21
|
});
|
|
21
22
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { readJson, writeJson } from '../../utils/fs-helpers.js';
|
|
3
3
|
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
|
+
import { mergeCursorCliMcpAllowlist } from '../../utils/cursor-cli-permissions.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Generate Cursor MCP config at .cursor/mcp.json
|
|
@@ -23,5 +24,6 @@ export function generate(cwd, envVars) {
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
writeJson(filePath, existing);
|
|
27
|
+
mergeCursorCliMcpAllowlist(cwd);
|
|
26
28
|
return filePath;
|
|
27
29
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { generate } from './cursor.js';
|
|
7
|
+
|
|
8
|
+
test('Cursor MCP generator writes server config and CLI MCP allow rule', () => {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-cursor-mcp-'));
|
|
10
|
+
|
|
11
|
+
generate(cwd, {
|
|
12
|
+
apiUrl: 'https://example.test',
|
|
13
|
+
apiToken: 'secret-token',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const mcpConfig = JSON.parse(readFileSync(join(cwd, '.cursor', 'mcp.json'), 'utf8'));
|
|
17
|
+
const cliConfig = JSON.parse(readFileSync(join(cwd, '.cursor', 'cli.json'), 'utf8'));
|
|
18
|
+
assert.equal(mcpConfig.mcpServers['security-review-mcp'].command, 'npx');
|
|
19
|
+
assert.deepEqual(cliConfig.permissions.allow, ['Mcp(security-review-mcp:*)']);
|
|
20
|
+
assert.deepEqual(cliConfig.permissions.deny, []);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('Cursor MCP generator preserves existing CLI permissions', () => {
|
|
24
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-cursor-mcp-merge-'));
|
|
25
|
+
const cliPath = join(cwd, '.cursor', 'cli.json');
|
|
26
|
+
mkdirSync(join(cwd, '.cursor'), { recursive: true });
|
|
27
|
+
writeFileSync(
|
|
28
|
+
cliPath,
|
|
29
|
+
JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
permissions: {
|
|
32
|
+
allow: ['Shell(git)'],
|
|
33
|
+
deny: ['Shell(rm)'],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
null,
|
|
37
|
+
2,
|
|
38
|
+
),
|
|
39
|
+
'utf8',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
generate(cwd, {
|
|
43
|
+
apiUrl: 'https://example.test',
|
|
44
|
+
apiToken: 'secret-token',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const cliConfig = JSON.parse(readFileSync(cliPath, 'utf8'));
|
|
48
|
+
assert.deepEqual(cliConfig.permissions.allow, ['Shell(git)', 'Mcp(security-review-mcp:*)']);
|
|
49
|
+
assert.deepEqual(cliConfig.permissions.deny, ['Shell(rm)']);
|
|
50
|
+
});
|
|
@@ -3,11 +3,11 @@ import { join } from 'node:path';
|
|
|
3
3
|
import {
|
|
4
4
|
GUARDRAILS_PROFILER_SKILL_REL_DIR,
|
|
5
5
|
GUARDRAILS_SELECTION_SKILL_REL_DIR,
|
|
6
|
-
MCP_SERVER_NAME,
|
|
7
6
|
THREAT_MODELLING_SKILL_REL_DIR,
|
|
8
7
|
VIBEREVIEW_SYNC_SKILL_REL_DIR,
|
|
9
8
|
} from '../../utils/constants.js';
|
|
10
|
-
import {
|
|
9
|
+
import { writeText } from '../../utils/fs-helpers.js';
|
|
10
|
+
import { mergeCursorCliMcpAllowlist } from '../../utils/cursor-cli-permissions.js';
|
|
11
11
|
import {
|
|
12
12
|
getRuleContent,
|
|
13
13
|
getProfileCommandContent,
|
|
@@ -46,34 +46,6 @@ function removeGeneratedText(filePath) {
|
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
* Merge `.cursor/cli.json` so Cursor CLI / Agent auto-allows security-review-mcp tools (no MCP approval prompts).
|
|
51
|
-
* @see https://cursor.com/docs/cli/reference/permissions
|
|
52
|
-
*/
|
|
53
|
-
function mergeCursorCliMcpAllowlist(cwd) {
|
|
54
|
-
const cliPath = join(cwd, '.cursor', 'cli.json');
|
|
55
|
-
const existed = existsSync(cliPath);
|
|
56
|
-
const existing = readJson(cliPath) || {};
|
|
57
|
-
if (!existing.permissions || typeof existing.permissions !== 'object') {
|
|
58
|
-
existing.permissions = {};
|
|
59
|
-
}
|
|
60
|
-
if (!Array.isArray(existing.permissions.allow)) {
|
|
61
|
-
existing.permissions.allow = [];
|
|
62
|
-
}
|
|
63
|
-
const token = `Mcp(${MCP_SERVER_NAME}:*)`;
|
|
64
|
-
if (!existing.permissions.allow.includes(token)) {
|
|
65
|
-
existing.permissions.allow.push(token);
|
|
66
|
-
}
|
|
67
|
-
// Cursor CLI schema requires permissions.deny to be a string[] (may be empty).
|
|
68
|
-
// @see https://cursor.com/docs/cli/reference/configuration
|
|
69
|
-
if (!Array.isArray(existing.permissions.deny)) {
|
|
70
|
-
existing.permissions.deny = [];
|
|
71
|
-
}
|
|
72
|
-
writeJson(cliPath, existing);
|
|
73
|
-
const action = existed ? 'updated' : 'created';
|
|
74
|
-
return { filePath: cliPath, action };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
49
|
/**
|
|
78
50
|
* Generate Cursor workspace rules at .cursor/rules/*.mdc
|
|
79
51
|
* Cursor uses .mdc format with YAML front matter.
|
|
@@ -135,7 +107,9 @@ export function generate(cwd, options = {}) {
|
|
|
135
107
|
const hooksAction = existsSync(hooksPath) ? 'updated' : 'created';
|
|
136
108
|
writeText(hooksPath, hooksContent);
|
|
137
109
|
|
|
138
|
-
const
|
|
110
|
+
const cursorCliPath = join(cwd, '.cursor', 'cli.json');
|
|
111
|
+
const cliPermissionsExisted = existsSync(cursorCliPath);
|
|
112
|
+
mergeCursorCliMcpAllowlist(cwd);
|
|
139
113
|
|
|
140
114
|
return [
|
|
141
115
|
{ ...baseRule, kind: 'rule' },
|
|
@@ -145,7 +119,7 @@ export function generate(cwd, options = {}) {
|
|
|
145
119
|
{ filePath: skillPath, action: skillAction, kind: 'skill' },
|
|
146
120
|
{ filePath: vibereviewSyncSkillPath, action: vibereviewSyncSkillAction, kind: 'skill' },
|
|
147
121
|
{ filePath: hooksPath, action: hooksAction, kind: 'hooks' },
|
|
148
|
-
{
|
|
122
|
+
{ filePath: cursorCliPath, action: cliPermissionsExisted ? 'updated' : 'created', kind: 'config' },
|
|
149
123
|
...(deletedLegacyRule ? [{ ...deletedLegacyRule, kind: 'cleanup' }] : []),
|
|
150
124
|
...(deletedLegacyCommand ? [{ ...deletedLegacyCommand, kind: 'cleanup' }] : []),
|
|
151
125
|
...(deletedLegacyAgent ? [{ ...deletedLegacyAgent, kind: 'cleanup' }] : []),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { MCP_SERVER_NAME } from './constants.js';
|
|
3
|
+
import { readJson, writeJson } from './fs-helpers.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merge project-level Cursor CLI permissions so cursor-agent can use the SRAI MCP tools.
|
|
7
|
+
* @see https://docs.cursor.com/cli/reference/configuration
|
|
8
|
+
* @see https://docs.cursor.com/cli/reference/permissions
|
|
9
|
+
*/
|
|
10
|
+
export function mergeCursorCliMcpAllowlist(cwd) {
|
|
11
|
+
const cliPath = join(cwd, '.cursor', 'cli.json');
|
|
12
|
+
const existing = readJson(cliPath) || {};
|
|
13
|
+
if (!existing.permissions || typeof existing.permissions !== 'object' || Array.isArray(existing.permissions)) {
|
|
14
|
+
existing.permissions = {};
|
|
15
|
+
}
|
|
16
|
+
if (!Array.isArray(existing.permissions.allow)) {
|
|
17
|
+
existing.permissions.allow = [];
|
|
18
|
+
}
|
|
19
|
+
const token = `Mcp(${MCP_SERVER_NAME}:*)`;
|
|
20
|
+
if (!existing.permissions.allow.includes(token)) {
|
|
21
|
+
existing.permissions.allow.push(token);
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(existing.permissions.deny)) {
|
|
24
|
+
existing.permissions.deny = [];
|
|
25
|
+
}
|
|
26
|
+
writeJson(cliPath, existing);
|
|
27
|
+
return cliPath;
|
|
28
|
+
}
|