ship-safe 6.1.1 → 6.3.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 (49) hide show
  1. package/README.md +748 -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 +85 -84
  11. package/cli/agents/injection-tester.js +500 -500
  12. package/cli/agents/legal-risk-agent.js +302 -0
  13. package/cli/agents/llm-redteam.js +251 -251
  14. package/cli/agents/mobile-scanner.js +231 -231
  15. package/cli/agents/orchestrator.js +322 -322
  16. package/cli/agents/pii-compliance-agent.js +301 -301
  17. package/cli/agents/scoring-engine.js +248 -248
  18. package/cli/agents/supabase-rls-agent.js +154 -154
  19. package/cli/agents/supply-chain-agent.js +650 -507
  20. package/cli/bin/ship-safe.js +464 -426
  21. package/cli/commands/agent.js +608 -608
  22. package/cli/commands/audit.js +1006 -980
  23. package/cli/commands/baseline.js +193 -193
  24. package/cli/commands/ci.js +342 -342
  25. package/cli/commands/deps.js +516 -516
  26. package/cli/commands/doctor.js +159 -159
  27. package/cli/commands/fix.js +218 -218
  28. package/cli/commands/hooks.js +268 -0
  29. package/cli/commands/init.js +407 -407
  30. package/cli/commands/legal.js +158 -0
  31. package/cli/commands/mcp.js +304 -304
  32. package/cli/commands/red-team.js +7 -1
  33. package/cli/commands/remediate.js +798 -798
  34. package/cli/commands/rotate.js +571 -571
  35. package/cli/commands/scan.js +569 -569
  36. package/cli/commands/score.js +449 -449
  37. package/cli/commands/watch.js +281 -281
  38. package/cli/hooks/patterns.js +313 -0
  39. package/cli/hooks/post-tool-use.js +140 -0
  40. package/cli/hooks/pre-tool-use.js +186 -0
  41. package/cli/index.js +73 -69
  42. package/cli/providers/llm-provider.js +397 -287
  43. package/cli/utils/autofix-rules.js +74 -74
  44. package/cli/utils/cache-manager.js +311 -311
  45. package/cli/utils/output.js +230 -230
  46. package/cli/utils/patterns.js +1121 -1121
  47. package/cli/utils/pdf-generator.js +94 -94
  48. package/package.json +69 -69
  49. package/configs/supabase/rls-templates.sql +0 -242
@@ -1,193 +1,193 @@
1
- /**
2
- * Baseline Command
3
- * =================
4
- *
5
- * Accept current findings as a baseline and only report new findings
6
- * on subsequent scans. This is how teams adopt security scanners:
7
- * baseline existing debt, focus on not making it worse.
8
- *
9
- * USAGE:
10
- * ship-safe baseline . Create a new baseline from current scan
11
- * ship-safe baseline . --diff Show what changed since baseline
12
- * ship-safe baseline . --clear Remove the baseline
13
- */
14
-
15
- import fs from 'fs';
16
- import path from 'path';
17
- import chalk from 'chalk';
18
- import ora from 'ora';
19
- import { buildOrchestrator } from '../agents/index.js';
20
- import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, MAX_FILE_SIZE } from '../utils/patterns.js';
21
- import { isHighEntropyMatch } from '../utils/entropy.js';
22
- import fg from 'fast-glob';
23
-
24
- const BASELINE_FILE = '.ship-safe/baseline.json';
25
-
26
- /**
27
- * Generate a fingerprint for a finding that survives line-number shifts.
28
- * Uses rule + relative file path + first 40 chars of matched text.
29
- */
30
- function fingerprint(finding, rootPath) {
31
- const relFile = path.relative(rootPath, finding.file || '').replace(/\\/g, '/');
32
- const matched = (finding.matched || '').slice(0, 40);
33
- return `${finding.rule}:${relFile}:${matched}`;
34
- }
35
-
36
- /**
37
- * Quick secret scan (same as audit Phase 1) to get current findings.
38
- */
39
- async function quickScan(rootPath) {
40
- const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
41
- const files = await fg('**/*', {
42
- cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true,
43
- });
44
-
45
- const filtered = files.filter(f => {
46
- const ext = path.extname(f).toLowerCase();
47
- if (SKIP_EXTENSIONS.has(ext)) return false;
48
- if (SKIP_FILENAMES.has(path.basename(f))) return false;
49
- try { return fs.statSync(f).size <= MAX_FILE_SIZE; } catch { return false; }
50
- });
51
-
52
- const findings = [];
53
- for (const file of filtered) {
54
- try {
55
- const content = fs.readFileSync(file, 'utf-8');
56
- const lines = content.split('\n');
57
- for (let i = 0; i < lines.length; i++) {
58
- if (/ship-safe-ignore/i.test(lines[i])) continue;
59
- for (const p of SECRET_PATTERNS) {
60
- p.pattern.lastIndex = 0;
61
- let m;
62
- while ((m = p.pattern.exec(lines[i])) !== null) {
63
- if (p.requiresEntropyCheck && !isHighEntropyMatch(m[0])) continue;
64
- findings.push({ file, line: i + 1, rule: p.name, matched: m[0], severity: p.severity });
65
- }
66
- }
67
- }
68
- } catch { /* skip */ }
69
- }
70
-
71
- return { findings, files: filtered };
72
- }
73
-
74
- /**
75
- * Run agents and combine with secret scan findings.
76
- */
77
- async function fullScan(rootPath) {
78
- const { findings: secretFindings, files } = await quickScan(rootPath);
79
-
80
- const orchestrator = buildOrchestrator();
81
- const { findings: agentFindings } = await orchestrator.runAll(rootPath, { quiet: true }); // ship-safe-ignore — orchestrator result, not LLM output triggering actions
82
-
83
- return [...secretFindings, ...agentFindings];
84
- }
85
-
86
- /**
87
- * Load existing baseline from disk.
88
- */
89
- function loadBaseline(rootPath) {
90
- const baselinePath = path.join(rootPath, BASELINE_FILE);
91
- if (!fs.existsSync(baselinePath)) return null;
92
- try {
93
- return JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
94
- } catch {
95
- return null;
96
- }
97
- }
98
-
99
- /**
100
- * Save baseline to disk.
101
- */
102
- function saveBaseline(rootPath, fingerprints, findingCount) {
103
- const baselinePath = path.join(rootPath, BASELINE_FILE);
104
- const dir = path.dirname(baselinePath);
105
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
106
-
107
- const baseline = {
108
- version: '4.3.0',
109
- createdAt: new Date().toISOString(),
110
- fingerprints,
111
- findingCount,
112
- };
113
-
114
- fs.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2));
115
- return baseline;
116
- }
117
-
118
- /**
119
- * Filter out baselined findings. Returns only new findings.
120
- */
121
- export function filterBaseline(findings, rootPath) {
122
- const baseline = loadBaseline(rootPath);
123
- if (!baseline) return findings;
124
-
125
- const baseSet = new Set(baseline.fingerprints);
126
- return findings.filter(f => !baseSet.has(fingerprint(f, rootPath)));
127
- }
128
-
129
- /**
130
- * Main baseline command.
131
- */
132
- export async function baselineCommand(targetPath = '.', options = {}) {
133
- const rootPath = path.resolve(targetPath);
134
-
135
- if (options.clear) {
136
- const baselinePath = path.join(rootPath, BASELINE_FILE);
137
- if (fs.existsSync(baselinePath)) {
138
- fs.unlinkSync(baselinePath);
139
- console.log(chalk.green(' Baseline removed.'));
140
- } else {
141
- console.log(chalk.gray(' No baseline found.'));
142
- }
143
- return;
144
- }
145
-
146
- if (options.diff) {
147
- const baseline = loadBaseline(rootPath);
148
- if (!baseline) {
149
- console.log(chalk.yellow(' No baseline found. Run `ship-safe baseline .` first.'));
150
- return;
151
- }
152
-
153
- const spinner = ora({ text: 'Scanning for comparison...', color: 'cyan' }).start();
154
- const findings = await fullScan(rootPath);
155
- spinner.stop();
156
-
157
- const currentFingerprints = new Set(findings.map(f => fingerprint(f, rootPath)));
158
- const baseSet = new Set(baseline.fingerprints);
159
-
160
- const newFindings = findings.filter(f => !baseSet.has(fingerprint(f, rootPath)));
161
- const resolvedCount = baseline.fingerprints.filter(fp => !currentFingerprints.has(fp)).length;
162
-
163
- console.log(chalk.cyan.bold('\n Baseline Comparison'));
164
- console.log(chalk.gray(` Baseline: ${baseline.findingCount} findings (${baseline.createdAt.slice(0, 10)})`));
165
- console.log(chalk.gray(` Current: ${findings.length} findings`));
166
- console.log();
167
- if (newFindings.length > 0) {
168
- console.log(chalk.red(` + ${newFindings.length} new finding(s)`));
169
- }
170
- if (resolvedCount > 0) {
171
- console.log(chalk.green(` - ${resolvedCount} resolved finding(s)`));
172
- }
173
- if (newFindings.length === 0 && resolvedCount === 0) {
174
- console.log(chalk.green(' No changes since baseline.'));
175
- }
176
- return;
177
- }
178
-
179
- // Default: create/update baseline
180
- const spinner = ora({ text: 'Running full scan for baseline...', color: 'cyan' }).start();
181
- const findings = await fullScan(rootPath);
182
- spinner.stop();
183
-
184
- const fingerprints = [...new Set(findings.map(f => fingerprint(f, rootPath)))];
185
- const baseline = saveBaseline(rootPath, fingerprints, findings.length);
186
-
187
- console.log(chalk.green.bold('\n Baseline created'));
188
- console.log(chalk.gray(` Findings baselined: ${findings.length}`));
189
- console.log(chalk.gray(` Unique fingerprints: ${fingerprints.length}`));
190
- console.log(chalk.gray(` Saved to: ${BASELINE_FILE}`));
191
- console.log();
192
- console.log(chalk.gray(' Run `ship-safe audit . --baseline` to only see new findings.'));
193
- }
1
+ /**
2
+ * Baseline Command
3
+ * =================
4
+ *
5
+ * Accept current findings as a baseline and only report new findings
6
+ * on subsequent scans. This is how teams adopt security scanners:
7
+ * baseline existing debt, focus on not making it worse.
8
+ *
9
+ * USAGE:
10
+ * ship-safe baseline . Create a new baseline from current scan
11
+ * ship-safe baseline . --diff Show what changed since baseline
12
+ * ship-safe baseline . --clear Remove the baseline
13
+ */
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import chalk from 'chalk';
18
+ import ora from 'ora';
19
+ import { buildOrchestrator } from '../agents/index.js';
20
+ import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, MAX_FILE_SIZE } from '../utils/patterns.js';
21
+ import { isHighEntropyMatch } from '../utils/entropy.js';
22
+ import fg from 'fast-glob';
23
+
24
+ const BASELINE_FILE = '.ship-safe/baseline.json';
25
+
26
+ /**
27
+ * Generate a fingerprint for a finding that survives line-number shifts.
28
+ * Uses rule + relative file path + first 40 chars of matched text.
29
+ */
30
+ function fingerprint(finding, rootPath) {
31
+ const relFile = path.relative(rootPath, finding.file || '').replace(/\\/g, '/');
32
+ const matched = (finding.matched || '').slice(0, 40);
33
+ return `${finding.rule}:${relFile}:${matched}`;
34
+ }
35
+
36
+ /**
37
+ * Quick secret scan (same as audit Phase 1) to get current findings.
38
+ */
39
+ async function quickScan(rootPath) {
40
+ const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
41
+ const files = await fg('**/*', {
42
+ cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true,
43
+ });
44
+
45
+ const filtered = files.filter(f => {
46
+ const ext = path.extname(f).toLowerCase();
47
+ if (SKIP_EXTENSIONS.has(ext)) return false;
48
+ if (SKIP_FILENAMES.has(path.basename(f))) return false;
49
+ try { return fs.statSync(f).size <= MAX_FILE_SIZE; } catch { return false; }
50
+ });
51
+
52
+ const findings = [];
53
+ for (const file of filtered) {
54
+ try {
55
+ const content = fs.readFileSync(file, 'utf-8');
56
+ const lines = content.split('\n');
57
+ for (let i = 0; i < lines.length; i++) {
58
+ if (/ship-safe-ignore/i.test(lines[i])) continue;
59
+ for (const p of SECRET_PATTERNS) {
60
+ p.pattern.lastIndex = 0;
61
+ let m;
62
+ while ((m = p.pattern.exec(lines[i])) !== null) {
63
+ if (p.requiresEntropyCheck && !isHighEntropyMatch(m[0])) continue;
64
+ findings.push({ file, line: i + 1, rule: p.name, matched: m[0], severity: p.severity });
65
+ }
66
+ }
67
+ }
68
+ } catch { /* skip */ }
69
+ }
70
+
71
+ return { findings, files: filtered };
72
+ }
73
+
74
+ /**
75
+ * Run agents and combine with secret scan findings.
76
+ */
77
+ async function fullScan(rootPath) {
78
+ const { findings: secretFindings, files } = await quickScan(rootPath);
79
+
80
+ const orchestrator = buildOrchestrator();
81
+ const { findings: agentFindings } = await orchestrator.runAll(rootPath, { quiet: true }); // ship-safe-ignore — orchestrator result, not LLM output triggering actions
82
+
83
+ return [...secretFindings, ...agentFindings];
84
+ }
85
+
86
+ /**
87
+ * Load existing baseline from disk.
88
+ */
89
+ function loadBaseline(rootPath) {
90
+ const baselinePath = path.join(rootPath, BASELINE_FILE);
91
+ if (!fs.existsSync(baselinePath)) return null;
92
+ try {
93
+ return JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Save baseline to disk.
101
+ */
102
+ function saveBaseline(rootPath, fingerprints, findingCount) {
103
+ const baselinePath = path.join(rootPath, BASELINE_FILE);
104
+ const dir = path.dirname(baselinePath);
105
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
106
+
107
+ const baseline = {
108
+ version: '4.3.0',
109
+ createdAt: new Date().toISOString(),
110
+ fingerprints,
111
+ findingCount,
112
+ };
113
+
114
+ fs.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2));
115
+ return baseline;
116
+ }
117
+
118
+ /**
119
+ * Filter out baselined findings. Returns only new findings.
120
+ */
121
+ export function filterBaseline(findings, rootPath) {
122
+ const baseline = loadBaseline(rootPath);
123
+ if (!baseline) return findings;
124
+
125
+ const baseSet = new Set(baseline.fingerprints);
126
+ return findings.filter(f => !baseSet.has(fingerprint(f, rootPath)));
127
+ }
128
+
129
+ /**
130
+ * Main baseline command.
131
+ */
132
+ export async function baselineCommand(targetPath = '.', options = {}) {
133
+ const rootPath = path.resolve(targetPath);
134
+
135
+ if (options.clear) {
136
+ const baselinePath = path.join(rootPath, BASELINE_FILE);
137
+ if (fs.existsSync(baselinePath)) {
138
+ fs.unlinkSync(baselinePath);
139
+ console.log(chalk.green(' Baseline removed.'));
140
+ } else {
141
+ console.log(chalk.gray(' No baseline found.'));
142
+ }
143
+ return;
144
+ }
145
+
146
+ if (options.diff) {
147
+ const baseline = loadBaseline(rootPath);
148
+ if (!baseline) {
149
+ console.log(chalk.yellow(' No baseline found. Run `ship-safe baseline .` first.'));
150
+ return;
151
+ }
152
+
153
+ const spinner = ora({ text: 'Scanning for comparison...', color: 'cyan' }).start();
154
+ const findings = await fullScan(rootPath);
155
+ spinner.stop();
156
+
157
+ const currentFingerprints = new Set(findings.map(f => fingerprint(f, rootPath)));
158
+ const baseSet = new Set(baseline.fingerprints);
159
+
160
+ const newFindings = findings.filter(f => !baseSet.has(fingerprint(f, rootPath)));
161
+ const resolvedCount = baseline.fingerprints.filter(fp => !currentFingerprints.has(fp)).length;
162
+
163
+ console.log(chalk.cyan.bold('\n Baseline Comparison'));
164
+ console.log(chalk.gray(` Baseline: ${baseline.findingCount} findings (${baseline.createdAt.slice(0, 10)})`));
165
+ console.log(chalk.gray(` Current: ${findings.length} findings`));
166
+ console.log();
167
+ if (newFindings.length > 0) {
168
+ console.log(chalk.red(` + ${newFindings.length} new finding(s)`));
169
+ }
170
+ if (resolvedCount > 0) {
171
+ console.log(chalk.green(` - ${resolvedCount} resolved finding(s)`));
172
+ }
173
+ if (newFindings.length === 0 && resolvedCount === 0) {
174
+ console.log(chalk.green(' No changes since baseline.'));
175
+ }
176
+ return;
177
+ }
178
+
179
+ // Default: create/update baseline
180
+ const spinner = ora({ text: 'Running full scan for baseline...', color: 'cyan' }).start();
181
+ const findings = await fullScan(rootPath);
182
+ spinner.stop();
183
+
184
+ const fingerprints = [...new Set(findings.map(f => fingerprint(f, rootPath)))];
185
+ const baseline = saveBaseline(rootPath, fingerprints, findings.length);
186
+
187
+ console.log(chalk.green.bold('\n Baseline created'));
188
+ console.log(chalk.gray(` Findings baselined: ${findings.length}`));
189
+ console.log(chalk.gray(` Unique fingerprints: ${fingerprints.length}`));
190
+ console.log(chalk.gray(` Saved to: ${BASELINE_FILE}`));
191
+ console.log();
192
+ console.log(chalk.gray(' Run `ship-safe audit . --baseline` to only see new findings.'));
193
+ }