ship-safe 5.0.0 → 6.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/cli/agents/agentic-security-agent.js +1 -1
- package/cli/agents/api-fuzzer.js +1 -1
- package/cli/agents/auth-bypass-agent.js +2 -2
- package/cli/agents/base-agent.js +2 -1
- package/cli/agents/config-auditor.js +3 -11
- package/cli/agents/exception-handler-agent.js +187 -0
- package/cli/agents/html-reporter.js +511 -370
- package/cli/agents/index.js +6 -0
- package/cli/agents/mcp-security-agent.js +182 -0
- package/cli/agents/pii-compliance-agent.js +301 -301
- package/cli/agents/scoring-engine.js +14 -6
- package/cli/agents/vibe-coding-agent.js +250 -0
- package/cli/bin/ship-safe.js +43 -6
- package/cli/commands/agent.js +6 -4
- package/cli/commands/audit.js +85 -14
- package/cli/commands/baseline.js +3 -2
- package/cli/commands/benchmark.js +327 -0
- package/cli/commands/ci.js +342 -260
- package/cli/commands/deps.js +73 -4
- package/cli/commands/diff.js +200 -0
- package/cli/commands/doctor.js +14 -4
- package/cli/commands/fix.js +218 -216
- package/cli/commands/init.js +349 -349
- package/cli/commands/mcp.js +304 -303
- package/cli/commands/red-team.js +2 -2
- package/cli/commands/remediate.js +155 -7
- package/cli/commands/scan.js +567 -565
- package/cli/commands/score.js +2 -0
- package/cli/commands/vibe-check.js +276 -0
- package/cli/commands/watch.js +161 -160
- package/cli/index.js +8 -1
- package/cli/utils/cache-manager.js +1 -1
- package/cli/utils/output.js +5 -2
- package/cli/utils/patterns.js +1121 -1104
- package/cli/utils/pdf-generator.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark Command
|
|
3
|
+
* =================
|
|
4
|
+
*
|
|
5
|
+
* Compare your project's security score against industry averages.
|
|
6
|
+
* Uses aggregated baseline data from publicly available research on
|
|
7
|
+
* typical vulnerability rates in web applications and open source projects.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* npx ship-safe benchmark [path] Compare against industry averages
|
|
11
|
+
* npx ship-safe benchmark . --json Output as JSON
|
|
12
|
+
*
|
|
13
|
+
* DATA SOURCES:
|
|
14
|
+
* - OWASP Web Application Security Statistics (2024)
|
|
15
|
+
* - Synopsys OSSRA Report (2024) — 84% of codebases have vulnerabilities
|
|
16
|
+
* - Snyk State of Open Source Security (2024)
|
|
17
|
+
* - GitHub Octoverse Security Report (2024)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import chalk from 'chalk';
|
|
23
|
+
import ora from 'ora';
|
|
24
|
+
import { buildOrchestrator } from '../agents/index.js';
|
|
25
|
+
import { ScoringEngine } from '../agents/scoring-engine.js';
|
|
26
|
+
import { runDepsAudit } from './deps.js';
|
|
27
|
+
import {
|
|
28
|
+
SECRET_PATTERNS,
|
|
29
|
+
SKIP_DIRS,
|
|
30
|
+
SKIP_EXTENSIONS,
|
|
31
|
+
SKIP_FILENAMES,
|
|
32
|
+
MAX_FILE_SIZE,
|
|
33
|
+
loadGitignorePatterns
|
|
34
|
+
} from '../utils/patterns.js';
|
|
35
|
+
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
36
|
+
import * as output from '../utils/output.js';
|
|
37
|
+
import fg from 'fast-glob';
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// INDUSTRY BENCHMARKS (aggregated from public research)
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
const BENCHMARKS = {
|
|
44
|
+
overall: {
|
|
45
|
+
label: 'Overall Security Score',
|
|
46
|
+
industry: 52, // Median web app security score
|
|
47
|
+
topQuartile: 78, // Top 25%
|
|
48
|
+
description: 'Average security score across web applications',
|
|
49
|
+
},
|
|
50
|
+
categories: {
|
|
51
|
+
secrets: {
|
|
52
|
+
label: 'Secret Management',
|
|
53
|
+
avgFindingsPerProject: 4.2,
|
|
54
|
+
pctWithIssues: 38,
|
|
55
|
+
description: '38% of projects have exposed secrets (GitHub secret scanning data)',
|
|
56
|
+
},
|
|
57
|
+
injection: {
|
|
58
|
+
label: 'Injection / Code Vulns',
|
|
59
|
+
avgFindingsPerProject: 6.1,
|
|
60
|
+
pctWithIssues: 49,
|
|
61
|
+
description: '49% of web apps have injection vulnerabilities (OWASP)',
|
|
62
|
+
},
|
|
63
|
+
auth: {
|
|
64
|
+
label: 'Auth & Access Control',
|
|
65
|
+
avgFindingsPerProject: 3.8,
|
|
66
|
+
pctWithIssues: 94,
|
|
67
|
+
description: 'Broken access control is #1 in OWASP Top 10 — affects 94% of apps tested',
|
|
68
|
+
},
|
|
69
|
+
deps: {
|
|
70
|
+
label: 'Dependencies',
|
|
71
|
+
avgFindingsPerProject: 5.3,
|
|
72
|
+
pctWithIssues: 84,
|
|
73
|
+
description: '84% of codebases have at least one known vulnerability (Synopsys OSSRA 2024)',
|
|
74
|
+
},
|
|
75
|
+
config: {
|
|
76
|
+
label: 'Security Misconfiguration',
|
|
77
|
+
avgFindingsPerProject: 2.9,
|
|
78
|
+
pctWithIssues: 62,
|
|
79
|
+
description: '62% of apps have security misconfiguration (OWASP)',
|
|
80
|
+
},
|
|
81
|
+
'supply-chain': {
|
|
82
|
+
label: 'Supply Chain',
|
|
83
|
+
avgFindingsPerProject: 1.7,
|
|
84
|
+
pctWithIssues: 91,
|
|
85
|
+
description: '91% of packages have no maintainer review process (Snyk)',
|
|
86
|
+
},
|
|
87
|
+
api: {
|
|
88
|
+
label: 'API Security',
|
|
89
|
+
avgFindingsPerProject: 2.4,
|
|
90
|
+
pctWithIssues: 41,
|
|
91
|
+
description: '41% of organizations experienced an API security incident (Salt Labs)',
|
|
92
|
+
},
|
|
93
|
+
llm: {
|
|
94
|
+
label: 'AI/LLM Security',
|
|
95
|
+
avgFindingsPerProject: 1.2,
|
|
96
|
+
pctWithIssues: 25,
|
|
97
|
+
description: 'Emerging category — 25% of AI-enabled apps have insecure configurations',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
// Percentile lookup for score comparison
|
|
101
|
+
percentiles: [
|
|
102
|
+
{ score: 95, percentile: 99 },
|
|
103
|
+
{ score: 90, percentile: 95 },
|
|
104
|
+
{ score: 85, percentile: 90 },
|
|
105
|
+
{ score: 80, percentile: 80 },
|
|
106
|
+
{ score: 75, percentile: 70 },
|
|
107
|
+
{ score: 70, percentile: 60 },
|
|
108
|
+
{ score: 60, percentile: 45 },
|
|
109
|
+
{ score: 50, percentile: 30 },
|
|
110
|
+
{ score: 40, percentile: 20 },
|
|
111
|
+
{ score: 30, percentile: 10 },
|
|
112
|
+
{ score: 0, percentile: 5 },
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// MAIN COMMAND
|
|
118
|
+
// =============================================================================
|
|
119
|
+
|
|
120
|
+
export async function benchmarkCommand(targetPath = '.', options = {}) {
|
|
121
|
+
const absolutePath = path.resolve(targetPath);
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(absolutePath)) {
|
|
124
|
+
output.error(`Path does not exist: ${absolutePath}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const projectName = path.basename(absolutePath);
|
|
129
|
+
|
|
130
|
+
console.log();
|
|
131
|
+
output.header('Security Benchmark');
|
|
132
|
+
console.log(chalk.gray(` Comparing ${projectName} against industry averages\n`));
|
|
133
|
+
|
|
134
|
+
const startTime = Date.now();
|
|
135
|
+
|
|
136
|
+
// ── Scan ──────────────────────────────────────────────────────────────────
|
|
137
|
+
const spinner = ora({ text: 'Running full security scan for benchmark...', color: 'cyan' }).start();
|
|
138
|
+
|
|
139
|
+
const allFiles = await findFiles(absolutePath);
|
|
140
|
+
const secretFindings = [];
|
|
141
|
+
|
|
142
|
+
for (const file of allFiles) {
|
|
143
|
+
try {
|
|
144
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
145
|
+
const lines = content.split('\n');
|
|
146
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
147
|
+
const line = lines[lineNum];
|
|
148
|
+
if (/ship-safe-ignore/i.test(line)) continue;
|
|
149
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
150
|
+
pattern.pattern.lastIndex = 0;
|
|
151
|
+
let match;
|
|
152
|
+
while ((match = pattern.pattern.exec(line)) !== null) {
|
|
153
|
+
if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
|
|
154
|
+
secretFindings.push({
|
|
155
|
+
file, line: lineNum + 1, column: match.index + 1,
|
|
156
|
+
matched: match[0], severity: pattern.severity,
|
|
157
|
+
category: pattern.category || 'secrets',
|
|
158
|
+
rule: pattern.name, title: pattern.name.replace(/_/g, ' '),
|
|
159
|
+
description: pattern.description,
|
|
160
|
+
confidence: getConfidence(pattern, match[0]),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch { /* skip */ }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const orchestrator = buildOrchestrator();
|
|
169
|
+
const results = await orchestrator.runAll(absolutePath, { quiet: true });
|
|
170
|
+
|
|
171
|
+
let depVulns = [];
|
|
172
|
+
try {
|
|
173
|
+
const depResult = await runDepsAudit(absolutePath);
|
|
174
|
+
depVulns = depResult.vulns || [];
|
|
175
|
+
} catch { /* skip */ }
|
|
176
|
+
|
|
177
|
+
spinner.stop();
|
|
178
|
+
|
|
179
|
+
// ── Score ─────────────────────────────────────────────────────────────────
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
const allFindings = [...secretFindings, ...results.findings].filter(f => {
|
|
182
|
+
const key = `${f.file}:${f.line}:${f.rule}`;
|
|
183
|
+
if (seen.has(key)) return false;
|
|
184
|
+
seen.add(key);
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const scoringEngine = new ScoringEngine();
|
|
189
|
+
const scoreResult = scoringEngine.compute(allFindings, depVulns);
|
|
190
|
+
const score = Math.round(scoreResult.score * 10) / 10;
|
|
191
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
192
|
+
|
|
193
|
+
// ── JSON Output ───────────────────────────────────────────────────────────
|
|
194
|
+
if (options.json) {
|
|
195
|
+
const percentile = getPercentile(score);
|
|
196
|
+
const catComparisons = {};
|
|
197
|
+
for (const [key, cat] of Object.entries(scoreResult.categories)) {
|
|
198
|
+
const bench = BENCHMARKS.categories[key];
|
|
199
|
+
if (!bench) continue;
|
|
200
|
+
const count = Object.values(cat.counts).reduce((a, b) => a + b, 0);
|
|
201
|
+
catComparisons[key] = {
|
|
202
|
+
label: bench.label,
|
|
203
|
+
yourFindings: count,
|
|
204
|
+
industryAvg: bench.avgFindingsPerProject,
|
|
205
|
+
betterThanAvg: count <= bench.avgFindingsPerProject,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
console.log(JSON.stringify({
|
|
209
|
+
project: projectName,
|
|
210
|
+
score, grade: scoreResult.grade.letter,
|
|
211
|
+
percentile,
|
|
212
|
+
industryMedian: BENCHMARKS.overall.industry,
|
|
213
|
+
topQuartile: BENCHMARKS.overall.topQuartile,
|
|
214
|
+
categories: catComparisons,
|
|
215
|
+
totalFindings: allFindings.length,
|
|
216
|
+
depVulns: depVulns.length,
|
|
217
|
+
duration: `${duration}s`,
|
|
218
|
+
}, null, 2));
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Display ───────────────────────────────────────────────────────────────
|
|
223
|
+
const percentile = getPercentile(score);
|
|
224
|
+
const vsIndustry = score - BENCHMARKS.overall.industry;
|
|
225
|
+
const vsColor = vsIndustry >= 0 ? chalk.green : chalk.red;
|
|
226
|
+
|
|
227
|
+
// Score comparison
|
|
228
|
+
console.log(chalk.white.bold(' Your Score vs Industry'));
|
|
229
|
+
console.log();
|
|
230
|
+
printScoreBar('You', score, scoreResult.grade.letter);
|
|
231
|
+
printScoreBar('Industry Median', BENCHMARKS.overall.industry, 'D');
|
|
232
|
+
printScoreBar('Top 25%', BENCHMARKS.overall.topQuartile, 'B');
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(` ${vsColor(`${vsIndustry >= 0 ? '+' : ''}${Math.round(vsIndustry)} pts`)} vs industry median`);
|
|
235
|
+
console.log(chalk.gray(` You're in the top ${100 - percentile}% of projects scanned`));
|
|
236
|
+
console.log();
|
|
237
|
+
|
|
238
|
+
// Category comparison
|
|
239
|
+
console.log(chalk.white.bold(' Category Comparison'));
|
|
240
|
+
console.log(chalk.gray(' ' + '─'.repeat(70)));
|
|
241
|
+
|
|
242
|
+
for (const [key, cat] of Object.entries(scoreResult.categories)) {
|
|
243
|
+
const bench = BENCHMARKS.categories[key];
|
|
244
|
+
if (!bench) continue;
|
|
245
|
+
const count = Object.values(cat.counts).reduce((a, b) => a + b, 0);
|
|
246
|
+
const better = count <= bench.avgFindingsPerProject;
|
|
247
|
+
const icon = better ? chalk.green('✓') : chalk.red('✗');
|
|
248
|
+
const countStr = String(count).padStart(3);
|
|
249
|
+
const avgStr = String(bench.avgFindingsPerProject).padStart(4);
|
|
250
|
+
|
|
251
|
+
console.log(
|
|
252
|
+
` ${icon} ${chalk.white(bench.label.padEnd(28))}` +
|
|
253
|
+
chalk.cyan(`You: ${countStr}`) +
|
|
254
|
+
chalk.gray(` | Avg: ${avgStr}`) +
|
|
255
|
+
(better ? chalk.green(' Better') : chalk.yellow(' Needs work'))
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
console.log();
|
|
259
|
+
|
|
260
|
+
// Risk context
|
|
261
|
+
const riskCategories = Object.entries(scoreResult.categories)
|
|
262
|
+
.filter(([key]) => BENCHMARKS.categories[key])
|
|
263
|
+
.filter(([, cat]) => {
|
|
264
|
+
const count = Object.values(cat.counts).reduce((a, b) => a + b, 0);
|
|
265
|
+
const bench = BENCHMARKS.categories[Object.keys(scoreResult.categories).find(k => scoreResult.categories[k] === cat)];
|
|
266
|
+
return bench && count > bench.avgFindingsPerProject;
|
|
267
|
+
})
|
|
268
|
+
.map(([key]) => BENCHMARKS.categories[key].label);
|
|
269
|
+
|
|
270
|
+
if (riskCategories.length > 0) {
|
|
271
|
+
console.log(chalk.yellow.bold(' Areas above industry average (needs attention):'));
|
|
272
|
+
for (const cat of riskCategories) {
|
|
273
|
+
console.log(chalk.yellow(` → ${cat}`));
|
|
274
|
+
}
|
|
275
|
+
console.log();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(chalk.gray(` Scanned in ${duration}s | ${allFiles.length} files | ${allFindings.length} findings | ${depVulns.length} dep CVEs`));
|
|
279
|
+
console.log();
|
|
280
|
+
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// =============================================================================
|
|
285
|
+
// HELPERS
|
|
286
|
+
// =============================================================================
|
|
287
|
+
|
|
288
|
+
function getPercentile(score) {
|
|
289
|
+
for (const { score: s, percentile } of BENCHMARKS.percentiles) {
|
|
290
|
+
if (score >= s) return percentile;
|
|
291
|
+
}
|
|
292
|
+
return 5;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function printScoreBar(label, score, grade) {
|
|
296
|
+
const barWidth = 40;
|
|
297
|
+
const filled = Math.round((score / 100) * barWidth);
|
|
298
|
+
const empty = barWidth - filled;
|
|
299
|
+
const gradeColors = { A: chalk.green, B: chalk.cyan, C: chalk.yellow, D: chalk.red, F: chalk.red };
|
|
300
|
+
const color = gradeColors[grade] || chalk.gray;
|
|
301
|
+
|
|
302
|
+
console.log(
|
|
303
|
+
` ${chalk.gray(label.padEnd(18))}` +
|
|
304
|
+
color('█'.repeat(filled)) +
|
|
305
|
+
chalk.gray('░'.repeat(empty)) +
|
|
306
|
+
` ${color(`${score}/100`)}`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function findFiles(rootPath) {
|
|
311
|
+
const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
|
|
312
|
+
const gitignoreGlobs = loadGitignorePatterns(rootPath);
|
|
313
|
+
globIgnore.push(...gitignoreGlobs);
|
|
314
|
+
|
|
315
|
+
const files = await fg('**/*', {
|
|
316
|
+
cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return files.filter(file => {
|
|
320
|
+
const ext = path.extname(file).toLowerCase();
|
|
321
|
+
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
322
|
+
if (SKIP_FILENAMES.has(path.basename(file))) return false;
|
|
323
|
+
if (path.basename(file).endsWith('.min.js') || path.basename(file).endsWith('.min.css')) return false;
|
|
324
|
+
try { if (fs.statSync(file).size > MAX_FILE_SIZE) return false; } catch { return false; }
|
|
325
|
+
return true;
|
|
326
|
+
});
|
|
327
|
+
}
|