@securityreviewai/securityreview-kit 0.1.39 → 0.1.41
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 +9 -1
- package/package.json +1 -1
- package/src/cli.js +20 -0
- package/src/commands/init.js +195 -15
- package/src/generators/mcp/claude.js +19 -8
- package/src/generators/mcp/claude.test.js +8 -5
- package/src/generators/rules/claude.js +21 -4
- package/src/generators/rules/claude.test.js +20 -4
- package/src/generators/rules/guardrails-init-profile.md +4 -2
- package/src/utils/constants.js +2 -2
- package/src/utils/profiler-agent.js +75 -4
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npx @securityreviewai/securityreview-kit init --switch-project
|
|
|
28
28
|
| Target | Flag | MCP Config | Workspace Rule |
|
|
29
29
|
|---|---|---|---|
|
|
30
30
|
| Cursor | `cursor` | `.cursor/mcp.json` | `.cursor/rules/srai-security-review.mdc`, `.cursor/rules/ctm_sync_rule.mdc`, `.cursor/commands/ctm_sync.md`, `.cursor/agents/ctm_sync.md`, `.cursor/commands/create-ide-workflow.md`, `.cursor/commands/srai-profile.md`, `.cursor/skills/threat-modelling/SKILL.md` |
|
|
31
|
-
| Claude Code | `claude` | `.
|
|
31
|
+
| Claude Code | `claude` | `.mcp.json` | `.claude/CLAUDE.md`, `.claude/settings.json`, `.claude/skills/threat-modelling/SKILL.md`, `.claude/skills/guardrails-profiler/SKILL.md`, `.claude/skills/guardrails-selection/SKILL.md`, `.claude/agents/ctm_sync.md`, `.claude/commands/guardrails-init-profile.md` |
|
|
32
32
|
| VS Code Copilot | `vscode` | `.vscode/mcp.json` | `.github/copilot-instructions.md`, `.github/skills/threat-modelling/SKILL.md`, `.github/skills/guardrails-profiler/SKILL.md`, `.github/skills/guardrails-selection/SKILL.md`, `.github/agents/ctm_sync.agent.md`, `.github/hooks/srai-session-policy.json` |
|
|
33
33
|
| Windsurf | `windsurf` | `.windsurf/mcp_config.json` | `.windsurf/rules/srai-security-review.md` |
|
|
34
34
|
| Codex | `codex` | `.codex/config.toml` | `.codex/AGENTS.md`, `.codex/skills/threat-modelling/SKILL.md`, `.codex/skills/guardrails-profiler/SKILL.md`, `.codex/skills/guardrails-selection/SKILL.md`, `.codex/agents/ctm_sync.toml`, `.codex/hooks.json`, `.codex/commands/guardrails-init-profile.md` |
|
|
@@ -53,6 +53,14 @@ Options:
|
|
|
53
53
|
--skip-rules Skip workspace rule installation
|
|
54
54
|
--profile-repo Run the guardrails profiler after init
|
|
55
55
|
--profiler-claude-login Run Claude Code login before profiling
|
|
56
|
+
--claude-auth-mode <mode>
|
|
57
|
+
Claude profiling auth mode: current, claudeai, console, api_key, gateway, bedrock, vertex, or setup_token
|
|
58
|
+
--claude-api-key <key> Anthropic API key for Claude profiling
|
|
59
|
+
--claude-base-url <url> Anthropic-compatible base URL for Claude profiling
|
|
60
|
+
--claude-auth-token <token>
|
|
61
|
+
Auth token for Claude profiling gateway mode
|
|
62
|
+
--claude-provider-model <model>
|
|
63
|
+
Optional Claude provider model override for gateway, Bedrock, or Vertex profiling
|
|
56
64
|
--profiler-copilot-login
|
|
57
65
|
Run GitHub Copilot CLI login before VS Code Copilot profiling
|
|
58
66
|
--profiler-codex-login Run Codex login before Codex profiling
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -41,6 +41,26 @@ export function run() {
|
|
|
41
41
|
'--profiler-claude-login',
|
|
42
42
|
'Before Claude Code profiling, run `claude auth login` in this terminal',
|
|
43
43
|
)
|
|
44
|
+
.option(
|
|
45
|
+
'--claude-auth-mode <mode>',
|
|
46
|
+
'Claude profiling auth mode: current, claudeai, console, api_key, gateway, bedrock, vertex, or setup_token',
|
|
47
|
+
)
|
|
48
|
+
.option(
|
|
49
|
+
'--claude-api-key <key>',
|
|
50
|
+
'Anthropic API key for Claude profiling when using --claude-auth-mode api_key',
|
|
51
|
+
)
|
|
52
|
+
.option(
|
|
53
|
+
'--claude-base-url <url>',
|
|
54
|
+
'Anthropic-compatible base URL for Claude profiling when using --claude-auth-mode gateway',
|
|
55
|
+
)
|
|
56
|
+
.option(
|
|
57
|
+
'--claude-auth-token <token>',
|
|
58
|
+
'Auth token for Claude profiling when using --claude-auth-mode gateway',
|
|
59
|
+
)
|
|
60
|
+
.option(
|
|
61
|
+
'--claude-provider-model <model>',
|
|
62
|
+
'Optional Claude provider model override for gateway, Bedrock, or Vertex profiling',
|
|
63
|
+
)
|
|
44
64
|
.option(
|
|
45
65
|
'--profiler-copilot-login',
|
|
46
66
|
'Before VS Code Copilot profiling, run `copilot login` in this terminal',
|
package/src/commands/init.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { input, checkbox, confirm, select } from '@inquirer/prompts';
|
|
2
|
+
import { input, checkbox, confirm, password, select } from '@inquirer/prompts';
|
|
3
3
|
import { TARGETS, TARGET_NAMES } from '../utils/constants.js';
|
|
4
4
|
import { detectTargets } from '../utils/detect.js';
|
|
5
5
|
import { ensureIdeClisForTargets } from '../utils/ide-cli-install.js';
|
|
6
6
|
import { writeGuardrailsSkillBundles } from '../utils/guardrails-profiler-bundle.js';
|
|
7
7
|
import {
|
|
8
8
|
pickProfilerAgentTarget,
|
|
9
|
-
|
|
9
|
+
runClaudeAuthLogin,
|
|
10
|
+
runClaudeSetupToken,
|
|
10
11
|
runCodexLogin,
|
|
11
12
|
runCopilotLogin,
|
|
12
13
|
runCursorAgentLogin,
|
|
@@ -53,6 +54,170 @@ function normalizeRuleResults(rawResult) {
|
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
async function resolveClaudeProfilerAuth(options, interactive, cwd) {
|
|
58
|
+
const providerModel = String(options.claudeProviderModel || process.env.ANTHROPIC_MODEL || '').trim();
|
|
59
|
+
let mode = String(options.claudeAuthMode || process.env.SECURITY_REVIEW_CLAUDE_AUTH_MODE || '').trim();
|
|
60
|
+
|
|
61
|
+
if (!mode && options.profilerClaudeLogin) {
|
|
62
|
+
mode = 'claudeai';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!mode && interactive) {
|
|
66
|
+
mode = await select({
|
|
67
|
+
message: 'How should Claude Code authenticate for this profiling run?',
|
|
68
|
+
default: 'current',
|
|
69
|
+
choices: [
|
|
70
|
+
{ name: 'Use current Claude Code auth/environment', value: 'current' },
|
|
71
|
+
{ name: 'Claude subscription login', value: 'claudeai' },
|
|
72
|
+
{ name: 'Anthropic Console login', value: 'console' },
|
|
73
|
+
{ name: 'Anthropic API key', value: 'api_key' },
|
|
74
|
+
{ name: 'Anthropic-compatible gateway / proxy', value: 'gateway' },
|
|
75
|
+
{ name: 'AWS Bedrock', value: 'bedrock' },
|
|
76
|
+
{ name: 'Google Vertex AI', value: 'vertex' },
|
|
77
|
+
{ name: 'Long-lived Claude token', value: 'setup_token' },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!mode) {
|
|
83
|
+
mode = 'current';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = {
|
|
87
|
+
mode,
|
|
88
|
+
model: providerModel || 'haiku',
|
|
89
|
+
envOverrides: {},
|
|
90
|
+
loginRunner: null,
|
|
91
|
+
loginLabel: '',
|
|
92
|
+
summary: '',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (mode === 'claudeai') {
|
|
96
|
+
result.loginRunner = () => runClaudeAuthLogin(cwd, { mode: 'claudeai' });
|
|
97
|
+
result.loginLabel = 'Claude subscription login';
|
|
98
|
+
result.summary = 'Claude subscription auth';
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (mode === 'console') {
|
|
103
|
+
result.loginRunner = () => runClaudeAuthLogin(cwd, { mode: 'console' });
|
|
104
|
+
result.loginLabel = 'Anthropic Console login';
|
|
105
|
+
result.summary = 'Anthropic Console auth';
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (mode === 'setup_token') {
|
|
110
|
+
result.loginRunner = () => runClaudeSetupToken(cwd);
|
|
111
|
+
result.loginLabel = 'Claude long-lived token setup';
|
|
112
|
+
result.summary = 'Claude subscription token auth';
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (mode === 'api_key') {
|
|
117
|
+
let apiKey = String(options.claudeApiKey || process.env.ANTHROPIC_API_KEY || '').trim();
|
|
118
|
+
if (!apiKey && interactive) {
|
|
119
|
+
apiKey = await password({
|
|
120
|
+
message: 'Anthropic API key for this profiling run:',
|
|
121
|
+
validate: (v) => (String(v || '').trim() ? true : 'API key is required'),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
result.envOverrides = {
|
|
125
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
126
|
+
ANTHROPIC_AUTH_TOKEN: null,
|
|
127
|
+
ANTHROPIC_BASE_URL: null,
|
|
128
|
+
CLAUDE_CODE_USE_BEDROCK: null,
|
|
129
|
+
CLAUDE_CODE_USE_VERTEX: null,
|
|
130
|
+
};
|
|
131
|
+
result.summary = 'Anthropic API key auth';
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (mode === 'gateway') {
|
|
136
|
+
let baseUrl = String(options.claudeBaseUrl || process.env.ANTHROPIC_BASE_URL || '').trim();
|
|
137
|
+
let authToken = String(options.claudeAuthToken || process.env.ANTHROPIC_AUTH_TOKEN || '').trim();
|
|
138
|
+
let model = providerModel;
|
|
139
|
+
|
|
140
|
+
if (interactive) {
|
|
141
|
+
if (!baseUrl) {
|
|
142
|
+
baseUrl = await input({
|
|
143
|
+
message: 'Anthropic-compatible base URL for this profiling run:',
|
|
144
|
+
validate: (v) => (String(v || '').trim() ? true : 'Base URL is required'),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (!authToken) {
|
|
148
|
+
authToken = await password({
|
|
149
|
+
message: 'Gateway auth token for this profiling run:',
|
|
150
|
+
validate: (v) => (String(v || '').trim() ? true : 'Auth token is required'),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (!providerModel) {
|
|
154
|
+
model = String(
|
|
155
|
+
await input({
|
|
156
|
+
message: 'Provider model override (leave blank to use haiku):',
|
|
157
|
+
default: '',
|
|
158
|
+
}),
|
|
159
|
+
).trim();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
result.model = model || 'haiku';
|
|
164
|
+
result.envOverrides = {
|
|
165
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
166
|
+
ANTHROPIC_AUTH_TOKEN: authToken,
|
|
167
|
+
ANTHROPIC_API_KEY: null,
|
|
168
|
+
CLAUDE_CODE_USE_BEDROCK: null,
|
|
169
|
+
CLAUDE_CODE_USE_VERTEX: null,
|
|
170
|
+
};
|
|
171
|
+
result.summary = `Anthropic-compatible gateway (${baseUrl || 'configured base URL'})`;
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (mode === 'bedrock') {
|
|
176
|
+
let model = providerModel;
|
|
177
|
+
if (interactive && !providerModel) {
|
|
178
|
+
model = String(
|
|
179
|
+
await input({
|
|
180
|
+
message: 'Bedrock model override (leave blank to use haiku):',
|
|
181
|
+
default: '',
|
|
182
|
+
}),
|
|
183
|
+
).trim();
|
|
184
|
+
}
|
|
185
|
+
result.model = model || 'haiku';
|
|
186
|
+
result.envOverrides = {
|
|
187
|
+
CLAUDE_CODE_USE_BEDROCK: 'true',
|
|
188
|
+
CLAUDE_CODE_USE_VERTEX: null,
|
|
189
|
+
ANTHROPIC_API_KEY: null,
|
|
190
|
+
ANTHROPIC_AUTH_TOKEN: null,
|
|
191
|
+
};
|
|
192
|
+
result.summary = 'AWS Bedrock credentials from your shell/environment';
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (mode === 'vertex') {
|
|
197
|
+
let model = providerModel;
|
|
198
|
+
if (interactive && !providerModel) {
|
|
199
|
+
model = String(
|
|
200
|
+
await input({
|
|
201
|
+
message: 'Vertex model override (leave blank to use haiku):',
|
|
202
|
+
default: '',
|
|
203
|
+
}),
|
|
204
|
+
).trim();
|
|
205
|
+
}
|
|
206
|
+
result.model = model || 'haiku';
|
|
207
|
+
result.envOverrides = {
|
|
208
|
+
CLAUDE_CODE_USE_VERTEX: 'true',
|
|
209
|
+
CLAUDE_CODE_USE_BEDROCK: null,
|
|
210
|
+
ANTHROPIC_API_KEY: null,
|
|
211
|
+
ANTHROPIC_AUTH_TOKEN: null,
|
|
212
|
+
};
|
|
213
|
+
result.summary = 'Google Vertex AI credentials from your shell/environment';
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
result.summary = 'current Claude Code auth/environment';
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
56
221
|
/**
|
|
57
222
|
* Resolve environment variables from flags, env, or interactive prompt.
|
|
58
223
|
*/
|
|
@@ -418,6 +583,7 @@ export async function initCommand(options) {
|
|
|
418
583
|
} else {
|
|
419
584
|
console.log('');
|
|
420
585
|
console.log(chalk.bold.white(` Starting profiler via ${TARGETS[agentTarget].name} CLI…`));
|
|
586
|
+
let claudeProfilerAuth = null;
|
|
421
587
|
if (agentTarget === 'cursor') {
|
|
422
588
|
console.log(
|
|
423
589
|
chalk.dim(
|
|
@@ -488,31 +654,33 @@ export async function initCommand(options) {
|
|
|
488
654
|
console.log('');
|
|
489
655
|
}
|
|
490
656
|
} else if (agentTarget === 'claude') {
|
|
657
|
+
claudeProfilerAuth = await resolveClaudeProfilerAuth(options, interactive, cwd);
|
|
491
658
|
console.log(
|
|
492
659
|
chalk.dim(
|
|
493
|
-
|
|
660
|
+
` Claude Code: profiling uses \`.mcp.json\`, \`.claude/settings.json\`, bypassed tool prompts for this profiling pass, and \`${claudeProfilerAuth.model}\` for this run.`,
|
|
494
661
|
),
|
|
495
662
|
);
|
|
663
|
+
console.log(chalk.dim(` Auth mode: ${claudeProfilerAuth.summary}.`));
|
|
496
664
|
|
|
497
|
-
|
|
498
|
-
|
|
665
|
+
const needsSetup = typeof claudeProfilerAuth.loginRunner === 'function';
|
|
666
|
+
let runLogin = needsSetup;
|
|
667
|
+
if (needsSetup && interactive && !options.claudeAuthMode && !options.profilerClaudeLogin) {
|
|
499
668
|
runLogin = await confirm({
|
|
500
|
-
message:
|
|
501
|
-
'Run Claude Code login in this terminal now? (Same init — profiling runs next. Choose No if already signed in.)',
|
|
669
|
+
message: `${claudeProfilerAuth.loginLabel} in this terminal now? (Same init — profiling runs next.)`,
|
|
502
670
|
default: true,
|
|
503
671
|
});
|
|
504
672
|
}
|
|
505
|
-
if (runLogin) {
|
|
673
|
+
if (runLogin && claudeProfilerAuth.loginRunner) {
|
|
506
674
|
console.log('');
|
|
507
|
-
console.log(chalk.bold.white(
|
|
508
|
-
console.log(chalk.dim(' Complete the
|
|
509
|
-
const loginResult =
|
|
675
|
+
console.log(chalk.bold.white(` ${claudeProfilerAuth.loginLabel}`));
|
|
676
|
+
console.log(chalk.dim(' Complete the prompt flow, then return here.\n'));
|
|
677
|
+
const loginResult = claudeProfilerAuth.loginRunner();
|
|
510
678
|
if (loginResult.ok) {
|
|
511
|
-
console.log(chalk.green(
|
|
679
|
+
console.log(chalk.green(` \u2713 ${claudeProfilerAuth.loginLabel} finished.`));
|
|
512
680
|
} else {
|
|
513
681
|
console.log(
|
|
514
682
|
chalk.yellow(
|
|
515
|
-
` \u26a0
|
|
683
|
+
` \u26a0 ${claudeProfilerAuth.loginLabel} exited with status ${loginResult.status ?? 'unknown'}. Profiling will still be attempted; configure auth and re-run init if it fails.`,
|
|
516
684
|
),
|
|
517
685
|
);
|
|
518
686
|
}
|
|
@@ -567,6 +735,8 @@ export async function initCommand(options) {
|
|
|
567
735
|
projectName: projectNameForSkill,
|
|
568
736
|
apiUrl: envVars?.apiUrl,
|
|
569
737
|
apiToken: envVars?.apiToken,
|
|
738
|
+
modelOverride: agentTarget === 'claude' ? claudeProfilerAuth?.model : undefined,
|
|
739
|
+
extraEnv: agentTarget === 'claude' ? claudeProfilerAuth?.envOverrides : undefined,
|
|
570
740
|
cursorTrust: !options.profilerNoTrust,
|
|
571
741
|
streamProgress: profilerVerbose,
|
|
572
742
|
showOutput: showProfilerOutput,
|
|
@@ -629,7 +799,7 @@ export async function initCommand(options) {
|
|
|
629
799
|
console.log(chalk.dim(' Typical fixes:'));
|
|
630
800
|
console.log(
|
|
631
801
|
chalk.dim(
|
|
632
|
-
' •
|
|
802
|
+
' • Choose the right auth mode: subscription, Console, API key, gateway, Bedrock, or Vertex.',
|
|
633
803
|
),
|
|
634
804
|
);
|
|
635
805
|
console.log(
|
|
@@ -639,7 +809,17 @@ export async function initCommand(options) {
|
|
|
639
809
|
);
|
|
640
810
|
console.log(
|
|
641
811
|
chalk.dim(
|
|
642
|
-
' •
|
|
812
|
+
' • API key / gateway: provide `--claude-api-key`, or `--claude-base-url` plus `--claude-auth-token`, or set the matching env vars before re-running init.',
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
console.log(
|
|
816
|
+
chalk.dim(
|
|
817
|
+
' • Bedrock / Vertex: make sure your cloud credentials are already available in this shell before profiling.',
|
|
818
|
+
),
|
|
819
|
+
);
|
|
820
|
+
console.log(
|
|
821
|
+
chalk.dim(
|
|
822
|
+
' • MCP missing: re-run init with Claude Code selected and MCP installation enabled so `.mcp.json` is written and `.claude/settings.json` enables `security-review-mcp`.',
|
|
643
823
|
),
|
|
644
824
|
);
|
|
645
825
|
} else if (agentTarget === 'codex') {
|
|
@@ -29,17 +29,17 @@ function getClaudeSessionStartHooks() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Generate Claude Code MCP config at .claude/settings.json
|
|
32
|
+
* Generate Claude Code project MCP config at .mcp.json and project settings at .claude/settings.json
|
|
33
33
|
*/
|
|
34
34
|
export function generate(cwd, envVars) {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
35
|
+
const mcpPath = join(cwd, '.mcp.json');
|
|
36
|
+
const mcpConfig = readJson(mcpPath) || {};
|
|
37
37
|
|
|
38
|
-
if (!
|
|
39
|
-
|
|
38
|
+
if (!mcpConfig.mcpServers) {
|
|
39
|
+
mcpConfig.mcpServers = {};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
mcpConfig.mcpServers[MCP_SERVER_NAME] = {
|
|
43
43
|
command: 'npx',
|
|
44
44
|
args: ['-y', `${MCP_SERVER_PACKAGE}@latest`],
|
|
45
45
|
env: {
|
|
@@ -48,6 +48,17 @@ export function generate(cwd, envVars) {
|
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
writeJson(mcpPath, mcpConfig);
|
|
52
|
+
|
|
53
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
54
|
+
const existing = readJson(settingsPath) || {};
|
|
55
|
+
const enabledServers = Array.isArray(existing.enabledMcpjsonServers)
|
|
56
|
+
? existing.enabledMcpjsonServers.filter((name) => typeof name === 'string' && name.trim())
|
|
57
|
+
: [];
|
|
58
|
+
if (!enabledServers.includes(MCP_SERVER_NAME)) {
|
|
59
|
+
existing.enabledMcpjsonServers = [...enabledServers, MCP_SERVER_NAME];
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
const existingSessionStart = Array.isArray(existing.SessionStart) ? existing.SessionStart : [];
|
|
52
63
|
const marker = 'MANDATORY SECURITY GATE (Claude Code Session Policy)';
|
|
53
64
|
const ours = getClaudeSessionStartHooks();
|
|
@@ -57,6 +68,6 @@ export function generate(cwd, envVars) {
|
|
|
57
68
|
);
|
|
58
69
|
existing.SessionStart = hasOurs ? existingSessionStart : [...existingSessionStart, ...ours];
|
|
59
70
|
|
|
60
|
-
writeJson(
|
|
61
|
-
return
|
|
71
|
+
writeJson(settingsPath, existing);
|
|
72
|
+
return mcpPath;
|
|
62
73
|
}
|
|
@@ -5,7 +5,7 @@ import { test } from 'node:test';
|
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
6
|
import { generate } from './claude.js';
|
|
7
7
|
|
|
8
|
-
test('Claude MCP generator writes mcp server and session hooks', () => {
|
|
8
|
+
test('Claude MCP generator writes .mcp.json, enables the project MCP server, and adds session hooks', () => {
|
|
9
9
|
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-mcp-'));
|
|
10
10
|
|
|
11
11
|
generate(cwd, {
|
|
@@ -13,10 +13,12 @@ test('Claude MCP generator writes mcp server and session hooks', () => {
|
|
|
13
13
|
apiToken: 'secret-token',
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
assert.equal(
|
|
19
|
-
assert.
|
|
16
|
+
const mcpConfig = JSON.parse(readFileSync(join(cwd, '.mcp.json'), 'utf8'));
|
|
17
|
+
const settings = JSON.parse(readFileSync(join(cwd, '.claude', 'settings.json'), 'utf8'));
|
|
18
|
+
assert.equal(mcpConfig.mcpServers['security-review-mcp'].command, 'npx');
|
|
19
|
+
assert.deepEqual(settings.enabledMcpjsonServers, ['security-review-mcp']);
|
|
20
|
+
assert.equal(Array.isArray(settings.SessionStart), true);
|
|
21
|
+
assert.match(settings.SessionStart[0].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
test('Claude MCP generator preserves existing SessionStart hooks', () => {
|
|
@@ -50,4 +52,5 @@ test('Claude MCP generator preserves existing SessionStart hooks', () => {
|
|
|
50
52
|
assert.equal(config.SessionStart.length, 2);
|
|
51
53
|
assert.match(config.SessionStart[0].hooks[0].prompt, /Existing hook/);
|
|
52
54
|
assert.match(config.SessionStart[1].hooks[0].prompt, /MANDATORY SECURITY GATE/);
|
|
55
|
+
assert.deepEqual(config.enabledMcpjsonServers, ['security-review-mcp']);
|
|
53
56
|
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
CTM_SYNC_AGENT_REL_PATH,
|
|
5
5
|
GUARDRAILS_PROFILER_SKILL_REL_DIR,
|
|
6
6
|
GUARDRAILS_SELECTION_SKILL_REL_DIR,
|
|
7
|
+
SENTINEL_END,
|
|
8
|
+
SENTINEL_START,
|
|
7
9
|
THREAT_MODELLING_SKILL_REL_DIR,
|
|
8
10
|
} from '../../utils/constants.js';
|
|
9
|
-
import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
|
|
11
|
+
import { readText, upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
|
|
10
12
|
import {
|
|
11
13
|
getCtmSyncWorkflowContent,
|
|
12
14
|
getGuardrailsInitProfileContent,
|
|
@@ -40,7 +42,7 @@ function getClaudeCtmSyncAgentContent(options = {}) {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
|
-
* Generate Claude Code workspace rule — appends to CLAUDE.md
|
|
45
|
+
* Generate Claude Code workspace rule — appends to .claude/CLAUDE.md
|
|
44
46
|
*/
|
|
45
47
|
export function generate(cwd, options = {}) {
|
|
46
48
|
const optionsWithSkillDirs = {
|
|
@@ -49,10 +51,25 @@ export function generate(cwd, options = {}) {
|
|
|
49
51
|
threatModellingSkillDir: THREAT_MODELLING_SKILL_REL_DIR.claude,
|
|
50
52
|
ctmSyncAgentPath: CTM_SYNC_AGENT_REL_PATH.claude,
|
|
51
53
|
};
|
|
52
|
-
const
|
|
54
|
+
const legacyRootPath = join(cwd, 'CLAUDE.md');
|
|
55
|
+
const filePath = join(cwd, '.claude', 'CLAUDE.md');
|
|
53
56
|
const content = getRuleContent(optionsWithSkillDirs);
|
|
54
57
|
const action = upsertSentinelBlock(filePath, content);
|
|
55
58
|
|
|
59
|
+
const legacyContent = readText(legacyRootPath);
|
|
60
|
+
if (legacyContent.includes(SENTINEL_START) && legacyContent.includes(SENTINEL_END)) {
|
|
61
|
+
const startIdx = legacyContent.indexOf(SENTINEL_START);
|
|
62
|
+
const endIdx = legacyContent.indexOf(SENTINEL_END);
|
|
63
|
+
const before = legacyContent.substring(0, startIdx);
|
|
64
|
+
const after = legacyContent.substring(endIdx + SENTINEL_END.length);
|
|
65
|
+
const migrated = `${before}${after}`.trim();
|
|
66
|
+
if (migrated) {
|
|
67
|
+
writeText(legacyRootPath, migrated + '\n');
|
|
68
|
+
} else if (existsSync(legacyRootPath)) {
|
|
69
|
+
unlinkSync(legacyRootPath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
const threatSkillPath = join(cwd, THREAT_MODELLING_SKILL_REL_DIR.claude, 'SKILL.md');
|
|
57
74
|
const threatSkill = writeGeneratedText(
|
|
58
75
|
threatSkillPath,
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { test } from 'node:test';
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
6
|
import { generate } from './claude.js';
|
|
7
7
|
|
|
8
|
-
test('Claude generator writes CLAUDE.md, threat skill, ctm_sync agent, and profiling command', () => {
|
|
8
|
+
test('Claude generator writes .claude/CLAUDE.md, threat skill, ctm_sync agent, and profiling command', () => {
|
|
9
9
|
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-'));
|
|
10
10
|
|
|
11
11
|
const results = generate(cwd, { projectName: 'SmokeProject' });
|
|
12
12
|
|
|
13
13
|
const expectedPaths = [
|
|
14
|
-
'CLAUDE.md',
|
|
14
|
+
'.claude/CLAUDE.md',
|
|
15
15
|
'.claude/skills/threat-modelling/SKILL.md',
|
|
16
16
|
'.claude/agents/ctm_sync.md',
|
|
17
17
|
'.claude/commands/guardrails-init-profile.md',
|
|
@@ -23,7 +23,7 @@ test('Claude generator writes CLAUDE.md, threat skill, ctm_sync agent, and profi
|
|
|
23
23
|
|
|
24
24
|
assert.deepEqual(results.map((entry) => entry.kind), ['rule', 'skill', 'agent', 'command']);
|
|
25
25
|
|
|
26
|
-
const instructions = readFileSync(join(cwd, 'CLAUDE.md'), 'utf8');
|
|
26
|
+
const instructions = readFileSync(join(cwd, '.claude/CLAUDE.md'), 'utf8');
|
|
27
27
|
assert.match(instructions, /\.claude\/skills\/guardrails-selection\/SKILL\.md/);
|
|
28
28
|
assert.match(instructions, /\.claude\/skills\/threat-modelling\/SKILL\.md/);
|
|
29
29
|
assert.match(instructions, /\.claude\/agents\/ctm_sync\.md/);
|
|
@@ -37,3 +37,19 @@ test('Claude generator writes CLAUDE.md, threat skill, ctm_sync agent, and profi
|
|
|
37
37
|
assert.match(agent, /model: inherit/);
|
|
38
38
|
assert.match(agent, /Configured SRAI project name: `SmokeProject`/);
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
test('Claude generator migrates the kit-managed root CLAUDE.md block into .claude/CLAUDE.md', () => {
|
|
42
|
+
const cwd = mkdtempSync(join(tmpdir(), 'securityreview-kit-claude-migrate-'));
|
|
43
|
+
const legacyPath = join(cwd, 'CLAUDE.md');
|
|
44
|
+
|
|
45
|
+
writeFileSync(
|
|
46
|
+
legacyPath,
|
|
47
|
+
'<!-- securityreview-kit:start -->\nold managed block\n<!-- securityreview-kit:end -->\n',
|
|
48
|
+
'utf8',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
generate(cwd, { projectName: 'SmokeProject' });
|
|
52
|
+
|
|
53
|
+
assert.equal(existsSync(join(cwd, '.claude/CLAUDE.md')), true);
|
|
54
|
+
assert.equal(existsSync(legacyPath), false);
|
|
55
|
+
});
|
|
@@ -33,12 +33,14 @@ You can still sign in manually with `agent login` (or `cursor-agent login`). To
|
|
|
33
33
|
|
|
34
34
|
## Claude Code CLI (scripted)
|
|
35
35
|
|
|
36
|
-
From the repo root, non-interactive runs should execute with the project settings file
|
|
36
|
+
From the repo root, non-interactive runs should execute with the project settings file, the project `.mcp.json` server config, explicit MCP-only loading, bypassed tool prompts for the profiling pass, and the Haiku model:
|
|
37
37
|
|
|
38
|
-
`claude -p "<your profiling instructions>" --settings .claude/settings.json --model haiku`
|
|
38
|
+
`claude -p "<your profiling instructions>" --settings .claude/settings.json --mcp-config "$(cat .mcp.json)" --strict-mcp-config --permission-mode bypassPermissions --model haiku`
|
|
39
39
|
|
|
40
40
|
During `securityreview-kit init`, choose **Yes** when asked to run Claude Code login, or pass **`--profiler-claude-login`** with **`--profile-repo`** so `claude auth login` and profiling stay in one run.
|
|
41
41
|
|
|
42
|
+
Claude profiling can also run with **Anthropic Console**, **ANTHROPIC_API_KEY**, an **Anthropic-compatible gateway** (`ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`), or cloud-provider credentials such as **AWS Bedrock** and **Google Vertex AI**. `securityreview-kit init` can branch into those auth modes before profiling.
|
|
43
|
+
|
|
42
44
|
## GitHub Copilot CLI (scripted)
|
|
43
45
|
|
|
44
46
|
From the repo root, non-interactive runs should load the SRAI MCP server and allow the tools needed to scan, write profile files, and call MCP:
|
package/src/utils/constants.js
CHANGED
|
@@ -17,8 +17,8 @@ export const TARGETS = {
|
|
|
17
17
|
},
|
|
18
18
|
claude: {
|
|
19
19
|
name: 'Claude Code',
|
|
20
|
-
mcpConfigPath: '.
|
|
21
|
-
rulePath: 'CLAUDE.md',
|
|
20
|
+
mcpConfigPath: '.mcp.json',
|
|
21
|
+
rulePath: '.claude/CLAUDE.md',
|
|
22
22
|
ruleMode: 'append',
|
|
23
23
|
detectDirs: ['.claude'],
|
|
24
24
|
},
|
|
@@ -15,8 +15,15 @@ export function getProfilerLogPath(cwd, target) {
|
|
|
15
15
|
return join(cwd, '.guardrails', 'logs', `profiler-${target}.log`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function buildProfilerEnv(target, streamProgress) {
|
|
18
|
+
function buildProfilerEnv(target, streamProgress, extraEnv = {}) {
|
|
19
19
|
const env = augmentPathEnv(process.env);
|
|
20
|
+
for (const [key, value] of Object.entries(extraEnv || {})) {
|
|
21
|
+
if (value == null || value === '') {
|
|
22
|
+
delete env[key];
|
|
23
|
+
} else {
|
|
24
|
+
env[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
20
27
|
if (target === 'codex' && streamProgress && !env.RUST_LOG) {
|
|
21
28
|
env.RUST_LOG = 'info';
|
|
22
29
|
}
|
|
@@ -113,6 +120,34 @@ export function runCopilotLogin(cwd) {
|
|
|
113
120
|
* Run Claude Code login in the current terminal.
|
|
114
121
|
*/
|
|
115
122
|
export function runClaudeLogin(cwd) {
|
|
123
|
+
return runClaudeAuthLogin(cwd, { mode: 'claudeai' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Run Claude Code auth login in the current terminal.
|
|
128
|
+
*/
|
|
129
|
+
export function runClaudeAuthLogin(cwd, options = {}) {
|
|
130
|
+
const env = augmentPathEnv(process.env);
|
|
131
|
+
if (!commandOk('claude', ['--version'], env)) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
status: null,
|
|
135
|
+
message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const mode = options.mode === 'console' ? '--console' : '--claudeai';
|
|
139
|
+
const r = spawnSync('claude', ['auth', 'login', mode], { cwd, stdio: 'inherit', env });
|
|
140
|
+
const spawnErr = r.error ? r.error.message : null;
|
|
141
|
+
if (r.status === null && spawnErr) {
|
|
142
|
+
return { ok: false, status: null, message: spawnErr };
|
|
143
|
+
}
|
|
144
|
+
return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Run Claude Code long-lived token setup in the current terminal.
|
|
149
|
+
*/
|
|
150
|
+
export function runClaudeSetupToken(cwd) {
|
|
116
151
|
const env = augmentPathEnv(process.env);
|
|
117
152
|
if (!commandOk('claude', ['--version'], env)) {
|
|
118
153
|
return {
|
|
@@ -121,7 +156,7 @@ export function runClaudeLogin(cwd) {
|
|
|
121
156
|
message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
|
|
122
157
|
};
|
|
123
158
|
}
|
|
124
|
-
const r = spawnSync('claude', ['
|
|
159
|
+
const r = spawnSync('claude', ['setup-token'], { cwd, stdio: 'inherit', env });
|
|
125
160
|
const spawnErr = r.error ? r.error.message : null;
|
|
126
161
|
if (r.status === null && spawnErr) {
|
|
127
162
|
return { ok: false, status: null, message: spawnErr };
|
|
@@ -178,6 +213,21 @@ export function buildCodexConfigOverrides(cwd, overrides = {}) {
|
|
|
178
213
|
];
|
|
179
214
|
}
|
|
180
215
|
|
|
216
|
+
export function buildClaudeAdditionalMcpConfig(cwd) {
|
|
217
|
+
const projectMcp = readJson(join(cwd, '.mcp.json'));
|
|
218
|
+
const sourceServer = projectMcp?.mcpServers?.[MCP_SERVER_NAME];
|
|
219
|
+
|
|
220
|
+
if (!sourceServer || typeof sourceServer !== 'object') {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return JSON.stringify({
|
|
225
|
+
mcpServers: {
|
|
226
|
+
[MCP_SERVER_NAME]: sourceServer,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
181
231
|
export function pickProfilerAgentTarget(targets) {
|
|
182
232
|
for (const t of PREFERRED_ORDER) {
|
|
183
233
|
if (targets.includes(t)) {
|
|
@@ -205,13 +255,15 @@ export function runProfilerAgent(
|
|
|
205
255
|
projectName,
|
|
206
256
|
apiUrl,
|
|
207
257
|
apiToken,
|
|
258
|
+
modelOverride,
|
|
259
|
+
extraEnv,
|
|
208
260
|
cursorTrust = true,
|
|
209
261
|
streamProgress = false,
|
|
210
262
|
showOutput = streamProgress,
|
|
211
263
|
},
|
|
212
264
|
) {
|
|
213
265
|
const prompt = buildProfilerAgentPrompt(projectName, target);
|
|
214
|
-
const env = buildProfilerEnv(target, streamProgress);
|
|
266
|
+
const env = buildProfilerEnv(target, streamProgress, extraEnv);
|
|
215
267
|
const opts = showOutput
|
|
216
268
|
? { cwd, stdio: 'inherit', env }
|
|
217
269
|
: { cwd, stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 };
|
|
@@ -251,7 +303,26 @@ export function runProfilerAgent(
|
|
|
251
303
|
return { ok: false, message: 'claude not on PATH' };
|
|
252
304
|
}
|
|
253
305
|
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
254
|
-
const
|
|
306
|
+
const mcpConfig = buildClaudeAdditionalMcpConfig(cwd);
|
|
307
|
+
if (!mcpConfig) {
|
|
308
|
+
return {
|
|
309
|
+
ok: false,
|
|
310
|
+
message:
|
|
311
|
+
'Claude profiling needs the SRAI MCP server in .mcp.json. Re-run init with MCP installation enabled.',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const args = [
|
|
315
|
+
'-p',
|
|
316
|
+
'--settings',
|
|
317
|
+
settingsPath,
|
|
318
|
+
'--mcp-config',
|
|
319
|
+
mcpConfig,
|
|
320
|
+
'--strict-mcp-config',
|
|
321
|
+
'--permission-mode',
|
|
322
|
+
'bypassPermissions',
|
|
323
|
+
'--model',
|
|
324
|
+
modelOverride || 'haiku',
|
|
325
|
+
];
|
|
255
326
|
if (streamProgress) {
|
|
256
327
|
args.push('--output-format', 'stream-json', '--include-partial-messages', '--include-hook-events', '--verbose');
|
|
257
328
|
}
|