@securityreviewai/securityreview-kit 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +11 -0
- package/src/commands/init.js +149 -7
- package/src/generators/rules/claude.js +11 -3
- package/src/generators/rules/codex.js +11 -3
- package/src/generators/rules/content.js +7 -0
- package/src/generators/rules/content.md +1 -1
- package/src/generators/rules/ctm_sync.md +1 -10
- package/src/generators/rules/cursor.js +8 -0
- package/src/generators/rules/guardrails-init-profile.md +28 -0
- package/src/generators/rules/guardrails-profiler/SKILL.md +163 -0
- package/src/generators/rules/guardrails-profiler/references/signal-registry.json +514 -0
- package/src/utils/guardrails-profiler-bundle.js +33 -0
- package/src/utils/ide-cli-install.js +82 -0
- package/src/utils/profiler-agent.js +85 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -26,6 +26,17 @@ export function run() {
|
|
|
26
26
|
.option('--switch-project', 'Fetch projects and only update mapped workspace rules')
|
|
27
27
|
.option('--skip-mcp', 'Skip MCP server config installation')
|
|
28
28
|
.option('--skip-rules', 'Skip workspace rule installation')
|
|
29
|
+
.option('--skip-ide-cli-install', 'Do not install Cursor / Claude Code / Codex CLIs when those targets are selected')
|
|
30
|
+
.option('--profile-repo', 'After init, run the guardrails profiler agent (non-interactive; needs cursor, claude, or codex target)')
|
|
31
|
+
.option('--no-profile-repo', 'Skip the optional profiler agent step after init')
|
|
32
|
+
.option(
|
|
33
|
+
'--profiler-no-trust',
|
|
34
|
+
'When profiling with Cursor, do not pass --trust (use if you need interactive workspace trust or login in the terminal)',
|
|
35
|
+
)
|
|
36
|
+
.option(
|
|
37
|
+
'--profiler-cursor-login',
|
|
38
|
+
'Before Cursor profiling, run cursor-agent login in this terminal (then profiling runs in the same init)',
|
|
39
|
+
)
|
|
29
40
|
.action(async (options) => {
|
|
30
41
|
try {
|
|
31
42
|
if (options.switchProject) {
|
package/src/commands/init.js
CHANGED
|
@@ -2,6 +2,13 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { input, checkbox, confirm, select } from '@inquirer/prompts';
|
|
3
3
|
import { TARGETS, TARGET_NAMES } from '../utils/constants.js';
|
|
4
4
|
import { detectTargets } from '../utils/detect.js';
|
|
5
|
+
import { ensureIdeClisForTargets } from '../utils/ide-cli-install.js';
|
|
6
|
+
import { writeGuardrailsProfilerBundle } from '../utils/guardrails-profiler-bundle.js';
|
|
7
|
+
import {
|
|
8
|
+
pickProfilerAgentTarget,
|
|
9
|
+
runCursorAgentLogin,
|
|
10
|
+
runProfilerAgent,
|
|
11
|
+
} from '../utils/profiler-agent.js';
|
|
5
12
|
import { fetchVibeReviewProjectNames, getStoredCredentials, normalizeApiUrl } from '../utils/srai.js';
|
|
6
13
|
|
|
7
14
|
// Dynamic imports for generators (avoids loading all at startup)
|
|
@@ -220,18 +227,34 @@ export async function initCommand(options) {
|
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
// Step 1: Resolve targets
|
|
223
|
-
console.log(chalk.bold.white(' Step 1 of
|
|
230
|
+
console.log(chalk.bold.white(' Step 1 of 5: Select Targets'));
|
|
224
231
|
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
225
232
|
const targets = await resolveTargets(options, interactive, cwd);
|
|
226
233
|
console.log(chalk.green(` ✓ Targets: ${targets.map((t) => TARGETS[t].name).join(', ')}`));
|
|
227
234
|
console.log('');
|
|
235
|
+
console.log(chalk.bold.white(' Step 1b: IDE / agent CLIs'));
|
|
236
|
+
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
237
|
+
const cliResults = ensureIdeClisForTargets(targets, { skipInstall: options.skipIdeCliInstall });
|
|
238
|
+
for (const r of cliResults) {
|
|
239
|
+
if (r.skipped) continue;
|
|
240
|
+
const label = TARGETS[r.target]?.name || r.target;
|
|
241
|
+
if (r.already) {
|
|
242
|
+
console.log(chalk.dim(` • ${label} CLI already available`));
|
|
243
|
+
} else if (r.ok) {
|
|
244
|
+
console.log(chalk.green(` \u2713 ${label} CLI install attempted`));
|
|
245
|
+
} else {
|
|
246
|
+
console.log(chalk.yellow(` \u26a0 ${label} CLI: ${r.message || 'install skipped or failed'}`));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
console.log('');
|
|
250
|
+
|
|
228
251
|
|
|
229
252
|
// Step 2: What to install (rules question first, then MCP)
|
|
230
253
|
let installMcp = !options.skipMcp;
|
|
231
254
|
let installRules = !options.skipRules;
|
|
232
255
|
|
|
233
256
|
if (interactive) {
|
|
234
|
-
console.log(chalk.bold.white(' Step 2 of
|
|
257
|
+
console.log(chalk.bold.white(' Step 2 of 5: Installation Options'));
|
|
235
258
|
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
236
259
|
installRules = await confirm({
|
|
237
260
|
message: '📋 Install workspace security rules?',
|
|
@@ -252,19 +275,19 @@ export async function initCommand(options) {
|
|
|
252
275
|
// Step 3: Resolve credentials (only needed when MCP is being installed)
|
|
253
276
|
let envVars = { apiUrl: '', apiToken: '' };
|
|
254
277
|
if (installMcp) {
|
|
255
|
-
console.log(chalk.bold.white(' Step 3 of
|
|
278
|
+
console.log(chalk.bold.white(' Step 3 of 5: SRAI Credentials'));
|
|
256
279
|
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
257
280
|
envVars = await resolveEnvVars(options, interactive, cwd);
|
|
258
281
|
console.log(chalk.green(' ✓ Credentials configured'));
|
|
259
282
|
} else {
|
|
260
|
-
console.log(chalk.dim(' Step 3 of
|
|
283
|
+
console.log(chalk.dim(' Step 3 of 5: Skipping SRAI credentials (MCP not selected).'));
|
|
261
284
|
}
|
|
262
285
|
console.log('');
|
|
263
286
|
|
|
264
287
|
// Step 4: Resolve project name (only needed when MCP is being installed)
|
|
265
288
|
let projectName = options.projectName || process.env.SECURITY_REVIEW_PROJECT_NAME || '';
|
|
266
289
|
if (installMcp) {
|
|
267
|
-
console.log(chalk.bold.white(' Step 4 of
|
|
290
|
+
console.log(chalk.bold.white(' Step 4 of 5: SRAI Project Mapping'));
|
|
268
291
|
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
269
292
|
projectName = await resolveProjectName(options, interactive, envVars.apiUrl, envVars.apiToken);
|
|
270
293
|
if (projectName) {
|
|
@@ -273,11 +296,14 @@ export async function initCommand(options) {
|
|
|
273
296
|
console.log(chalk.dim(' No project name provided. Rules will use a generic project lookup instruction.'));
|
|
274
297
|
}
|
|
275
298
|
} else {
|
|
276
|
-
console.log(chalk.dim(' Step 4 of
|
|
299
|
+
console.log(chalk.dim(' Step 4 of 5: Skipping SRAI project mapping (MCP not selected).'));
|
|
277
300
|
}
|
|
278
301
|
console.log('');
|
|
279
302
|
|
|
280
|
-
|
|
303
|
+
const projectNameForSkill =
|
|
304
|
+
(projectName || options.projectName || process.env.SECURITY_REVIEW_PROJECT_NAME || '').trim();
|
|
305
|
+
|
|
306
|
+
console.log(chalk.bold.white(' Step 5 of 5: Installing workspace files'));
|
|
281
307
|
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
282
308
|
|
|
283
309
|
const results = [];
|
|
@@ -322,6 +348,17 @@ export async function initCommand(options) {
|
|
|
322
348
|
}
|
|
323
349
|
}
|
|
324
350
|
|
|
351
|
+
if (installRules) {
|
|
352
|
+
try {
|
|
353
|
+
const bundlePath = writeGuardrailsProfilerBundle(cwd, { projectName: projectNameForSkill });
|
|
354
|
+
console.log(chalk.green(` \u2713 Guardrails profiler bundle → ${bundlePath}`));
|
|
355
|
+
results.push({ target: 'kit', type: 'bundle', status: 'ok', path: bundlePath });
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.log(chalk.red(` \u2717 Guardrails profiler bundle failed: ${err.message}`));
|
|
358
|
+
results.push({ target: 'kit', type: 'bundle', status: 'error', error: err.message });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
325
362
|
// Summary
|
|
326
363
|
const ok = results.filter((r) => r.status === 'ok').length;
|
|
327
364
|
const errors = results.filter((r) => r.status === 'error').length;
|
|
@@ -335,6 +372,111 @@ export async function initCommand(options) {
|
|
|
335
372
|
chalk.bold.yellow(` ⚠ Done with ${errors} error(s). ${ok} configuration(s) installed.`),
|
|
336
373
|
);
|
|
337
374
|
}
|
|
375
|
+
console.log('');
|
|
376
|
+
|
|
377
|
+
const profilerEligible =
|
|
378
|
+
installMcp && installRules && projectNameForSkill && options.profileRepo !== false;
|
|
379
|
+
|
|
380
|
+
let shouldProfile = false;
|
|
381
|
+
if (profilerEligible) {
|
|
382
|
+
if (interactive) {
|
|
383
|
+
shouldProfile = await confirm({
|
|
384
|
+
message:
|
|
385
|
+
'Profile this repository and push the default guardrail pack to SecurityReview.ai (runs your IDE agent CLI)?',
|
|
386
|
+
default: true,
|
|
387
|
+
});
|
|
388
|
+
} else {
|
|
389
|
+
shouldProfile = Boolean(options.profileRepo);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (shouldProfile) {
|
|
394
|
+
const agentTarget = pickProfilerAgentTarget(targets);
|
|
395
|
+
if (!agentTarget) {
|
|
396
|
+
console.log(
|
|
397
|
+
chalk.yellow(
|
|
398
|
+
' \u26a0 Profiling needs Cursor, Claude Code, or Codex in your targets. Add one and re-run, or run the guardrails-init-profile command in your IDE.',
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
} else {
|
|
402
|
+
console.log('');
|
|
403
|
+
console.log(chalk.bold.white(` Starting profiler via ${TARGETS[agentTarget].name} CLI…`));
|
|
404
|
+
if (agentTarget === 'cursor') {
|
|
405
|
+
console.log(
|
|
406
|
+
chalk.dim(
|
|
407
|
+
' Cursor: headless profiling uses `--trust` and `--approve-mcps` on this folder (you confirmed above).',
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
if (options.profilerNoTrust) {
|
|
411
|
+
console.log(
|
|
412
|
+
chalk.dim(
|
|
413
|
+
' You passed `--profiler-no-trust`: complete any login or workspace-trust prompts in this terminal.',
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let runLogin = Boolean(options.profilerCursorLogin);
|
|
419
|
+
if (!runLogin && interactive) {
|
|
420
|
+
runLogin = await confirm({
|
|
421
|
+
message:
|
|
422
|
+
'Run Cursor Agent login in this terminal now? (Same init — profiling runs next. Choose No if already signed in.)',
|
|
423
|
+
default: true,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (runLogin) {
|
|
427
|
+
console.log('');
|
|
428
|
+
console.log(chalk.bold.white(' Cursor Agent login'));
|
|
429
|
+
console.log(chalk.dim(' Complete the browser or code prompt, then return here.\n'));
|
|
430
|
+
const loginResult = runCursorAgentLogin(cwd);
|
|
431
|
+
if (loginResult.ok) {
|
|
432
|
+
console.log(chalk.green(' \u2713 Cursor Agent login step finished.'));
|
|
433
|
+
} else {
|
|
434
|
+
console.log(
|
|
435
|
+
chalk.yellow(
|
|
436
|
+
` \u26a0 Login exited with status ${loginResult.status ?? 'unknown'}. Profiling will still be attempted; sign in and re-run init if it fails.`,
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
console.log('');
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
console.log(chalk.dim(' (Sign-in or approvals may be required in your terminal.)'));
|
|
444
|
+
}
|
|
445
|
+
console.log('');
|
|
446
|
+
const pr = runProfilerAgent(cwd, {
|
|
447
|
+
target: agentTarget,
|
|
448
|
+
projectName: projectNameForSkill,
|
|
449
|
+
cursorTrust: !options.profilerNoTrust,
|
|
450
|
+
});
|
|
451
|
+
if (pr.ok) {
|
|
452
|
+
console.log(chalk.green(' \u2713 Profiler agent finished.'));
|
|
453
|
+
} else {
|
|
454
|
+
const detail =
|
|
455
|
+
pr.message ||
|
|
456
|
+
(typeof pr.status === 'number' ? `exit status ${pr.status}` : 'unknown error');
|
|
457
|
+
console.log(
|
|
458
|
+
chalk.yellow(
|
|
459
|
+
` \u26a0 Profiler agent exited with an error: ${detail}. You can run the guardrails-init-profile workflow manually.`,
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
if (agentTarget === 'cursor') {
|
|
463
|
+
console.log('');
|
|
464
|
+
console.log(chalk.dim(' Typical fixes:'));
|
|
465
|
+
console.log(
|
|
466
|
+
chalk.dim(
|
|
467
|
+
' • Not signed in: re-run `securityreview-kit init` and choose Yes for “Run Cursor Agent login”, or pass `--profiler-cursor-login` with `--profile-repo`.',
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
console.log(
|
|
471
|
+
chalk.dim(
|
|
472
|
+
' • Want interactive trust instead of `--trust`: run `securityreview-kit init ... --profiler-no-trust` and answer the prompts, then profile again.',
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
338
480
|
console.log('');
|
|
339
481
|
console.log(chalk.dim(' Run `securityreview-kit status` to verify your setup.'));
|
|
340
482
|
console.log('');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
|
|
3
|
-
import { getRuleContent } from './content.js';
|
|
2
|
+
import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
|
|
3
|
+
import { getRuleContent, getGuardrailsInitProfileContent } from './content.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Generate Claude Code workspace rule — appends to CLAUDE.md
|
|
@@ -9,5 +9,13 @@ export function generate(cwd, options = {}) {
|
|
|
9
9
|
const filePath = join(cwd, 'CLAUDE.md');
|
|
10
10
|
const content = getRuleContent(options);
|
|
11
11
|
const action = upsertSentinelBlock(filePath, content);
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
const guardrailsInitPath = join(cwd, '.claude', 'commands', 'guardrails-init-profile.md');
|
|
14
|
+
const guardrailsInitContent = getGuardrailsInitProfileContent(options);
|
|
15
|
+
writeText(guardrailsInitPath, guardrailsInitContent);
|
|
16
|
+
|
|
17
|
+
return [
|
|
18
|
+
{ filePath, action, kind: 'rule' },
|
|
19
|
+
{ filePath: guardrailsInitPath, action: 'created', kind: 'command' },
|
|
20
|
+
];
|
|
13
21
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
|
|
3
|
-
import { getRuleContent } from './content.js';
|
|
2
|
+
import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
|
|
3
|
+
import { getRuleContent, getGuardrailsInitProfileContent } from './content.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Generate Codex workspace rule — appends to AGENTS.md
|
|
@@ -9,5 +9,13 @@ export function generate(cwd, options = {}) {
|
|
|
9
9
|
const filePath = join(cwd, 'AGENTS.md');
|
|
10
10
|
const content = getRuleContent(options);
|
|
11
11
|
const action = upsertSentinelBlock(filePath, content);
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
const guardrailsInitPath = join(cwd, '.codex', 'commands', 'guardrails-init-profile.md');
|
|
14
|
+
const guardrailsInitContent = getGuardrailsInitProfileContent(options);
|
|
15
|
+
writeText(guardrailsInitPath, guardrailsInitContent);
|
|
16
|
+
|
|
17
|
+
return [
|
|
18
|
+
{ filePath, action, kind: 'rule' },
|
|
19
|
+
{ filePath: guardrailsInitPath, action: 'created', kind: 'command' },
|
|
20
|
+
];
|
|
13
21
|
}
|
|
@@ -71,6 +71,13 @@ export function getGuardrailsRuleContent(options = {}) {
|
|
|
71
71
|
return readTemplate('guardrails_rule.md', options);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Guardrails init profile command (repo scan + profile.json + SRAI upload).
|
|
76
|
+
*/
|
|
77
|
+
export function getGuardrailsInitProfileContent(options = {}) {
|
|
78
|
+
return readTemplate('guardrails-init-profile.md', options);
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
/**
|
|
75
82
|
* Returns the hooks.json content for Cursor session hooks.
|
|
76
83
|
*/
|
|
@@ -119,4 +119,4 @@ When running `ctm_sync` (dedicated agent/command where available, or the same st
|
|
|
119
119
|
| **Analysis** | `get_threat_scenarios`, `get_countermeasures`, `get_components`, `get_data_dictionaries`, `get_security_objectives`, `get_findings`, `get_security_test_cases` |
|
|
120
120
|
| **Integrations** | `fetch_jira_issue`, `fetch_confluence_page`, `search_confluence_pages`, `fetch_and_link_to_srai` |
|
|
121
121
|
| **AI IDE CTM** | `create_ai_ide_workflow`, `create_ai_ide_event` (and any `list_*` AI IDE workflow tools exposed by the server) |
|
|
122
|
-
| **
|
|
122
|
+
| **Vibe profile & default packs** | `update_vibe_profile`, `write_default_pack` (used by the guardrails profiler / init flow — not part of `ctm_sync`) |
|
|
@@ -34,14 +34,7 @@ When invoked:
|
|
|
34
34
|
6. **Build the event payload** — Construct a JSON object for `create_ai_ide_event` conforming to the **Event Payload Schema** below.
|
|
35
35
|
7. **Upload the payload** using `security-review-mcp`:
|
|
36
36
|
- Call `create_ai_ide_event` with the JSON payload.
|
|
37
|
-
|
|
38
|
-
- `description`: a concise summary of the system/project derived from the threat model context (what it does, what data it handles, its overall purpose)
|
|
39
|
-
- `architecture_notes`: a list of architecture observations extracted from the threat model — deployment topology, trust boundaries, data flows, integration points
|
|
40
|
-
- `tech_categories`: a list of technology category labels identified during threat modeling (e.g. `"backend"`, `"frontend"`, `"database"`, `"cloud"`, `"mobile"`)
|
|
41
|
-
- `user_groups`: a list of user roles and groups surfaced in the threat model (e.g. `"admin"`, `"end user"`, `"service account"`)
|
|
42
|
-
- `compliance_requirements`: a list of compliance requirements or standards referenced in the threat model or conversation (e.g. `"PCI-DSS"`, `"HIPAA"`, `"SOC 2"`)
|
|
43
|
-
- `language_stacks`: a list of programming languages, runtimes, and major frameworks identified in the codebase or threat model context (e.g. `"Python/FastAPI"`, `"TypeScript/React"`, `"Java/Spring"`)
|
|
44
|
-
- Omit any field for which no data is available in context — do **not** invent values. Pass an empty list `[]` only if the field is reasonably expected but simply unpopulated.
|
|
37
|
+
- **Stop here.** Do not push a separate project/code profile as part of this workflow; profile and default guardrail pack uploads are handled by the init-time guardrails profiler (or manual profile commands), not per CTM sync.
|
|
45
38
|
|
|
46
39
|
---
|
|
47
40
|
|
|
@@ -155,8 +148,6 @@ Use the following IDs and names exactly when populating `owasp_top_10_2025_mappi
|
|
|
155
148
|
- Never skip upload when a threat model exists.
|
|
156
149
|
- Never invent missing values; use empty strings/arrays if data is unavailable.
|
|
157
150
|
- Never omit `chat_session_id` from the payload.
|
|
158
|
-
- Never skip the `update_vibe_project_profile` call when profile-relevant data (architecture, tech, users, compliance, languages, or description) can be derived from context.
|
|
159
151
|
- Return a compact confirmation after upload including:
|
|
160
152
|
- Whether an existing workflow was reused or a new named workflow was created
|
|
161
|
-
- Confirmation that the project profile was updated
|
|
162
153
|
- Count of guardrails applied (existing vs IDE-generated)
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getCreateIdeWorkflowCommandContent,
|
|
10
10
|
getThreatModellingSkillContent,
|
|
11
11
|
getGuardrailsRuleContent,
|
|
12
|
+
getGuardrailsInitProfileContent,
|
|
12
13
|
getHooksContent,
|
|
13
14
|
} from './content.js';
|
|
14
15
|
|
|
@@ -44,6 +45,7 @@ export function generate(cwd, options = {}) {
|
|
|
44
45
|
const ctmSyncAgentPath = join(cwd, '.cursor', 'agents', 'ctm_sync.md');
|
|
45
46
|
const createIdeWorkflowCommandPath = join(cwd, '.cursor', 'commands', 'create-ide-workflow.md');
|
|
46
47
|
const profileCommandPath = join(cwd, '.cursor', 'commands', 'srai-profile.md');
|
|
48
|
+
const guardrailsInitProfileCommandPath = join(cwd, '.cursor', 'commands', 'guardrails-init-profile.md');
|
|
47
49
|
const skillPath = join(cwd, '.cursor', 'skills', 'threat-modelling', 'SKILL.md');
|
|
48
50
|
const hooksPath = join(cwd, '.cursor', 'hooks.json');
|
|
49
51
|
|
|
@@ -53,6 +55,7 @@ export function generate(cwd, options = {}) {
|
|
|
53
55
|
const ctmSyncWorkflowContent = getCtmSyncWorkflowContent(options);
|
|
54
56
|
const createIdeWorkflowCommandContent = getCreateIdeWorkflowCommandContent(options);
|
|
55
57
|
const profileCommandContent = getProfileCommandContent(options);
|
|
58
|
+
const guardrailsInitProfileCommandContent = getGuardrailsInitProfileContent(options);
|
|
56
59
|
const skillContent = getThreatModellingSkillContent(options);
|
|
57
60
|
|
|
58
61
|
const baseRule = writeCursorRule(
|
|
@@ -72,6 +75,10 @@ export function generate(cwd, options = {}) {
|
|
|
72
75
|
const createIdeWorkflowCommand = writeCursorCommand(createIdeWorkflowCommandPath, createIdeWorkflowCommandContent);
|
|
73
76
|
|
|
74
77
|
const profileCommand = writeCursorCommand(profileCommandPath, profileCommandContent);
|
|
78
|
+
const guardrailsInitProfileCommand = writeCursorCommand(
|
|
79
|
+
guardrailsInitProfileCommandPath,
|
|
80
|
+
guardrailsInitProfileCommandContent,
|
|
81
|
+
);
|
|
75
82
|
const skillAction = existsSync(skillPath) ? 'updated' : 'created';
|
|
76
83
|
writeText(skillPath, skillContent);
|
|
77
84
|
|
|
@@ -87,6 +94,7 @@ export function generate(cwd, options = {}) {
|
|
|
87
94
|
{ ...ctmSyncAgent, kind: 'agent' },
|
|
88
95
|
{ ...createIdeWorkflowCommand, kind: 'command' },
|
|
89
96
|
{ ...profileCommand, kind: 'command' },
|
|
97
|
+
{ ...guardrailsInitProfileCommand, kind: 'command' },
|
|
90
98
|
{ filePath: skillPath, action: skillAction, kind: 'skill' },
|
|
91
99
|
{ filePath: hooksPath, action: hooksAction, kind: 'hooks' },
|
|
92
100
|
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: guardrails-init-profile
|
|
3
|
+
description: Run the Security Review Kit guardrails profiler — scan the repo, write profile.json, push profile and default guardrail pack to SecurityReview.ai via MCP.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Guardrails init profile
|
|
7
|
+
|
|
8
|
+
Execute the workflow defined in **`.securityreview-kit/guardrails-profiler/SKILL.md`** end-to-end in this workspace.
|
|
9
|
+
|
|
10
|
+
Configured SRAI project name: `<SRAI_PROJECT_NAME>`
|
|
11
|
+
|
|
12
|
+
**You must:**
|
|
13
|
+
|
|
14
|
+
1. Read `.securityreview-kit/guardrails-profiler/SKILL.md` and follow every step (use the signal registry at `.securityreview-kit/guardrails-profiler/references/signal-registry.json`).
|
|
15
|
+
2. Write `.guardrails/profile.json` and **`profile.json`** at the project root as specified.
|
|
16
|
+
3. Call **`update_vibe_profile`** and **`write_default_pack`** on `security-review-mcp` after resolving `project_id` for `<SRAI_PROJECT_NAME>`.
|
|
17
|
+
|
|
18
|
+
Do not skip MCP upload when credentials and MCP are available.
|
|
19
|
+
|
|
20
|
+
## Cursor CLI (scripted)
|
|
21
|
+
|
|
22
|
+
From the repo root, non-interactive runs should include workspace trust and MCP approval (matches `securityreview-kit init`):
|
|
23
|
+
|
|
24
|
+
`cursor-agent -p "<your profiling instructions>" --trust --approve-mcps`
|
|
25
|
+
|
|
26
|
+
During `securityreview-kit init`, choose **Yes** when asked to run Cursor login in-terminal, or pass **`--profiler-cursor-login`** with **`--profile-repo`** so login and profiling stay in one run.
|
|
27
|
+
|
|
28
|
+
You can still sign in manually with `cursor-agent login`. To handle trust/login interactively in the terminal, omit `--trust` and `--approve-mcps`.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: guardrails-profiler
|
|
3
|
+
description: Profile a codebase to detect its technology stack and generate a guardrails profile for security-aware AI code generation, then publish the profile and default guardrail pack to SecurityReview.ai via security-review-mcp. Use when Security Review Kit init runs profiling, when `.guardrails/profile.json` is missing, or when the developer asks to profile or re-profile the project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Guardrails Profiler
|
|
7
|
+
|
|
8
|
+
Profile a codebase's technology stack, write `.guardrails/profile.json`, write a combined **`profile.json` in the project root**, and upload to SRAI using `update_vibe_profile` and `write_default_pack`.
|
|
9
|
+
|
|
10
|
+
Configured SRAI project name: `<SRAI_PROJECT_NAME>`
|
|
11
|
+
|
|
12
|
+
## Canonical paths
|
|
13
|
+
|
|
14
|
+
- **Signal registry (read-only):** `.securityreview-kit/guardrails-profiler/references/signal-registry.json`
|
|
15
|
+
- **Local guardrails file:** `.guardrails/profile.json`
|
|
16
|
+
- **Combined manifest (project root):** `profile.json` — includes guardrails profile, vibe profile fields for MCP, and default pack payload
|
|
17
|
+
|
|
18
|
+
## When This Runs
|
|
19
|
+
|
|
20
|
+
1. **Kit init**: User opted in to profile the repo and push the default pack.
|
|
21
|
+
2. **First-run / missing profile**: No `.guardrails/profile.json` and guardrails are needed before threat modeling.
|
|
22
|
+
3. **Explicit re-profile**: Developer asks to refresh the profile.
|
|
23
|
+
|
|
24
|
+
## Quick Check: Should I Profile?
|
|
25
|
+
|
|
26
|
+
Before profiling, check if a profile already exists:
|
|
27
|
+
|
|
28
|
+
- If `.guardrails/profile.json` exists and has a valid `schema_version`: **SKIP** unless the developer asked to re-profile.
|
|
29
|
+
- If missing: **PROCEED**.
|
|
30
|
+
|
|
31
|
+
## Profiling Procedure
|
|
32
|
+
|
|
33
|
+
Follow these steps in order.
|
|
34
|
+
|
|
35
|
+
### Step 1: Locate the Project Root
|
|
36
|
+
|
|
37
|
+
The project root is the current working directory. Confirm with markers such as `.git/`, `package.json`, `go.mod`, etc.
|
|
38
|
+
|
|
39
|
+
### Step 2: Read the Signal Registry
|
|
40
|
+
|
|
41
|
+
Read `.securityreview-kit/guardrails-profiler/references/signal-registry.json`. Use its categories (`universal`, `languages`, `frameworks`, `auth_identity`, `ai_agent`, `infrastructure`, `ci_cd`, `cloud_compute`, `databases`, `messaging`, `api_protocols`, etc.) to map detected signals to guardrail pack IDs.
|
|
42
|
+
|
|
43
|
+
### Step 3: Scan for Signals
|
|
44
|
+
|
|
45
|
+
Same methodology as the upstream guardrails-profiler skill:
|
|
46
|
+
|
|
47
|
+
#### 3a. Manifest and Config File Detection
|
|
48
|
+
|
|
49
|
+
List files in the project root (1–2 levels deep). Detect manifests (`package.json`, `pyproject.toml`, `go.mod`, `pom.xml`, `Dockerfile`, `.github/workflows/`, `next.config.*`, etc.) per the registry.
|
|
50
|
+
|
|
51
|
+
#### 3b. Dependency Parsing
|
|
52
|
+
|
|
53
|
+
For each manifest found, read dependencies and match names against `dependency_signals` in the registry. Prefer manifests over extension-only guesses.
|
|
54
|
+
|
|
55
|
+
#### 3c. Content Signals (targeted only)
|
|
56
|
+
|
|
57
|
+
When needed, grep specific files (e.g. Terraform providers, K8s `apiVersion`, CloudFormation) — do not read the entire repository.
|
|
58
|
+
|
|
59
|
+
#### 3d. File Extension Fallback
|
|
60
|
+
|
|
61
|
+
If no manifest exists for a language, use dominant extensions as a last resort.
|
|
62
|
+
|
|
63
|
+
### Step 4: Assemble the Guardrails Profile Object
|
|
64
|
+
|
|
65
|
+
Build the object for `.guardrails/profile.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"schema_version": "1.0",
|
|
70
|
+
"project_name": "<directory name>",
|
|
71
|
+
"profiled_at": "<ISO 8601 timestamp>",
|
|
72
|
+
"profiled_by": "<ide or cli id, e.g. cursor-agent, claude, codex>",
|
|
73
|
+
"detection_summary": {
|
|
74
|
+
"languages": [],
|
|
75
|
+
"frameworks": [],
|
|
76
|
+
"infrastructure": [],
|
|
77
|
+
"databases": [],
|
|
78
|
+
"auth": [],
|
|
79
|
+
"ai_agent": [],
|
|
80
|
+
"ci_cd": [],
|
|
81
|
+
"cloud_compute": [],
|
|
82
|
+
"messaging": [],
|
|
83
|
+
"api_protocols": [],
|
|
84
|
+
"mobile": false
|
|
85
|
+
},
|
|
86
|
+
"guardrail_packs": [],
|
|
87
|
+
"pack_count": 0
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Rules for `guardrail_packs`:
|
|
92
|
+
|
|
93
|
+
1. Always include `owasp-asvs`.
|
|
94
|
+
2. Include `owasp-masvs` if mobile stacks are detected (flutter, react-native, swift, objective-c, kotlin per registry).
|
|
95
|
+
3. Add language, framework, auth, AI, infra, CI/CD, cloud, DB, messaging, and API packs per registry matches.
|
|
96
|
+
4. Deduplicate and set `pack_count`.
|
|
97
|
+
|
|
98
|
+
Do **not** invent packs or signals; if the repo is empty, use universal baseline only and empty category arrays where appropriate.
|
|
99
|
+
|
|
100
|
+
### Step 5: Write `.guardrails/profile.json`
|
|
101
|
+
|
|
102
|
+
Create `.guardrails/` if needed and write the profile file.
|
|
103
|
+
|
|
104
|
+
### Step 6: Build `profile.json` (project root)
|
|
105
|
+
|
|
106
|
+
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
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"schema_version": "2.0",
|
|
111
|
+
"srai_project_name": "<SRAI_PROJECT_NAME>",
|
|
112
|
+
"guardrails_profile": {},
|
|
113
|
+
"vibe_profile": {
|
|
114
|
+
"description": "<short summary of what the repo appears to be, from detected stack only>",
|
|
115
|
+
"architecture_notes": [],
|
|
116
|
+
"tech_categories": [],
|
|
117
|
+
"user_groups": [],
|
|
118
|
+
"compliance_requirements": [],
|
|
119
|
+
"language_stacks": []
|
|
120
|
+
},
|
|
121
|
+
"default_guardrail_pack": {
|
|
122
|
+
"guardrail_packs": [],
|
|
123
|
+
"pack_count": 0
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Populate `guardrails_profile` with the **same object** written to `.guardrails/profile.json`.
|
|
129
|
+
|
|
130
|
+
Populate `default_guardrail_pack.guardrail_packs` with the deduplicated pack id list (same as `guardrails_profile.guardrail_packs`) and `pack_count`.
|
|
131
|
+
|
|
132
|
+
Derive `vibe_profile` fields **only** from what you observed:
|
|
133
|
+
|
|
134
|
+
- `tech_categories`: e.g. `backend`, `frontend`, `database`, `cloud`, `mobile` when supported by files/deps.
|
|
135
|
+
- `language_stacks`: strings like `TypeScript/Node`, `Python/FastAPI` from detection.
|
|
136
|
+
- `architecture_notes`: short bullets (e.g. "Dockerfile present", "GitHub Actions CI") — not speculative threat narratives.
|
|
137
|
+
- `user_groups` / `compliance_requirements`: only if explicit in repo (e.g. compliance docs); else `[]`.
|
|
138
|
+
|
|
139
|
+
### Step 7: Upload to SecurityReview.ai (security-review-mcp)
|
|
140
|
+
|
|
141
|
+
1. Resolve `project_id`: `find_project_by_name` with `name="<SRAI_PROJECT_NAME>"`. If missing, follow existing kit rules (`list_projects`, `create_project`).
|
|
142
|
+
|
|
143
|
+
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).
|
|
144
|
+
|
|
145
|
+
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).
|
|
146
|
+
|
|
147
|
+
4. Confirm success to the user: paths written (`profile.json`, `.guardrails/profile.json`) and that both MCP calls completed or the exact error.
|
|
148
|
+
|
|
149
|
+
### Step 8: Report
|
|
150
|
+
|
|
151
|
+
Give a concise summary of detected stack, pack count, and upload status.
|
|
152
|
+
|
|
153
|
+
## Empty / New Repository Handling
|
|
154
|
+
|
|
155
|
+
If there are no signals:
|
|
156
|
+
|
|
157
|
+
1. Optionally read `.git/config` for hints.
|
|
158
|
+
2. Emit minimal profile: `owasp-asvs` only, empty summaries where appropriate.
|
|
159
|
+
3. Still write `profile.json` and attempt MCP calls with honest minimal `vibe_profile` data.
|
|
160
|
+
|
|
161
|
+
## IDE-Specific Notes
|
|
162
|
+
|
|
163
|
+
When run from Cursor Agent CLI, Claude Code, or Codex CLI, set `profiled_by` to a stable id (`cursor-agent`, `claude`, `codex`).
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_doc": "Maps filesystem signals to guardrail pack IDs. Each entry defines what to look for and which pack to activate.",
|
|
3
|
+
|
|
4
|
+
"universal": {
|
|
5
|
+
"_doc": "Always included regardless of detection",
|
|
6
|
+
"packs": ["owasp-asvs"]
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
"mobile_universal": {
|
|
10
|
+
"_doc": "Included when any mobile platform is detected",
|
|
11
|
+
"trigger_packs": ["flutter", "react-native", "swift", "objective-c", "kotlin"],
|
|
12
|
+
"packs": ["owasp-masvs"]
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
"languages": {
|
|
16
|
+
"typescript": {
|
|
17
|
+
"manifest_files": ["tsconfig.json", "tsconfig.base.json"],
|
|
18
|
+
"file_extensions": [".ts", ".tsx"],
|
|
19
|
+
"pack": "typescript"
|
|
20
|
+
},
|
|
21
|
+
"nodejs": {
|
|
22
|
+
"manifest_files": ["package.json"],
|
|
23
|
+
"file_extensions": [".js", ".mjs", ".cjs"],
|
|
24
|
+
"pack": "nodejs"
|
|
25
|
+
},
|
|
26
|
+
"python": {
|
|
27
|
+
"manifest_files": ["requirements.txt", "pyproject.toml", "setup.py", "setup.cfg", "Pipfile", "poetry.lock"],
|
|
28
|
+
"file_extensions": [".py"],
|
|
29
|
+
"pack": null,
|
|
30
|
+
"_note": "Python has no standalone pack; frameworks (django, flask, fastapi) cover it"
|
|
31
|
+
},
|
|
32
|
+
"go": {
|
|
33
|
+
"manifest_files": ["go.mod", "go.sum"],
|
|
34
|
+
"file_extensions": [".go"],
|
|
35
|
+
"pack": "go"
|
|
36
|
+
},
|
|
37
|
+
"java": {
|
|
38
|
+
"manifest_files": ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"],
|
|
39
|
+
"file_extensions": [".java"],
|
|
40
|
+
"pack": null,
|
|
41
|
+
"_note": "Java framework packs (spring-boot, java-ee) are more specific"
|
|
42
|
+
},
|
|
43
|
+
"kotlin": {
|
|
44
|
+
"manifest_files": ["build.gradle.kts"],
|
|
45
|
+
"file_extensions": [".kt", ".kts"],
|
|
46
|
+
"dependency_signals": { "build.gradle.kts": ["kotlin"] },
|
|
47
|
+
"pack": "kotlin"
|
|
48
|
+
},
|
|
49
|
+
"php": {
|
|
50
|
+
"manifest_files": ["composer.json", "composer.lock"],
|
|
51
|
+
"file_extensions": [".php"],
|
|
52
|
+
"pack": "php"
|
|
53
|
+
},
|
|
54
|
+
"ruby": {
|
|
55
|
+
"manifest_files": ["Gemfile", "Gemfile.lock"],
|
|
56
|
+
"file_extensions": [".rb"],
|
|
57
|
+
"pack": null,
|
|
58
|
+
"_note": "Ruby framework pack (ruby-on-rails) is more specific"
|
|
59
|
+
},
|
|
60
|
+
"rust": {
|
|
61
|
+
"manifest_files": ["Cargo.toml", "Cargo.lock"],
|
|
62
|
+
"file_extensions": [".rs"],
|
|
63
|
+
"pack": "rust"
|
|
64
|
+
},
|
|
65
|
+
"c": {
|
|
66
|
+
"file_extensions": [".c", ".h"],
|
|
67
|
+
"manifest_files": ["CMakeLists.txt", "Makefile"],
|
|
68
|
+
"pack": "c"
|
|
69
|
+
},
|
|
70
|
+
"cpp": {
|
|
71
|
+
"file_extensions": [".cpp", ".cxx", ".cc", ".hpp", ".hxx"],
|
|
72
|
+
"manifest_files": ["CMakeLists.txt"],
|
|
73
|
+
"pack": "cpp"
|
|
74
|
+
},
|
|
75
|
+
"swift": {
|
|
76
|
+
"manifest_files": ["Package.swift"],
|
|
77
|
+
"file_extensions": [".swift"],
|
|
78
|
+
"config_files": ["*.xcodeproj", "*.xcworkspace"],
|
|
79
|
+
"pack": "swift"
|
|
80
|
+
},
|
|
81
|
+
"objective-c": {
|
|
82
|
+
"file_extensions": [".m", ".mm"],
|
|
83
|
+
"pack": "objective-c"
|
|
84
|
+
},
|
|
85
|
+
"groovy": {
|
|
86
|
+
"file_extensions": [".groovy"],
|
|
87
|
+
"pack": "groovy"
|
|
88
|
+
},
|
|
89
|
+
"csharp": {
|
|
90
|
+
"manifest_files": ["*.csproj", "*.sln"],
|
|
91
|
+
"file_extensions": [".cs"],
|
|
92
|
+
"pack": null,
|
|
93
|
+
"_note": "C# framework pack (dotnet-aspnet-core) is more specific"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
"frameworks": {
|
|
98
|
+
"express": {
|
|
99
|
+
"ecosystem": "nodejs",
|
|
100
|
+
"dependency_signals": { "package.json": ["express"] },
|
|
101
|
+
"pack": "express"
|
|
102
|
+
},
|
|
103
|
+
"fastify": {
|
|
104
|
+
"ecosystem": "nodejs",
|
|
105
|
+
"dependency_signals": { "package.json": ["fastify"] },
|
|
106
|
+
"pack": "fastify"
|
|
107
|
+
},
|
|
108
|
+
"honojs": {
|
|
109
|
+
"ecosystem": "nodejs",
|
|
110
|
+
"dependency_signals": { "package.json": ["hono"] },
|
|
111
|
+
"pack": "honojs"
|
|
112
|
+
},
|
|
113
|
+
"nestjs": {
|
|
114
|
+
"ecosystem": "nodejs",
|
|
115
|
+
"dependency_signals": { "package.json": ["@nestjs/core"] },
|
|
116
|
+
"config_files": ["nest-cli.json"],
|
|
117
|
+
"pack": "nestjs"
|
|
118
|
+
},
|
|
119
|
+
"nextjs": {
|
|
120
|
+
"ecosystem": "nodejs",
|
|
121
|
+
"dependency_signals": { "package.json": ["next"] },
|
|
122
|
+
"config_files": ["next.config.js", "next.config.mjs", "next.config.ts"],
|
|
123
|
+
"pack": "nextjs"
|
|
124
|
+
},
|
|
125
|
+
"react": {
|
|
126
|
+
"ecosystem": "nodejs",
|
|
127
|
+
"dependency_signals": { "package.json": ["react", "react-dom"] },
|
|
128
|
+
"pack": "react"
|
|
129
|
+
},
|
|
130
|
+
"angular": {
|
|
131
|
+
"ecosystem": "nodejs",
|
|
132
|
+
"dependency_signals": { "package.json": ["@angular/core"] },
|
|
133
|
+
"config_files": ["angular.json"],
|
|
134
|
+
"pack": "angular"
|
|
135
|
+
},
|
|
136
|
+
"vuejs": {
|
|
137
|
+
"ecosystem": "nodejs",
|
|
138
|
+
"dependency_signals": { "package.json": ["vue"] },
|
|
139
|
+
"config_files": ["vue.config.js", "vite.config.ts", "nuxt.config.ts"],
|
|
140
|
+
"pack": "vuejs"
|
|
141
|
+
},
|
|
142
|
+
"electron": {
|
|
143
|
+
"ecosystem": "nodejs",
|
|
144
|
+
"dependency_signals": { "package.json": ["electron"] },
|
|
145
|
+
"pack": "electron"
|
|
146
|
+
},
|
|
147
|
+
"react-native": {
|
|
148
|
+
"ecosystem": "nodejs",
|
|
149
|
+
"dependency_signals": { "package.json": ["react-native"] },
|
|
150
|
+
"pack": "react-native"
|
|
151
|
+
},
|
|
152
|
+
"django": {
|
|
153
|
+
"ecosystem": "python",
|
|
154
|
+
"dependency_signals": {
|
|
155
|
+
"requirements.txt": ["django", "Django"],
|
|
156
|
+
"pyproject.toml": ["django", "Django"],
|
|
157
|
+
"Pipfile": ["django", "Django"]
|
|
158
|
+
},
|
|
159
|
+
"config_files": ["manage.py"],
|
|
160
|
+
"directory_signals": ["*/settings.py", "*/wsgi.py"],
|
|
161
|
+
"pack": "django"
|
|
162
|
+
},
|
|
163
|
+
"flask": {
|
|
164
|
+
"ecosystem": "python",
|
|
165
|
+
"dependency_signals": {
|
|
166
|
+
"requirements.txt": ["flask", "Flask"],
|
|
167
|
+
"pyproject.toml": ["flask", "Flask"]
|
|
168
|
+
},
|
|
169
|
+
"pack": "flask"
|
|
170
|
+
},
|
|
171
|
+
"fastapi": {
|
|
172
|
+
"ecosystem": "python",
|
|
173
|
+
"dependency_signals": {
|
|
174
|
+
"requirements.txt": ["fastapi"],
|
|
175
|
+
"pyproject.toml": ["fastapi"]
|
|
176
|
+
},
|
|
177
|
+
"pack": "fastapi"
|
|
178
|
+
},
|
|
179
|
+
"java-spring-boot": {
|
|
180
|
+
"ecosystem": "java",
|
|
181
|
+
"dependency_signals": {
|
|
182
|
+
"pom.xml": ["spring-boot-starter"],
|
|
183
|
+
"build.gradle": ["org.springframework.boot"]
|
|
184
|
+
},
|
|
185
|
+
"pack": "java-spring-boot"
|
|
186
|
+
},
|
|
187
|
+
"spring-security": {
|
|
188
|
+
"ecosystem": "java",
|
|
189
|
+
"dependency_signals": {
|
|
190
|
+
"pom.xml": ["spring-boot-starter-security", "spring-security"],
|
|
191
|
+
"build.gradle": ["spring-boot-starter-security", "spring-security"]
|
|
192
|
+
},
|
|
193
|
+
"pack": "spring-security"
|
|
194
|
+
},
|
|
195
|
+
"java-ee": {
|
|
196
|
+
"ecosystem": "java",
|
|
197
|
+
"dependency_signals": {
|
|
198
|
+
"pom.xml": ["javax.servlet", "jakarta.servlet", "javax.enterprise", "jakarta.enterprise"],
|
|
199
|
+
"build.gradle": ["javax.servlet", "jakarta.servlet"]
|
|
200
|
+
},
|
|
201
|
+
"config_files": ["web.xml"],
|
|
202
|
+
"pack": "java-ee"
|
|
203
|
+
},
|
|
204
|
+
"ruby-on-rails": {
|
|
205
|
+
"ecosystem": "ruby",
|
|
206
|
+
"dependency_signals": { "Gemfile": ["rails"] },
|
|
207
|
+
"config_files": ["config/routes.rb", "config/application.rb"],
|
|
208
|
+
"pack": "ruby-on-rails"
|
|
209
|
+
},
|
|
210
|
+
"laravel": {
|
|
211
|
+
"ecosystem": "php",
|
|
212
|
+
"dependency_signals": { "composer.json": ["laravel/framework"] },
|
|
213
|
+
"config_files": ["artisan"],
|
|
214
|
+
"directory_signals": ["app/Http/Controllers"],
|
|
215
|
+
"pack": "laravel"
|
|
216
|
+
},
|
|
217
|
+
"symfony": {
|
|
218
|
+
"ecosystem": "php",
|
|
219
|
+
"dependency_signals": { "composer.json": ["symfony/framework-bundle"] },
|
|
220
|
+
"config_files": ["config/bundles.php", "symfony.lock"],
|
|
221
|
+
"pack": "symfony"
|
|
222
|
+
},
|
|
223
|
+
"dotnet-aspnet-core": {
|
|
224
|
+
"ecosystem": "csharp",
|
|
225
|
+
"dependency_signals": { "*.csproj": ["Microsoft.AspNetCore"] },
|
|
226
|
+
"config_files": ["Program.cs", "Startup.cs", "appsettings.json"],
|
|
227
|
+
"pack": "dotnet-aspnet-core"
|
|
228
|
+
},
|
|
229
|
+
"flutter": {
|
|
230
|
+
"ecosystem": "dart",
|
|
231
|
+
"manifest_files": ["pubspec.yaml"],
|
|
232
|
+
"dependency_signals": { "pubspec.yaml": ["flutter"] },
|
|
233
|
+
"pack": "flutter"
|
|
234
|
+
},
|
|
235
|
+
"apollo-graphql": {
|
|
236
|
+
"ecosystem": "nodejs",
|
|
237
|
+
"dependency_signals": { "package.json": ["@apollo/server", "apollo-server", "apollo-server-express"] },
|
|
238
|
+
"pack": "apollo-graphql"
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
"auth_identity": {
|
|
243
|
+
"jwt": {
|
|
244
|
+
"dependency_signals": {
|
|
245
|
+
"package.json": ["jsonwebtoken", "jose", "@auth/core"],
|
|
246
|
+
"requirements.txt": ["pyjwt", "python-jose"],
|
|
247
|
+
"pyproject.toml": ["pyjwt", "python-jose"],
|
|
248
|
+
"Gemfile": ["jwt"],
|
|
249
|
+
"pom.xml": ["jjwt", "nimbus-jose-jwt"]
|
|
250
|
+
},
|
|
251
|
+
"pack": "jwt"
|
|
252
|
+
},
|
|
253
|
+
"oauth-oidc": {
|
|
254
|
+
"dependency_signals": {
|
|
255
|
+
"package.json": ["openid-client", "passport-oauth2", "oauth4webapi"],
|
|
256
|
+
"requirements.txt": ["authlib", "oauthlib"],
|
|
257
|
+
"pyproject.toml": ["authlib", "oauthlib"]
|
|
258
|
+
},
|
|
259
|
+
"pack": "oauth-oidc"
|
|
260
|
+
},
|
|
261
|
+
"auth0": {
|
|
262
|
+
"dependency_signals": {
|
|
263
|
+
"package.json": ["@auth0/nextjs-auth0", "auth0", "express-openid-connect"],
|
|
264
|
+
"requirements.txt": ["auth0-python"],
|
|
265
|
+
"pyproject.toml": ["auth0-python"]
|
|
266
|
+
},
|
|
267
|
+
"pack": "auth0"
|
|
268
|
+
},
|
|
269
|
+
"okta": {
|
|
270
|
+
"dependency_signals": {
|
|
271
|
+
"package.json": ["@okta/okta-sdk-nodejs", "@okta/oidc-middleware"],
|
|
272
|
+
"pom.xml": ["okta-spring-boot-starter"]
|
|
273
|
+
},
|
|
274
|
+
"pack": "okta"
|
|
275
|
+
},
|
|
276
|
+
"keycloak": {
|
|
277
|
+
"dependency_signals": {
|
|
278
|
+
"package.json": ["keycloak-connect", "keycloak-js"],
|
|
279
|
+
"pom.xml": ["keycloak-spring-boot-starter"]
|
|
280
|
+
},
|
|
281
|
+
"config_files": ["keycloak.json"],
|
|
282
|
+
"pack": "keycloak"
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
"ai_agent": {
|
|
287
|
+
"langchain": {
|
|
288
|
+
"dependency_signals": {
|
|
289
|
+
"requirements.txt": ["langchain"],
|
|
290
|
+
"pyproject.toml": ["langchain"],
|
|
291
|
+
"package.json": ["langchain"]
|
|
292
|
+
},
|
|
293
|
+
"pack": "langchain"
|
|
294
|
+
},
|
|
295
|
+
"langgraph": {
|
|
296
|
+
"dependency_signals": {
|
|
297
|
+
"requirements.txt": ["langgraph"],
|
|
298
|
+
"pyproject.toml": ["langgraph"]
|
|
299
|
+
},
|
|
300
|
+
"pack": "langgraph"
|
|
301
|
+
},
|
|
302
|
+
"llamaindex": {
|
|
303
|
+
"dependency_signals": {
|
|
304
|
+
"requirements.txt": ["llama-index", "llama_index"],
|
|
305
|
+
"pyproject.toml": ["llama-index", "llama_index"]
|
|
306
|
+
},
|
|
307
|
+
"pack": "llamaindex"
|
|
308
|
+
},
|
|
309
|
+
"crewai": {
|
|
310
|
+
"dependency_signals": {
|
|
311
|
+
"requirements.txt": ["crewai"],
|
|
312
|
+
"pyproject.toml": ["crewai"]
|
|
313
|
+
},
|
|
314
|
+
"pack": "crewai"
|
|
315
|
+
},
|
|
316
|
+
"mastra": {
|
|
317
|
+
"dependency_signals": {
|
|
318
|
+
"package.json": ["@mastra/core", "mastra"]
|
|
319
|
+
},
|
|
320
|
+
"pack": "mastra"
|
|
321
|
+
},
|
|
322
|
+
"ai-sdk": {
|
|
323
|
+
"dependency_signals": {
|
|
324
|
+
"package.json": ["ai", "@ai-sdk/openai", "@ai-sdk/anthropic"]
|
|
325
|
+
},
|
|
326
|
+
"pack": "ai-sdk"
|
|
327
|
+
},
|
|
328
|
+
"claude-agent-sdk": {
|
|
329
|
+
"dependency_signals": {
|
|
330
|
+
"package.json": ["@anthropic-ai/sdk"],
|
|
331
|
+
"requirements.txt": ["anthropic"],
|
|
332
|
+
"pyproject.toml": ["anthropic"]
|
|
333
|
+
},
|
|
334
|
+
"pack": "claude-agent-sdk"
|
|
335
|
+
},
|
|
336
|
+
"openai-agents-sdk": {
|
|
337
|
+
"dependency_signals": {
|
|
338
|
+
"package.json": ["openai"],
|
|
339
|
+
"requirements.txt": ["openai"],
|
|
340
|
+
"pyproject.toml": ["openai"]
|
|
341
|
+
},
|
|
342
|
+
"pack": "openai-agents-sdk"
|
|
343
|
+
},
|
|
344
|
+
"mcp": {
|
|
345
|
+
"dependency_signals": {
|
|
346
|
+
"package.json": ["@modelcontextprotocol/sdk"],
|
|
347
|
+
"requirements.txt": ["mcp"],
|
|
348
|
+
"pyproject.toml": ["mcp"]
|
|
349
|
+
},
|
|
350
|
+
"pack": "mcp"
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
"infrastructure": {
|
|
355
|
+
"docker": {
|
|
356
|
+
"config_files": ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", ".dockerignore"],
|
|
357
|
+
"pack": "docker"
|
|
358
|
+
},
|
|
359
|
+
"kubernetes": {
|
|
360
|
+
"config_files": ["helmfile.yaml", "Chart.yaml", "kustomization.yaml"],
|
|
361
|
+
"directory_signals": ["k8s/", "kubernetes/", "helm/"],
|
|
362
|
+
"content_signals": { "*.yaml": ["apiVersion", "kind: Deployment", "kind: Service", "kind: Pod"] },
|
|
363
|
+
"pack": "kubernetes"
|
|
364
|
+
},
|
|
365
|
+
"terraform-aws": {
|
|
366
|
+
"file_extensions": [".tf"],
|
|
367
|
+
"content_signals": { "*.tf": ["provider \"aws\"", "aws_"] },
|
|
368
|
+
"pack": "terraform-aws"
|
|
369
|
+
},
|
|
370
|
+
"terraform-azure": {
|
|
371
|
+
"file_extensions": [".tf"],
|
|
372
|
+
"content_signals": { "*.tf": ["provider \"azurerm\"", "azurerm_"] },
|
|
373
|
+
"pack": "terraform-azure"
|
|
374
|
+
},
|
|
375
|
+
"terraform-gcp": {
|
|
376
|
+
"file_extensions": [".tf"],
|
|
377
|
+
"content_signals": { "*.tf": ["provider \"google\"", "google_"] },
|
|
378
|
+
"pack": "terraform-gcp"
|
|
379
|
+
},
|
|
380
|
+
"cloudformation": {
|
|
381
|
+
"content_signals": { "*.yaml": ["AWSTemplateFormatVersion"], "*.json": ["AWSTemplateFormatVersion"] },
|
|
382
|
+
"config_files": ["template.yaml", "template.json", "samconfig.toml"],
|
|
383
|
+
"pack": "cloudformation"
|
|
384
|
+
},
|
|
385
|
+
"nginx": {
|
|
386
|
+
"config_files": ["nginx.conf"],
|
|
387
|
+
"directory_signals": ["nginx/"],
|
|
388
|
+
"pack": "nginx"
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
"ci_cd": {
|
|
393
|
+
"github-actions": {
|
|
394
|
+
"directory_signals": [".github/workflows/"],
|
|
395
|
+
"pack": "github-actions"
|
|
396
|
+
},
|
|
397
|
+
"jenkins-pipeline": {
|
|
398
|
+
"config_files": ["Jenkinsfile"],
|
|
399
|
+
"pack": "jenkins-pipeline"
|
|
400
|
+
},
|
|
401
|
+
"argocd": {
|
|
402
|
+
"config_files": ["argocd-cm.yaml"],
|
|
403
|
+
"content_signals": { "*.yaml": ["argoproj.io"] },
|
|
404
|
+
"pack": "argocd"
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
"cloud_compute": {
|
|
409
|
+
"aws-lambda": {
|
|
410
|
+
"config_files": ["serverless.yml", "serverless.yaml", "serverless.ts", "sam.yaml", "samconfig.toml"],
|
|
411
|
+
"content_signals": {
|
|
412
|
+
"serverless.yml": ["provider:", "aws"],
|
|
413
|
+
"template.yaml": ["AWS::Serverless::Function", "AWS::Lambda::Function"]
|
|
414
|
+
},
|
|
415
|
+
"pack": "aws-lambda"
|
|
416
|
+
},
|
|
417
|
+
"azure-functions": {
|
|
418
|
+
"config_files": ["host.json", "local.settings.json"],
|
|
419
|
+
"directory_signals": ["function_app.py"],
|
|
420
|
+
"pack": "azure-functions"
|
|
421
|
+
},
|
|
422
|
+
"gcp-cloud-run": {
|
|
423
|
+
"config_files": ["app.yaml", "cloudbuild.yaml"],
|
|
424
|
+
"content_signals": { "*.yaml": ["gcr.io", "run.googleapis.com"] },
|
|
425
|
+
"pack": "gcp-cloud-run"
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
"databases": {
|
|
430
|
+
"mongodb": {
|
|
431
|
+
"dependency_signals": {
|
|
432
|
+
"package.json": ["mongoose", "mongodb"],
|
|
433
|
+
"requirements.txt": ["pymongo", "motor", "mongoengine"],
|
|
434
|
+
"pyproject.toml": ["pymongo", "motor"],
|
|
435
|
+
"Gemfile": ["mongoid"]
|
|
436
|
+
},
|
|
437
|
+
"pack": "mongodb"
|
|
438
|
+
},
|
|
439
|
+
"postgresql": {
|
|
440
|
+
"dependency_signals": {
|
|
441
|
+
"package.json": ["pg", "knex", "prisma", "typeorm", "sequelize"],
|
|
442
|
+
"requirements.txt": ["psycopg2", "asyncpg", "sqlalchemy"],
|
|
443
|
+
"pyproject.toml": ["psycopg2", "asyncpg", "sqlalchemy"],
|
|
444
|
+
"Gemfile": ["pg"],
|
|
445
|
+
"pom.xml": ["postgresql"]
|
|
446
|
+
},
|
|
447
|
+
"pack": "postgresql"
|
|
448
|
+
},
|
|
449
|
+
"mysql-mariadb": {
|
|
450
|
+
"dependency_signals": {
|
|
451
|
+
"package.json": ["mysql2", "mysql"],
|
|
452
|
+
"requirements.txt": ["mysqlclient", "pymysql", "aiomysql"],
|
|
453
|
+
"pyproject.toml": ["mysqlclient", "pymysql"],
|
|
454
|
+
"pom.xml": ["mysql-connector-java"]
|
|
455
|
+
},
|
|
456
|
+
"pack": "mysql-mariadb"
|
|
457
|
+
},
|
|
458
|
+
"redis": {
|
|
459
|
+
"dependency_signals": {
|
|
460
|
+
"package.json": ["redis", "ioredis"],
|
|
461
|
+
"requirements.txt": ["redis", "aioredis"],
|
|
462
|
+
"pyproject.toml": ["redis"],
|
|
463
|
+
"Gemfile": ["redis"]
|
|
464
|
+
},
|
|
465
|
+
"pack": "redis"
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
"messaging": {
|
|
470
|
+
"kafka": {
|
|
471
|
+
"dependency_signals": {
|
|
472
|
+
"package.json": ["kafkajs"],
|
|
473
|
+
"requirements.txt": ["confluent-kafka", "aiokafka"],
|
|
474
|
+
"pyproject.toml": ["confluent-kafka"],
|
|
475
|
+
"pom.xml": ["kafka-clients", "spring-kafka"]
|
|
476
|
+
},
|
|
477
|
+
"pack": "kafka"
|
|
478
|
+
},
|
|
479
|
+
"rabbitmq": {
|
|
480
|
+
"dependency_signals": {
|
|
481
|
+
"package.json": ["amqplib"],
|
|
482
|
+
"requirements.txt": ["pika", "aio-pika"],
|
|
483
|
+
"pyproject.toml": ["pika"],
|
|
484
|
+
"pom.xml": ["spring-boot-starter-amqp"]
|
|
485
|
+
},
|
|
486
|
+
"pack": "rabbitmq"
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
"api_protocols": {
|
|
491
|
+
"grpc": {
|
|
492
|
+
"dependency_signals": {
|
|
493
|
+
"package.json": ["@grpc/grpc-js"],
|
|
494
|
+
"requirements.txt": ["grpcio"],
|
|
495
|
+
"pyproject.toml": ["grpcio"],
|
|
496
|
+
"pom.xml": ["grpc-netty"]
|
|
497
|
+
},
|
|
498
|
+
"file_extensions": [".proto"],
|
|
499
|
+
"pack": "grpc"
|
|
500
|
+
},
|
|
501
|
+
"websockets": {
|
|
502
|
+
"dependency_signals": {
|
|
503
|
+
"package.json": ["ws", "socket.io"],
|
|
504
|
+
"requirements.txt": ["websockets"],
|
|
505
|
+
"pyproject.toml": ["websockets"]
|
|
506
|
+
},
|
|
507
|
+
"pack": "websockets"
|
|
508
|
+
},
|
|
509
|
+
"openapi-rest-api": {
|
|
510
|
+
"config_files": ["openapi.yaml", "openapi.json", "swagger.yaml", "swagger.json"],
|
|
511
|
+
"pack": "openapi-rest-api"
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { copyFileSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ensureDir, writeText } from './fs-helpers.js';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
function sanitizeProjectName(value) {
|
|
9
|
+
return String(value || '').replace(/[\r\n`]/g, ' ').trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function injectProjectName(content, projectName) {
|
|
13
|
+
const resolved = sanitizeProjectName(projectName) || '<SRAI_PROJECT_NAME>';
|
|
14
|
+
return content.replaceAll('{{SRAI_PROJECT_NAME}}', resolved).replaceAll('<SRAI_PROJECT_NAME>', resolved);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const BUNDLE_ROOT = join(__dirname, '..', 'generators', 'rules', 'guardrails-profiler');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Writes `.securityreview-kit/guardrails-profiler/` (SKILL + signal registry) into the target workspace.
|
|
21
|
+
*/
|
|
22
|
+
export function writeGuardrailsProfilerBundle(cwd, options = {}) {
|
|
23
|
+
const destBase = join(cwd, '.securityreview-kit', 'guardrails-profiler');
|
|
24
|
+
const destRefs = join(destBase, 'references');
|
|
25
|
+
ensureDir(destRefs);
|
|
26
|
+
|
|
27
|
+
const skillTemplate = readFileSync(join(BUNDLE_ROOT, 'SKILL.md'), 'utf-8');
|
|
28
|
+
writeText(join(destBase, 'SKILL.md'), injectProjectName(skillTemplate, options.projectName));
|
|
29
|
+
|
|
30
|
+
copyFileSync(join(BUNDLE_ROOT, 'references', 'signal-registry.json'), join(destRefs, 'signal-registry.json'));
|
|
31
|
+
|
|
32
|
+
return destBase;
|
|
33
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const AGENT_CLI_TARGETS = new Set(['cursor', 'claude', 'codex']);
|
|
4
|
+
|
|
5
|
+
function commandOk(cmd, args = ['--version']) {
|
|
6
|
+
const r = spawnSync(cmd, args, { stdio: 'ignore' });
|
|
7
|
+
return r.status === 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function runShell(script) {
|
|
11
|
+
return spawnSync(script, { shell: true, stdio: 'inherit' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ensure Cursor / Claude Code / Codex CLIs are present when those targets are selected.
|
|
16
|
+
* Installation uses vendor scripts or npm where appropriate.
|
|
17
|
+
*/
|
|
18
|
+
export function ensureIdeCliForTarget(target, options = {}) {
|
|
19
|
+
const { skipInstall = false } = options;
|
|
20
|
+
|
|
21
|
+
if (!AGENT_CLI_TARGETS.has(target)) {
|
|
22
|
+
return { target, ok: true, skipped: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (target === 'cursor') {
|
|
26
|
+
if (commandOk('cursor-agent', ['--version'])) {
|
|
27
|
+
return { target, ok: true, already: true };
|
|
28
|
+
}
|
|
29
|
+
if (skipInstall) {
|
|
30
|
+
return { target, ok: false, message: 'cursor-agent not found on PATH' };
|
|
31
|
+
}
|
|
32
|
+
if (process.platform === 'win32') {
|
|
33
|
+
return {
|
|
34
|
+
target,
|
|
35
|
+
ok: false,
|
|
36
|
+
message: 'Install Cursor CLI manually: https://cursor.com/cli',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
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' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (target === 'claude') {
|
|
46
|
+
if (commandOk('claude', ['--version'])) {
|
|
47
|
+
return { target, ok: true, already: true };
|
|
48
|
+
}
|
|
49
|
+
if (skipInstall) {
|
|
50
|
+
return { target, ok: false, message: 'claude not found on PATH' };
|
|
51
|
+
}
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
const r = runShell('powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://claude.ai/install.ps1 | iex"');
|
|
54
|
+
return r.status === 0
|
|
55
|
+
? { target, ok: true }
|
|
56
|
+
: { target, ok: false, message: 'Claude Code install failed' };
|
|
57
|
+
}
|
|
58
|
+
const r = runShell('curl -fsSL https://claude.ai/install.sh | bash');
|
|
59
|
+
return r.status === 0
|
|
60
|
+
? { target, ok: true }
|
|
61
|
+
: { target, ok: false, message: 'Claude Code install failed' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (target === 'codex') {
|
|
65
|
+
if (commandOk('codex', ['--version'])) {
|
|
66
|
+
return { target, ok: true, already: true };
|
|
67
|
+
}
|
|
68
|
+
if (skipInstall) {
|
|
69
|
+
return { target, ok: false, message: 'codex not found on PATH' };
|
|
70
|
+
}
|
|
71
|
+
const r = runShell('npm install -g @openai/codex');
|
|
72
|
+
return r.status === 0
|
|
73
|
+
? { target, ok: true }
|
|
74
|
+
: { target, ok: false, message: 'Codex CLI npm install failed' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { target, ok: true, skipped: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function ensureIdeClisForTargets(targets, options = {}) {
|
|
81
|
+
return targets.map((t) => ensureIdeCliForTarget(t, options));
|
|
82
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const PREFERRED_ORDER = ['cursor', 'claude', 'codex'];
|
|
4
|
+
|
|
5
|
+
function commandOk(cmd, args = ['--version']) {
|
|
6
|
+
const r = spawnSync(cmd, args, { stdio: 'ignore' });
|
|
7
|
+
return r.status === 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildProfilerAgentPrompt(projectName) {
|
|
11
|
+
const p = String(projectName || '').trim() || '<SRAI_PROJECT_NAME>';
|
|
12
|
+
return [
|
|
13
|
+
'Security Review Kit: run guardrails initialization profiling for this workspace.',
|
|
14
|
+
`SRAI project name: "${p}".`,
|
|
15
|
+
'Open and follow every step in .securityreview-kit/guardrails-profiler/SKILL.md.',
|
|
16
|
+
'Use .securityreview-kit/guardrails-profiler/references/signal-registry.json as the signal registry.',
|
|
17
|
+
'Write .guardrails/profile.json and profile.json at the repository root as defined in the skill.',
|
|
18
|
+
`Resolve project_id with find_project_by_name for "${p}", then call update_vibe_profile and write_default_pack via security-review-mcp.`,
|
|
19
|
+
'Do not invent stack details, compliance, or user groups; ground everything in the repository.',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run Cursor Agent OAuth/login in the current terminal (stdio inherited).
|
|
25
|
+
* Call this from init before profiling so the user does not leave the kit flow.
|
|
26
|
+
*/
|
|
27
|
+
export function runCursorAgentLogin(cwd) {
|
|
28
|
+
if (!commandOk('cursor-agent', ['--version'])) {
|
|
29
|
+
return { ok: false, status: null, message: 'cursor-agent not on PATH' };
|
|
30
|
+
}
|
|
31
|
+
const r = spawnSync('cursor-agent', ['login'], { cwd, stdio: 'inherit', env: { ...process.env } });
|
|
32
|
+
return { ok: r.status === 0, status: r.status };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function pickProfilerAgentTarget(targets) {
|
|
36
|
+
for (const t of PREFERRED_ORDER) {
|
|
37
|
+
if (targets.includes(t)) {
|
|
38
|
+
return t;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Spawn the IDE agent CLI to execute the profiler skill (user must be logged in where required).
|
|
46
|
+
*
|
|
47
|
+
* @param {object} opts
|
|
48
|
+
* @param {boolean} [opts.cursorTrust=true] When true, passes `--trust` and `--approve-mcps` so headless init is not blocked by
|
|
49
|
+
* workspace trust or MCP approval (user confirmed profiling in the kit). Set false with `--profiler-no-trust`
|
|
50
|
+
* if you need an interactive trust/login/MCP flow in the same terminal.
|
|
51
|
+
*/
|
|
52
|
+
export function runProfilerAgent(cwd, { target, projectName, cursorTrust = true }) {
|
|
53
|
+
const prompt = buildProfilerAgentPrompt(projectName);
|
|
54
|
+
const opts = { cwd, stdio: 'inherit', env: { ...process.env } };
|
|
55
|
+
|
|
56
|
+
if (target === 'cursor') {
|
|
57
|
+
if (!commandOk('cursor-agent', ['--version'])) {
|
|
58
|
+
return { ok: false, message: 'cursor-agent not on PATH' };
|
|
59
|
+
}
|
|
60
|
+
const args = ['-p', prompt];
|
|
61
|
+
if (cursorTrust) {
|
|
62
|
+
args.push('--trust', '--approve-mcps');
|
|
63
|
+
}
|
|
64
|
+
const r = spawnSync('cursor-agent', args, opts);
|
|
65
|
+
return { ok: r.status === 0, status: r.status };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (target === 'claude') {
|
|
69
|
+
if (!commandOk('claude', ['--version'])) {
|
|
70
|
+
return { ok: false, message: 'claude not on PATH' };
|
|
71
|
+
}
|
|
72
|
+
const r = spawnSync('claude', ['-p', prompt], opts);
|
|
73
|
+
return { ok: r.status === 0, status: r.status };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (target === 'codex') {
|
|
77
|
+
if (!commandOk('codex', ['--version'])) {
|
|
78
|
+
return { ok: false, message: 'codex not on PATH' };
|
|
79
|
+
}
|
|
80
|
+
const r = spawnSync('codex', ['exec', prompt], opts);
|
|
81
|
+
return { ok: r.status === 0, status: r.status };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { ok: false, message: 'unsupported agent target' };
|
|
85
|
+
}
|