ship-safe 6.1.0 → 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 (48) hide show
  1. package/README.md +735 -594
  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 +40 -4
  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 -979
  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 -567
  34. package/cli/commands/score.js +449 -448
  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 +1 -0
  44. package/cli/utils/patterns.js +1121 -1121
  45. package/cli/utils/pdf-generator.js +94 -94
  46. package/package.json +69 -68
  47. package/cli/__tests__/agents.test.js +0 -1301
  48. package/configs/supabase/rls-templates.sql +0 -242
@@ -1,248 +1,248 @@
1
- /**
2
- * Enhanced Scoring Engine
3
- * ========================
4
- *
5
- * Risk-based scoring with 8 categories, EPSS integration,
6
- * KEV flagging, and historical trend tracking.
7
- *
8
- * Score = 100 - sum(category deductions)
9
- * Each category has a weight and max deduction cap.
10
- */
11
-
12
- import fs from 'fs';
13
- import path from 'path';
14
- import { getComplianceSummary } from '../utils/compliance-map.js';
15
-
16
- // =============================================================================
17
- // SCORING CONFIGURATION
18
- // =============================================================================
19
-
20
- // Weights aligned with OWASP Top 10 2025:
21
- // A01 Broken Access Control (auth: 15), A02 Security Misconfiguration (config: 8),
22
- // A03 Software Supply Chain Failures (supply-chain: 12, deps: 13),
23
- // A05 Injection (injection: 15), A07 Auth Failures (secrets: 15),
24
- // A10 Mishandling of Exceptional Conditions (→ injection category)
25
- // + API Security (10), AI/LLM Security (12) — weights sum to 100
26
- const CATEGORIES = {
27
- secrets: { weight: 15, label: 'Secrets', deductions: { critical: 25, high: 15, medium: 5 } },
28
- injection: { weight: 15, label: 'Code Vulnerabilities', deductions: { critical: 20, high: 10, medium: 3 } },
29
- deps: { weight: 13, label: 'Dependencies', deductions: { critical: 20, high: 10, medium: 5, moderate: 5 } },
30
- auth: { weight: 15, label: 'Auth & Access Control', deductions: { critical: 20, high: 10, medium: 3 } },
31
- config: { weight: 8, label: 'Configuration', deductions: { critical: 15, high: 8, medium: 3 } },
32
- 'supply-chain':{ weight: 12, label: 'Supply Chain', deductions: { critical: 15, high: 8, medium: 3 } },
33
- api: { weight: 10, label: 'API Security', deductions: { critical: 15, high: 8, medium: 3 } },
34
- llm: { weight: 12, label: 'AI/LLM Security', deductions: { critical: 15, high: 8, medium: 3 } },
35
- };
36
-
37
- // Fallback categories for findings that don't match a known category
38
- const FALLBACK_CATEGORY_MAP = {
39
- 'secret': 'secrets',
40
- 'vulnerability': 'injection',
41
- 'ssrf': 'injection', // OWASP 2025: SSRF merged into A01 Broken Access Control
42
- 'history': 'secrets',
43
- 'cicd': 'config',
44
- 'mobile': 'injection',
45
- 'privacy': 'config',
46
- 'mcp': 'llm',
47
- 'agentic': 'llm',
48
- 'rag': 'llm',
49
- 'vibe': 'injection', // Vibe coding findings → Code Vulnerabilities
50
- 'exception': 'injection', // OWASP A10:2025 — Mishandling of Exceptional Conditions
51
- 'agent-config': 'llm', // Agent config security → AI/LLM category
52
- 'recon': null, // skip recon findings
53
- };
54
-
55
- const GRADES = [
56
- { min: 90, letter: 'A', label: 'Ship it!', color: 'green' },
57
- { min: 75, letter: 'B', label: 'Minor issues to review', color: 'cyan' },
58
- { min: 60, letter: 'C', label: 'Fix before shipping', color: 'yellow' },
59
- { min: 40, letter: 'D', label: 'Significant security risks', color: 'red' },
60
- { min: 0, letter: 'F', label: 'Not safe to ship', color: 'red' },
61
- ];
62
-
63
- // =============================================================================
64
- // SCORING ENGINE
65
- // =============================================================================
66
-
67
- export class ScoringEngine {
68
- /**
69
- * Compute the security score from agent findings + dependency vulnerabilities.
70
- *
71
- * @param {object[]} findings — Array of finding objects from agents
72
- * @param {object[]} depVulns — Array of dependency CVE objects
73
- * @returns {object} — { score, grade, categories, breakdown }
74
- */
75
- compute(findings = [], depVulns = []) {
76
- const categoryResults = {};
77
-
78
- // Initialize all categories
79
- for (const [key, config] of Object.entries(CATEGORIES)) {
80
- categoryResults[key] = {
81
- label: config.label,
82
- weight: config.weight,
83
- counts: { critical: 0, high: 0, medium: 0, low: 0 },
84
- deduction: 0,
85
- maxDeduction: config.weight, // Cap at category weight
86
- findings: [],
87
- };
88
- }
89
-
90
- // ── Classify findings into categories ─────────────────────────────────────
91
- for (const finding of findings) {
92
- const cat = this.resolveCategory(finding.category);
93
- if (!cat || !categoryResults[cat]) continue;
94
-
95
- const sev = finding.severity || 'medium';
96
- categoryResults[cat].counts[sev] = (categoryResults[cat].counts[sev] || 0) + 1;
97
- categoryResults[cat].findings.push(finding);
98
- }
99
-
100
- // ── Add dependency vulnerabilities ────────────────────────────────────────
101
- for (const vuln of depVulns) {
102
- const sev = vuln.severity || 'medium';
103
- categoryResults.deps.counts[sev] = (categoryResults.deps.counts[sev] || 0) + 1;
104
- }
105
-
106
- // ── Compute deductions per category (confidence-weighted) ─────────────────
107
- const CONFIDENCE_MULTIPLIER = { high: 1.0, medium: 0.6, low: 0.3 };
108
-
109
- for (const [key, config] of Object.entries(CATEGORIES)) {
110
- const result = categoryResults[key];
111
- let deduction = 0;
112
-
113
- // Count-based deductions for deps (no per-finding confidence)
114
- for (const [sev, pts] of Object.entries(config.deductions)) {
115
- if (key === 'deps') {
116
- deduction += (result.counts[sev] || 0) * pts;
117
- }
118
- }
119
-
120
- // Per-finding confidence-weighted deductions for agent findings
121
- if (key !== 'deps') {
122
- for (const finding of result.findings) {
123
- const sev = finding.severity || 'medium';
124
- const pts = config.deductions[sev] || 0;
125
- const confidence = finding.confidence || 'high';
126
- const multiplier = CONFIDENCE_MULTIPLIER[confidence] || 1.0;
127
- deduction += pts * multiplier;
128
- }
129
- }
130
-
131
- result.deduction = Math.min(deduction, result.maxDeduction);
132
- }
133
-
134
- // ── Compute total score ───────────────────────────────────────────────────
135
- const totalDeduction = Object.values(categoryResults).reduce(
136
- (sum, r) => sum + r.deduction, 0
137
- );
138
- const score = Math.max(0, 100 - totalDeduction);
139
- const grade = GRADES.find(g => score >= g.min);
140
-
141
- // ── Compliance mapping ─────────────────────────────────────────────────
142
- let compliance;
143
- try {
144
- compliance = getComplianceSummary(findings);
145
- } catch {
146
- compliance = null;
147
- }
148
-
149
- return {
150
- score,
151
- grade,
152
- categories: categoryResults,
153
- totalFindings: findings.length,
154
- totalDepVulns: depVulns.length,
155
- compliance,
156
- };
157
- }
158
-
159
- /**
160
- * Map a finding category to a scoring category.
161
- */
162
- resolveCategory(findingCategory) {
163
- if (CATEGORIES[findingCategory]) return findingCategory;
164
- if (FALLBACK_CATEGORY_MAP[findingCategory] !== undefined) {
165
- return FALLBACK_CATEGORY_MAP[findingCategory];
166
- }
167
- return 'injection'; // default fallback
168
- }
169
-
170
- /**
171
- * Save score to history file for trend tracking.
172
- */
173
- saveToHistory(rootPath, scoreResult, suppressions = null) {
174
- const historyDir = path.join(rootPath, '.ship-safe');
175
- const historyFile = path.join(historyDir, 'history.json');
176
-
177
- try {
178
- if (!fs.existsSync(historyDir)) {
179
- fs.mkdirSync(historyDir, { recursive: true });
180
- }
181
-
182
- let history = [];
183
- if (fs.existsSync(historyFile)) {
184
- try {
185
- history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
186
- } catch { history = []; }
187
- }
188
-
189
- const entry = {
190
- timestamp: new Date().toISOString(),
191
- score: scoreResult.score,
192
- grade: scoreResult.grade.letter,
193
- totalFindings: scoreResult.totalFindings,
194
- totalDepVulns: scoreResult.totalDepVulns,
195
- categoryScores: Object.fromEntries(
196
- Object.entries(scoreResult.categories).map(([k, v]) => [k, {
197
- deduction: v.deduction,
198
- counts: v.counts,
199
- }])
200
- ),
201
- };
202
- if (suppressions) entry.suppressions = suppressions;
203
- history.push(entry);
204
-
205
- // Keep last 100 entries
206
- if (history.length > 100) history = history.slice(-100);
207
-
208
- fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
209
- } catch {
210
- // Don't fail if history save fails
211
- }
212
- }
213
-
214
- /**
215
- * Load score history for trend display.
216
- */
217
- loadHistory(rootPath) {
218
- const historyFile = path.join(rootPath, '.ship-safe', 'history.json');
219
- try {
220
- if (fs.existsSync(historyFile)) {
221
- return JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
222
- }
223
- } catch { /* ignore */ }
224
- return [];
225
- }
226
-
227
- /**
228
- * Get trend summary comparing current to last scan.
229
- */
230
- getTrend(rootPath, currentScore) {
231
- const history = this.loadHistory(rootPath);
232
- if (history.length < 2) return null;
233
-
234
- const previous = history[history.length - 2];
235
- const diff = currentScore - previous.score;
236
-
237
- return {
238
- previousScore: previous.score,
239
- currentScore,
240
- diff,
241
- direction: diff > 0 ? 'improved' : diff < 0 ? 'regressed' : 'unchanged',
242
- previousDate: previous.timestamp,
243
- };
244
- }
245
- }
246
-
247
- export { GRADES, CATEGORIES };
248
- export default ScoringEngine;
1
+ /**
2
+ * Enhanced Scoring Engine
3
+ * ========================
4
+ *
5
+ * Risk-based scoring with 8 categories, EPSS integration,
6
+ * KEV flagging, and historical trend tracking.
7
+ *
8
+ * Score = 100 - sum(category deductions)
9
+ * Each category has a weight and max deduction cap.
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { getComplianceSummary } from '../utils/compliance-map.js';
15
+
16
+ // =============================================================================
17
+ // SCORING CONFIGURATION
18
+ // =============================================================================
19
+
20
+ // Weights aligned with OWASP Top 10 2025:
21
+ // A01 Broken Access Control (auth: 15), A02 Security Misconfiguration (config: 8),
22
+ // A03 Software Supply Chain Failures (supply-chain: 12, deps: 13),
23
+ // A05 Injection (injection: 15), A07 Auth Failures (secrets: 15),
24
+ // A10 Mishandling of Exceptional Conditions (→ injection category)
25
+ // + API Security (10), AI/LLM Security (12) — weights sum to 100
26
+ const CATEGORIES = {
27
+ secrets: { weight: 15, label: 'Secrets', deductions: { critical: 25, high: 15, medium: 5 } },
28
+ injection: { weight: 15, label: 'Code Vulnerabilities', deductions: { critical: 20, high: 10, medium: 3 } },
29
+ deps: { weight: 13, label: 'Dependencies', deductions: { critical: 20, high: 10, medium: 5, moderate: 5 } },
30
+ auth: { weight: 15, label: 'Auth & Access Control', deductions: { critical: 20, high: 10, medium: 3 } },
31
+ config: { weight: 8, label: 'Configuration', deductions: { critical: 15, high: 8, medium: 3 } },
32
+ 'supply-chain':{ weight: 12, label: 'Supply Chain', deductions: { critical: 15, high: 8, medium: 3 } },
33
+ api: { weight: 10, label: 'API Security', deductions: { critical: 15, high: 8, medium: 3 } },
34
+ llm: { weight: 12, label: 'AI/LLM Security', deductions: { critical: 15, high: 8, medium: 3 } },
35
+ };
36
+
37
+ // Fallback categories for findings that don't match a known category
38
+ const FALLBACK_CATEGORY_MAP = {
39
+ 'secret': 'secrets',
40
+ 'vulnerability': 'injection',
41
+ 'ssrf': 'injection', // OWASP 2025: SSRF merged into A01 Broken Access Control
42
+ 'history': 'secrets',
43
+ 'cicd': 'config',
44
+ 'mobile': 'injection',
45
+ 'privacy': 'config',
46
+ 'mcp': 'llm',
47
+ 'agentic': 'llm',
48
+ 'rag': 'llm',
49
+ 'vibe': 'injection', // Vibe coding findings → Code Vulnerabilities
50
+ 'exception': 'injection', // OWASP A10:2025 — Mishandling of Exceptional Conditions
51
+ 'agent-config': 'llm', // Agent config security → AI/LLM category
52
+ 'recon': null, // skip recon findings
53
+ };
54
+
55
+ const GRADES = [
56
+ { min: 90, letter: 'A', label: 'Ship it!', color: 'green' },
57
+ { min: 75, letter: 'B', label: 'Minor issues to review', color: 'cyan' },
58
+ { min: 60, letter: 'C', label: 'Fix before shipping', color: 'yellow' },
59
+ { min: 40, letter: 'D', label: 'Significant security risks', color: 'red' },
60
+ { min: 0, letter: 'F', label: 'Not safe to ship', color: 'red' },
61
+ ];
62
+
63
+ // =============================================================================
64
+ // SCORING ENGINE
65
+ // =============================================================================
66
+
67
+ export class ScoringEngine {
68
+ /**
69
+ * Compute the security score from agent findings + dependency vulnerabilities.
70
+ *
71
+ * @param {object[]} findings — Array of finding objects from agents
72
+ * @param {object[]} depVulns — Array of dependency CVE objects
73
+ * @returns {object} — { score, grade, categories, breakdown }
74
+ */
75
+ compute(findings = [], depVulns = []) {
76
+ const categoryResults = {};
77
+
78
+ // Initialize all categories
79
+ for (const [key, config] of Object.entries(CATEGORIES)) {
80
+ categoryResults[key] = {
81
+ label: config.label,
82
+ weight: config.weight,
83
+ counts: { critical: 0, high: 0, medium: 0, low: 0 },
84
+ deduction: 0,
85
+ maxDeduction: config.weight, // Cap at category weight
86
+ findings: [],
87
+ };
88
+ }
89
+
90
+ // ── Classify findings into categories ─────────────────────────────────────
91
+ for (const finding of findings) {
92
+ const cat = this.resolveCategory(finding.category);
93
+ if (!cat || !categoryResults[cat]) continue;
94
+
95
+ const sev = finding.severity || 'medium';
96
+ categoryResults[cat].counts[sev] = (categoryResults[cat].counts[sev] || 0) + 1;
97
+ categoryResults[cat].findings.push(finding);
98
+ }
99
+
100
+ // ── Add dependency vulnerabilities ────────────────────────────────────────
101
+ for (const vuln of depVulns) {
102
+ const sev = vuln.severity || 'medium';
103
+ categoryResults.deps.counts[sev] = (categoryResults.deps.counts[sev] || 0) + 1;
104
+ }
105
+
106
+ // ── Compute deductions per category (confidence-weighted) ─────────────────
107
+ const CONFIDENCE_MULTIPLIER = { high: 1.0, medium: 0.6, low: 0.3 };
108
+
109
+ for (const [key, config] of Object.entries(CATEGORIES)) {
110
+ const result = categoryResults[key];
111
+ let deduction = 0;
112
+
113
+ // Count-based deductions for deps (no per-finding confidence)
114
+ for (const [sev, pts] of Object.entries(config.deductions)) {
115
+ if (key === 'deps') {
116
+ deduction += (result.counts[sev] || 0) * pts;
117
+ }
118
+ }
119
+
120
+ // Per-finding confidence-weighted deductions for agent findings
121
+ if (key !== 'deps') {
122
+ for (const finding of result.findings) {
123
+ const sev = finding.severity || 'medium';
124
+ const pts = config.deductions[sev] || 0;
125
+ const confidence = finding.confidence || 'high';
126
+ const multiplier = CONFIDENCE_MULTIPLIER[confidence] || 1.0;
127
+ deduction += pts * multiplier;
128
+ }
129
+ }
130
+
131
+ result.deduction = Math.min(deduction, result.maxDeduction);
132
+ }
133
+
134
+ // ── Compute total score ───────────────────────────────────────────────────
135
+ const totalDeduction = Object.values(categoryResults).reduce(
136
+ (sum, r) => sum + r.deduction, 0
137
+ );
138
+ const score = Math.max(0, 100 - totalDeduction);
139
+ const grade = GRADES.find(g => score >= g.min);
140
+
141
+ // ── Compliance mapping ─────────────────────────────────────────────────
142
+ let compliance;
143
+ try {
144
+ compliance = getComplianceSummary(findings);
145
+ } catch {
146
+ compliance = null;
147
+ }
148
+
149
+ return {
150
+ score,
151
+ grade,
152
+ categories: categoryResults,
153
+ totalFindings: findings.length,
154
+ totalDepVulns: depVulns.length,
155
+ compliance,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Map a finding category to a scoring category.
161
+ */
162
+ resolveCategory(findingCategory) {
163
+ if (CATEGORIES[findingCategory]) return findingCategory;
164
+ if (FALLBACK_CATEGORY_MAP[findingCategory] !== undefined) {
165
+ return FALLBACK_CATEGORY_MAP[findingCategory];
166
+ }
167
+ return 'injection'; // default fallback
168
+ }
169
+
170
+ /**
171
+ * Save score to history file for trend tracking.
172
+ */
173
+ saveToHistory(rootPath, scoreResult, suppressions = null) {
174
+ const historyDir = path.join(rootPath, '.ship-safe');
175
+ const historyFile = path.join(historyDir, 'history.json');
176
+
177
+ try {
178
+ if (!fs.existsSync(historyDir)) {
179
+ fs.mkdirSync(historyDir, { recursive: true });
180
+ }
181
+
182
+ let history = [];
183
+ if (fs.existsSync(historyFile)) {
184
+ try {
185
+ history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
186
+ } catch { history = []; }
187
+ }
188
+
189
+ const entry = {
190
+ timestamp: new Date().toISOString(),
191
+ score: scoreResult.score,
192
+ grade: scoreResult.grade.letter,
193
+ totalFindings: scoreResult.totalFindings,
194
+ totalDepVulns: scoreResult.totalDepVulns,
195
+ categoryScores: Object.fromEntries(
196
+ Object.entries(scoreResult.categories).map(([k, v]) => [k, {
197
+ deduction: v.deduction,
198
+ counts: v.counts,
199
+ }])
200
+ ),
201
+ };
202
+ if (suppressions) entry.suppressions = suppressions;
203
+ history.push(entry);
204
+
205
+ // Keep last 100 entries
206
+ if (history.length > 100) history = history.slice(-100);
207
+
208
+ fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
209
+ } catch {
210
+ // Don't fail if history save fails
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Load score history for trend display.
216
+ */
217
+ loadHistory(rootPath) {
218
+ const historyFile = path.join(rootPath, '.ship-safe', 'history.json');
219
+ try {
220
+ if (fs.existsSync(historyFile)) {
221
+ return JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
222
+ }
223
+ } catch { /* ignore */ }
224
+ return [];
225
+ }
226
+
227
+ /**
228
+ * Get trend summary comparing current to last scan.
229
+ */
230
+ getTrend(rootPath, currentScore) {
231
+ const history = this.loadHistory(rootPath);
232
+ if (history.length < 2) return null;
233
+
234
+ const previous = history[history.length - 2];
235
+ const diff = currentScore - previous.score;
236
+
237
+ return {
238
+ previousScore: previous.score,
239
+ currentScore,
240
+ diff,
241
+ direction: diff > 0 ? 'improved' : diff < 0 ? 'regressed' : 'unchanged',
242
+ previousDate: previous.timestamp,
243
+ };
244
+ }
245
+ }
246
+
247
+ export { GRADES, CATEGORIES };
248
+ export default ScoringEngine;