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.
- package/README.md +200 -307
- package/cli/agents/api-fuzzer.js +224 -0
- package/cli/agents/auth-bypass-agent.js +326 -0
- package/cli/agents/base-agent.js +240 -0
- package/cli/agents/cicd-scanner.js +200 -0
- package/cli/agents/config-auditor.js +413 -0
- package/cli/agents/git-history-scanner.js +167 -0
- package/cli/agents/html-reporter.js +363 -0
- package/cli/agents/index.js +56 -0
- package/cli/agents/injection-tester.js +401 -0
- package/cli/agents/llm-redteam.js +251 -0
- package/cli/agents/mobile-scanner.js +225 -0
- package/cli/agents/orchestrator.js +152 -0
- package/cli/agents/policy-engine.js +149 -0
- package/cli/agents/recon-agent.js +196 -0
- package/cli/agents/sbom-generator.js +176 -0
- package/cli/agents/scoring-engine.js +207 -0
- package/cli/agents/ssrf-prober.js +130 -0
- package/cli/agents/supply-chain-agent.js +274 -0
- package/cli/bin/ship-safe.js +119 -2
- package/cli/commands/agent.js +606 -0
- package/cli/commands/audit.js +565 -0
- package/cli/commands/deps.js +447 -0
- package/cli/commands/fix.js +3 -3
- package/cli/commands/init.js +86 -3
- package/cli/commands/mcp.js +2 -2
- package/cli/commands/red-team.js +315 -0
- package/cli/commands/remediate.js +4 -4
- package/cli/commands/rotate.js +6 -6
- package/cli/commands/scan.js +64 -23
- package/cli/commands/score.js +446 -0
- package/cli/commands/watch.js +160 -0
- package/cli/index.js +40 -2
- package/cli/providers/llm-provider.js +288 -0
- package/cli/utils/entropy.js +6 -0
- package/cli/utils/output.js +42 -2
- package/cli/utils/patterns.js +393 -1
- 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 };
|
package/cli/utils/entropy.js
CHANGED
|
@@ -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;
|
package/cli/utils/output.js
CHANGED
|
@@ -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
|
|
141
|
+
console.log(chalk.green.bold(' \u2714 No issues detected!'));
|
|
123
142
|
} else {
|
|
124
|
-
|
|
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
|
*/
|