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 +78 -0
- package/bin/securityreview-kit.js +5 -0
- package/package.json +44 -0
- package/src/cli.js +47 -0
- package/src/commands/init.js +225 -0
- package/src/commands/status.js +86 -0
- package/src/generators/mcp/claude.js +27 -0
- package/src/generators/mcp/codex.js +44 -0
- package/src/generators/mcp/cursor.js +27 -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 +13 -0
- package/src/generators/rules/codex.js +13 -0
- package/src/generators/rules/content.js +12 -0
- package/src/generators/rules/content.md +66 -0
- package/src/generators/rules/cursor.js +23 -0
- package/src/generators/rules/gemini.js +13 -0
- package/src/generators/rules/vscode.js +13 -0
- package/src/generators/rules/windsurf.js +13 -0
- package/src/utils/constants.js +63 -0
- package/src/utils/detect.js +27 -0
- package/src/utils/fs-helpers.js +82 -0
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.
|
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
|
+
}
|