ship-safe 3.1.0 → 4.0.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 (38) hide show
  1. package/README.md +200 -307
  2. package/cli/agents/api-fuzzer.js +224 -0
  3. package/cli/agents/auth-bypass-agent.js +326 -0
  4. package/cli/agents/base-agent.js +240 -0
  5. package/cli/agents/cicd-scanner.js +200 -0
  6. package/cli/agents/config-auditor.js +413 -0
  7. package/cli/agents/git-history-scanner.js +167 -0
  8. package/cli/agents/html-reporter.js +363 -0
  9. package/cli/agents/index.js +56 -0
  10. package/cli/agents/injection-tester.js +401 -0
  11. package/cli/agents/llm-redteam.js +251 -0
  12. package/cli/agents/mobile-scanner.js +225 -0
  13. package/cli/agents/orchestrator.js +152 -0
  14. package/cli/agents/policy-engine.js +149 -0
  15. package/cli/agents/recon-agent.js +196 -0
  16. package/cli/agents/sbom-generator.js +176 -0
  17. package/cli/agents/scoring-engine.js +207 -0
  18. package/cli/agents/ssrf-prober.js +130 -0
  19. package/cli/agents/supply-chain-agent.js +274 -0
  20. package/cli/bin/ship-safe.js +119 -2
  21. package/cli/commands/agent.js +606 -0
  22. package/cli/commands/audit.js +565 -0
  23. package/cli/commands/deps.js +447 -0
  24. package/cli/commands/fix.js +3 -3
  25. package/cli/commands/init.js +86 -3
  26. package/cli/commands/mcp.js +2 -2
  27. package/cli/commands/red-team.js +315 -0
  28. package/cli/commands/remediate.js +4 -4
  29. package/cli/commands/rotate.js +6 -6
  30. package/cli/commands/scan.js +64 -23
  31. package/cli/commands/score.js +446 -0
  32. package/cli/commands/watch.js +160 -0
  33. package/cli/index.js +40 -2
  34. package/cli/providers/llm-provider.js +288 -0
  35. package/cli/utils/entropy.js +6 -0
  36. package/cli/utils/output.js +42 -2
  37. package/cli/utils/patterns.js +393 -1
  38. package/package.json +19 -15
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Multi-LLM Provider
3
+ * ===================
4
+ *
5
+ * Abstraction layer for LLM providers.
6
+ * Supports: Anthropic (Claude), OpenAI, Google (Gemini), Ollama (local).
7
+ *
8
+ * USAGE:
9
+ * const provider = createProvider('anthropic', apiKey);
10
+ * const result = await provider.classify(findings, context);
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+
16
+ // =============================================================================
17
+ // PROVIDER INTERFACE
18
+ // =============================================================================
19
+
20
+ class BaseLLMProvider {
21
+ constructor(name, apiKey, options = {}) {
22
+ this.name = name;
23
+ this.apiKey = apiKey;
24
+ this.model = options.model || null;
25
+ this.baseUrl = options.baseUrl || null;
26
+ }
27
+
28
+ /**
29
+ * Send a prompt to the LLM and get a text response.
30
+ */
31
+ async complete(systemPrompt, userPrompt, options = {}) {
32
+ throw new Error(`${this.name}.complete() not implemented`);
33
+ }
34
+
35
+ /**
36
+ * Classify security findings using the LLM.
37
+ */
38
+ async classify(findings, context) {
39
+ const prompt = this.buildClassificationPrompt(findings, context);
40
+ const response = await this.complete(
41
+ 'You are a security expert. Respond with JSON only, no markdown.',
42
+ prompt,
43
+ { maxTokens: 4096 }
44
+ );
45
+ return this.parseJSON(response);
46
+ }
47
+
48
+ buildClassificationPrompt(findings, context) {
49
+ const items = findings.map(f => ({
50
+ id: `${f.file}:${f.line}`,
51
+ rule: f.rule,
52
+ severity: f.severity,
53
+ title: f.title,
54
+ matched: f.matched?.slice(0, 100),
55
+ description: f.description,
56
+ }));
57
+
58
+ return `Classify each finding as REAL or FALSE_POSITIVE. For REAL findings, provide a specific fix.
59
+
60
+ Respond with JSON array ONLY:
61
+ [{"id":"<id>","classification":"REAL"|"FALSE_POSITIVE","reason":"<brief reason>","fix":"<specific fix or null>"}]
62
+
63
+ Findings:
64
+ ${JSON.stringify(items, null, 2)}`;
65
+ }
66
+
67
+ parseJSON(text) {
68
+ const cleaned = text
69
+ .replace(/^```(?:json)?\s*/i, '')
70
+ .replace(/\s*```\s*$/i, '')
71
+ .trim();
72
+ try {
73
+ return JSON.parse(cleaned);
74
+ } catch {
75
+ return [];
76
+ }
77
+ }
78
+ }
79
+
80
+ // =============================================================================
81
+ // ANTHROPIC PROVIDER (Claude)
82
+ // =============================================================================
83
+
84
+ class AnthropicProvider extends BaseLLMProvider {
85
+ constructor(apiKey, options = {}) {
86
+ super('Anthropic', apiKey, options);
87
+ this.model = options.model || 'claude-haiku-4-5-20251001';
88
+ this.baseUrl = options.baseUrl || 'https://api.anthropic.com/v1/messages';
89
+ }
90
+
91
+ async complete(systemPrompt, userPrompt, options = {}) {
92
+ const response = await fetch(this.baseUrl, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'x-api-key': this.apiKey,
96
+ 'anthropic-version': '2023-06-01',
97
+ 'content-type': 'application/json',
98
+ },
99
+ body: JSON.stringify({
100
+ model: this.model,
101
+ max_tokens: options.maxTokens || 2048,
102
+ system: systemPrompt,
103
+ messages: [{ role: 'user', content: userPrompt }],
104
+ }),
105
+ });
106
+
107
+ if (!response.ok) {
108
+ const body = await response.text();
109
+ throw new Error(`Anthropic API error ${response.status}: ${body.slice(0, 200)}`);
110
+ }
111
+
112
+ const data = await response.json();
113
+ return data.content?.[0]?.text || '';
114
+ }
115
+ }
116
+
117
+ // =============================================================================
118
+ // OPENAI PROVIDER (GPT-4o, etc.)
119
+ // =============================================================================
120
+
121
+ class OpenAIProvider extends BaseLLMProvider {
122
+ constructor(apiKey, options = {}) {
123
+ super('OpenAI', apiKey, options);
124
+ this.model = options.model || 'gpt-4o-mini';
125
+ this.baseUrl = options.baseUrl || 'https://api.openai.com/v1/chat/completions';
126
+ }
127
+
128
+ async complete(systemPrompt, userPrompt, options = {}) {
129
+ const response = await fetch(this.baseUrl, {
130
+ method: 'POST',
131
+ headers: {
132
+ 'Authorization': `Bearer ${this.apiKey}`,
133
+ 'Content-Type': 'application/json',
134
+ },
135
+ body: JSON.stringify({
136
+ model: this.model,
137
+ max_tokens: options.maxTokens || 2048,
138
+ messages: [
139
+ { role: 'system', content: systemPrompt },
140
+ { role: 'user', content: userPrompt },
141
+ ],
142
+ }),
143
+ });
144
+
145
+ if (!response.ok) {
146
+ const body = await response.text();
147
+ throw new Error(`OpenAI API error ${response.status}: ${body.slice(0, 200)}`);
148
+ }
149
+
150
+ const data = await response.json();
151
+ return data.choices?.[0]?.message?.content || '';
152
+ }
153
+ }
154
+
155
+ // =============================================================================
156
+ // GOOGLE PROVIDER (Gemini)
157
+ // =============================================================================
158
+
159
+ class GoogleProvider extends BaseLLMProvider {
160
+ constructor(apiKey, options = {}) {
161
+ super('Google', apiKey, options);
162
+ this.model = options.model || 'gemini-2.0-flash';
163
+ }
164
+
165
+ async complete(systemPrompt, userPrompt, options = {}) {
166
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
167
+
168
+ const response = await fetch(url, {
169
+ method: 'POST',
170
+ headers: { 'Content-Type': 'application/json' },
171
+ body: JSON.stringify({
172
+ systemInstruction: { parts: [{ text: systemPrompt }] },
173
+ contents: [{ parts: [{ text: userPrompt }] }],
174
+ generationConfig: { maxOutputTokens: options.maxTokens || 2048 },
175
+ }),
176
+ });
177
+
178
+ if (!response.ok) {
179
+ const body = await response.text();
180
+ throw new Error(`Google API error ${response.status}: ${body.slice(0, 200)}`);
181
+ }
182
+
183
+ const data = await response.json();
184
+ return data.candidates?.[0]?.content?.parts?.[0]?.text || '';
185
+ }
186
+ }
187
+
188
+ // =============================================================================
189
+ // OLLAMA PROVIDER (Local models)
190
+ // =============================================================================
191
+
192
+ class OllamaProvider extends BaseLLMProvider {
193
+ constructor(apiKey, options = {}) {
194
+ super('Ollama', null, options);
195
+ this.model = options.model || 'llama3.2';
196
+ this.baseUrl = options.baseUrl || 'http://localhost:11434/api/chat';
197
+ }
198
+
199
+ async complete(systemPrompt, userPrompt, options = {}) {
200
+ const response = await fetch(this.baseUrl, {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify({
204
+ model: this.model,
205
+ messages: [
206
+ { role: 'system', content: systemPrompt },
207
+ { role: 'user', content: userPrompt },
208
+ ],
209
+ stream: false,
210
+ }),
211
+ });
212
+
213
+ if (!response.ok) {
214
+ const body = await response.text();
215
+ throw new Error(`Ollama error ${response.status}: ${body.slice(0, 200)}`);
216
+ }
217
+
218
+ const data = await response.json();
219
+ return data.message?.content || '';
220
+ }
221
+ }
222
+
223
+ // =============================================================================
224
+ // FACTORY
225
+ // =============================================================================
226
+
227
+ /**
228
+ * Create an LLM provider instance.
229
+ *
230
+ * @param {string} provider — 'anthropic' | 'openai' | 'google' | 'ollama'
231
+ * @param {string} apiKey — API key (null for Ollama)
232
+ * @param {object} options — { model, baseUrl }
233
+ */
234
+ export function createProvider(provider, apiKey, options = {}) {
235
+ switch (provider.toLowerCase()) {
236
+ case 'anthropic':
237
+ case 'claude':
238
+ return new AnthropicProvider(apiKey, options);
239
+ case 'openai':
240
+ case 'gpt':
241
+ return new OpenAIProvider(apiKey, options);
242
+ case 'google':
243
+ case 'gemini':
244
+ return new GoogleProvider(apiKey, options);
245
+ case 'ollama':
246
+ case 'local':
247
+ return new OllamaProvider(apiKey, options);
248
+ default:
249
+ throw new Error(`Unknown LLM provider: ${provider}. Use: anthropic, openai, google, ollama`);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Auto-detect the best available LLM provider from environment variables.
255
+ */
256
+ export function autoDetectProvider(rootPath) {
257
+ // Check env vars
258
+ const envKeys = {
259
+ ANTHROPIC_API_KEY: 'anthropic',
260
+ OPENAI_API_KEY: 'openai',
261
+ GOOGLE_API_KEY: 'google',
262
+ GEMINI_API_KEY: 'google',
263
+ };
264
+
265
+ for (const [envVar, provider] of Object.entries(envKeys)) {
266
+ if (process.env[envVar]) {
267
+ return createProvider(provider, process.env[envVar]);
268
+ }
269
+ }
270
+
271
+ // Check .env file
272
+ if (rootPath) {
273
+ const envPath = path.join(rootPath, '.env');
274
+ if (fs.existsSync(envPath)) {
275
+ try {
276
+ const content = fs.readFileSync(envPath, 'utf-8');
277
+ for (const [envVar, provider] of Object.entries(envKeys)) {
278
+ const match = content.match(new RegExp(`^${envVar}\\s*=\\s*["']?([^"'\\s]+)`, 'm'));
279
+ if (match) return createProvider(provider, match[1]);
280
+ }
281
+ } catch { /* ignore */ }
282
+ }
283
+ }
284
+
285
+ return null;
286
+ }
287
+
288
+ export default { createProvider, autoDetectProvider };
@@ -100,6 +100,12 @@ export function isHighEntropyMatch(matched) {
100
100
  /^(insert|replace|changeme|placeholder|todo|fixme)/i,
101
101
  /([-_]here|[-_]goes|[-_]key|[-_]token|[-_]secret)$/i,
102
102
  /^[a-z]+[-_][a-z]+[-_][a-z]+$/, // looks like-a-passphrase not a key
103
+ /^(add[-_]?your|put[-_]?your|enter[-_]?your|set[-_]?your)/i,
104
+ /^(secret|password|token|apikey|api_key|key|value)[-_]?[0-9]*$/i,
105
+ /^(n\/a|null|undefined|none|empty|blank)/i,
106
+ /^(demo|staging|dev|development|local)[-_]/i,
107
+ /^(abcdef|qwerty|asdfgh|123456|letmein)/i,
108
+ /(.)\1{5,}/, // 6+ repeated chars: aaaaaaa, 111111
103
109
  ];
104
110
 
105
111
  if (PLACEHOLDER_PATTERNS.some(p => p.test(value))) return false;
@@ -101,6 +101,21 @@ export function finding(file, line, patternName, severity, matched, description,
101
101
  console.log(` ${chalk.gray('Why:')} ${description}`);
102
102
  }
103
103
 
104
+ /**
105
+ * Print a vulnerability finding (code issue — show matched code, not masked)
106
+ */
107
+ export function vulnerabilityFinding(file, line, patternName, severity, matched, description) {
108
+ const color = severityColors[severity] || chalk.white;
109
+ const icon = severityIcons[severity] || '';
110
+ const snippet = matched.length > 80 ? matched.slice(0, 80) + '…' : matched;
111
+
112
+ console.log();
113
+ console.log(chalk.white.bold(`${file}:${line}`));
114
+ console.log(` ${icon}${color(`[${severity.toUpperCase()}]`)} ${chalk.white(patternName)}`);
115
+ console.log(` ${chalk.gray('Code:')} ${chalk.cyan(snippet)}`);
116
+ console.log(` ${chalk.gray('Why:')} ${description}`);
117
+ }
118
+
104
119
  /**
105
120
  * Mask the middle of a secret for safe display
106
121
  */
@@ -113,15 +128,27 @@ export function maskSecret(secret) {
113
128
 
114
129
  /**
115
130
  * Print a summary box
131
+ *
132
+ * stats can include:
133
+ * total, critical, high, medium, filesScanned
134
+ * secretsTotal (optional), vulnsTotal (optional)
116
135
  */
117
136
  export function summary(stats) {
118
137
  console.log();
119
138
  console.log(chalk.cyan('='.repeat(60)));
120
139
 
121
140
  if (stats.total === 0) {
122
- console.log(chalk.green.bold(' \u2714 No secrets detected!'));
141
+ console.log(chalk.green.bold(' \u2714 No issues detected!'));
123
142
  } else {
124
- console.log(chalk.red.bold(` \u26a0 Found ${stats.total} potential secret(s)`));
143
+ const secretsTotal = stats.secretsTotal ?? stats.total;
144
+ const vulnsTotal = stats.vulnsTotal ?? 0;
145
+
146
+ if (secretsTotal > 0) {
147
+ console.log(chalk.red.bold(` \u26a0 Found ${secretsTotal} secret(s)`));
148
+ }
149
+ if (vulnsTotal > 0) {
150
+ console.log(chalk.yellow.bold(` \u26a0 Found ${vulnsTotal} code vulnerability/vulnerabilities`));
151
+ }
125
152
 
126
153
  if (stats.critical > 0) {
127
154
  console.log(chalk.red(` \u2022 Critical: ${stats.critical}`));
@@ -138,6 +165,19 @@ export function summary(stats) {
138
165
  console.log(chalk.cyan('='.repeat(60)));
139
166
  }
140
167
 
168
+ /**
169
+ * Print recommended actions after finding code vulnerabilities
170
+ */
171
+ export function vulnRecommendations() {
172
+ console.log();
173
+ console.log(chalk.yellow.bold('Code Vulnerability Actions:'));
174
+ console.log();
175
+ console.log(chalk.white('1.') + ' Fix the flagged code patterns (see "Why" descriptions above)');
176
+ console.log(chalk.white('2.') + ' Use # ship-safe-ignore on lines that are safe (e.g. internal tools, controlled input)');
177
+ console.log(chalk.white('3.') + ' Run npx ship-safe checklist for a full launch-day security review');
178
+ console.log();
179
+ }
180
+
141
181
  /**
142
182
  * Print recommended actions after finding secrets
143
183
  */