ship-safe 6.1.1 → 6.2.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 +735 -641
  2. package/cli/agents/api-fuzzer.js +345 -345
  3. package/cli/agents/auth-bypass-agent.js +348 -348
  4. package/cli/agents/base-agent.js +272 -272
  5. package/cli/agents/cicd-scanner.js +236 -201
  6. package/cli/agents/config-auditor.js +521 -521
  7. package/cli/agents/deep-analyzer.js +6 -2
  8. package/cli/agents/git-history-scanner.js +170 -170
  9. package/cli/agents/html-reporter.js +568 -568
  10. package/cli/agents/index.js +84 -84
  11. package/cli/agents/injection-tester.js +500 -500
  12. package/cli/agents/llm-redteam.js +251 -251
  13. package/cli/agents/mobile-scanner.js +231 -231
  14. package/cli/agents/orchestrator.js +322 -322
  15. package/cli/agents/pii-compliance-agent.js +301 -301
  16. package/cli/agents/scoring-engine.js +248 -248
  17. package/cli/agents/supabase-rls-agent.js +154 -154
  18. package/cli/agents/supply-chain-agent.js +650 -507
  19. package/cli/bin/ship-safe.js +452 -426
  20. package/cli/commands/agent.js +608 -608
  21. package/cli/commands/audit.js +986 -980
  22. package/cli/commands/baseline.js +193 -193
  23. package/cli/commands/ci.js +342 -342
  24. package/cli/commands/deps.js +516 -516
  25. package/cli/commands/doctor.js +159 -159
  26. package/cli/commands/fix.js +218 -218
  27. package/cli/commands/hooks.js +268 -0
  28. package/cli/commands/init.js +407 -407
  29. package/cli/commands/mcp.js +304 -304
  30. package/cli/commands/red-team.js +7 -1
  31. package/cli/commands/remediate.js +798 -798
  32. package/cli/commands/rotate.js +571 -571
  33. package/cli/commands/scan.js +569 -569
  34. package/cli/commands/score.js +449 -449
  35. package/cli/commands/watch.js +281 -281
  36. package/cli/hooks/patterns.js +313 -0
  37. package/cli/hooks/post-tool-use.js +140 -0
  38. package/cli/hooks/pre-tool-use.js +186 -0
  39. package/cli/index.js +73 -69
  40. package/cli/providers/llm-provider.js +397 -287
  41. package/cli/utils/autofix-rules.js +74 -74
  42. package/cli/utils/cache-manager.js +311 -311
  43. package/cli/utils/output.js +230 -230
  44. package/cli/utils/patterns.js +1121 -1121
  45. package/cli/utils/pdf-generator.js +94 -94
  46. package/package.json +69 -69
  47. package/configs/supabase/rls-templates.sql +0 -242
@@ -1,304 +1,304 @@
1
- /**
2
- * MCP Server
3
- * ==========
4
- *
5
- * Exposes ship-safe as a Model Context Protocol (MCP) server.
6
- * Allows AI editors (Claude Desktop, Cursor, Windsurf, Zed) to call
7
- * ship-safe's security tools directly during conversations.
8
- *
9
- * USAGE:
10
- * npx ship-safe mcp Start the MCP server (stdio transport)
11
- *
12
- * SETUP (Claude Desktop):
13
- * Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
14
- * {
15
- * "mcpServers": {
16
- * "ship-safe": {
17
- * "command": "npx",
18
- * "args": ["ship-safe", "mcp"]
19
- * }
20
- * }
21
- * }
22
- *
23
- * AVAILABLE TOOLS:
24
- * scan_secrets - Scan a directory for leaked secrets
25
- * get_checklist - Return the launch-day security checklist
26
- * analyze_file - Analyze a single file for security issues
27
- *
28
- * PROTOCOL:
29
- * JSON-RPC 2.0 over stdio (MCP spec: https://modelcontextprotocol.io)
30
- */
31
-
32
- import fs from 'fs';
33
- import path from 'path';
34
- import fg from 'fast-glob';
35
- import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, TEST_FILE_PATTERNS, MAX_FILE_SIZE } from '../utils/patterns.js';
36
- import { isHighEntropyMatch } from '../utils/entropy.js';
37
-
38
- // =============================================================================
39
- // MCP TOOL DEFINITIONS
40
- // =============================================================================
41
-
42
- const TOOLS = [
43
- {
44
- name: 'scan_secrets',
45
- description: 'Scan a directory or file for leaked secrets, API keys, and credentials. Returns structured findings with severity, file location, and remediation advice.',
46
- inputSchema: {
47
- type: 'object',
48
- properties: {
49
- path: {
50
- type: 'string',
51
- description: 'The directory or file path to scan. Use "." for the current directory.',
52
- },
53
- includeTests: {
54
- type: 'boolean',
55
- description: 'Whether to include test files in the scan (default: false)',
56
- },
57
- },
58
- required: ['path'],
59
- },
60
- },
61
- {
62
- name: 'get_checklist',
63
- description: 'Return the ship-safe launch-day security checklist as structured data. Use this to guide users through pre-launch security checks.',
64
- inputSchema: {
65
- type: 'object',
66
- properties: {},
67
- },
68
- },
69
- {
70
- name: 'analyze_file',
71
- description: 'Analyze a single file for security issues including secrets, hardcoded credentials, and dangerous patterns.',
72
- inputSchema: {
73
- type: 'object',
74
- properties: {
75
- path: {
76
- type: 'string',
77
- description: 'The absolute or relative path to the file to analyze.',
78
- },
79
- },
80
- required: ['path'],
81
- },
82
- },
83
- ];
84
-
85
- // =============================================================================
86
- // TOOL IMPLEMENTATIONS
87
- // =============================================================================
88
-
89
- async function scanSecrets({ path: targetPath, includeTests = false }) {
90
- const absolutePath = path.resolve(targetPath);
91
-
92
- if (!fs.existsSync(absolutePath)) {
93
- return { error: `Path does not exist: ${absolutePath}` };
94
- }
95
-
96
- const stat = fs.statSync(absolutePath);
97
- const files = stat.isFile()
98
- ? [absolutePath]
99
- : await findFiles(absolutePath, includeTests);
100
-
101
- const results = [];
102
-
103
- for (const file of files) {
104
- const findings = scanFile(file);
105
- if (findings.length > 0) {
106
- results.push({ file: path.relative(process.cwd(), file), findings });
107
- }
108
- }
109
-
110
- return {
111
- filesScanned: files.length,
112
- totalFindings: results.reduce((sum, r) => sum + r.findings.length, 0),
113
- clean: results.length === 0,
114
- findings: results,
115
- summary: results.length === 0
116
- ? 'No secrets detected.'
117
- : `Found ${results.reduce((s, r) => s + r.findings.length, 0)} secret(s) across ${results.length} file(s).`,
118
- remediation: results.length > 0
119
- ? 'Move secrets to environment variables. Add .env to .gitignore. Rotate any already-committed credentials.'
120
- : null,
121
- };
122
- }
123
-
124
- function getChecklist() {
125
- return {
126
- title: 'Ship Safe Launch-Day Security Checklist',
127
- items: [
128
- { id: 1, category: 'Secrets', check: 'No API keys hardcoded in source code', command: 'npx ship-safe scan .' },
129
- { id: 2, category: 'Secrets', check: '.env file is in .gitignore', command: null },
130
- { id: 3, category: 'Secrets', check: '.env.example exists with placeholder values', command: 'npx ship-safe fix' },
131
- { id: 4, category: 'Database', check: 'Row Level Security (RLS) enabled on all Supabase tables', command: null },
132
- { id: 5, category: 'Database', check: 'Service role key is server-side only (never in frontend)', command: null },
133
- { id: 6, category: 'Auth', check: 'Authentication required on all sensitive API routes', command: null },
134
- { id: 7, category: 'Auth', check: 'JWT tokens expire within 24 hours', command: null },
135
- { id: 8, category: 'Headers', check: 'Security headers configured (CSP, X-Frame-Options, HSTS)', command: 'npx ship-safe init --headers' },
136
- { id: 9, category: 'API', check: 'Rate limiting implemented on auth and AI endpoints', command: null },
137
- { id: 10, category: 'API', check: 'Input validation on all API endpoints', command: null },
138
- { id: 11, category: 'AI', check: 'Token limits set on all LLM API calls', command: null },
139
- { id: 12, category: 'AI', check: 'Budget caps configured in AI provider dashboard', command: null },
140
- { id: 13, category: 'CI/CD', check: 'ship-safe scan runs in CI pipeline', command: null },
141
- { id: 14, category: 'CI/CD', check: 'Pre-push hook installed', command: 'npx ship-safe guard' },
142
- ],
143
- };
144
- }
145
-
146
- async function analyzeFile({ path: filePath }) {
147
- const absolutePath = path.resolve(filePath);
148
-
149
- if (!fs.existsSync(absolutePath)) {
150
- return { error: `File does not exist: ${absolutePath}` };
151
- }
152
-
153
- const findings = scanFile(absolutePath);
154
-
155
- return {
156
- file: filePath,
157
- totalFindings: findings.length,
158
- clean: findings.length === 0,
159
- findings,
160
- summary: findings.length === 0
161
- ? `No secrets detected in ${path.basename(filePath)}.`
162
- : `Found ${findings.length} potential secret(s) in ${path.basename(filePath)}.`,
163
- };
164
- }
165
-
166
- // =============================================================================
167
- // SCAN UTILITIES (shared with scan command)
168
- // =============================================================================
169
-
170
- async function findFiles(rootPath, includeTests) {
171
- const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
172
- const files = await fg('**/*', { cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true });
173
-
174
- return files.filter(file => {
175
- const ext = path.extname(file).toLowerCase();
176
- if (SKIP_EXTENSIONS.has(ext)) return false;
177
- if (SKIP_FILENAMES.has(path.basename(file))) return false;
178
- const basename = path.basename(file);
179
- if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
180
- if (!includeTests && TEST_FILE_PATTERNS.some(p => p.test(file))) return false;
181
- try {
182
- return fs.statSync(file).size <= MAX_FILE_SIZE;
183
- } catch { return false; }
184
- });
185
- }
186
-
187
- function scanFile(filePath) {
188
- const findings = [];
189
- try {
190
- const content = fs.readFileSync(filePath, 'utf-8');
191
- const lines = content.split('\n');
192
-
193
- for (let lineNum = 0; lineNum < lines.length; lineNum++) {
194
- const line = lines[lineNum];
195
- if (/ship-safe-ignore/i.test(line)) continue;
196
-
197
- for (const pattern of SECRET_PATTERNS) {
198
- pattern.pattern.lastIndex = 0;
199
- let match;
200
- while ((match = pattern.pattern.exec(line)) !== null) {
201
- if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
202
- findings.push({
203
- line: lineNum + 1,
204
- type: pattern.name,
205
- severity: pattern.severity,
206
- description: pattern.description,
207
- fix: 'Move to environment variable. Never commit to source control.',
208
- });
209
- }
210
- }
211
- }
212
- } catch {}
213
- return findings;
214
- }
215
-
216
- // =============================================================================
217
- // MCP STDIO SERVER
218
- // =============================================================================
219
-
220
- export async function mcpCommand() {
221
- // MCP uses JSON-RPC 2.0 over stdio
222
- process.stdin.setEncoding('utf-8');
223
-
224
- let buffer = '';
225
-
226
- process.stdin.on('data', async (chunk) => {
227
- buffer += chunk;
228
-
229
- // MCP messages are newline-delimited JSON
230
- const lines = buffer.split('\n');
231
- buffer = lines.pop(); // Keep incomplete line in buffer
232
-
233
- for (const line of lines) {
234
- if (!line.trim()) continue;
235
-
236
- try {
237
- const request = JSON.parse(line);
238
- const response = await handleRequest(request);
239
- process.stdout.write(JSON.stringify(response) + '\n');
240
- } catch (err) {
241
- const errorResponse = {
242
- jsonrpc: '2.0',
243
- id: null,
244
- error: { code: -32700, message: 'Parse error', data: err.message },
245
- };
246
- process.stdout.write(JSON.stringify(errorResponse) + '\n');
247
- }
248
- }
249
- });
250
-
251
- process.stdin.on('end', () => process.exit(0));
252
- }
253
-
254
- async function handleRequest(request) {
255
- const { jsonrpc, id, method, params } = request;
256
-
257
- const respond = (result) => ({ jsonrpc: '2.0', id, result });
258
- const respondError = (code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
259
-
260
- switch (method) {
261
- case 'initialize':
262
- return respond({
263
- protocolVersion: '2024-11-05',
264
- capabilities: { tools: {} },
265
- serverInfo: { name: 'ship-safe', version: '3.0.0' },
266
- });
267
-
268
- case 'tools/list':
269
- return respond({ tools: TOOLS });
270
-
271
- case 'tools/call': {
272
- const { name, arguments: args } = params;
273
-
274
- try {
275
- let result;
276
- switch (name) {
277
- case 'scan_secrets':
278
- result = await scanSecrets(args);
279
- break;
280
- case 'get_checklist':
281
- result = getChecklist();
282
- break;
283
- case 'analyze_file':
284
- result = await analyzeFile(args);
285
- break;
286
- default:
287
- return respondError(-32601, `Unknown tool: ${name}`);
288
- }
289
-
290
- return respond({
291
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
292
- });
293
- } catch (err) {
294
- return respondError(-32603, err.message);
295
- }
296
- }
297
-
298
- case 'notifications/initialized':
299
- return null; // No response needed for notifications
300
-
301
- default:
302
- return respondError(-32601, `Method not found: ${method}`);
303
- }
304
- }
1
+ /**
2
+ * MCP Server
3
+ * ==========
4
+ *
5
+ * Exposes ship-safe as a Model Context Protocol (MCP) server.
6
+ * Allows AI editors (Claude Desktop, Cursor, Windsurf, Zed) to call
7
+ * ship-safe's security tools directly during conversations.
8
+ *
9
+ * USAGE:
10
+ * npx ship-safe mcp Start the MCP server (stdio transport)
11
+ *
12
+ * SETUP (Claude Desktop):
13
+ * Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
14
+ * {
15
+ * "mcpServers": {
16
+ * "ship-safe": {
17
+ * "command": "npx",
18
+ * "args": ["ship-safe", "mcp"]
19
+ * }
20
+ * }
21
+ * }
22
+ *
23
+ * AVAILABLE TOOLS:
24
+ * scan_secrets - Scan a directory for leaked secrets
25
+ * get_checklist - Return the launch-day security checklist
26
+ * analyze_file - Analyze a single file for security issues
27
+ *
28
+ * PROTOCOL:
29
+ * JSON-RPC 2.0 over stdio (MCP spec: https://modelcontextprotocol.io)
30
+ */
31
+
32
+ import fs from 'fs';
33
+ import path from 'path';
34
+ import fg from 'fast-glob';
35
+ import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, TEST_FILE_PATTERNS, MAX_FILE_SIZE } from '../utils/patterns.js';
36
+ import { isHighEntropyMatch } from '../utils/entropy.js';
37
+
38
+ // =============================================================================
39
+ // MCP TOOL DEFINITIONS
40
+ // =============================================================================
41
+
42
+ const TOOLS = [
43
+ {
44
+ name: 'scan_secrets',
45
+ description: 'Scan a directory or file for leaked secrets, API keys, and credentials. Returns structured findings with severity, file location, and remediation advice.',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ path: {
50
+ type: 'string',
51
+ description: 'The directory or file path to scan. Use "." for the current directory.',
52
+ },
53
+ includeTests: {
54
+ type: 'boolean',
55
+ description: 'Whether to include test files in the scan (default: false)',
56
+ },
57
+ },
58
+ required: ['path'],
59
+ },
60
+ },
61
+ {
62
+ name: 'get_checklist',
63
+ description: 'Return the ship-safe launch-day security checklist as structured data. Use this to guide users through pre-launch security checks.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {},
67
+ },
68
+ },
69
+ {
70
+ name: 'analyze_file',
71
+ description: 'Analyze a single file for security issues including secrets, hardcoded credentials, and dangerous patterns.',
72
+ inputSchema: {
73
+ type: 'object',
74
+ properties: {
75
+ path: {
76
+ type: 'string',
77
+ description: 'The absolute or relative path to the file to analyze.',
78
+ },
79
+ },
80
+ required: ['path'],
81
+ },
82
+ },
83
+ ];
84
+
85
+ // =============================================================================
86
+ // TOOL IMPLEMENTATIONS
87
+ // =============================================================================
88
+
89
+ async function scanSecrets({ path: targetPath, includeTests = false }) {
90
+ const absolutePath = path.resolve(targetPath);
91
+
92
+ if (!fs.existsSync(absolutePath)) {
93
+ return { error: `Path does not exist: ${absolutePath}` };
94
+ }
95
+
96
+ const stat = fs.statSync(absolutePath);
97
+ const files = stat.isFile()
98
+ ? [absolutePath]
99
+ : await findFiles(absolutePath, includeTests);
100
+
101
+ const results = [];
102
+
103
+ for (const file of files) {
104
+ const findings = scanFile(file);
105
+ if (findings.length > 0) {
106
+ results.push({ file: path.relative(process.cwd(), file), findings });
107
+ }
108
+ }
109
+
110
+ return {
111
+ filesScanned: files.length,
112
+ totalFindings: results.reduce((sum, r) => sum + r.findings.length, 0),
113
+ clean: results.length === 0,
114
+ findings: results,
115
+ summary: results.length === 0
116
+ ? 'No secrets detected.'
117
+ : `Found ${results.reduce((s, r) => s + r.findings.length, 0)} secret(s) across ${results.length} file(s).`,
118
+ remediation: results.length > 0
119
+ ? 'Move secrets to environment variables. Add .env to .gitignore. Rotate any already-committed credentials.'
120
+ : null,
121
+ };
122
+ }
123
+
124
+ function getChecklist() {
125
+ return {
126
+ title: 'Ship Safe Launch-Day Security Checklist',
127
+ items: [
128
+ { id: 1, category: 'Secrets', check: 'No API keys hardcoded in source code', command: 'npx ship-safe scan .' },
129
+ { id: 2, category: 'Secrets', check: '.env file is in .gitignore', command: null },
130
+ { id: 3, category: 'Secrets', check: '.env.example exists with placeholder values', command: 'npx ship-safe fix' },
131
+ { id: 4, category: 'Database', check: 'Row Level Security (RLS) enabled on all Supabase tables', command: null },
132
+ { id: 5, category: 'Database', check: 'Service role key is server-side only (never in frontend)', command: null },
133
+ { id: 6, category: 'Auth', check: 'Authentication required on all sensitive API routes', command: null },
134
+ { id: 7, category: 'Auth', check: 'JWT tokens expire within 24 hours', command: null },
135
+ { id: 8, category: 'Headers', check: 'Security headers configured (CSP, X-Frame-Options, HSTS)', command: 'npx ship-safe init --headers' },
136
+ { id: 9, category: 'API', check: 'Rate limiting implemented on auth and AI endpoints', command: null },
137
+ { id: 10, category: 'API', check: 'Input validation on all API endpoints', command: null },
138
+ { id: 11, category: 'AI', check: 'Token limits set on all LLM API calls', command: null },
139
+ { id: 12, category: 'AI', check: 'Budget caps configured in AI provider dashboard', command: null },
140
+ { id: 13, category: 'CI/CD', check: 'ship-safe scan runs in CI pipeline', command: null },
141
+ { id: 14, category: 'CI/CD', check: 'Pre-push hook installed', command: 'npx ship-safe guard' },
142
+ ],
143
+ };
144
+ }
145
+
146
+ async function analyzeFile({ path: filePath }) {
147
+ const absolutePath = path.resolve(filePath);
148
+
149
+ if (!fs.existsSync(absolutePath)) {
150
+ return { error: `File does not exist: ${absolutePath}` };
151
+ }
152
+
153
+ const findings = scanFile(absolutePath);
154
+
155
+ return {
156
+ file: filePath,
157
+ totalFindings: findings.length,
158
+ clean: findings.length === 0,
159
+ findings,
160
+ summary: findings.length === 0
161
+ ? `No secrets detected in ${path.basename(filePath)}.`
162
+ : `Found ${findings.length} potential secret(s) in ${path.basename(filePath)}.`,
163
+ };
164
+ }
165
+
166
+ // =============================================================================
167
+ // SCAN UTILITIES (shared with scan command)
168
+ // =============================================================================
169
+
170
+ async function findFiles(rootPath, includeTests) {
171
+ const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
172
+ const files = await fg('**/*', { cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true });
173
+
174
+ return files.filter(file => {
175
+ const ext = path.extname(file).toLowerCase();
176
+ if (SKIP_EXTENSIONS.has(ext)) return false;
177
+ if (SKIP_FILENAMES.has(path.basename(file))) return false;
178
+ const basename = path.basename(file);
179
+ if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
180
+ if (!includeTests && TEST_FILE_PATTERNS.some(p => p.test(file))) return false;
181
+ try {
182
+ return fs.statSync(file).size <= MAX_FILE_SIZE;
183
+ } catch { return false; }
184
+ });
185
+ }
186
+
187
+ function scanFile(filePath) {
188
+ const findings = [];
189
+ try {
190
+ const content = fs.readFileSync(filePath, 'utf-8');
191
+ const lines = content.split('\n');
192
+
193
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
194
+ const line = lines[lineNum];
195
+ if (/ship-safe-ignore/i.test(line)) continue;
196
+
197
+ for (const pattern of SECRET_PATTERNS) {
198
+ pattern.pattern.lastIndex = 0;
199
+ let match;
200
+ while ((match = pattern.pattern.exec(line)) !== null) {
201
+ if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
202
+ findings.push({
203
+ line: lineNum + 1,
204
+ type: pattern.name,
205
+ severity: pattern.severity,
206
+ description: pattern.description,
207
+ fix: 'Move to environment variable. Never commit to source control.',
208
+ });
209
+ }
210
+ }
211
+ }
212
+ } catch {}
213
+ return findings;
214
+ }
215
+
216
+ // =============================================================================
217
+ // MCP STDIO SERVER
218
+ // =============================================================================
219
+
220
+ export async function mcpCommand() {
221
+ // MCP uses JSON-RPC 2.0 over stdio
222
+ process.stdin.setEncoding('utf-8');
223
+
224
+ let buffer = '';
225
+
226
+ process.stdin.on('data', async (chunk) => {
227
+ buffer += chunk;
228
+
229
+ // MCP messages are newline-delimited JSON
230
+ const lines = buffer.split('\n');
231
+ buffer = lines.pop(); // Keep incomplete line in buffer
232
+
233
+ for (const line of lines) {
234
+ if (!line.trim()) continue;
235
+
236
+ try {
237
+ const request = JSON.parse(line);
238
+ const response = await handleRequest(request);
239
+ process.stdout.write(JSON.stringify(response) + '\n');
240
+ } catch (err) {
241
+ const errorResponse = {
242
+ jsonrpc: '2.0',
243
+ id: null,
244
+ error: { code: -32700, message: 'Parse error', data: err.message },
245
+ };
246
+ process.stdout.write(JSON.stringify(errorResponse) + '\n');
247
+ }
248
+ }
249
+ });
250
+
251
+ process.stdin.on('end', () => process.exit(0));
252
+ }
253
+
254
+ async function handleRequest(request) {
255
+ const { jsonrpc, id, method, params } = request;
256
+
257
+ const respond = (result) => ({ jsonrpc: '2.0', id, result });
258
+ const respondError = (code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
259
+
260
+ switch (method) {
261
+ case 'initialize':
262
+ return respond({
263
+ protocolVersion: '2024-11-05',
264
+ capabilities: { tools: {} },
265
+ serverInfo: { name: 'ship-safe', version: '3.0.0' },
266
+ });
267
+
268
+ case 'tools/list':
269
+ return respond({ tools: TOOLS });
270
+
271
+ case 'tools/call': {
272
+ const { name, arguments: args } = params;
273
+
274
+ try {
275
+ let result;
276
+ switch (name) {
277
+ case 'scan_secrets':
278
+ result = await scanSecrets(args);
279
+ break;
280
+ case 'get_checklist':
281
+ result = getChecklist();
282
+ break;
283
+ case 'analyze_file':
284
+ result = await analyzeFile(args);
285
+ break;
286
+ default:
287
+ return respondError(-32601, `Unknown tool: ${name}`);
288
+ }
289
+
290
+ return respond({
291
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
292
+ });
293
+ } catch (err) {
294
+ return respondError(-32603, err.message);
295
+ }
296
+ }
297
+
298
+ case 'notifications/initialized':
299
+ return null; // No response needed for notifications
300
+
301
+ default:
302
+ return respondError(-32601, `Method not found: ${method}`);
303
+ }
304
+ }
@@ -52,6 +52,8 @@ export async function redTeamCommand(targetPath = '.', options = {}) {
52
52
  if (options.deep) orchestratorOpts.deep = true;
53
53
  if (options.local) orchestratorOpts.local = true;
54
54
  if (options.model) orchestratorOpts.model = options.model;
55
+ if (options.provider) orchestratorOpts.provider = options.provider;
56
+ if (options.baseUrl) orchestratorOpts.baseUrl = options.baseUrl;
55
57
  if (options.budget) orchestratorOpts.budget = options.budget;
56
58
 
57
59
  const results = await orchestrator.runAll(absolutePath, orchestratorOpts); // ship-safe-ignore — orchestrator result, not LLM output triggering actions
@@ -86,7 +88,11 @@ export async function redTeamCommand(targetPath = '.', options = {}) {
86
88
 
87
89
  // ── 5. AI classification (if provider available) ────────────────────────────
88
90
  if (options.ai !== false) {
89
- const provider = autoDetectProvider(absolutePath);
91
+ const provider = autoDetectProvider(absolutePath, {
92
+ provider: options.provider,
93
+ baseUrl: options.baseUrl,
94
+ model: options.model,
95
+ });
90
96
  if (provider && filteredFindings.length > 0 && filteredFindings.length <= 50) {
91
97
  const aiSpinner = ora({ text: `Classifying ${filteredFindings.length} finding(s) with ${provider.name}...`, color: 'cyan' }).start();
92
98
  try {