@securityreviewai/securityreview-kit 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securityreviewai/securityreview-kit",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Bootstrap security-review-mcp for AI IDEs and CLI tools",
5
5
  "author": "Debarshi Das <debarshi.das@we45.com>",
6
6
  "license": "UNLICENSED",
@@ -41,7 +41,7 @@ function normalizeRuleResults(rawResult) {
41
41
  }
42
42
 
43
43
  if (entry && typeof entry.filePath === 'string') {
44
- const allowedKinds = new Set(['rule', 'command', 'agent', 'skill']);
44
+ const allowedKinds = new Set(['rule', 'command', 'agent', 'skill', 'hooks', 'config']);
45
45
  const kind = allowedKinds.has(entry.kind) ? entry.kind : 'rule';
46
46
  return { filePath: entry.filePath, action: entry.action || 'created', kind };
47
47
  }
@@ -336,6 +336,8 @@ export async function initCommand(options) {
336
336
  command: 'Workspace command',
337
337
  agent: 'Workspace agent',
338
338
  skill: 'Workspace skill',
339
+ hooks: 'Hooks',
340
+ config: 'CLI config',
339
341
  };
340
342
  const label = labelByKind[rule.kind] || 'Workspace rule';
341
343
  console.log(chalk.green(` ✓ ${label} → ${rule.filePath} (${rule.action})`));
@@ -63,6 +63,19 @@ export async function statusCommand() {
63
63
  console.log(
64
64
  ` Workspace Rule: ${ruleHasSrai ? chalk.green('✓ Installed') : chalk.dim('✗ Not found')} ${chalk.dim(target.rulePath)}`,
65
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
+ }
66
79
  console.log('');
67
80
  }
68
81
 
@@ -25,7 +25,7 @@ function normalizeRuleResults(rawResult) {
25
25
  }
26
26
 
27
27
  if (entry && typeof entry.filePath === 'string') {
28
- const allowedKinds = new Set(['rule', 'command', 'agent', 'skill']);
28
+ const allowedKinds = new Set(['rule', 'command', 'agent', 'skill', 'hooks', 'config']);
29
29
  const kind = allowedKinds.has(entry.kind) ? entry.kind : 'rule';
30
30
  return { filePath: entry.filePath, action: entry.action || 'created', kind };
31
31
  }
@@ -180,6 +180,8 @@ export async function switchProjectCommand(options = {}) {
180
180
  command: 'Workspace command',
181
181
  agent: 'Workspace agent',
182
182
  skill: 'Workspace skill',
183
+ hooks: 'Hooks',
184
+ config: 'CLI config',
183
185
  };
184
186
  const label = labelByKind[rule.kind] || 'Workspace rule';
185
187
  console.log(chalk.green(` ✓ ${label} → ${rule.filePath} (${rule.action})`));
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { writeText } from '../../utils/fs-helpers.js';
4
- import { GUARDRAILS_PROFILER_SKILL_REL_DIR } from '../../utils/constants.js';
3
+ import { GUARDRAILS_PROFILER_SKILL_REL_DIR, MCP_SERVER_NAME } from '../../utils/constants.js';
4
+ import { readJson, writeJson, writeText } from '../../utils/fs-helpers.js';
5
5
  import {
6
6
  getRuleContent,
7
7
  getProfileCommandContent,
@@ -34,6 +34,32 @@ function writeCursorCommand(filePath, content) {
34
34
  return { filePath, action };
35
35
  }
36
36
 
37
+ /**
38
+ * Merge `.cursor/cli.json` so Cursor CLI / Agent auto-allows security-review-mcp tools (no MCP approval prompts).
39
+ * @see https://cursor.com/docs/cli/reference/permissions
40
+ */
41
+ function mergeCursorCliMcpAllowlist(cwd) {
42
+ const cliPath = join(cwd, '.cursor', 'cli.json');
43
+ const existed = existsSync(cliPath);
44
+ const existing = readJson(cliPath) || {};
45
+ if (!existing.permissions || typeof existing.permissions !== 'object') {
46
+ existing.permissions = {};
47
+ }
48
+ if (!Array.isArray(existing.permissions.allow)) {
49
+ existing.permissions.allow = [];
50
+ }
51
+ const token = `Mcp(${MCP_SERVER_NAME}:*)`;
52
+ if (!existing.permissions.allow.includes(token)) {
53
+ existing.permissions.allow.push(token);
54
+ }
55
+ if (existing.permissions.deny !== undefined && !Array.isArray(existing.permissions.deny)) {
56
+ existing.permissions.deny = [];
57
+ }
58
+ writeJson(cliPath, existing);
59
+ const action = existed ? 'updated' : 'created';
60
+ return { filePath: cliPath, action };
61
+ }
62
+
37
63
  /**
38
64
  * Generate Cursor workspace rules at .cursor/rules/*.mdc
39
65
  * Cursor uses .mdc format with YAML front matter.
@@ -90,6 +116,8 @@ export function generate(cwd, options = {}) {
90
116
  const hooksAction = existsSync(hooksPath) ? 'updated' : 'created';
91
117
  writeText(hooksPath, hooksContent);
92
118
 
119
+ const cliPermissions = mergeCursorCliMcpAllowlist(cwd);
120
+
93
121
  return [
94
122
  { ...baseRule, kind: 'rule' },
95
123
  { ...ctmSyncTriggerRule, kind: 'rule' },
@@ -101,5 +129,6 @@ export function generate(cwd, options = {}) {
101
129
  { ...guardrailsInitProfileCommand, kind: 'command' },
102
130
  { filePath: skillPath, action: skillAction, kind: 'skill' },
103
131
  { filePath: hooksPath, action: hooksAction, kind: 'hooks' },
132
+ { ...cliPermissions, kind: 'config' },
104
133
  ];
105
134
  }
@@ -17,6 +17,8 @@ Configured SRAI project name: `<SRAI_PROJECT_NAME>`
17
17
 
18
18
  Do not skip MCP upload when credentials and MCP are available.
19
19
 
20
+ Do **not** ask the user to verbally approve MCP for `security-review-mcp`. The repo’s **`.cursor/cli.json`** should list `Mcp(security-review-mcp:*)` under `permissions.allow`; call the MCP tools directly.
21
+
20
22
  ## Cursor CLI (scripted)
21
23
 
22
24
  From the repo root, non-interactive runs should include workspace trust, MCP approval, and **streaming progress** (matches default `securityreview-kit init`):
@@ -104,21 +104,13 @@ Create `.guardrails/` if needed and write the profile file.
104
104
 
105
105
  ### Step 6: Build `profile.json` (project root)
106
106
 
107
- Write **`profile.json`** at the project root with this structure (all parts required; use empty strings or `[]` when unknown — never fabricate compliance or user groups):
107
+ Write **`profile.json`** at the project root with **only** these parts:
108
108
 
109
109
  ```json
110
110
  {
111
111
  "schema_version": "2.0",
112
112
  "srai_project_name": "<SRAI_PROJECT_NAME>",
113
113
  "guardrails_profile": {},
114
- "vibe_profile": {
115
- "description": "<short summary of what the repo appears to be, from detected stack only>",
116
- "architecture_notes": [],
117
- "tech_categories": [],
118
- "user_groups": [],
119
- "compliance_requirements": [],
120
- "language_stacks": []
121
- },
122
114
  "default_guardrail_pack": {
123
115
  "guardrail_packs": [],
124
116
  "pack_count": 0
@@ -126,26 +118,22 @@ Write **`profile.json`** at the project root with this structure (all parts requ
126
118
  }
127
119
  ```
128
120
 
129
- Populate `guardrails_profile` with the **same object** written to `.guardrails/profile.json`.
130
-
131
- Populate `default_guardrail_pack.guardrail_packs` with the deduplicated pack id list (same as `guardrails_profile.guardrail_packs`) and `pack_count`.
121
+ - Populate **`guardrails_profile`** with the **same object** written to `.guardrails/profile.json` (detection summary, packs, etc.).
122
+ - Populate **`default_guardrail_pack`** with the deduplicated `guardrail_packs` list (same ids as in `guardrails_profile`) and `pack_count`.
132
123
 
133
- Derive `vibe_profile` fields **only** from what you observed:
134
-
135
- - `tech_categories`: e.g. `backend`, `frontend`, `database`, `cloud`, `mobile` when supported by files/deps.
136
- - `language_stacks`: strings like `TypeScript/Node`, `Python/FastAPI` from detection.
137
- - `architecture_notes`: short bullets (e.g. "Dockerfile present", "GitHub Actions CI") — not speculative threat narratives.
138
- - `user_groups` / `compliance_requirements`: only if explicit in repo (e.g. compliance docs); else `[]`.
124
+ **Do not** add a separate `vibe_profile` block and do **not** populate narrative fields such as long `description`, `architecture_notes`, `tech_categories`, `user_groups`, `compliance_requirements`, or `language_stacks`. The server-facing “vibe” update is driven **only** from the technical **`guardrails_profile`** plus the default pack (see Step 7).
139
125
 
140
126
  ### Step 7: Upload to SecurityReview.ai (security-review-mcp)
141
127
 
142
128
  1. Resolve `project_id`: `find_project_by_name` with `name="<SRAI_PROJECT_NAME>"`. If missing, follow existing kit rules (`list_projects`, `create_project`).
143
129
 
144
- 2. Call **`update_vibe_profile`** with `project_id` and the fields from `profile.json.vibe_profile` (map parameter names to the MCP tool’s expected shape; pass only fields the tool accepts).
130
+ 2. Call **`update_vibe_profile`** with `project_id` and arguments mapped **only** from **`profile.json.guardrails_profile`** (and any required `project_id` fields) per the MCP tool’s documented schema. Treat the guardrails detection object as the profile payload — **not** a separate prose vibe document.
131
+
132
+ 3. Call **`write_default_pack`** with `project_id` and the payload from **`profile.json.default_guardrail_pack`** (match the MCP tool’s schema).
145
133
 
146
- 3. Call **`write_default_pack`** with `project_id` and the default pack payload from `profile.json.default_guardrail_pack` (match the MCP tool’s schema typically the pack id list and metadata the server expects).
134
+ 4. **MCP approval:** Do **not** ask the user to “approve MCP” or “say you approve” for `security-review-mcp`. Security Review Kit installs **`.cursor/cli.json`** with `Mcp(security-review-mcp:*)` in `permissions.allow` so Cursor CLI runs tools without that prompt. Invoke `find_project_by_name`, `update_vibe_profile`, and `write_default_pack` directly. If a call still fails with permissions, report the error and suggest verifying Cursor **auto-run** / CLI permissions — not a conversational approval step.
147
135
 
148
- 4. Confirm success to the user: paths written (`profile.json`, `.guardrails/profile.json`) and that both MCP calls completed or the exact error.
136
+ 5. Confirm success: paths written (`profile.json`, `.guardrails/profile.json`) and whether both MCP calls succeeded, or the exact error.
149
137
 
150
138
  ### Step 8: Report
151
139
 
@@ -157,7 +145,7 @@ If there are no signals:
157
145
 
158
146
  1. Optionally read `.git/config` for hints.
159
147
  2. Emit minimal profile: `owasp-asvs` only, empty summaries where appropriate.
160
- 3. Still write `profile.json` and attempt MCP calls with honest minimal `vibe_profile` data.
148
+ 3. Still write `profile.json` (minimal `guardrails_profile` + `default_guardrail_pack`) and attempt MCP calls.
161
149
 
162
150
  ## IDE-Specific Notes
163
151
 
@@ -0,0 +1,47 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { homedir } from 'node:os';
4
+ import { delimiter, join } from 'node:path';
5
+
6
+ /**
7
+ * Extra PATH segments where Cursor / other CLIs are often installed.
8
+ * GUI-launched terminals load shell rc files; Node subprocesses often do not.
9
+ */
10
+ export function augmentPathEnv(baseEnv = process.env) {
11
+ const home = homedir();
12
+ const extra = [
13
+ join(home, '.local', 'bin'),
14
+ join(home, '.cursor', 'bin'),
15
+ '/opt/homebrew/bin',
16
+ '/usr/local/bin',
17
+ ];
18
+ if (process.platform === 'win32') {
19
+ extra.push(join(home, 'AppData', 'Local', 'Programs', 'cursor'));
20
+ }
21
+ const pathKey = process.platform === 'win32' ? 'Path' : 'PATH';
22
+ const current = baseEnv[pathKey] || baseEnv.PATH || '';
23
+ const merged = [...extra.filter((p) => p), current].filter(Boolean).join(delimiter);
24
+ return { ...baseEnv, [pathKey]: merged, PATH: merged };
25
+ }
26
+
27
+ /**
28
+ * Executable that responds to `cursor-agent --version`, or null.
29
+ */
30
+ export function resolveCursorAgentExecutable() {
31
+ const env = augmentPathEnv();
32
+ const candidates = [
33
+ 'cursor-agent',
34
+ join(homedir(), '.local', 'bin', 'cursor-agent'),
35
+ join(homedir(), '.cursor', 'bin', 'cursor-agent'),
36
+ ];
37
+ for (const cmd of candidates) {
38
+ if (cmd !== 'cursor-agent' && !existsSync(cmd)) {
39
+ continue;
40
+ }
41
+ const r = spawnSync(cmd, ['--version'], { stdio: 'ignore', env });
42
+ if (r.status === 0) {
43
+ return cmd;
44
+ }
45
+ }
46
+ return null;
47
+ }
@@ -1,9 +1,10 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import { augmentPathEnv, resolveCursorAgentExecutable } from './cursor-agent-path.js';
2
3
 
3
4
  const AGENT_CLI_TARGETS = new Set(['cursor', 'claude', 'codex']);
4
5
 
5
- function commandOk(cmd, args = ['--version']) {
6
- const r = spawnSync(cmd, args, { stdio: 'ignore' });
6
+ function commandOk(cmd, args = ['--version'], env = process.env) {
7
+ const r = spawnSync(cmd, args, { stdio: 'ignore', env });
7
8
  return r.status === 0;
8
9
  }
9
10
 
@@ -23,11 +24,16 @@ export function ensureIdeCliForTarget(target, options = {}) {
23
24
  }
24
25
 
25
26
  if (target === 'cursor') {
26
- if (commandOk('cursor-agent', ['--version'])) {
27
+ if (resolveCursorAgentExecutable()) {
27
28
  return { target, ok: true, already: true };
28
29
  }
29
30
  if (skipInstall) {
30
- return { target, ok: false, message: 'cursor-agent not found on PATH' };
31
+ return {
32
+ target,
33
+ ok: false,
34
+ message:
35
+ 'cursor-agent not found (install from https://cursor.com/cli; Node may need ~/.local/bin — kit augments PATH when running the profiler)',
36
+ };
31
37
  }
32
38
  if (process.platform === 'win32') {
33
39
  return {
@@ -37,13 +43,22 @@ export function ensureIdeCliForTarget(target, options = {}) {
37
43
  };
38
44
  }
39
45
  const r = runShell('curl -fsSL https://cursor.com/install | bash');
40
- return r.status === 0
41
- ? { target, ok: true }
42
- : { target, ok: false, message: 'Cursor CLI install script failed' };
46
+ if (r.status !== 0) {
47
+ return { target, ok: false, message: 'Cursor CLI install script failed' };
48
+ }
49
+ if (!resolveCursorAgentExecutable()) {
50
+ return {
51
+ target,
52
+ ok: false,
53
+ message:
54
+ 'cursor-agent not found after install; open a new shell or ensure ~/.local/bin exists and re-run init',
55
+ };
56
+ }
57
+ return { target, ok: true };
43
58
  }
44
59
 
45
60
  if (target === 'claude') {
46
- if (commandOk('claude', ['--version'])) {
61
+ if (commandOk('claude', ['--version'], augmentPathEnv())) {
47
62
  return { target, ok: true, already: true };
48
63
  }
49
64
  if (skipInstall) {
@@ -62,7 +77,7 @@ export function ensureIdeCliForTarget(target, options = {}) {
62
77
  }
63
78
 
64
79
  if (target === 'codex') {
65
- if (commandOk('codex', ['--version'])) {
80
+ if (commandOk('codex', ['--version'], augmentPathEnv())) {
66
81
  return { target, ok: true, already: true };
67
82
  }
68
83
  if (skipInstall) {
@@ -1,10 +1,11 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { GUARDRAILS_PROFILER_SKILL_REL_DIR } from './constants.js';
3
+ import { augmentPathEnv, resolveCursorAgentExecutable } from './cursor-agent-path.js';
3
4
 
4
5
  const PREFERRED_ORDER = ['cursor', 'claude', 'codex'];
5
6
 
6
- function commandOk(cmd, args = ['--version']) {
7
- const r = spawnSync(cmd, args, { stdio: 'ignore' });
7
+ function commandOk(cmd, args = ['--version'], env = process.env) {
8
+ const r = spawnSync(cmd, args, { stdio: 'ignore', env });
8
9
  return r.status === 0;
9
10
  }
10
11
 
@@ -29,11 +30,22 @@ export function buildProfilerAgentPrompt(projectName, agentTarget = 'cursor') {
29
30
  * Call this from init before profiling so the user does not leave the kit flow.
30
31
  */
31
32
  export function runCursorAgentLogin(cwd) {
32
- if (!commandOk('cursor-agent', ['--version'])) {
33
- return { ok: false, status: null, message: 'cursor-agent not on PATH' };
33
+ const bin = resolveCursorAgentExecutable();
34
+ if (!bin) {
35
+ return {
36
+ ok: false,
37
+ status: null,
38
+ message:
39
+ 'cursor-agent not found (install from https://cursor.com/cli and ensure ~/.local/bin is on PATH, or re-run init without --skip-ide-cli-install)',
40
+ };
34
41
  }
35
- const r = spawnSync('cursor-agent', ['login'], { cwd, stdio: 'inherit', env: { ...process.env } });
36
- return { ok: r.status === 0, status: r.status };
42
+ const env = augmentPathEnv(process.env);
43
+ const r = spawnSync(bin, ['login'], { cwd, stdio: 'inherit', env });
44
+ const spawnErr = r.error ? r.error.message : null;
45
+ if (r.status === null && spawnErr) {
46
+ return { ok: false, status: null, message: spawnErr };
47
+ }
48
+ return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
37
49
  }
38
50
 
39
51
  export function pickProfilerAgentTarget(targets) {
@@ -57,7 +69,8 @@ export function pickProfilerAgentTarget(targets) {
57
69
  */
58
70
  export function runProfilerAgent(cwd, { target, projectName, cursorTrust = true, streamProgress = true }) {
59
71
  const prompt = buildProfilerAgentPrompt(projectName, target);
60
- const opts = { cwd, stdio: 'inherit', env: { ...process.env } };
72
+ const env = augmentPathEnv(process.env);
73
+ const opts = { cwd, stdio: 'inherit', env };
61
74
 
62
75
  if (streamProgress) {
63
76
  console.error(
@@ -67,8 +80,13 @@ export function runProfilerAgent(cwd, { target, projectName, cursorTrust = true,
67
80
  }
68
81
 
69
82
  if (target === 'cursor') {
70
- if (!commandOk('cursor-agent', ['--version'])) {
71
- return { ok: false, message: 'cursor-agent not on PATH' };
83
+ const bin = resolveCursorAgentExecutable();
84
+ if (!bin) {
85
+ return {
86
+ ok: false,
87
+ message:
88
+ 'cursor-agent not found. Install via Cursor CLI (https://cursor.com/cli) or run init Step 1b without --skip-ide-cli-install; ensure your shell PATH includes ~/.local/bin (Node may not inherit it).',
89
+ };
72
90
  }
73
91
  const args = ['-p', prompt];
74
92
  if (streamProgress) {
@@ -77,12 +95,15 @@ export function runProfilerAgent(cwd, { target, projectName, cursorTrust = true,
77
95
  if (cursorTrust) {
78
96
  args.push('--trust', '--approve-mcps');
79
97
  }
80
- const r = spawnSync('cursor-agent', args, opts);
98
+ const r = spawnSync(bin, args, opts);
99
+ if (r.error) {
100
+ return { ok: false, message: r.error.message };
101
+ }
81
102
  return { ok: r.status === 0, status: r.status };
82
103
  }
83
104
 
84
105
  if (target === 'claude') {
85
- if (!commandOk('claude', ['--version'])) {
106
+ if (!commandOk('claude', ['--version'], env)) {
86
107
  return { ok: false, message: 'claude not on PATH' };
87
108
  }
88
109
  const args = streamProgress
@@ -93,7 +114,7 @@ export function runProfilerAgent(cwd, { target, projectName, cursorTrust = true,
93
114
  }
94
115
 
95
116
  if (target === 'codex') {
96
- if (!commandOk('codex', ['--version'])) {
117
+ if (!commandOk('codex', ['--version'], env)) {
97
118
  return { ok: false, message: 'codex not on PATH' };
98
119
  }
99
120
  const args = streamProgress ? ['exec', '--json', prompt] : ['exec', prompt];