securityreview-kit 0.1.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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # securityreview-kit
2
+
3
+ > Bootstrap [security-review-mcp](https://www.npmjs.com/package/security-review-mcp) for AI IDEs and CLI tools in one command.
4
+
5
+ **securityreview-kit** configures the SRAI security review MCP server and installs workspace rules so your AI assistant consults security threat models and countermeasures *before* generating code.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # Interactive mode (recommended)
11
+ npx securityreview-kit init
12
+
13
+ # Or specify targets directly
14
+ npx securityreview-kit init --target cursor --api-url https://api.example.com --api-key YOUR_TOKEN
15
+
16
+ # Install for multiple targets
17
+ npx securityreview-kit init --target cursor claude vscode
18
+
19
+ # Install for all supported targets
20
+ npx securityreview-kit init --all --api-url https://api.example.com --api-key YOUR_TOKEN
21
+ ```
22
+
23
+ ## Supported Targets
24
+
25
+ | Target | Flag | MCP Config | Workspace Rule |
26
+ |---|---|---|---|
27
+ | Cursor | `cursor` | `.cursor/mcp.json` | `.cursor/rules/srai-security-review.mdc` |
28
+ | Claude Code | `claude` | `.claude/settings.json` | `CLAUDE.md` |
29
+ | VS Code Copilot | `vscode` | `.vscode/mcp.json` | `.github/copilot-instructions.md` |
30
+ | Windsurf | `windsurf` | `.windsurf/mcp_config.json` | `.windsurf/rules/srai-security-review.md` |
31
+ | Codex | `codex` | `.codex/config.toml` | `AGENTS.md` |
32
+ | Gemini CLI | `gemini` | `.gemini/settings.json` | `GEMINI.md` |
33
+ | Antigravity | `antigravity` | `.gemini/settings.json` | `GEMINI.md` |
34
+
35
+ ## Commands
36
+
37
+ ### `securityreview-kit init`
38
+
39
+ Configure security-review-mcp for your IDE/CLI. Runs interactively when no flags are provided.
40
+
41
+ ```
42
+ Options:
43
+ -t, --target <name...> Target IDE/CLI (cursor, claude, vscode, windsurf, codex, gemini, antigravity)
44
+ -a, --all Install for all supported targets
45
+ --api-url <url> SRAI API URL (or set SECURITY_REVIEW_API_URL env var)
46
+ --api-key <token> SRAI API Token (or set SECURITY_REVIEW_API_TOKEN env var)
47
+ --skip-mcp Skip MCP server config installation
48
+ --skip-rules Skip workspace rule installation
49
+ ```
50
+
51
+ ### `securityreview-kit status`
52
+
53
+ Show current configuration status for all supported targets in the workspace.
54
+
55
+ ## Environment Variables
56
+
57
+ | Variable | Description |
58
+ |---|---|
59
+ | `SECURITY_REVIEW_API_URL` | SRAI platform API endpoint |
60
+ | `SECURITY_REVIEW_API_TOKEN` | Your SRAI API token |
61
+
62
+ These can be provided via CLI flags, environment variables, or interactive prompts.
63
+
64
+ ## What Gets Installed
65
+
66
+ **MCP Server Config** — tells your IDE how to launch the `security-review-mcp` server via `npx`.
67
+
68
+ **Workspace Rules** — instructs the AI assistant to consult SRAI threat models and countermeasures before generating security-relevant code. The rules trigger on changes involving authentication, data handling, API endpoints, cryptography, infrastructure, and third-party integrations.
69
+
70
+ ## How It Works
71
+
72
+ 1. Run `securityreview-kit init`
73
+ 2. Select your IDE/CLI target(s)
74
+ 3. Enter your SRAI credentials
75
+ 4. The tool creates/merges MCP config and workspace rule files
76
+ 5. Your AI assistant now has access to SRAI security reviews
77
+
78
+ The tool is **idempotent** — running it multiple times safely updates existing configs without duplicating content.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/cli.js';
4
+
5
+ run();
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "securityreview-kit",
3
+ "version": "0.1.0",
4
+ "description": "Bootstrap security-review-mcp for AI IDEs and CLI tools",
5
+ "author": "Debarshi Das <debarshi.das@we45.com>",
6
+ "license": "UNLICENSED",
7
+ "type": "module",
8
+ "bin": {
9
+ "securityreview-kit": "./bin/securityreview-kit.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "src/",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "scripts": {
20
+ "test": "node --test src/**/*.test.js",
21
+ "start": "node bin/securityreview-kit.js"
22
+ },
23
+ "keywords": [
24
+ "security",
25
+ "mcp",
26
+ "security-review",
27
+ "srai",
28
+ "ai-ide",
29
+ "cursor",
30
+ "claude",
31
+ "codex",
32
+ "gemini",
33
+ "windsurf",
34
+ "vscode"
35
+ ],
36
+ "dependencies": {
37
+ "chalk": "^5.4.0",
38
+ "commander": "^13.0.0",
39
+ "inquirer": "^12.0.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }
package/src/cli.js ADDED
@@ -0,0 +1,47 @@
1
+ import { Command } from 'commander';
2
+ import { initCommand } from './commands/init.js';
3
+ import { statusCommand } from './commands/status.js';
4
+ import { TARGET_NAMES } from './utils/constants.js';
5
+
6
+ export function run() {
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('securityreview-kit')
11
+ .description('Bootstrap security-review-mcp for AI IDEs and CLI tools')
12
+ .version('0.1.0');
13
+
14
+ program
15
+ .command('init')
16
+ .description('Configure security-review-mcp for your IDE / CLI tool')
17
+ .option(
18
+ '-t, --target <name...>',
19
+ `Target IDE/CLI (${TARGET_NAMES.join(', ')}). Omit for interactive mode.`,
20
+ )
21
+ .option('-a, --all', 'Install for all supported targets')
22
+ .option('--api-url <url>', 'SRAI API URL (or set SECURITY_REVIEW_API_URL env var)')
23
+ .option('--api-key <token>', 'SRAI API Token (or set SECURITY_REVIEW_API_TOKEN env var)')
24
+ .option('--skip-mcp', 'Skip MCP server config installation')
25
+ .option('--skip-rules', 'Skip workspace rule installation')
26
+ .action(async (options) => {
27
+ try {
28
+ await initCommand(options);
29
+ } catch (err) {
30
+ if (err.name === 'ExitPromptError') {
31
+ // User cancelled interactive prompt
32
+ console.log('\n Cancelled.\n');
33
+ process.exit(0);
34
+ }
35
+ throw err;
36
+ }
37
+ });
38
+
39
+ program
40
+ .command('status')
41
+ .description('Show current security-review-mcp configuration status')
42
+ .action(async () => {
43
+ await statusCommand();
44
+ });
45
+
46
+ program.parse();
47
+ }
@@ -0,0 +1,225 @@
1
+ import chalk from 'chalk';
2
+ import { input, select, checkbox, confirm } from '@inquirer/prompts';
3
+ import { TARGETS, TARGET_NAMES } from '../utils/constants.js';
4
+ import { detectTargets } from '../utils/detect.js';
5
+
6
+ // Dynamic imports for generators (avoids loading all at startup)
7
+ const mcpGenerators = {
8
+ cursor: () => import('../generators/mcp/cursor.js'),
9
+ claude: () => import('../generators/mcp/claude.js'),
10
+ vscode: () => import('../generators/mcp/vscode.js'),
11
+ windsurf: () => import('../generators/mcp/windsurf.js'),
12
+ codex: () => import('../generators/mcp/codex.js'),
13
+ gemini: () => import('../generators/mcp/gemini.js'),
14
+ antigravity: () => import('../generators/mcp/gemini.js'),
15
+ };
16
+
17
+ const ruleGenerators = {
18
+ cursor: () => import('../generators/rules/cursor.js'),
19
+ claude: () => import('../generators/rules/claude.js'),
20
+ vscode: () => import('../generators/rules/vscode.js'),
21
+ windsurf: () => import('../generators/rules/windsurf.js'),
22
+ codex: () => import('../generators/rules/codex.js'),
23
+ gemini: () => import('../generators/rules/gemini.js'),
24
+ antigravity: () => import('../generators/rules/antigravity.js'),
25
+ };
26
+
27
+ /**
28
+ * Resolve environment variables from flags, env, or interactive prompt.
29
+ */
30
+ async function resolveEnvVars(options, interactive) {
31
+ let apiUrl = options.apiUrl || process.env.SECURITY_REVIEW_API_URL || '';
32
+ let apiToken = options.apiKey || process.env.SECURITY_REVIEW_API_TOKEN || '';
33
+
34
+ if (interactive) {
35
+ if (!apiUrl) {
36
+ apiUrl = await input({
37
+ message: '🔗 SRAI API URL:',
38
+ default: 'https://api.securityreview.ai',
39
+ validate: (v) => (v.startsWith('http') ? true : 'Must be a valid URL'),
40
+ });
41
+ } else {
42
+ console.log(chalk.dim(` API URL: ${apiUrl} (from env/flags)`));
43
+ }
44
+
45
+ if (!apiToken) {
46
+ apiToken = await input({
47
+ message: '🔑 SRAI API Token:',
48
+ validate: (v) => (v.length > 0 ? true : 'Token is required'),
49
+ });
50
+ } else {
51
+ console.log(chalk.dim(` API Token: ${'•'.repeat(8)} (from env/flags)`));
52
+ }
53
+ } else {
54
+ if (!apiUrl || !apiToken) {
55
+ console.log(
56
+ chalk.yellow(
57
+ '⚠ Missing credentials. Set SECURITY_REVIEW_API_URL and SECURITY_REVIEW_API_TOKEN\n' +
58
+ ' environment variables, pass --api-url and --api-key flags, or run in interactive mode.',
59
+ ),
60
+ );
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ return { apiUrl, apiToken };
66
+ }
67
+
68
+ /**
69
+ * Resolve targets from flags or interactive prompt.
70
+ */
71
+ async function resolveTargets(options, interactive, cwd) {
72
+ // Flag mode: explicit target(s)
73
+ if (options.target) {
74
+ const targets = Array.isArray(options.target) ? options.target : [options.target];
75
+ for (const t of targets) {
76
+ if (!TARGET_NAMES.includes(t)) {
77
+ console.log(chalk.red(`✗ Unknown target: ${t}`));
78
+ console.log(chalk.dim(` Valid targets: ${TARGET_NAMES.join(', ')}`));
79
+ process.exit(1);
80
+ }
81
+ }
82
+ return targets;
83
+ }
84
+
85
+ if (options.all) {
86
+ return [...TARGET_NAMES];
87
+ }
88
+
89
+ if (!interactive) {
90
+ console.log(chalk.red('✗ No target specified. Use --target <name> or --all.'));
91
+ process.exit(1);
92
+ }
93
+
94
+ // Interactive: detect and offer choices
95
+ const detected = detectTargets(cwd);
96
+
97
+ console.log('');
98
+ if (detected.length > 0) {
99
+ console.log(
100
+ chalk.dim(` Detected IDEs in workspace: ${detected.map((d) => TARGETS[d].name).join(', ')}`),
101
+ );
102
+ }
103
+
104
+ const selected = await checkbox({
105
+ message: '🎯 Select target IDE(s) / CLI(s) to configure:',
106
+ choices: TARGET_NAMES.map((key) => ({
107
+ name: `${TARGETS[key].name}`,
108
+ value: key,
109
+ checked: detected.includes(key),
110
+ })),
111
+ validate: (v) => (v.length > 0 ? true : 'Select at least one target'),
112
+ });
113
+
114
+ return selected;
115
+ }
116
+
117
+ /**
118
+ * Main init command handler.
119
+ */
120
+ export async function initCommand(options) {
121
+ const cwd = process.cwd();
122
+ const interactive = !options.target && !options.all;
123
+
124
+ // Banner
125
+ console.log('');
126
+ console.log(chalk.bold.cyan(' ╔══════════════════════════════════════╗'));
127
+ console.log(chalk.bold.cyan(' ║') + chalk.bold(' 🛡️ Security Review Kit — Init ') + chalk.bold.cyan(' ║'));
128
+ console.log(chalk.bold.cyan(' ╚══════════════════════════════════════╝'));
129
+ console.log('');
130
+
131
+ if (interactive) {
132
+ console.log(chalk.dim(' Interactive setup — follow the prompts below.\n'));
133
+ }
134
+
135
+ // Step 1: Resolve targets
136
+ console.log(chalk.bold.white(' Step 1 of 3: Select Targets'));
137
+ console.log(chalk.dim(' ─────────────────────────────────'));
138
+ const targets = await resolveTargets(options, interactive, cwd);
139
+ console.log(chalk.green(` ✓ Targets: ${targets.map((t) => TARGETS[t].name).join(', ')}`));
140
+ console.log('');
141
+
142
+ // Step 2: Resolve credentials
143
+ console.log(chalk.bold.white(' Step 2 of 3: SRAI Credentials'));
144
+ console.log(chalk.dim(' ─────────────────────────────────'));
145
+ const envVars = await resolveEnvVars(options, interactive);
146
+ console.log(chalk.green(' ✓ Credentials configured'));
147
+ console.log('');
148
+
149
+ // Step 3: What to install
150
+ let installMcp = !options.skipMcp;
151
+ let installRules = !options.skipRules;
152
+
153
+ if (interactive) {
154
+ console.log(chalk.bold.white(' Step 3 of 3: Installation Options'));
155
+ console.log(chalk.dim(' ─────────────────────────────────'));
156
+ installMcp = await confirm({
157
+ message: '📡 Install MCP server configuration?',
158
+ default: true,
159
+ });
160
+ installRules = await confirm({
161
+ message: '📋 Install workspace security rules?',
162
+ default: true,
163
+ });
164
+ }
165
+
166
+ if (!installMcp && !installRules) {
167
+ console.log(chalk.yellow(' ⚠ Nothing to install. Exiting.'));
168
+ return;
169
+ }
170
+
171
+ console.log('');
172
+ console.log(chalk.bold.white(' Installing...'));
173
+ console.log(chalk.dim(' ─────────────────────────────────'));
174
+
175
+ const results = [];
176
+
177
+ for (const target of targets) {
178
+ const targetInfo = TARGETS[target];
179
+ console.log('');
180
+ console.log(chalk.bold(` ${targetInfo.name}`));
181
+
182
+ if (installMcp) {
183
+ try {
184
+ const gen = await mcpGenerators[target]();
185
+ const mcpPath = gen.generate(cwd, envVars);
186
+ console.log(chalk.green(` ✓ MCP config → ${typeof mcpPath === 'string' ? mcpPath : mcpPath}`));
187
+ results.push({ target, type: 'mcp', status: 'ok', path: mcpPath });
188
+ } catch (err) {
189
+ console.log(chalk.red(` ✗ MCP config failed: ${err.message}`));
190
+ results.push({ target, type: 'mcp', status: 'error', error: err.message });
191
+ }
192
+ }
193
+
194
+ if (installRules) {
195
+ try {
196
+ const gen = await ruleGenerators[target]();
197
+ const result = gen.generate(cwd);
198
+ const rulePath = typeof result === 'string' ? result : result.filePath;
199
+ const action = typeof result === 'string' ? 'created' : result.action;
200
+ console.log(chalk.green(` ✓ Workspace rule → ${rulePath} (${action})`));
201
+ results.push({ target, type: 'rule', status: 'ok', path: rulePath, action });
202
+ } catch (err) {
203
+ console.log(chalk.red(` ✗ Workspace rule failed: ${err.message}`));
204
+ results.push({ target, type: 'rule', status: 'error', error: err.message });
205
+ }
206
+ }
207
+ }
208
+
209
+ // Summary
210
+ const ok = results.filter((r) => r.status === 'ok').length;
211
+ const errors = results.filter((r) => r.status === 'error').length;
212
+
213
+ console.log('');
214
+ console.log(chalk.dim(' ─────────────────────────────────'));
215
+ if (errors === 0) {
216
+ console.log(chalk.bold.green(` ✅ Done! ${ok} configuration(s) installed successfully.`));
217
+ } else {
218
+ console.log(
219
+ chalk.bold.yellow(` ⚠ Done with ${errors} error(s). ${ok} configuration(s) installed.`),
220
+ );
221
+ }
222
+ console.log('');
223
+ console.log(chalk.dim(' Run `securityreview-kit status` to verify your setup.'));
224
+ console.log('');
225
+ }
@@ -0,0 +1,86 @@
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
+ console.log('');
67
+ }
68
+
69
+ if (!anyFound) {
70
+ console.log(chalk.yellow(' No security-review-mcp configurations found in this workspace.'));
71
+ console.log(chalk.dim(' Run `securityreview-kit init` to set up.'));
72
+ console.log('');
73
+ }
74
+
75
+ // Check env vars
76
+ console.log(chalk.bold(' Environment'));
77
+ const apiUrl = process.env.SECURITY_REVIEW_API_URL;
78
+ const apiToken = process.env.SECURITY_REVIEW_API_TOKEN;
79
+ console.log(
80
+ ` SECURITY_REVIEW_API_URL: ${apiUrl ? chalk.green('✓ Set') : chalk.yellow('✗ Not set')}`,
81
+ );
82
+ console.log(
83
+ ` SECURITY_REVIEW_API_TOKEN: ${apiToken ? chalk.green('✓ Set') : chalk.yellow('✗ Not set')}`,
84
+ );
85
+ console.log('');
86
+ }
@@ -0,0 +1,27 @@
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 Claude Code MCP config at .claude/settings.json
7
+ */
8
+ export function generate(cwd, envVars) {
9
+ const filePath = join(cwd, '.claude', 'settings.json');
10
+ const existing = readJson(filePath) || {};
11
+
12
+ if (!existing.mcpServers) {
13
+ existing.mcpServers = {};
14
+ }
15
+
16
+ existing.mcpServers[MCP_SERVER_NAME] = {
17
+ command: 'npx',
18
+ args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
19
+ env: {
20
+ SECURITY_REVIEW_API_URL: envVars.apiUrl,
21
+ SECURITY_REVIEW_API_TOKEN: envVars.apiToken,
22
+ },
23
+ };
24
+
25
+ writeJson(filePath, existing);
26
+ return filePath;
27
+ }
@@ -0,0 +1,44 @@
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
+ /**
6
+ * Generate Codex MCP config at .codex/config.toml
7
+ * Codex uses TOML format. We use simple string templating since the structure
8
+ * is straightforward and avoids a TOML library dependency.
9
+ */
10
+ export function generate(cwd, envVars) {
11
+ const filePath = join(cwd, '.codex', 'config.toml');
12
+ const existing = readText(filePath);
13
+
14
+ const serverBlock = `
15
+ [mcp_servers.${MCP_SERVER_NAME}]
16
+ command = "npx"
17
+ args = ["-y", "${MCP_SERVER_PACKAGE}@latest"]
18
+
19
+ [mcp_servers.${MCP_SERVER_NAME}.env]
20
+ SECURITY_REVIEW_API_URL = "${envVars.apiUrl}"
21
+ SECURITY_REVIEW_API_TOKEN = "${envVars.apiToken}"
22
+ `.trim();
23
+
24
+ // Check if we already have this server configured
25
+ if (existing.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
26
+ // Replace the existing block — find from the server header to the next
27
+ // section header or end of file
28
+ const regex = new RegExp(
29
+ `\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?(?=\\n\\[(?!mcp_servers\\.${MCP_SERVER_NAME})|$)`,
30
+ );
31
+ const updated = existing.replace(regex, serverBlock);
32
+ writeText(filePath, updated);
33
+ } else if (existing) {
34
+ // Append to existing file
35
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
36
+ writeText(filePath, existing + separator + serverBlock + '\n');
37
+ } else {
38
+ // New file
39
+ ensureDir(join(cwd, '.codex'));
40
+ writeText(filePath, serverBlock + '\n');
41
+ }
42
+
43
+ return filePath;
44
+ }
@@ -0,0 +1,27 @@
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 Cursor MCP config at .cursor/mcp.json
7
+ */
8
+ export function generate(cwd, envVars) {
9
+ const filePath = join(cwd, '.cursor', 'mcp.json');
10
+ const existing = readJson(filePath) || {};
11
+
12
+ if (!existing.mcpServers) {
13
+ existing.mcpServers = {};
14
+ }
15
+
16
+ existing.mcpServers[MCP_SERVER_NAME] = {
17
+ command: 'npx',
18
+ args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
19
+ env: {
20
+ SECURITY_REVIEW_API_URL: envVars.apiUrl,
21
+ SECURITY_REVIEW_API_TOKEN: envVars.apiToken,
22
+ },
23
+ };
24
+
25
+ writeJson(filePath, existing);
26
+ return filePath;
27
+ }
@@ -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
+ }
@@ -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
+
5
+ /**
6
+ * Generate VS Code Copilot MCP config at .vscode/mcp.json
7
+ * Uses the VS Code input variable pattern for secure credential prompting.
8
+ */
9
+ export function generate(cwd, envVars) {
10
+ const filePath = join(cwd, '.vscode', 'mcp.json');
11
+ const existing = readJson(filePath) || {};
12
+
13
+ if (!existing.servers) {
14
+ existing.servers = {};
15
+ }
16
+
17
+ existing.servers[MCP_SERVER_NAME] = {
18
+ type: 'stdio',
19
+ command: 'npx',
20
+ args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
21
+ env: {
22
+ SECURITY_REVIEW_API_URL: envVars.apiUrl,
23
+ SECURITY_REVIEW_API_TOKEN: envVars.apiToken,
24
+ },
25
+ };
26
+
27
+ writeJson(filePath, existing);
28
+ return filePath;
29
+ }
@@ -0,0 +1,27 @@
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 Windsurf MCP config at .windsurf/mcp_config.json
7
+ */
8
+ export function generate(cwd, envVars) {
9
+ const filePath = join(cwd, '.windsurf', 'mcp_config.json');
10
+ const existing = readJson(filePath) || {};
11
+
12
+ if (!existing.mcpServers) {
13
+ existing.mcpServers = {};
14
+ }
15
+
16
+ existing.mcpServers[MCP_SERVER_NAME] = {
17
+ command: 'npx',
18
+ args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
19
+ env: {
20
+ SECURITY_REVIEW_API_URL: envVars.apiUrl,
21
+ SECURITY_REVIEW_API_TOKEN: envVars.apiToken,
22
+ },
23
+ };
24
+
25
+ writeJson(filePath, existing);
26
+ return filePath;
27
+ }
@@ -0,0 +1,22 @@
1
+ import { join } from 'node:path';
2
+ import { writeText } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate Antigravity workspace rule at .agent/rules/srai-security-review.md
7
+ * Antigravity uses .agent/rules/ with YAML frontmatter (trigger: always_on).
8
+ */
9
+ export function generate(cwd) {
10
+ const filePath = join(cwd, '.agent', 'rules', 'srai-security-review.md');
11
+ const content = getRuleContent();
12
+
13
+ const rule = `---
14
+ trigger: always_on
15
+ ---
16
+
17
+ ${content}
18
+ `;
19
+
20
+ writeText(filePath, rule);
21
+ return filePath;
22
+ }
@@ -0,0 +1,13 @@
1
+ import { join } from 'node:path';
2
+ import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate Claude Code workspace rule — appends to CLAUDE.md
7
+ */
8
+ export function generate(cwd) {
9
+ const filePath = join(cwd, 'CLAUDE.md');
10
+ const content = getRuleContent();
11
+ const action = upsertSentinelBlock(filePath, content);
12
+ return { filePath, action };
13
+ }
@@ -0,0 +1,13 @@
1
+ import { join } from 'node:path';
2
+ import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate Codex workspace rule — appends to AGENTS.md
7
+ */
8
+ export function generate(cwd) {
9
+ const filePath = join(cwd, 'AGENTS.md');
10
+ const content = getRuleContent();
11
+ const action = upsertSentinelBlock(filePath, content);
12
+ return { filePath, action };
13
+ }
@@ -0,0 +1,12 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ /**
8
+ * Returns the shared rule content markdown.
9
+ */
10
+ export function getRuleContent() {
11
+ return readFileSync(join(__dirname, 'content.md'), 'utf-8').trim();
12
+ }
@@ -0,0 +1,66 @@
1
+ # SRAI Security Review
2
+
3
+ **THIS RULE IS NON-NEGOTIABLE.** You MUST execute the security review workflow described below BEFORE writing, modifying, or suggesting ANY code. Do NOT skip, defer, or summarize these steps. Execute them by calling the MCP tools directly.
4
+
5
+ You have access to the `security-review-mcp` MCP server. Use it.
6
+
7
+ ## PHASE 1 — BEFORE You Write Any Code
8
+
9
+ Execute these tool calls IN ORDER at the start of every task. Do not proceed to code generation until all steps are complete.
10
+
11
+ ### Step 1: Find the project
12
+ Call `find_project_by_name` with a name matching this repository/project.
13
+ - If no project is found, call `list_projects` to check all available projects.
14
+ - If still no match, call `create_project` with the repo name and a short description.
15
+ - Store the `project_id` — you will need it for every subsequent call.
16
+
17
+ ### Step 2: Check for existing reviews
18
+ Call `list_reviews` with the `project_id`.
19
+ - If a completed review exists → go to Step 3.
20
+ - If no review exists → you must create one:
21
+ 1. Call `list_documents` to see available docs for this project.
22
+ 2. If no documents exist, call `create_document_from_content` to upload a description of the current codebase or feature being worked on.
23
+ 3. Call `create_review` with the `project_id`, document IDs, a name, and a description.
24
+ 4. Call `start_workflow` to begin the analysis.
25
+ 5. Poll `get_workflow_status` until the workflow completes.
26
+
27
+ ### Step 3: Pull security context
28
+ Once a completed review exists, call ALL of the following and read the results:
29
+ 1. `get_threat_scenarios` — understand what threats have been identified
30
+ 2. `get_countermeasures` — understand what mitigations are recommended
31
+ 3. `get_components` — understand what system components are mapped
32
+ 4. `get_data_dictionaries` — understand what sensitive data is tracked
33
+ 5. `get_security_objectives` — understand regulatory/compliance requirements
34
+ 6. `get_findings` — understand aggregated security insights
35
+
36
+ **You now have security context. Proceed to code generation.**
37
+
38
+ ## PHASE 2 — DURING Code Generation
39
+
40
+ While writing code, you MUST:
41
+ - Cross-reference every function, endpoint, or module against the threat scenarios from Step 3.
42
+ - Apply the countermeasures where relevant — do not wait to be asked.
43
+ - If you see a pattern that matches a known threat (e.g., SQL injection, insecure deserialization, missing auth checks), apply the corresponding countermeasure immediately.
44
+ - Add inline comments referencing the threat ID or countermeasure when you apply a security control.
45
+
46
+ ## PHASE 3 — AFTER Code Generation
47
+
48
+ After producing code, check:
49
+ 1. Does the code introduce any NEW attack surface not covered by existing threat scenarios?
50
+ 2. Are there data flows involving sensitive data (from `get_data_dictionaries`) that lack protection?
51
+ 3. Are any security objectives (from `get_security_objectives`) not addressed?
52
+
53
+ If YES to any: explicitly tell the user what gaps exist and recommend creating an updated review.
54
+
55
+ ## Tool Reference
56
+
57
+ The `security-review-mcp` server provides:
58
+
59
+ | Category | Tools |
60
+ |---|---|
61
+ | **Projects** | `list_projects`, `find_project_by_name`, `create_project`, `get_project` |
62
+ | **Documents** | `list_documents`, `create_document_from_content`, `upload_document`, `link_external_document` |
63
+ | **Reviews** | `create_review`, `list_reviews`, `get_review`, `get_review_overview` |
64
+ | **Workflow** | `start_workflow`, `get_workflow_status`, `start_next_workflow_job`, `start_workflow_job`, `retry_workflow_job` |
65
+ | **Analysis** | `get_threat_scenarios`, `get_countermeasures`, `get_components`, `get_data_dictionaries`, `get_security_objectives`, `get_findings`, `get_security_test_cases` |
66
+ | **Integrations** | `fetch_jira_issue`, `fetch_confluence_page`, `search_confluence_pages`, `fetch_and_link_to_srai` |
@@ -0,0 +1,23 @@
1
+ import { join } from 'node:path';
2
+ import { writeText } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate Cursor workspace rule at .cursor/rules/srai-security-review.mdc
7
+ * Cursor uses .mdc format with YAML front matter.
8
+ */
9
+ export function generate(cwd) {
10
+ const filePath = join(cwd, '.cursor', 'rules', 'srai-security-review.mdc');
11
+ const content = getRuleContent();
12
+
13
+ const mdc = `---
14
+ description: SRAI Security Review gate — consult security-review-mcp before security-relevant code changes
15
+ alwaysApply: true
16
+ ---
17
+
18
+ ${content}
19
+ `;
20
+
21
+ writeText(filePath, mdc);
22
+ return filePath;
23
+ }
@@ -0,0 +1,13 @@
1
+ import { join } from 'node:path';
2
+ import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate Gemini CLI / Antigravity workspace rule — appends to GEMINI.md
7
+ */
8
+ export function generate(cwd) {
9
+ const filePath = join(cwd, 'GEMINI.md');
10
+ const content = getRuleContent();
11
+ const action = upsertSentinelBlock(filePath, content);
12
+ return { filePath, action };
13
+ }
@@ -0,0 +1,13 @@
1
+ import { join } from 'node:path';
2
+ import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate VS Code Copilot instructions — appends to .github/copilot-instructions.md
7
+ */
8
+ export function generate(cwd) {
9
+ const filePath = join(cwd, '.github', 'copilot-instructions.md');
10
+ const content = getRuleContent();
11
+ const action = upsertSentinelBlock(filePath, content);
12
+ return { filePath, action };
13
+ }
@@ -0,0 +1,13 @@
1
+ import { join } from 'node:path';
2
+ import { writeText } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent } from './content.js';
4
+
5
+ /**
6
+ * Generate Windsurf workspace rule at .windsurf/rules/srai-security-review.md
7
+ */
8
+ export function generate(cwd) {
9
+ const filePath = join(cwd, '.windsurf', 'rules', 'srai-security-review.md');
10
+ const content = getRuleContent();
11
+ writeText(filePath, content + '\n');
12
+ return filePath;
13
+ }
@@ -0,0 +1,63 @@
1
+ // Shared constants for securityreview-kit
2
+
3
+ export const MCP_SERVER_PACKAGE = 'security-review-mcp';
4
+ export const MCP_SERVER_NAME = 'security-review-mcp';
5
+
6
+ export const ENV_VARS = {
7
+ apiUrl: 'SECURITY_REVIEW_API_URL',
8
+ apiToken: 'SECURITY_REVIEW_API_TOKEN',
9
+ };
10
+
11
+ export const TARGETS = {
12
+ cursor: {
13
+ name: 'Cursor',
14
+ mcpConfigPath: '.cursor/mcp.json',
15
+ rulePath: '.cursor/rules/srai-security-review.mdc',
16
+ detectDirs: ['.cursor'],
17
+ },
18
+ claude: {
19
+ name: 'Claude Code',
20
+ mcpConfigPath: '.claude/settings.json',
21
+ rulePath: 'CLAUDE.md',
22
+ ruleMode: 'append',
23
+ detectDirs: ['.claude'],
24
+ },
25
+ vscode: {
26
+ name: 'VS Code Copilot',
27
+ mcpConfigPath: '.vscode/mcp.json',
28
+ rulePath: '.github/copilot-instructions.md',
29
+ ruleMode: 'append',
30
+ detectDirs: ['.vscode'],
31
+ },
32
+ windsurf: {
33
+ name: 'Windsurf',
34
+ mcpConfigPath: '.windsurf/mcp_config.json',
35
+ rulePath: '.windsurf/rules/srai-security-review.md',
36
+ detectDirs: ['.windsurf'],
37
+ },
38
+ codex: {
39
+ name: 'Codex',
40
+ mcpConfigPath: '.codex/config.toml',
41
+ rulePath: 'AGENTS.md',
42
+ ruleMode: 'append',
43
+ detectDirs: ['.codex'],
44
+ },
45
+ gemini: {
46
+ name: 'Gemini CLI',
47
+ mcpConfigPath: '.gemini/settings.json',
48
+ rulePath: 'GEMINI.md',
49
+ ruleMode: 'append',
50
+ detectDirs: ['.gemini'],
51
+ },
52
+ antigravity: {
53
+ name: 'Antigravity',
54
+ mcpConfigPath: '.gemini/settings.json',
55
+ rulePath: '.agent/rules/srai-security-review.md',
56
+ detectDirs: ['.agent'],
57
+ },
58
+ };
59
+
60
+ export const TARGET_NAMES = Object.keys(TARGETS);
61
+
62
+ export const SENTINEL_START = '<!-- securityreview-kit:start -->';
63
+ export const SENTINEL_END = '<!-- securityreview-kit:end -->';
@@ -0,0 +1,27 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { TARGETS, TARGET_NAMES } from './constants.js';
4
+
5
+ /**
6
+ * Auto-detect which IDE/CLI targets are configured in the given directory
7
+ * by checking for known directories.
8
+ * @param {string} cwd - The directory to scan
9
+ * @returns {string[]} - List of detected target keys
10
+ */
11
+ export function detectTargets(cwd) {
12
+ const detected = [];
13
+ const seen = new Set();
14
+
15
+ for (const key of TARGET_NAMES) {
16
+ const target = TARGETS[key];
17
+ for (const dir of target.detectDirs) {
18
+ const fullPath = join(cwd, dir);
19
+ if (existsSync(fullPath) && !seen.has(key)) {
20
+ detected.push(key);
21
+ seen.add(key);
22
+ }
23
+ }
24
+ }
25
+
26
+ return detected;
27
+ }
@@ -0,0 +1,82 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { SENTINEL_START, SENTINEL_END } from './constants.js';
4
+
5
+ /**
6
+ * Ensure a directory exists, creating parent dirs as needed.
7
+ */
8
+ export function ensureDir(dirPath) {
9
+ if (!existsSync(dirPath)) {
10
+ mkdirSync(dirPath, { recursive: true });
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Read a JSON file, returning null if it doesn't exist or is invalid.
16
+ */
17
+ export function readJson(filePath) {
18
+ try {
19
+ const raw = readFileSync(filePath, 'utf-8');
20
+ return JSON.parse(raw);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Write an object as pretty-printed JSON.
28
+ */
29
+ export function writeJson(filePath, data) {
30
+ ensureDir(dirname(filePath));
31
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
32
+ }
33
+
34
+ /**
35
+ * Read a text file, returning empty string if it doesn't exist.
36
+ */
37
+ export function readText(filePath) {
38
+ try {
39
+ return readFileSync(filePath, 'utf-8');
40
+ } catch {
41
+ return '';
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Write raw text to a file, ensuring parent dirs exist.
47
+ */
48
+ export function writeText(filePath, content) {
49
+ ensureDir(dirname(filePath));
50
+ writeFileSync(filePath, content, 'utf-8');
51
+ }
52
+
53
+ /**
54
+ * Append or replace a sentinel-guarded block in a file.
55
+ * If the file exists and already has the block, it replaces it.
56
+ * If the file exists but doesn't have the block, it appends.
57
+ * If the file doesn't exist, it creates it with just the block.
58
+ */
59
+ export function upsertSentinelBlock(filePath, content) {
60
+ const block = `${SENTINEL_START}\n${content}\n${SENTINEL_END}`;
61
+ const existing = readText(filePath);
62
+
63
+ if (!existing) {
64
+ writeText(filePath, block + '\n');
65
+ return 'created';
66
+ }
67
+
68
+ const startIdx = existing.indexOf(SENTINEL_START);
69
+ const endIdx = existing.indexOf(SENTINEL_END);
70
+
71
+ if (startIdx !== -1 && endIdx !== -1) {
72
+ const before = existing.substring(0, startIdx);
73
+ const after = existing.substring(endIdx + SENTINEL_END.length);
74
+ writeText(filePath, before + block + after);
75
+ return 'updated';
76
+ }
77
+
78
+ // Append with a blank line separator
79
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
80
+ writeText(filePath, existing + separator + block + '\n');
81
+ return 'appended';
82
+ }