@securityreviewai/securityreview-kit 0.1.50 → 0.1.52
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 +105 -0
- package/bin/securityreview-kit.js +5 -0
- package/package.json +30 -24
- package/src/cli.js +109 -0
- package/src/commands/init.js +851 -0
- package/src/commands/status.js +99 -0
- package/src/commands/switch-project.js +207 -0
- package/src/generators/mcp/claude.js +85 -0
- package/src/generators/mcp/claude.test.js +64 -0
- package/src/generators/mcp/codex.js +70 -0
- package/src/generators/mcp/codex.test.js +43 -0
- package/src/generators/mcp/cursor.js +29 -0
- package/src/generators/mcp/cursor.test.js +50 -0
- package/src/generators/mcp/gemini.js +28 -0
- package/src/generators/mcp/vscode.js +29 -0
- package/src/generators/mcp/windsurf.js +27 -0
- package/src/generators/rules/antigravity.js +22 -0
- package/src/generators/rules/claude.js +87 -0
- package/src/generators/rules/claude.test.js +60 -0
- package/src/generators/rules/codex.js +141 -0
- package/src/generators/rules/codex.test.js +59 -0
- package/src/generators/rules/content.js +110 -0
- package/src/generators/rules/cursor.js +128 -0
- package/src/generators/rules/gemini.js +13 -0
- package/src/generators/rules/guardrails-init-profile.md +56 -0
- package/src/generators/rules/guardrails-profiler/SKILL.md +130 -0
- package/src/generators/rules/guardrails-profiler/references/signal-registry.json +514 -0
- package/src/generators/rules/guardrails-selection/references/category-threat-map.md +232 -0
- package/src/generators/rules/guardrails_rule.md +94 -0
- package/src/generators/rules/hooks.json +11 -0
- package/src/generators/rules/srai-profile.md +32 -0
- package/src/generators/rules/vscode.js +101 -0
- package/src/generators/rules/vscode.test.js +54 -0
- package/src/generators/rules/windsurf.js +13 -0
- package/src/utils/constants.js +95 -0
- package/src/utils/cursor-agent-path.js +67 -0
- package/src/utils/cursor-cli-permissions.js +28 -0
- package/src/utils/detect.js +27 -0
- package/src/utils/fs-helpers.js +82 -0
- package/src/utils/guardrails-profiler-bundle.js +84 -0
- package/src/utils/ide-cli-install.js +138 -0
- package/src/utils/profiler-agent.js +446 -0
- package/src/utils/profiler-agent.test.js +81 -0
- package/src/utils/srai.js +252 -0
- package/dist/api.js +0 -44
- package/dist/commands/guardrails.js +0 -13
- package/dist/commands/init.js +0 -88
- package/dist/commands/profile.js +0 -14
- package/dist/commands/status.js +0 -27
- package/dist/commands/sync.js +0 -6
- package/dist/config.js +0 -18
- package/dist/fs.js +0 -43
- package/dist/index.js +0 -44
- package/dist/profile.js +0 -113
- package/dist/scaffold/claude-code.js +0 -43
- package/dist/scaffold/codex.js +0 -41
- package/dist/scaffold/cursor.js +0 -45
- package/dist/scaffold/gemini.js +0 -10
- package/dist/scaffold/index.js +0 -22
- package/dist/scaffold/mcp.js +0 -15
- package/dist/scaffold/rules.js +0 -191
- package/dist/scaffold/vibreview.js +0 -30
- package/dist/scaffold/vscode.js +0 -28
- package/dist/scaffold/windsurf.js +0 -10
- package/dist/sync/index.js +0 -34
- package/dist/sync/payload.js +0 -23
- package/dist/sync/state.js +0 -12
- package/dist/types.js +0 -1
- package/templates/claude/CLAUDE.md +0 -13
- package/templates/claude/agents/guardrail_profiler.md +0 -12
- package/templates/claude/agents/threat_modeler.md +0 -5
- package/templates/claude/skills/vibreview/SKILL.md +0 -21
- package/templates/claude/skills/vibreview/guardrail_patterns.md +0 -12
- package/templates/cursor/rules/vibreview-security.mdc +0 -8
- /package/{templates/shared → src/generators/rules}/content.md +0 -0
- /package/{templates/shared/guardrails-selection.md → src/generators/rules/guardrails-selection/SKILL.md} +0 -0
- /package/{templates/shared/threat-modelling.md → src/generators/rules/skill.md} +0 -0
- /package/{templates/shared → src/generators/rules}/vibereview-sync/SKILL.md +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { TARGETS, TARGET_NAMES, SENTINEL_START } from '../utils/constants.js';
|
|
5
|
+
import { readText, readJson } from '../utils/fs-helpers.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Status command — show what's configured in the current workspace.
|
|
9
|
+
*/
|
|
10
|
+
export async function statusCommand() {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(chalk.bold.cyan(' ╔══════════════════════════════════════╗'));
|
|
15
|
+
console.log(chalk.bold.cyan(' ║') + chalk.bold(' 🛡️ Security Review Kit — Status ') + chalk.bold.cyan(' ║'));
|
|
16
|
+
console.log(chalk.bold.cyan(' ╚══════════════════════════════════════╝'));
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(chalk.dim(` Workspace: ${cwd}`));
|
|
19
|
+
console.log('');
|
|
20
|
+
|
|
21
|
+
let anyFound = false;
|
|
22
|
+
|
|
23
|
+
for (const key of TARGET_NAMES) {
|
|
24
|
+
const target = TARGETS[key];
|
|
25
|
+
const mcpPath = join(cwd, target.mcpConfigPath);
|
|
26
|
+
const rulePath = join(cwd, target.rulePath);
|
|
27
|
+
|
|
28
|
+
const mcpExists = existsSync(mcpPath);
|
|
29
|
+
const ruleExists = existsSync(rulePath);
|
|
30
|
+
|
|
31
|
+
// Check if the MCP config actually has our server
|
|
32
|
+
let mcpHasServer = false;
|
|
33
|
+
if (mcpExists) {
|
|
34
|
+
if (target.mcpConfigPath.endsWith('.toml')) {
|
|
35
|
+
const content = readText(mcpPath);
|
|
36
|
+
mcpHasServer = content.includes('[mcp_servers.security-review-mcp]');
|
|
37
|
+
} else {
|
|
38
|
+
const json = readJson(mcpPath);
|
|
39
|
+
const servers = json?.mcpServers || json?.servers || {};
|
|
40
|
+
mcpHasServer = 'security-review-mcp' in servers;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if rule file has our sentinel
|
|
45
|
+
let ruleHasSrai = false;
|
|
46
|
+
if (ruleExists) {
|
|
47
|
+
if (target.ruleMode === 'append') {
|
|
48
|
+
const content = readText(rulePath);
|
|
49
|
+
ruleHasSrai = content.includes(SENTINEL_START) || content.includes('SRAI Security Review');
|
|
50
|
+
} else {
|
|
51
|
+
// Standalone rule file — existence is enough
|
|
52
|
+
ruleHasSrai = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!mcpHasServer && !ruleHasSrai) continue;
|
|
57
|
+
|
|
58
|
+
anyFound = true;
|
|
59
|
+
console.log(chalk.bold(` ${target.name}`));
|
|
60
|
+
console.log(
|
|
61
|
+
` MCP Config: ${mcpHasServer ? chalk.green('✓ Configured') : chalk.dim('✗ Not found')} ${chalk.dim(target.mcpConfigPath)}`,
|
|
62
|
+
);
|
|
63
|
+
console.log(
|
|
64
|
+
` Workspace Rule: ${ruleHasSrai ? chalk.green('✓ Installed') : chalk.dim('✗ Not found')} ${chalk.dim(target.rulePath)}`,
|
|
65
|
+
);
|
|
66
|
+
if (key === 'cursor') {
|
|
67
|
+
const cliPath = join(cwd, '.cursor', 'cli.json');
|
|
68
|
+
const cliJson = readJson(cliPath);
|
|
69
|
+
const allow = cliJson?.permissions?.allow;
|
|
70
|
+
const hasMcpAllow =
|
|
71
|
+
Array.isArray(allow) &&
|
|
72
|
+
allow.some(
|
|
73
|
+
(e) => typeof e === 'string' && e.toLowerCase().includes('mcp(security-review-mcp'),
|
|
74
|
+
);
|
|
75
|
+
console.log(
|
|
76
|
+
` CLI MCP allow: ${hasMcpAllow ? chalk.green('\u2713 security-review-mcp') : chalk.dim('\u2717 Missing in .cursor/cli.json')} ${chalk.dim('.cursor/cli.json')}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
console.log('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!anyFound) {
|
|
83
|
+
console.log(chalk.yellow(' No security-review-mcp configurations found in this workspace.'));
|
|
84
|
+
console.log(chalk.dim(' Run `securityreview-kit init` to set up.'));
|
|
85
|
+
console.log('');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check env vars
|
|
89
|
+
console.log(chalk.bold(' Environment'));
|
|
90
|
+
const apiUrl = process.env.SECURITY_REVIEW_API_URL;
|
|
91
|
+
const apiToken = process.env.SECURITY_REVIEW_API_TOKEN;
|
|
92
|
+
console.log(
|
|
93
|
+
` SECURITY_REVIEW_API_URL: ${apiUrl ? chalk.green('✓ Set') : chalk.yellow('✗ Not set')}`,
|
|
94
|
+
);
|
|
95
|
+
console.log(
|
|
96
|
+
` SECURITY_REVIEW_API_TOKEN: ${apiToken ? chalk.green('✓ Set') : chalk.yellow('✗ Not set')}`,
|
|
97
|
+
);
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { input, select } from '@inquirer/prompts';
|
|
5
|
+
import { SENTINEL_START, TARGET_NAMES, TARGETS } from '../utils/constants.js';
|
|
6
|
+
import { readJson, readText } from '../utils/fs-helpers.js';
|
|
7
|
+
import { fetchVibeReviewProjectNames, getStoredCredentials, normalizeApiUrl } from '../utils/srai.js';
|
|
8
|
+
|
|
9
|
+
const ruleGenerators = {
|
|
10
|
+
cursor: () => import('../generators/rules/cursor.js'),
|
|
11
|
+
claude: () => import('../generators/rules/claude.js'),
|
|
12
|
+
vscode: () => import('../generators/rules/vscode.js'),
|
|
13
|
+
windsurf: () => import('../generators/rules/windsurf.js'),
|
|
14
|
+
codex: () => import('../generators/rules/codex.js'),
|
|
15
|
+
gemini: () => import('../generators/rules/gemini.js'),
|
|
16
|
+
antigravity: () => import('../generators/rules/antigravity.js'),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function normalizeRuleResults(rawResult) {
|
|
20
|
+
const entries = Array.isArray(rawResult) ? rawResult : [rawResult];
|
|
21
|
+
|
|
22
|
+
return entries.map((entry) => {
|
|
23
|
+
if (typeof entry === 'string') {
|
|
24
|
+
return { filePath: entry, action: 'created', kind: 'rule' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (entry && typeof entry.filePath === 'string') {
|
|
28
|
+
const allowedKinds = new Set(['rule', 'command', 'agent', 'skill', 'hooks', 'config']);
|
|
29
|
+
const kind = allowedKinds.has(entry.kind) ? entry.kind : 'rule';
|
|
30
|
+
return { filePath: entry.filePath, action: entry.action || 'created', kind };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error('Rule generator returned an invalid result.');
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasConfiguredMcpServer(cwd, target) {
|
|
38
|
+
const mcpPath = join(cwd, target.mcpConfigPath);
|
|
39
|
+
if (!existsSync(mcpPath)) return false;
|
|
40
|
+
|
|
41
|
+
if (mcpPath.endsWith('.toml')) {
|
|
42
|
+
const content = readText(mcpPath);
|
|
43
|
+
return content.includes('[mcp_servers.security-review-mcp]');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const json = readJson(mcpPath);
|
|
47
|
+
const servers = json?.mcpServers || json?.servers || {};
|
|
48
|
+
return 'security-review-mcp' in servers;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasInstalledRule(cwd, target) {
|
|
52
|
+
const rulePath = join(cwd, target.rulePath);
|
|
53
|
+
if (!existsSync(rulePath)) return false;
|
|
54
|
+
|
|
55
|
+
if (target.ruleMode === 'append') {
|
|
56
|
+
const content = readText(rulePath);
|
|
57
|
+
return content.includes(SENTINEL_START) || content.includes('SRAI Security Review');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveConfiguredTargets(cwd) {
|
|
64
|
+
return TARGET_NAMES.filter((key) => {
|
|
65
|
+
const target = TARGETS[key];
|
|
66
|
+
return hasConfiguredMcpServer(cwd, target) || hasInstalledRule(cwd, target);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function resolveCredentials(options, cwd) {
|
|
71
|
+
let apiUrl = normalizeApiUrl(options.apiUrl || process.env.SECURITY_REVIEW_API_URL || '');
|
|
72
|
+
let apiToken = String(options.apiKey || process.env.SECURITY_REVIEW_API_TOKEN || '').trim();
|
|
73
|
+
|
|
74
|
+
if (!apiUrl || !apiToken) {
|
|
75
|
+
const stored = getStoredCredentials(cwd);
|
|
76
|
+
if (!apiUrl && stored.apiUrl) {
|
|
77
|
+
apiUrl = stored.apiUrl;
|
|
78
|
+
}
|
|
79
|
+
if (!apiToken && stored.apiToken) {
|
|
80
|
+
apiToken = stored.apiToken;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!apiUrl) {
|
|
85
|
+
apiUrl = await input({
|
|
86
|
+
message: '🔗 SRAI API URL:',
|
|
87
|
+
default: 'app.demo.securityreview.ai',
|
|
88
|
+
validate: (v) => {
|
|
89
|
+
const normalized = normalizeApiUrl(v);
|
|
90
|
+
if (!normalized) return 'Must be a valid URL';
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
new URL(normalized);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return 'Must be a valid URL';
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
apiUrl = normalizeApiUrl(apiUrl);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(chalk.dim(` API URL: ${apiUrl} (from saved config/env/flags)`));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!apiToken) {
|
|
106
|
+
apiToken = await input({
|
|
107
|
+
message: '🔑 SRAI API Token:',
|
|
108
|
+
validate: (v) => (v.length > 0 ? true : 'Token is required'),
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
console.log(chalk.dim(` API Token: ${'•'.repeat(8)} (from saved config/env/flags)`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { apiUrl, apiToken };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function resolveProjectName(options, apiUrl, apiToken) {
|
|
118
|
+
const pinnedProject = (options.projectName || process.env.SECURITY_REVIEW_PROJECT_NAME || '').trim();
|
|
119
|
+
const projectNames = await fetchVibeReviewProjectNames(apiUrl, apiToken);
|
|
120
|
+
|
|
121
|
+
return select({
|
|
122
|
+
message: '🧩 Select SRAI project (vibe review enabled only):',
|
|
123
|
+
choices: projectNames.map((name) => ({
|
|
124
|
+
name,
|
|
125
|
+
value: name,
|
|
126
|
+
})),
|
|
127
|
+
default: projectNames.includes(pinnedProject) ? pinnedProject : undefined,
|
|
128
|
+
pageSize: 12,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function switchProjectCommand(options = {}) {
|
|
133
|
+
const cwd = process.cwd();
|
|
134
|
+
const targets = resolveConfiguredTargets(cwd);
|
|
135
|
+
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log(chalk.bold.cyan(' ╔══════════════════════════════════════╗'));
|
|
138
|
+
console.log(chalk.bold.cyan(' ║') + chalk.bold(' 🛡️ Security Review Kit — Switch ') + chalk.bold.cyan(' ║'));
|
|
139
|
+
console.log(chalk.bold.cyan(' ╚══════════════════════════════════════╝'));
|
|
140
|
+
console.log('');
|
|
141
|
+
|
|
142
|
+
if (targets.length === 0) {
|
|
143
|
+
console.log(chalk.yellow(' ⚠ No configured targets found in this workspace.'));
|
|
144
|
+
console.log(chalk.dim(' Run `securityreview-kit init` first.'));
|
|
145
|
+
console.log('');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(chalk.dim(` Targets to update: ${targets.map((t) => TARGETS[t].name).join(', ')}`));
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(chalk.bold.white(' Step 1 of 2: SRAI Credentials'));
|
|
152
|
+
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
153
|
+
const envVars = await resolveCredentials(options, cwd);
|
|
154
|
+
console.log(chalk.green(' ✓ Credentials configured'));
|
|
155
|
+
console.log('');
|
|
156
|
+
|
|
157
|
+
console.log(chalk.bold.white(' Step 2 of 2: SRAI Project Mapping'));
|
|
158
|
+
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
159
|
+
const projectName = await resolveProjectName(options, envVars.apiUrl, envVars.apiToken);
|
|
160
|
+
console.log(chalk.green(` ✓ Project mapped: ${projectName}`));
|
|
161
|
+
console.log('');
|
|
162
|
+
|
|
163
|
+
console.log(chalk.bold.white(' Updating workspace rules...'));
|
|
164
|
+
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
165
|
+
|
|
166
|
+
const results = [];
|
|
167
|
+
|
|
168
|
+
for (const target of targets) {
|
|
169
|
+
const targetInfo = TARGETS[target];
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log(chalk.bold(` ${targetInfo.name}`));
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const gen = await ruleGenerators[target]();
|
|
175
|
+
const generatedRules = normalizeRuleResults(gen.generate(cwd, { projectName }));
|
|
176
|
+
|
|
177
|
+
for (const rule of generatedRules) {
|
|
178
|
+
const labelByKind = {
|
|
179
|
+
rule: 'Workspace rule',
|
|
180
|
+
command: 'Workspace command',
|
|
181
|
+
agent: 'Workspace agent',
|
|
182
|
+
skill: 'Workspace skill',
|
|
183
|
+
hooks: 'Hooks',
|
|
184
|
+
config: 'CLI config',
|
|
185
|
+
};
|
|
186
|
+
const label = labelByKind[rule.kind] || 'Workspace rule';
|
|
187
|
+
console.log(chalk.green(` ✓ ${label} → ${rule.filePath} (${rule.action})`));
|
|
188
|
+
results.push({ target, type: rule.kind, status: 'ok', path: rule.filePath, action: rule.action });
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.log(chalk.red(` ✗ Workspace rule failed: ${err.message}`));
|
|
192
|
+
results.push({ target, type: 'rule', status: 'error', error: err.message });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const ok = results.filter((r) => r.status === 'ok').length;
|
|
197
|
+
const errors = results.filter((r) => r.status === 'error').length;
|
|
198
|
+
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
201
|
+
if (errors === 0) {
|
|
202
|
+
console.log(chalk.bold.green(` ✅ Done! ${ok} rule update(s) applied successfully.`));
|
|
203
|
+
} else {
|
|
204
|
+
console.log(chalk.bold.yellow(` ⚠ Done with ${errors} error(s). ${ok} rule update(s) applied.`));
|
|
205
|
+
}
|
|
206
|
+
console.log('');
|
|
207
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJson, writeJson } from '../../utils/fs-helpers.js';
|
|
3
|
+
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
|
+
|
|
5
|
+
const CLAUDE_MCP_PERMISSION = `mcp__${MCP_SERVER_NAME}`;
|
|
6
|
+
|
|
7
|
+
function getClaudeSessionStartHooks() {
|
|
8
|
+
const prompt = [
|
|
9
|
+
'MANDATORY SECURITY GATE (Claude Code Session Policy)',
|
|
10
|
+
'',
|
|
11
|
+
'For any request with security impact, follow this order:',
|
|
12
|
+
'1. Fetch Vibe Guardrails first using .claude/skills/guardrails-selection/SKILL.md.',
|
|
13
|
+
'2. Run PWNISMS threat modelling using .claude/skills/threat-modelling/SKILL.md.',
|
|
14
|
+
'3. Implement secure code using the hydrated guardrails and threat findings.',
|
|
15
|
+
'4. Read .claude/skills/vibereview-sync/SKILL.md. Write a structured .md artifact under vibereview/, do not read sibling markdown files there just to infer format, validate it, and call sync_ai_ide_markdown directly after implementation or threat-model updates.',
|
|
16
|
+
'',
|
|
17
|
+
'Do not use project-profile exploration tools during normal coding tasks. No blocking and no deferral: guardrails first, PWNISMS second, implementation third, VibeReview sync last.',
|
|
18
|
+
].join('\n');
|
|
19
|
+
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
matcher: '.*',
|
|
23
|
+
hooks: [
|
|
24
|
+
{
|
|
25
|
+
type: 'prompt',
|
|
26
|
+
prompt,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate Claude Code project MCP config at .mcp.json and project settings at .claude/settings.json
|
|
35
|
+
*/
|
|
36
|
+
export function generate(cwd, envVars) {
|
|
37
|
+
const mcpPath = join(cwd, '.mcp.json');
|
|
38
|
+
const mcpConfig = readJson(mcpPath) || {};
|
|
39
|
+
|
|
40
|
+
if (!mcpConfig.mcpServers) {
|
|
41
|
+
mcpConfig.mcpServers = {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
mcpConfig.mcpServers[MCP_SERVER_NAME] = {
|
|
45
|
+
command: 'npx',
|
|
46
|
+
args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
|
|
47
|
+
env: {
|
|
48
|
+
SECURITY_REVIEW_API_URL: envVars.apiUrl,
|
|
49
|
+
SECURITY_REVIEW_API_TOKEN: envVars.apiToken,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
writeJson(mcpPath, mcpConfig);
|
|
54
|
+
|
|
55
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
56
|
+
const existing = readJson(settingsPath) || {};
|
|
57
|
+
const enabledServers = Array.isArray(existing.enabledMcpjsonServers)
|
|
58
|
+
? existing.enabledMcpjsonServers.filter((name) => typeof name === 'string' && name.trim())
|
|
59
|
+
: [];
|
|
60
|
+
if (!enabledServers.includes(MCP_SERVER_NAME)) {
|
|
61
|
+
existing.enabledMcpjsonServers = [...enabledServers, MCP_SERVER_NAME];
|
|
62
|
+
}
|
|
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
|
+
|
|
74
|
+
const existingSessionStart = Array.isArray(existing.SessionStart) ? existing.SessionStart : [];
|
|
75
|
+
const marker = 'MANDATORY SECURITY GATE (Claude Code Session Policy)';
|
|
76
|
+
const ours = getClaudeSessionStartHooks();
|
|
77
|
+
const hasOurs = existingSessionStart.some((entry) =>
|
|
78
|
+
Array.isArray(entry?.hooks) &&
|
|
79
|
+
entry.hooks.some((hook) => hook?.type === 'prompt' && typeof hook?.prompt === 'string' && hook.prompt.includes(marker)),
|
|
80
|
+
);
|
|
81
|
+
existing.SessionStart = hasOurs ? existingSessionStart : [...existingSessionStart, ...ours];
|
|
82
|
+
|
|
83
|
+
writeJson(settingsPath, existing);
|
|
84
|
+
return mcpPath;
|
|
85
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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 './claude.js';
|
|
7
|
+
|
|
8
|
+
test('Claude MCP generator writes .mcp.json, enables the project MCP server, and adds session hooks', () => {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-mcp-'));
|
|
10
|
+
|
|
11
|
+
generate(cwd, {
|
|
12
|
+
apiUrl: 'https://example.test',
|
|
13
|
+
apiToken: 'secret-token',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const mcpConfig = JSON.parse(readFileSync(join(cwd, '.mcp.json'), 'utf8'));
|
|
17
|
+
const settings = JSON.parse(readFileSync(join(cwd, '.claude', 'settings.json'), 'utf8'));
|
|
18
|
+
assert.equal(mcpConfig.mcpServers['security-review-mcp'].command, 'npx');
|
|
19
|
+
assert.deepEqual(settings.enabledMcpjsonServers, ['security-review-mcp']);
|
|
20
|
+
assert.deepEqual(settings.permissions.allow, ['mcp__security-review-mcp']);
|
|
21
|
+
assert.equal(Array.isArray(settings.SessionStart), true);
|
|
22
|
+
assert.match(settings.SessionStart[0].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
23
|
+
assert.match(settings.SessionStart[0].hooks[0].prompt, /vibereview\//);
|
|
24
|
+
assert.match(settings.SessionStart[0].hooks[0].prompt, /sync_ai_ide_markdown/);
|
|
25
|
+
assert.match(settings.SessionStart[0].hooks[0].prompt, /vibereview-sync\/SKILL\.md/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('Claude MCP generator preserves existing SessionStart hooks', () => {
|
|
29
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-mcp-merge-'));
|
|
30
|
+
const configPath = join(cwd, '.claude', 'settings.json');
|
|
31
|
+
mkdirSync(join(cwd, '.claude'), { recursive: true });
|
|
32
|
+
|
|
33
|
+
writeFileSync(
|
|
34
|
+
configPath,
|
|
35
|
+
JSON.stringify(
|
|
36
|
+
{
|
|
37
|
+
SessionStart: [
|
|
38
|
+
{
|
|
39
|
+
matcher: '.*',
|
|
40
|
+
hooks: [{ type: 'prompt', prompt: 'Existing hook' }],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
permissions: {
|
|
44
|
+
allow: ['Bash(npm run test:*)'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
null,
|
|
48
|
+
2,
|
|
49
|
+
),
|
|
50
|
+
'utf8',
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
generate(cwd, {
|
|
54
|
+
apiUrl: 'https://example.test',
|
|
55
|
+
apiToken: 'secret-token',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
59
|
+
assert.equal(config.SessionStart.length, 2);
|
|
60
|
+
assert.match(config.SessionStart[0].hooks[0].prompt, /Existing hook/);
|
|
61
|
+
assert.match(config.SessionStart[1].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
62
|
+
assert.deepEqual(config.enabledMcpjsonServers, ['security-review-mcp']);
|
|
63
|
+
assert.deepEqual(config.permissions.allow, ['Bash(npm run test:*)', 'mcp__security-review-mcp']);
|
|
64
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readText, writeText, ensureDir } from '../../utils/fs-helpers.js';
|
|
3
|
+
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
|
+
|
|
5
|
+
function ensureHooksFeature(existing) {
|
|
6
|
+
const featureLine = 'codex_hooks = true';
|
|
7
|
+
const featuresHeader = '[features]';
|
|
8
|
+
|
|
9
|
+
if (!existing) {
|
|
10
|
+
return `${featuresHeader}\n${featureLine}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (existing.includes(featuresHeader)) {
|
|
14
|
+
const featuresSectionPattern = /(\[features\][\s\S]*?)(?=\n\[[^\]]+\]|$)/;
|
|
15
|
+
return existing.replace(featuresSectionPattern, (section) => {
|
|
16
|
+
if (/\bcodex_hooks\s*=/.test(section)) {
|
|
17
|
+
return section.replace(/codex_hooks\s*=\s*(true|false)/, featureLine);
|
|
18
|
+
}
|
|
19
|
+
const separator = section.endsWith('\n') ? '' : '\n';
|
|
20
|
+
return `${section}${separator}${featureLine}\n`;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
25
|
+
return `${existing}${separator}${featuresHeader}\n${featureLine}\n`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate Codex MCP config at .codex/config.toml
|
|
30
|
+
* Codex uses TOML format. We use simple string templating since the structure
|
|
31
|
+
* is straightforward and avoids a TOML library dependency.
|
|
32
|
+
*/
|
|
33
|
+
export function generate(cwd, envVars) {
|
|
34
|
+
const filePath = join(cwd, '.codex', 'config.toml');
|
|
35
|
+
const existing = readText(filePath);
|
|
36
|
+
|
|
37
|
+
const serverBlock = `
|
|
38
|
+
[mcp_servers.${MCP_SERVER_NAME}]
|
|
39
|
+
command = "npx"
|
|
40
|
+
args = ["-y", "${MCP_SERVER_PACKAGE}@latest"]
|
|
41
|
+
default_tools_approval_mode = "approve"
|
|
42
|
+
|
|
43
|
+
[mcp_servers.${MCP_SERVER_NAME}.env]
|
|
44
|
+
SECURITY_REVIEW_API_URL = "${envVars.apiUrl}"
|
|
45
|
+
SECURITY_REVIEW_API_TOKEN = "${envVars.apiToken}"
|
|
46
|
+
`.trim();
|
|
47
|
+
|
|
48
|
+
const existingWithHooks = ensureHooksFeature(existing).trim();
|
|
49
|
+
|
|
50
|
+
// Check if we already have this server configured
|
|
51
|
+
if (existingWithHooks.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
|
|
52
|
+
// Replace the existing block — find from the server header to the next
|
|
53
|
+
// section header or end of file
|
|
54
|
+
const regex = new RegExp(
|
|
55
|
+
`\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?(?=\\n\\[(?!mcp_servers\\.${MCP_SERVER_NAME})|$)`,
|
|
56
|
+
);
|
|
57
|
+
const updated = existingWithHooks.replace(regex, serverBlock);
|
|
58
|
+
writeText(filePath, updated);
|
|
59
|
+
} else if (existingWithHooks) {
|
|
60
|
+
// Append to existing file
|
|
61
|
+
const separator = existingWithHooks.endsWith('\n') ? '\n' : '\n\n';
|
|
62
|
+
writeText(filePath, existingWithHooks + separator + serverBlock + '\n');
|
|
63
|
+
} else {
|
|
64
|
+
// New file
|
|
65
|
+
ensureDir(join(cwd, '.codex'));
|
|
66
|
+
writeText(filePath, existingWithHooks + '\n\n' + serverBlock + '\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return filePath;
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, 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 './codex.js';
|
|
7
|
+
|
|
8
|
+
test('Codex MCP generator enables hooks and writes MCP server block', () => {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-codex-mcp-'));
|
|
10
|
+
|
|
11
|
+
generate(cwd, {
|
|
12
|
+
apiUrl: 'https://example.test',
|
|
13
|
+
apiToken: 'secret-token',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const config = readFileSync(join(cwd, '.codex/config.toml'), 'utf8');
|
|
17
|
+
assert.match(config, /\[features\]\ncodex_hooks = true/);
|
|
18
|
+
assert.match(config, /\[mcp_servers\.security-review-mcp\]/);
|
|
19
|
+
assert.match(config, /default_tools_approval_mode = "approve"/);
|
|
20
|
+
assert.match(config, /SECURITY_REVIEW_API_URL = "https:\/\/example\.test"/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('Codex MCP generator preserves existing features while forcing codex_hooks', () => {
|
|
24
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-codex-mcp-merge-'));
|
|
25
|
+
const configPath = join(cwd, '.codex', 'config.toml');
|
|
26
|
+
mkdirSync(join(cwd, '.codex'), { recursive: true });
|
|
27
|
+
|
|
28
|
+
writeFileSync(
|
|
29
|
+
configPath,
|
|
30
|
+
'[features]\nshell_snapshot = true\ncodex_hooks = false\n\n[profiles.default]\nmodel = "gpt-5.4"\n',
|
|
31
|
+
'utf8',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
generate(cwd, {
|
|
35
|
+
apiUrl: 'https://example.test',
|
|
36
|
+
apiToken: 'secret-token',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const config = readFileSync(configPath, 'utf8');
|
|
40
|
+
assert.match(config, /\[features\][\s\S]*shell_snapshot = true/);
|
|
41
|
+
assert.match(config, /\[features\][\s\S]*codex_hooks = true/);
|
|
42
|
+
assert.match(config, /\[profiles\.default\]\nmodel = "gpt-5\.4"/);
|
|
43
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJson, writeJson } from '../../utils/fs-helpers.js';
|
|
3
|
+
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
|
+
import { mergeCursorCliMcpAllowlist } from '../../utils/cursor-cli-permissions.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate Cursor MCP config at .cursor/mcp.json
|
|
8
|
+
*/
|
|
9
|
+
export function generate(cwd, envVars) {
|
|
10
|
+
const filePath = join(cwd, '.cursor', 'mcp.json');
|
|
11
|
+
const existing = readJson(filePath) || {};
|
|
12
|
+
|
|
13
|
+
if (!existing.mcpServers) {
|
|
14
|
+
existing.mcpServers = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
existing.mcpServers[MCP_SERVER_NAME] = {
|
|
18
|
+
command: 'npx',
|
|
19
|
+
args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
|
|
20
|
+
env: {
|
|
21
|
+
SECURITY_REVIEW_API_URL: envVars.apiUrl,
|
|
22
|
+
SECURITY_REVIEW_API_TOKEN: envVars.apiToken,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
writeJson(filePath, existing);
|
|
27
|
+
mergeCursorCliMcpAllowlist(cwd);
|
|
28
|
+
return filePath;
|
|
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
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJson, writeJson } from '../../utils/fs-helpers.js';
|
|
3
|
+
import { MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from '../../utils/constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate Gemini CLI / Antigravity MCP config at .gemini/settings.json
|
|
7
|
+
* Both Gemini CLI and Antigravity use the same config file path.
|
|
8
|
+
*/
|
|
9
|
+
export function generate(cwd, envVars) {
|
|
10
|
+
const filePath = join(cwd, '.gemini', 'settings.json');
|
|
11
|
+
const existing = readJson(filePath) || {};
|
|
12
|
+
|
|
13
|
+
if (!existing.mcpServers) {
|
|
14
|
+
existing.mcpServers = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
existing.mcpServers[MCP_SERVER_NAME] = {
|
|
18
|
+
command: 'npx',
|
|
19
|
+
args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
|
|
20
|
+
env: {
|
|
21
|
+
SECURITY_REVIEW_API_URL: envVars.apiUrl,
|
|
22
|
+
SECURITY_REVIEW_API_TOKEN: envVars.apiToken,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
writeJson(filePath, existing);
|
|
27
|
+
return filePath;
|
|
28
|
+
}
|