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,218 +1,218 @@
1
- /**
2
- * Fix Command
3
- * ===========
4
- *
5
- * Scans for secrets and generates a .env.example file with placeholder values.
6
- * Also shows a summary of what to move to environment variables.
7
- *
8
- * USAGE:
9
- * ship-safe fix Scan and generate .env.example
10
- * ship-safe fix --dry-run Preview what would be generated (don't write file)
11
- */
12
-
13
- import fs from 'fs';
14
- import path from 'path';
15
- import ora from 'ora';
16
- import chalk from 'chalk';
17
- import {
18
- SECRET_PATTERNS,
19
- SKIP_DIRS,
20
- SKIP_EXTENSIONS,
21
- SKIP_FILENAMES,
22
- TEST_FILE_PATTERNS,
23
- MAX_FILE_SIZE
24
- } from '../utils/patterns.js';
25
- import { isHighEntropyMatch } from '../utils/entropy.js';
26
- import fg from 'fast-glob';
27
- import * as output from '../utils/output.js';
28
-
29
- // =============================================================================
30
- // MAIN COMMAND
31
- // =============================================================================
32
-
33
- export async function fixCommand(options = {}) {
34
- const cwd = process.cwd();
35
-
36
- const spinner = ora({ text: 'Scanning for secrets...', color: 'cyan' }).start();
37
-
38
- try {
39
- const files = await findFiles(cwd);
40
- const results = [];
41
-
42
- for (const file of files) {
43
- const findings = await scanFile(file);
44
- if (findings.length > 0) {
45
- results.push({ file, findings });
46
- }
47
- }
48
-
49
- spinner.stop();
50
-
51
- if (results.length === 0) {
52
- output.success('No secrets found — nothing to fix!');
53
- console.log(chalk.gray('\nYour codebase looks clean. Keep it that way with:'));
54
- console.log(chalk.gray(' npx ship-safe guard # Block pushes if secrets are found'));
55
- return;
56
- }
57
-
58
- // Build env var suggestions from findings
59
- const envVars = buildEnvVarSuggestions(results);
60
-
61
- output.header('Fix Report');
62
- printFindings(results, cwd);
63
- printEnvExample(envVars, options.dryRun);
64
-
65
- } catch (err) {
66
- spinner.fail('Fix scan failed');
67
- output.error(err.message);
68
- process.exit(1);
69
- }
70
- }
71
-
72
- // =============================================================================
73
- // SCAN (same logic as scan command, reused here)
74
- // =============================================================================
75
-
76
- async function findFiles(rootPath) {
77
- const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
78
- const files = await fg('**/*', {
79
- cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true
80
- });
81
-
82
- const filtered = [];
83
- for (const file of files) {
84
- const ext = path.extname(file).toLowerCase();
85
- if (SKIP_EXTENSIONS.has(ext)) continue;
86
- if (SKIP_FILENAMES.has(path.basename(file))) continue;
87
- const basename = path.basename(file);
88
- if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) continue;
89
- if (TEST_FILE_PATTERNS.some(p => p.test(file))) continue;
90
- if (basename === '.env.example') continue; // Don't scan example files
91
- try {
92
- const stats = fs.statSync(file);
93
- if (stats.size > MAX_FILE_SIZE) continue;
94
- } catch { continue; }
95
- filtered.push(file);
96
- }
97
- return filtered;
98
- }
99
-
100
- async function scanFile(filePath) {
101
- const findings = [];
102
- try {
103
- const content = fs.readFileSync(filePath, 'utf-8');
104
- const lines = content.split('\n');
105
-
106
- for (let lineNum = 0; lineNum < lines.length; lineNum++) {
107
- const line = lines[lineNum];
108
- if (/ship-safe-ignore/i.test(line)) continue;
109
-
110
- for (const pattern of SECRET_PATTERNS) {
111
- pattern.pattern.lastIndex = 0;
112
- let match;
113
- while ((match = pattern.pattern.exec(line)) !== null) {
114
- if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
115
- findings.push({
116
- line: lineNum + 1,
117
- matched: match[0],
118
- patternName: pattern.name,
119
- severity: pattern.severity,
120
- });
121
- }
122
- }
123
- }
124
- } catch {}
125
- return findings;
126
- }
127
-
128
- // =============================================================================
129
- // ENV VAR GENERATION
130
- // =============================================================================
131
-
132
- function buildEnvVarSuggestions(results) {
133
- const seen = new Set();
134
- const vars = [];
135
-
136
- for (const { findings } of results) {
137
- for (const f of findings) {
138
- const varName = patternToEnvVar(f.patternName);
139
- if (!seen.has(varName)) {
140
- seen.add(varName);
141
- vars.push({ name: varName, comment: f.patternName });
142
- }
143
- }
144
- }
145
-
146
- return vars;
147
- }
148
-
149
- /**
150
- * Convert a pattern name to a sensible env var name.
151
- * e.g. "OpenAI API Key" → "OPENAI_API_KEY" // ship-safe-ignore — env var name in doc comment, not a secret value
152
- */
153
- function patternToEnvVar(patternName) {
154
- return patternName
155
- .toUpperCase()
156
- .replace(/[^A-Z0-9\s]/g, '')
157
- .trim()
158
- .replace(/\s+/g, '_');
159
- }
160
-
161
- // =============================================================================
162
- // OUTPUT
163
- // =============================================================================
164
-
165
- function printFindings(results, rootPath) {
166
- const total = results.reduce((sum, r) => sum + r.findings.length, 0);
167
- console.log(chalk.red.bold(`\n Found ${total} secret(s) across ${results.length} file(s)\n`));
168
-
169
- for (const { file, findings } of results) {
170
- const relPath = path.relative(rootPath, file);
171
- console.log(chalk.white.bold(` ${relPath}`));
172
- for (const f of findings) {
173
- console.log(chalk.gray(` Line ${f.line}: `) + chalk.yellow(f.patternName));
174
- }
175
- }
176
- }
177
-
178
- function printEnvExample(envVars, dryRun) {
179
- const lines = [
180
- '# .env.example',
181
- '# Generated by ship-safe — replace placeholder values with your actual secrets.',
182
- '# Copy this file to .env and fill in the values.',
183
- '# NEVER commit .env — only commit .env.example',
184
- '',
185
- ];
186
-
187
- for (const { name, comment } of envVars) {
188
- lines.push(`# ${comment}`);
189
- lines.push(`${name}=your_${name.toLowerCase()}_here`);
190
- lines.push('');
191
- }
192
-
193
- const content = lines.join('\n');
194
-
195
- output.header(dryRun ? '.env.example Preview (dry run)' : 'Generated .env.example');
196
- console.log();
197
- console.log(chalk.gray(content));
198
-
199
- if (!dryRun) {
200
- const envExamplePath = path.join(process.cwd(), '.env.example');
201
-
202
- if (fs.existsSync(envExamplePath)) {
203
- output.warning('.env.example already exists — skipping. Use --force to overwrite.');
204
- } else {
205
- fs.writeFileSync(envExamplePath, content);
206
- output.success('Created .env.example');
207
- }
208
-
209
- console.log();
210
- console.log(chalk.cyan.bold('Next steps:'));
211
- console.log(chalk.white('1.') + chalk.gray(' Copy .env.example to .env'));
212
- console.log(chalk.white('2.') + chalk.gray(' Replace placeholder values with your real secrets'));
213
- console.log(chalk.white('3.') + chalk.gray(' Remove the hardcoded values from your source code'));
214
- console.log(chalk.white('4.') + chalk.gray(' Verify .env is in your .gitignore'));
215
- console.log(chalk.white('5.') + chalk.gray(' Run npx ship-safe scan . to confirm clean'));
216
- console.log();
217
- }
218
- }
1
+ /**
2
+ * Fix Command
3
+ * ===========
4
+ *
5
+ * Scans for secrets and generates a .env.example file with placeholder values.
6
+ * Also shows a summary of what to move to environment variables.
7
+ *
8
+ * USAGE:
9
+ * ship-safe fix Scan and generate .env.example
10
+ * ship-safe fix --dry-run Preview what would be generated (don't write file)
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import ora from 'ora';
16
+ import chalk from 'chalk';
17
+ import {
18
+ SECRET_PATTERNS,
19
+ SKIP_DIRS,
20
+ SKIP_EXTENSIONS,
21
+ SKIP_FILENAMES,
22
+ TEST_FILE_PATTERNS,
23
+ MAX_FILE_SIZE
24
+ } from '../utils/patterns.js';
25
+ import { isHighEntropyMatch } from '../utils/entropy.js';
26
+ import fg from 'fast-glob';
27
+ import * as output from '../utils/output.js';
28
+
29
+ // =============================================================================
30
+ // MAIN COMMAND
31
+ // =============================================================================
32
+
33
+ export async function fixCommand(options = {}) {
34
+ const cwd = process.cwd();
35
+
36
+ const spinner = ora({ text: 'Scanning for secrets...', color: 'cyan' }).start();
37
+
38
+ try {
39
+ const files = await findFiles(cwd);
40
+ const results = [];
41
+
42
+ for (const file of files) {
43
+ const findings = await scanFile(file);
44
+ if (findings.length > 0) {
45
+ results.push({ file, findings });
46
+ }
47
+ }
48
+
49
+ spinner.stop();
50
+
51
+ if (results.length === 0) {
52
+ output.success('No secrets found — nothing to fix!');
53
+ console.log(chalk.gray('\nYour codebase looks clean. Keep it that way with:'));
54
+ console.log(chalk.gray(' npx ship-safe guard # Block pushes if secrets are found'));
55
+ return;
56
+ }
57
+
58
+ // Build env var suggestions from findings
59
+ const envVars = buildEnvVarSuggestions(results);
60
+
61
+ output.header('Fix Report');
62
+ printFindings(results, cwd);
63
+ printEnvExample(envVars, options.dryRun);
64
+
65
+ } catch (err) {
66
+ spinner.fail('Fix scan failed');
67
+ output.error(err.message);
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+ // =============================================================================
73
+ // SCAN (same logic as scan command, reused here)
74
+ // =============================================================================
75
+
76
+ async function findFiles(rootPath) {
77
+ const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
78
+ const files = await fg('**/*', {
79
+ cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true
80
+ });
81
+
82
+ const filtered = [];
83
+ for (const file of files) {
84
+ const ext = path.extname(file).toLowerCase();
85
+ if (SKIP_EXTENSIONS.has(ext)) continue;
86
+ if (SKIP_FILENAMES.has(path.basename(file))) continue;
87
+ const basename = path.basename(file);
88
+ if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) continue;
89
+ if (TEST_FILE_PATTERNS.some(p => p.test(file))) continue;
90
+ if (basename === '.env.example') continue; // Don't scan example files
91
+ try {
92
+ const stats = fs.statSync(file);
93
+ if (stats.size > MAX_FILE_SIZE) continue;
94
+ } catch { continue; }
95
+ filtered.push(file);
96
+ }
97
+ return filtered;
98
+ }
99
+
100
+ async function scanFile(filePath) {
101
+ const findings = [];
102
+ try {
103
+ const content = fs.readFileSync(filePath, 'utf-8');
104
+ const lines = content.split('\n');
105
+
106
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
107
+ const line = lines[lineNum];
108
+ if (/ship-safe-ignore/i.test(line)) continue;
109
+
110
+ for (const pattern of SECRET_PATTERNS) {
111
+ pattern.pattern.lastIndex = 0;
112
+ let match;
113
+ while ((match = pattern.pattern.exec(line)) !== null) {
114
+ if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
115
+ findings.push({
116
+ line: lineNum + 1,
117
+ matched: match[0],
118
+ patternName: pattern.name,
119
+ severity: pattern.severity,
120
+ });
121
+ }
122
+ }
123
+ }
124
+ } catch {}
125
+ return findings;
126
+ }
127
+
128
+ // =============================================================================
129
+ // ENV VAR GENERATION
130
+ // =============================================================================
131
+
132
+ function buildEnvVarSuggestions(results) {
133
+ const seen = new Set();
134
+ const vars = [];
135
+
136
+ for (const { findings } of results) {
137
+ for (const f of findings) {
138
+ const varName = patternToEnvVar(f.patternName);
139
+ if (!seen.has(varName)) {
140
+ seen.add(varName);
141
+ vars.push({ name: varName, comment: f.patternName });
142
+ }
143
+ }
144
+ }
145
+
146
+ return vars;
147
+ }
148
+
149
+ /**
150
+ * Convert a pattern name to a sensible env var name.
151
+ * e.g. "OpenAI API Key" → "OPENAI_API_KEY" // ship-safe-ignore — env var name in doc comment, not a secret value
152
+ */
153
+ function patternToEnvVar(patternName) {
154
+ return patternName
155
+ .toUpperCase()
156
+ .replace(/[^A-Z0-9\s]/g, '')
157
+ .trim()
158
+ .replace(/\s+/g, '_');
159
+ }
160
+
161
+ // =============================================================================
162
+ // OUTPUT
163
+ // =============================================================================
164
+
165
+ function printFindings(results, rootPath) {
166
+ const total = results.reduce((sum, r) => sum + r.findings.length, 0);
167
+ console.log(chalk.red.bold(`\n Found ${total} secret(s) across ${results.length} file(s)\n`));
168
+
169
+ for (const { file, findings } of results) {
170
+ const relPath = path.relative(rootPath, file);
171
+ console.log(chalk.white.bold(` ${relPath}`));
172
+ for (const f of findings) {
173
+ console.log(chalk.gray(` Line ${f.line}: `) + chalk.yellow(f.patternName));
174
+ }
175
+ }
176
+ }
177
+
178
+ function printEnvExample(envVars, dryRun) {
179
+ const lines = [
180
+ '# .env.example',
181
+ '# Generated by ship-safe — replace placeholder values with your actual secrets.',
182
+ '# Copy this file to .env and fill in the values.',
183
+ '# NEVER commit .env — only commit .env.example',
184
+ '',
185
+ ];
186
+
187
+ for (const { name, comment } of envVars) {
188
+ lines.push(`# ${comment}`);
189
+ lines.push(`${name}=your_${name.toLowerCase()}_here`);
190
+ lines.push('');
191
+ }
192
+
193
+ const content = lines.join('\n');
194
+
195
+ output.header(dryRun ? '.env.example Preview (dry run)' : 'Generated .env.example');
196
+ console.log();
197
+ console.log(chalk.gray(content));
198
+
199
+ if (!dryRun) {
200
+ const envExamplePath = path.join(process.cwd(), '.env.example');
201
+
202
+ if (fs.existsSync(envExamplePath)) {
203
+ output.warning('.env.example already exists — skipping. Use --force to overwrite.');
204
+ } else {
205
+ fs.writeFileSync(envExamplePath, content);
206
+ output.success('Created .env.example');
207
+ }
208
+
209
+ console.log();
210
+ console.log(chalk.cyan.bold('Next steps:'));
211
+ console.log(chalk.white('1.') + chalk.gray(' Copy .env.example to .env'));
212
+ console.log(chalk.white('2.') + chalk.gray(' Replace placeholder values with your real secrets'));
213
+ console.log(chalk.white('3.') + chalk.gray(' Remove the hardcoded values from your source code'));
214
+ console.log(chalk.white('4.') + chalk.gray(' Verify .env is in your .gitignore'));
215
+ console.log(chalk.white('5.') + chalk.gray(' Run npx ship-safe scan . to confirm clean'));
216
+ console.log();
217
+ }
218
+ }