security-mcp 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +77 -21
  2. package/defaults/checklists/ai.json +25 -0
  3. package/defaults/checklists/api.json +27 -0
  4. package/defaults/checklists/infra.json +27 -0
  5. package/defaults/checklists/mobile.json +25 -0
  6. package/defaults/checklists/payments.json +25 -0
  7. package/defaults/checklists/web.json +30 -0
  8. package/defaults/control-catalog.json +549 -0
  9. package/defaults/evidence-map.json +194 -0
  10. package/defaults/security-exceptions.json +4 -0
  11. package/defaults/security-policy.json +41 -2
  12. package/defaults/security-tools.json +41 -0
  13. package/dist/ci/pr-gate.js +2 -3
  14. package/dist/cli/index.js +63 -23
  15. package/dist/cli/install.js +47 -15
  16. package/dist/cli/onboarding.js +590 -0
  17. package/dist/cli/update.js +124 -0
  18. package/dist/gate/baseline.js +115 -0
  19. package/dist/gate/catalog.js +55 -0
  20. package/dist/gate/checks/ai-redteam.js +374 -0
  21. package/dist/gate/checks/ai.js +45 -14
  22. package/dist/gate/checks/api.js +93 -0
  23. package/dist/gate/checks/crypto.js +153 -0
  24. package/dist/gate/checks/database.js +144 -0
  25. package/dist/gate/checks/dependencies.js +130 -0
  26. package/dist/gate/checks/dlp.js +153 -0
  27. package/dist/gate/checks/graphql.js +122 -0
  28. package/dist/gate/checks/infra.js +126 -12
  29. package/dist/gate/checks/k8s.js +190 -0
  30. package/dist/gate/checks/playbook.js +160 -0
  31. package/dist/gate/checks/runtime.js +263 -0
  32. package/dist/gate/checks/sbom.js +199 -0
  33. package/dist/gate/checks/scanners.js +450 -0
  34. package/dist/gate/checks/secrets.js +119 -27
  35. package/dist/gate/diff.js +2 -2
  36. package/dist/gate/evidence.js +116 -0
  37. package/dist/gate/exceptions.js +85 -0
  38. package/dist/gate/policy.js +189 -17
  39. package/dist/gate/threat-intel.js +157 -0
  40. package/dist/mcp/server.js +938 -9
  41. package/dist/repo/fs.js +10 -5
  42. package/dist/repo/search.js +13 -1
  43. package/dist/review/store.js +208 -0
  44. package/dist/tests/run.js +103 -0
  45. package/package.json +13 -3
  46. package/prompts/SECURITY_PROMPT.md +455 -1
  47. package/skills/senior-security-engineer/SKILL.md +81 -4
@@ -122,5 +122,199 @@
122
122
  "src/**/tool-router*.ts",
123
123
  "src/**/tool*.ts",
124
124
  "src/**/agent*.ts"
125
+ ],
126
+ "mfa_enforced": [
127
+ "src/**/mfa*.ts",
128
+ "src/**/mfa*.js",
129
+ "src/**/totp*.ts",
130
+ "src/**/webauthn*.ts",
131
+ "src/**/passkey*.ts",
132
+ "src/**/two-factor*.ts",
133
+ "src/**/2fa*.ts",
134
+ "infra/**",
135
+ "terraform/**",
136
+ "k8s/**"
137
+ ],
138
+ "audit_logging_configured": [
139
+ "src/**/audit*.ts",
140
+ "src/**/audit*.js",
141
+ "src/**/logger*.ts",
142
+ "src/**/logging*.ts",
143
+ "src/**/log*.ts",
144
+ "lib/**/audit*.ts",
145
+ "infra/**",
146
+ "terraform/**",
147
+ "k8s/**",
148
+ "helm/**"
149
+ ],
150
+ "log_retention_policy": [
151
+ "infra/**",
152
+ "terraform/**",
153
+ "k8s/**",
154
+ "helm/**",
155
+ "docs/**",
156
+ "security/**",
157
+ ".mcp/**"
158
+ ],
159
+ "alerting_configured": [
160
+ "infra/**",
161
+ "terraform/**",
162
+ "k8s/**",
163
+ "helm/**",
164
+ "src/**/alert*.ts",
165
+ "src/**/alarm*.ts",
166
+ "src/**/monitor*.ts",
167
+ "src/**/pagerduty*.ts",
168
+ "src/**/opsgenie*.ts"
169
+ ],
170
+ "email_filtering": [
171
+ "infra/**",
172
+ "terraform/**",
173
+ "docs/**",
174
+ "security/**"
175
+ ],
176
+ "public_surface_hardened": [
177
+ "infra/**",
178
+ "terraform/**",
179
+ "k8s/**",
180
+ "helm/**",
181
+ "src/**/waf*.ts",
182
+ "src/**/ddos*.ts",
183
+ "nginx/**",
184
+ "caddy/**"
185
+ ],
186
+ "rate_limiting_present": [
187
+ "src/**/rate-limit*.ts",
188
+ "src/**/rateLimit*.ts",
189
+ "src/**/throttle*.ts",
190
+ "src/**/middleware*.ts",
191
+ "middleware.ts",
192
+ "middleware.js",
193
+ "app/api/**",
194
+ "src/api/**"
195
+ ],
196
+ "egress_controls": [
197
+ "infra/**",
198
+ "terraform/**",
199
+ "k8s/**",
200
+ "helm/**",
201
+ "src/**/egress*.ts",
202
+ "src/**/firewall*.ts",
203
+ "src/**/network*.ts"
204
+ ],
205
+ "dlp_configured": [
206
+ "src/**/dlp*.ts",
207
+ "src/**/dlp*.js",
208
+ "src/**/pii*.ts",
209
+ "src/**/redact*.ts",
210
+ "src/**/sanitize*.ts",
211
+ "infra/**",
212
+ "terraform/**"
213
+ ],
214
+ "artifact_signing": [
215
+ ".github/workflows/**",
216
+ ".gitlab-ci.yml",
217
+ "cloudbuild.yaml",
218
+ "Makefile",
219
+ "scripts/**",
220
+ "infra/**",
221
+ "cosign.pub",
222
+ "*.pub"
223
+ ],
224
+ "hermetic_build_configured": [
225
+ ".github/workflows/**",
226
+ ".gitlab-ci.yml",
227
+ "cloudbuild.yaml",
228
+ "Makefile",
229
+ "scripts/**",
230
+ "BUILD",
231
+ "BUILD.bazel"
232
+ ],
233
+ "ir_playbook_present": [
234
+ "security/**",
235
+ "docs/security/**",
236
+ "runbooks/**",
237
+ "playbooks/**",
238
+ ".mcp/**"
239
+ ],
240
+ "ir_playbook_tested": [
241
+ "security/**",
242
+ "docs/security/**",
243
+ "runbooks/**",
244
+ "playbooks/**"
245
+ ],
246
+ "network_segmentation": [
247
+ "infra/**",
248
+ "terraform/**",
249
+ "k8s/**",
250
+ "helm/**"
251
+ ],
252
+ "zero_trust_network": [
253
+ "infra/**",
254
+ "terraform/**",
255
+ "k8s/**",
256
+ "helm/**",
257
+ "src/**/mtls*.ts",
258
+ "src/**/spiffe*.ts",
259
+ "src/**/zero-trust*.ts"
260
+ ],
261
+ "backup_encryption": [
262
+ "infra/**",
263
+ "terraform/**",
264
+ "k8s/**"
265
+ ],
266
+ "backup_tested": [
267
+ "docs/**",
268
+ "security/**",
269
+ "runbooks/**"
270
+ ],
271
+ "dns_monitoring": [
272
+ "infra/**",
273
+ "terraform/**"
274
+ ],
275
+ "pan_encryption_configured": [
276
+ "infra/**",
277
+ "terraform/**",
278
+ "src/**/payment*.ts",
279
+ "src/**/card*.ts",
280
+ "src/**/stripe*.ts",
281
+ "src/**/braintree*.ts"
282
+ ],
283
+ "no_plaintext_pan": [
284
+ "**/*.ts",
285
+ "**/*.js",
286
+ "**/*.py",
287
+ "**/*.go",
288
+ "**/*.java"
289
+ ],
290
+ "input_validation_schema": [
291
+ "src/**",
292
+ "app/**",
293
+ "lib/**",
294
+ "server/**"
295
+ ],
296
+ "output_encoding_present": [
297
+ "src/**",
298
+ "app/**",
299
+ "lib/**"
300
+ ],
301
+ "service_account_per_workload": [
302
+ "infra/**",
303
+ "terraform/**",
304
+ "k8s/**",
305
+ "helm/**"
306
+ ],
307
+ "human_in_loop_for_actions": [
308
+ "ai/**",
309
+ "src/**/agent*.ts",
310
+ "src/**/tool*.ts",
311
+ "src/**/confirm*.ts",
312
+ "src/**/approval*.ts"
313
+ ],
314
+ "input_length_limits": [
315
+ "src/**",
316
+ "app/**",
317
+ "middleware.ts",
318
+ "middleware.js"
125
319
  ]
126
320
  }
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "exceptions": []
4
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "security-policy",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Default security gate policy for security-mcp. Copy to .mcp/policies/security-policy.json and customize for your project.",
5
5
  "required_checks": {
6
6
  "secrets_scan": { "severity_block": ["HIGH", "CRITICAL"] },
@@ -8,6 +8,44 @@
8
8
  "sast": { "severity_block": ["CRITICAL"] },
9
9
  "iac_scan": { "severity_block": ["HIGH", "CRITICAL"] }
10
10
  },
11
+ "environments": {
12
+ "dev": {
13
+ "severity_block": ["CRITICAL"],
14
+ "required_scanners": ["gitleaks"],
15
+ "required_checks": ["secrets_scan"],
16
+ "vulnerability_slas": {
17
+ "CRITICAL": "7d",
18
+ "HIGH": "30d",
19
+ "MEDIUM": "90d",
20
+ "LOW": "never"
21
+ }
22
+ },
23
+ "staging": {
24
+ "severity_block": ["HIGH", "CRITICAL"],
25
+ "required_scanners": ["gitleaks", "semgrep", "osv-scanner"],
26
+ "required_checks": ["secrets_scan", "dependency_scan", "sast"],
27
+ "vulnerability_slas": {
28
+ "CRITICAL": "24h",
29
+ "HIGH": "7d",
30
+ "MEDIUM": "30d",
31
+ "LOW": "90d",
32
+ "CISA_KEV": "24h"
33
+ }
34
+ },
35
+ "prod": {
36
+ "severity_block": ["MEDIUM", "HIGH", "CRITICAL"],
37
+ "required_scanners": ["gitleaks", "semgrep", "osv-scanner", "trivy", "checkov"],
38
+ "required_checks": ["secrets_scan", "dependency_scan", "sast", "iac_scan"],
39
+ "vulnerability_slas": {
40
+ "CRITICAL": "24h",
41
+ "HIGH": "7d",
42
+ "MEDIUM": "30d",
43
+ "LOW": "90d",
44
+ "CISA_KEV": "24h",
45
+ "HIGH_EPSS": "48h"
46
+ }
47
+ }
48
+ },
11
49
  "requirements": [
12
50
  {
13
51
  "id": "ZERO_TRUST",
@@ -84,7 +122,8 @@
84
122
  "HIGH": "7d",
85
123
  "MEDIUM": "30d",
86
124
  "LOW": "90d",
87
- "CISA_KEV": "24h"
125
+ "CISA_KEV": "24h",
126
+ "HIGH_EPSS": "48h"
88
127
  },
89
128
  "exceptions": {
90
129
  "require_ticket": true,
@@ -0,0 +1,41 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "fail_closed": true,
4
+ "scanners": {
5
+ "gitleaks": {
6
+ "command": "gitleaks",
7
+ "args": ["version"],
8
+ "required_for": ["all"]
9
+ },
10
+ "semgrep": {
11
+ "command": "semgrep",
12
+ "args": ["--version"],
13
+ "required_for": ["web", "api", "ai"]
14
+ },
15
+ "osv-scanner": {
16
+ "command": "osv-scanner",
17
+ "args": ["--version"],
18
+ "required_for": ["web", "api", "ai"]
19
+ },
20
+ "checkov": {
21
+ "command": "checkov",
22
+ "args": ["--version"],
23
+ "required_for": ["infra"]
24
+ },
25
+ "trivy": {
26
+ "command": "trivy",
27
+ "args": ["--version"],
28
+ "required_for": ["infra"]
29
+ },
30
+ "conftest": {
31
+ "command": "conftest",
32
+ "args": ["--version"],
33
+ "required_for": ["infra"]
34
+ },
35
+ "zaproxy": {
36
+ "command": "zaproxy",
37
+ "args": ["-version"],
38
+ "required_for": ["web", "api"]
39
+ }
40
+ }
41
+ }
@@ -1,6 +1,6 @@
1
1
  import { runPrGate } from "../gate/policy.js";
2
- // Allowlist refs to the same safe character set enforced in diff.ts. CWE-88.
3
- const SAFE_REF_RE = /^[a-zA-Z0-9_.\-/]+$/;
2
+ // Allow safe git revision operators (~ and ^) plus ref/path characters. CWE-88.
3
+ const SAFE_REF_RE = /^[a-zA-Z0-9_./~^-]+$/;
4
4
  function safeEnvRef(envVar, defaultValue) {
5
5
  const val = process.env[envVar] || defaultValue;
6
6
  if (!SAFE_REF_RE.test(val)) {
@@ -20,7 +20,6 @@ async function main() {
20
20
  process.exit(2);
21
21
  }
22
22
  }
23
- // eslint-disable-next-line unicorn/prefer-top-level-await
24
23
  main().catch((err) => {
25
24
  console.error("security gate crashed:", err);
26
25
  process.exit(3);
package/dist/cli/index.js CHANGED
@@ -14,6 +14,7 @@ import { fileURLToPath } from "url";
14
14
  import { dirname, resolve } from "path";
15
15
  import { runInstall } from "./install.js";
16
16
  import { main as runServer } from "../mcp/server.js";
17
+ import { notifyIfUpdateAvailable } from "./update.js";
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
19
  const require = createRequire(import.meta.url);
19
20
  function getVersion() {
@@ -26,25 +27,44 @@ function getVersion() {
26
27
  }
27
28
  }
28
29
  const VERSION = getVersion();
30
+ function getConfigSnippet(useGlobalBinary) {
31
+ return {
32
+ mcpServers: {
33
+ "security-mcp": useGlobalBinary
34
+ ? {
35
+ command: "security-mcp",
36
+ args: ["serve"]
37
+ }
38
+ : {
39
+ command: "npx",
40
+ args: ["-y", "security-mcp@latest", "serve"]
41
+ }
42
+ }
43
+ };
44
+ }
29
45
  const HELP = `
30
46
  security-mcp v${VERSION}
31
47
 
32
48
  AI security MCP server and gate for Claude Code, Cursor, Copilot, Codex, Replit, and any MCP-compatible editor.
33
49
 
34
50
  USAGE
35
- npx security-mcp <command> [options]
51
+ npx -y security-mcp@latest <command> [options]
36
52
 
37
53
  COMMANDS
38
54
  serve Start the MCP server over stdio (default for editors)
39
55
  install Auto-detect installed editors and write MCP configs
56
+ install-global Install using the globally installed security-mcp binary
40
57
  config Print MCP config JSON for manual editor setup
41
58
 
42
59
  OPTIONS (install)
43
- --claude-code Write config for Claude Code only
44
- --cursor Write config for Cursor only
45
- --vscode Write config for VS Code only
46
- --global Write to global editor config (default)
47
- --dry-run Print what would change without writing
60
+ --claude-code Write config for Claude Code only
61
+ --cursor Write config for Cursor only
62
+ --vscode Write config for VS Code only
63
+ --global Write to global editor config (default)
64
+ --use-global-binary Write configs that execute "security-mcp serve" instead of npx
65
+ --dry-run Print what would change without writing
66
+ --yes Skip interactive setup questions (install with defaults)
67
+ --non-interactive Same as --yes (for CI environments)
48
68
 
49
69
  OPTIONS (general)
50
70
  --version Print version
@@ -52,26 +72,31 @@ OPTIONS (general)
52
72
 
53
73
  EXAMPLES
54
74
  # Start MCP server (called automatically by editors):
55
- npx -y security-mcp serve
75
+ npx -y security-mcp@latest serve
56
76
 
57
77
  # Install into all detected editors:
58
- npx security-mcp install
78
+ npx -y security-mcp@latest install
79
+
80
+ # Install globally once, then configure editors to use the global binary:
81
+ npm install -g security-mcp@latest
82
+ security-mcp install-global
59
83
 
60
84
  # Install into Claude Code only:
61
- npx security-mcp install --claude-code
85
+ npx -y security-mcp@latest install --claude-code
62
86
 
63
87
  # Preview install without writing:
64
- npx security-mcp install --dry-run
88
+ npx -y security-mcp@latest install --dry-run
65
89
 
66
90
  # Print JSON config snippet:
67
- npx security-mcp config
91
+ npx -y security-mcp@latest config
92
+ security-mcp config --use-global-binary
68
93
 
69
94
  EDITOR CONFIG (add manually if install fails):
70
95
  {
71
96
  "mcpServers": {
72
97
  "security-mcp": {
73
98
  "command": "npx",
74
- "args": ["-y", "security-mcp", "serve"]
99
+ "args": ["-y", "security-mcp@latest", "serve"]
75
100
  }
76
101
  }
77
102
  }
@@ -83,16 +108,9 @@ EDITOR CONFIG (add manually if install fails):
83
108
  MORE INFO
84
109
  https://github.com/AbrahamOO/security-mcp
85
110
  `;
86
- const CONFIG_SNIPPET = {
87
- mcpServers: {
88
- "security-mcp": {
89
- command: "npx",
90
- args: ["-y", "security-mcp", "serve"]
91
- }
92
- }
93
- };
94
111
  async function main() {
95
112
  const args = process.argv.slice(2);
113
+ const useGlobalBinary = args.includes("--use-global-binary");
96
114
  if (args.includes("--version") || args.includes("-v")) {
97
115
  process.stdout.write(`security-mcp v${VERSION}\n`);
98
116
  process.exit(0);
@@ -102,6 +120,12 @@ async function main() {
102
120
  process.exit(0);
103
121
  }
104
122
  const command = args[0] ?? "serve";
123
+ if (command === "serve") {
124
+ void notifyIfUpdateAvailable(VERSION);
125
+ }
126
+ else {
127
+ await notifyIfUpdateAvailable(VERSION);
128
+ }
105
129
  switch (command) {
106
130
  case "serve": {
107
131
  // MCP stdio server - never write to stdout except via MCP protocol
@@ -109,19 +133,35 @@ async function main() {
109
133
  break;
110
134
  }
111
135
  case "install": {
136
+ const noEditorFlag = !args.includes("--claude-code") && !args.includes("--cursor") && !args.includes("--vscode");
137
+ const options = {
138
+ claudeCode: args.includes("--claude-code"),
139
+ cursor: args.includes("--cursor"),
140
+ vscode: args.includes("--vscode"),
141
+ dryRun: args.includes("--dry-run"),
142
+ useGlobalBinary,
143
+ all: noEditorFlag,
144
+ interactive: !args.includes("--yes") && !args.includes("--non-interactive")
145
+ };
146
+ await runInstall(options);
147
+ break;
148
+ }
149
+ case "install-global": {
150
+ const noEditorFlag = !args.includes("--claude-code") && !args.includes("--cursor") && !args.includes("--vscode");
112
151
  const options = {
113
152
  claudeCode: args.includes("--claude-code"),
114
153
  cursor: args.includes("--cursor"),
115
154
  vscode: args.includes("--vscode"),
116
155
  dryRun: args.includes("--dry-run"),
117
- // If no editor flag specified, install to all detected
118
- all: !args.includes("--claude-code") && !args.includes("--cursor") && !args.includes("--vscode")
156
+ useGlobalBinary: true,
157
+ all: noEditorFlag,
158
+ interactive: !args.includes("--yes") && !args.includes("--non-interactive")
119
159
  };
120
160
  await runInstall(options);
121
161
  break;
122
162
  }
123
163
  case "config": {
124
- process.stdout.write(JSON.stringify(CONFIG_SNIPPET, null, 2) + "\n");
164
+ process.stdout.write(JSON.stringify(getConfigSnippet(useGlobalBinary), null, 2) + "\n");
125
165
  process.stdout.write("\nAdd the above to your editor's MCP config file.\n");
126
166
  process.stdout.write(" Claude Code: ~/.claude/settings.json\n");
127
167
  process.stdout.write(" Cursor: ~/.cursor/mcp.json\n");
@@ -7,13 +7,9 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from
7
7
  import { dirname, join, resolve } from "node:path";
8
8
  import { homedir, platform } from "node:os";
9
9
  import { fileURLToPath } from "node:url";
10
+ import { runOnboarding, installSecurityTools, commandExists, SECURITY_TOOLS } from "./onboarding.js";
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  const PKG_ROOT = resolve(__dirname, "../..");
12
- const MCP_ENTRY = {
13
- type: "stdio",
14
- command: "npx",
15
- args: ["-y", "security-mcp", "serve"]
16
- };
17
13
  function resolveHome(p) {
18
14
  return p.replace(/^~/, homedir());
19
15
  }
@@ -28,7 +24,7 @@ function getVsCodeSettingsPath() {
28
24
  return join(homedir(), ".config", "Code", "User", "settings.json");
29
25
  }
30
26
  function getEditorTargets(opts) {
31
- const claudeCodePath = resolveHome("~/.claude.json");
27
+ const claudeCodePath = resolveHome("~/.claude/settings.json");
32
28
  const cursorGlobalPath = resolveHome("~/.cursor/mcp.json");
33
29
  const cursorLocalPath = ".cursor/mcp.json";
34
30
  const vscodePath = getVsCodeSettingsPath();
@@ -79,10 +75,23 @@ function readJsonSafe(filePath) {
79
75
  return {};
80
76
  }
81
77
  }
82
- function writeMcpServersJson(configPath, dryRun) {
78
+ function getMcpEntry(useGlobalBinary) {
79
+ return useGlobalBinary
80
+ ? {
81
+ type: "stdio",
82
+ command: "security-mcp",
83
+ args: ["serve"]
84
+ }
85
+ : {
86
+ type: "stdio",
87
+ command: "npx",
88
+ args: ["-y", "security-mcp@latest", "serve"]
89
+ };
90
+ }
91
+ function writeMcpServersJson(configPath, dryRun, useGlobalBinary) {
83
92
  const existing = readJsonSafe(configPath);
84
93
  const servers = existing["mcpServers"] ?? {};
85
- servers["security-mcp"] = MCP_ENTRY;
94
+ servers["security-mcp"] = getMcpEntry(useGlobalBinary);
86
95
  existing["mcpServers"] = servers;
87
96
  const content = JSON.stringify(existing, null, 2) + "\n";
88
97
  if (!dryRun) {
@@ -91,10 +100,10 @@ function writeMcpServersJson(configPath, dryRun) {
91
100
  }
92
101
  return configPath;
93
102
  }
94
- function writeVsCodeSettings(configPath, dryRun) {
103
+ function writeVsCodeSettings(configPath, dryRun, useGlobalBinary) {
95
104
  const existing = readJsonSafe(configPath);
96
105
  const servers = existing["mcp.servers"] ?? {};
97
- servers["security-mcp"] = MCP_ENTRY;
106
+ servers["security-mcp"] = getMcpEntry(useGlobalBinary);
98
107
  existing["mcp.servers"] = servers;
99
108
  const content = JSON.stringify(existing, null, 2) + "\n";
100
109
  if (!dryRun) {
@@ -108,7 +117,19 @@ function installPolicy(dryRun) {
108
117
  const policyDest = join(process.cwd(), ".mcp", "policies", "security-policy.json");
109
118
  const evidenceSrc = join(PKG_ROOT, "defaults", "evidence-map.json");
110
119
  const evidenceDest = join(process.cwd(), ".mcp", "mappings", "evidence-map.json");
111
- for (const { src, dest } of [{ src: policySrc, dest: policyDest }, { src: evidenceSrc, dest: evidenceDest }]) {
120
+ const catalogSrc = join(PKG_ROOT, "defaults", "control-catalog.json");
121
+ const catalogDest = join(process.cwd(), ".mcp", "catalog", "control-catalog.json");
122
+ const scannersSrc = join(PKG_ROOT, "defaults", "security-tools.json");
123
+ const scannersDest = join(process.cwd(), ".mcp", "scanners", "security-tools.json");
124
+ const exceptionsSrc = join(PKG_ROOT, "defaults", "security-exceptions.json");
125
+ const exceptionsDest = join(process.cwd(), ".mcp", "exceptions", "security-exceptions.json");
126
+ for (const { src, dest } of [
127
+ { src: policySrc, dest: policyDest },
128
+ { src: evidenceSrc, dest: evidenceDest },
129
+ { src: catalogSrc, dest: catalogDest },
130
+ { src: scannersSrc, dest: scannersDest },
131
+ { src: exceptionsSrc, dest: exceptionsDest }
132
+ ]) {
112
133
  if (!existsSync(src)) {
113
134
  process.stdout.write(` [skip] ${src} not found in package\n`);
114
135
  continue;
@@ -139,13 +160,23 @@ function installSkill(dryRun) {
139
160
  }
140
161
  export async function runInstall(opts) {
141
162
  const dryRun = opts.dryRun;
163
+ // ── Interactive onboarding (skipped when --yes or non-TTY) ──────────────
164
+ if (opts.interactive && !dryRun) {
165
+ const onboarding = await runOnboarding();
166
+ if (onboarding?.installTools) {
167
+ const toInstall = SECURITY_TOOLS.filter((t) => !commandExists(t.id));
168
+ process.stdout.write("\nInstalling security scanning tools...\n");
169
+ await installSecurityTools(toInstall);
170
+ process.stdout.write("\n");
171
+ }
172
+ }
142
173
  process.stdout.write(`\nsecurity-mcp installer${dryRun ? " (dry-run)" : ""}\n`);
143
174
  process.stdout.write("=".repeat(40) + "\n\n");
144
175
  const targets = getEditorTargets(opts);
145
176
  if (targets.length === 0) {
146
177
  process.stdout.write("No supported editors detected automatically.\n" +
147
178
  "Run with --claude-code, --cursor, or --vscode to target a specific editor.\n" +
148
- 'Or add the config manually (run "npx security-mcp config" for the snippet).\n\n');
179
+ 'Or add the config manually (run "npx -y security-mcp@latest config" for the snippet).\n\n');
149
180
  return;
150
181
  }
151
182
  for (const target of targets) {
@@ -153,10 +184,10 @@ export async function runInstall(opts) {
153
184
  try {
154
185
  let written;
155
186
  if (target.type === "vscode-settings") {
156
- written = writeVsCodeSettings(target.configPath, dryRun);
187
+ written = writeVsCodeSettings(target.configPath, dryRun, opts.useGlobalBinary);
157
188
  }
158
189
  else {
159
- written = writeMcpServersJson(target.configPath, dryRun);
190
+ written = writeMcpServersJson(target.configPath, dryRun, opts.useGlobalBinary);
160
191
  }
161
192
  process.stdout.write(` ${dryRun ? "[dry-run] would update" : "updated"}: ${written}\n`);
162
193
  }
@@ -176,8 +207,9 @@ export async function runInstall(opts) {
176
207
  process.stdout.write(dryRun
177
208
  ? "Dry-run complete. Re-run without --dry-run to apply.\n"
178
209
  : "Done! Restart your editor to activate the security-mcp server.\n");
210
+ process.stdout.write(`Install mode: ${opts.useGlobalBinary ? "global binary (security-mcp serve)" : "npx (npx -y security-mcp@latest serve)"}\n`);
179
211
  process.stdout.write("\nNext steps:\n");
180
212
  process.stdout.write(" 1. Restart your editor.\n");
181
213
  process.stdout.write(' 2. In Claude Code, type /senior-security-engineer to activate the security persona.\n');
182
- process.stdout.write(' 3. Ask your AI: "Run security.run_pr_gate" to check your current diff.\n\n');
214
+ process.stdout.write(' 3. Ask your AI: "Run security.start_review with mode=recent_changes" to begin an auditable review.\n\n');
183
215
  }