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,407 +1,407 @@
1
- /**
2
- * Init Command
3
- * ============
4
- *
5
- * Initialize security configurations in the current project.
6
- * Copies pre-configured security files from ship-safe.
7
- *
8
- * USAGE:
9
- * ship-safe init Copy all security configs
10
- * ship-safe init --gitignore Only copy .gitignore
11
- * ship-safe init --headers Only copy security headers
12
- * ship-safe init -f Force overwrite existing files
13
- *
14
- * FILES COPIED:
15
- * - .gitignore (merged with existing if present)
16
- * - nextjs-security-headers.js (for Next.js projects)
17
- */
18
-
19
- import fs from 'fs';
20
- import path from 'path';
21
- import { fileURLToPath } from 'url';
22
- import chalk from 'chalk';
23
- import * as output from '../utils/output.js';
24
-
25
- // Get the directory of this module (for finding config files)
26
- const __filename = fileURLToPath(import.meta.url); // ship-safe-ignore — module's own path via import.meta.url, not user input
27
- const __dirname = path.dirname(__filename);
28
- const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
29
-
30
- // =============================================================================
31
- // MAIN INIT FUNCTION
32
- // =============================================================================
33
-
34
- export async function initCommand(options = {}) {
35
- const targetDir = process.cwd();
36
-
37
- console.log();
38
- output.header('Initializing Security Configs');
39
- console.log();
40
- console.log(chalk.gray(`Target directory: ${targetDir}`));
41
- console.log();
42
-
43
- const results = {
44
- copied: [],
45
- skipped: [],
46
- merged: [],
47
- errors: []
48
- };
49
-
50
- // Determine which files to copy.
51
- // If a specific flag is set, only run that category.
52
- // With no flags, run everything.
53
- // Handle --openclaw flag separately
54
- if (options.openclaw) {
55
- return handleOpenClawInit(targetDir, options.force, results);
56
- }
57
-
58
- const hasSpecificFlag = options.gitignore || options.headers || options.agents;
59
- const copyGitignore = hasSpecificFlag ? !!options.gitignore : true;
60
- const copyHeaders = hasSpecificFlag ? !!options.headers : true;
61
- const copyAgents = hasSpecificFlag ? !!options.agents : true;
62
-
63
- // Copy .gitignore
64
- if (copyGitignore) {
65
- await handleGitignore(targetDir, options.force, results);
66
- }
67
-
68
- // Copy security headers
69
- if (copyHeaders) {
70
- await handleSecurityHeaders(targetDir, options.force, results);
71
- }
72
-
73
- // Append security rules to AI agent instruction files
74
- if (copyAgents) {
75
- await handleAgentFiles(targetDir, options.force, results);
76
- }
77
-
78
- // Print summary
79
- printSummary(results);
80
- }
81
-
82
- // =============================================================================
83
- // GITIGNORE HANDLING
84
- // =============================================================================
85
-
86
- async function handleGitignore(targetDir, force, results) {
87
- // Note: We use 'gitignore-template' because npm excludes dotfiles from packages
88
- const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'gitignore-template');
89
- const targetPath = path.join(targetDir, '.gitignore');
90
-
91
- // Check if source exists
92
- if (!fs.existsSync(sourcePath)) {
93
- results.errors.push({
94
- file: '.gitignore',
95
- error: 'Source file not found in ship-safe package'
96
- });
97
- return;
98
- }
99
-
100
- const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
101
-
102
- // Check if target exists
103
- if (fs.existsSync(targetPath)) {
104
- if (force) {
105
- // Overwrite
106
- fs.writeFileSync(targetPath, sourceContent);
107
- results.copied.push('.gitignore (overwritten)');
108
- } else {
109
- // Merge: append ship-safe patterns to existing
110
- const existingContent = fs.readFileSync(targetPath, 'utf-8');
111
-
112
- // Check if already has ship-safe content
113
- if (existingContent.includes('# SHIP SAFE')) {
114
- results.skipped.push('.gitignore (already contains ship-safe patterns)');
115
- return;
116
- }
117
-
118
- // Append ship-safe section
119
- const mergedContent = existingContent.trim() + '\n\n' +
120
- '# =============================================================================\n' +
121
- '# SHIP SAFE ADDITIONS\n' +
122
- '# Added by: npx ship-safe init\n' +
123
- '# =============================================================================\n\n' +
124
- extractSecurityPatterns(sourceContent);
125
-
126
- fs.writeFileSync(targetPath, mergedContent);
127
- results.merged.push('.gitignore');
128
- }
129
- } else {
130
- // Create new
131
- fs.writeFileSync(targetPath, sourceContent);
132
- results.copied.push('.gitignore');
133
- }
134
- }
135
-
136
- /**
137
- * Extract the most important security patterns from our .gitignore
138
- */
139
- function extractSecurityPatterns(fullGitignore) {
140
- // Extract key sections
141
- const patterns = `
142
- # Environment files
143
- .env
144
- .env.local
145
- .env*.local
146
- *.env
147
-
148
- # Private keys & certificates
149
- *.pem
150
- *.key
151
- *.p12
152
- *.pfx
153
-
154
- # Credentials
155
- *credentials*
156
- *.secrets.json
157
- secrets.yml
158
- secrets.yaml
159
-
160
- # Service accounts
161
- **/service-account*.json
162
- *-firebase-adminsdk-*.json
163
-
164
- # AWS
165
- .aws/credentials
166
-
167
- # Database files
168
- *.sqlite
169
- *.sqlite3
170
- *.db
171
-
172
- # Logs (may contain sensitive data)
173
- *.log
174
- logs/
175
- `;
176
-
177
- return patterns.trim();
178
- }
179
-
180
- // =============================================================================
181
- // SECURITY HEADERS HANDLING
182
- // =============================================================================
183
-
184
- async function handleSecurityHeaders(targetDir, force, results) {
185
- const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'nextjs-security-headers.js');
186
- const targetPath = path.join(targetDir, 'security-headers.config.js');
187
-
188
- // Check if source exists
189
- if (!fs.existsSync(sourcePath)) {
190
- results.errors.push({
191
- file: 'security-headers.config.js',
192
- error: 'Source file not found in ship-safe package'
193
- });
194
- return;
195
- }
196
-
197
- // Detect if this is a Next.js project
198
- const packageJsonPath = path.join(targetDir, 'package.json');
199
- let isNextProject = false;
200
-
201
- if (fs.existsSync(packageJsonPath)) {
202
- try {
203
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
204
- isNextProject = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
205
- } catch {
206
- // Ignore parse errors
207
- }
208
- }
209
-
210
- // Check if target exists
211
- if (fs.existsSync(targetPath) && !force) {
212
- results.skipped.push('security-headers.config.js (already exists, use -f to overwrite)');
213
- return;
214
- }
215
-
216
- // Copy the file
217
- const content = fs.readFileSync(sourcePath, 'utf-8');
218
- fs.writeFileSync(targetPath, content);
219
- results.copied.push('security-headers.config.js');
220
-
221
- // Show integration instructions
222
- if (isNextProject) {
223
- console.log(chalk.cyan('\nNext.js detected! Add to your next.config.js:\n'));
224
- console.log(chalk.gray(' const { securityHeadersConfig } = require(\'./security-headers.config.js\');'));
225
- console.log(chalk.gray(' module.exports = { ...securityHeadersConfig, /* your config */ };'));
226
- console.log();
227
- }
228
- }
229
-
230
- // =============================================================================
231
- // AGENT FILE HANDLING
232
- // =============================================================================
233
-
234
- // Marker used for idempotency — if this string is present we already added our section.
235
- const AGENT_MARKER = '<!-- ship-safe -->';
236
-
237
- // The security rules block appended to existing agent instruction files.
238
- const AGENT_SECTION = `
239
- ## Security (ship-safe)
240
- ${AGENT_MARKER}
241
- - Always run \`npx ship-safe scan .\` before committing code
242
- - Never hardcode API keys, tokens, or credentials in source files — use environment variables
243
- - If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
244
- `;
245
-
246
- // Standalone content for a new Cursor rules file.
247
- const CURSOR_RULE_CONTENT = `---
248
- description: Prevent API key leaks and credential exposure in generated code
249
- alwaysApply: true
250
- ---
251
-
252
- - Always run \`npx ship-safe scan .\` before committing code
253
- - Never hardcode API keys, tokens, or credentials in source files — use environment variables
254
- - If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
255
- `;
256
-
257
- async function handleAgentFiles(targetDir, force, results) {
258
- // Files where we append a section if they already exist, or create if they don't.
259
- const appendTargets = [
260
- { file: 'CLAUDE.md', label: 'CLAUDE.md' },
261
- { file: '.windsurfrules', label: '.windsurfrules' },
262
- { file: path.join('.github', 'copilot-instructions.md'), label: '.github/copilot-instructions.md' },
263
- ];
264
-
265
- for (const { file, label } of appendTargets) {
266
- const targetPath = path.join(targetDir, file);
267
-
268
- if (fs.existsSync(targetPath)) {
269
- const existing = fs.readFileSync(targetPath, 'utf-8');
270
- if (existing.includes(AGENT_MARKER)) {
271
- results.skipped.push(`${label} (already contains ship-safe rules)`);
272
- continue;
273
- }
274
- if (force || true) { // always append unless already present
275
- fs.writeFileSync(targetPath, existing.trimEnd() + '\n' + AGENT_SECTION);
276
- results.merged.push(label);
277
- }
278
- } else {
279
- // Ensure parent directory exists (e.g. .github/)
280
- const parentDir = path.dirname(targetPath);
281
- if (!fs.existsSync(parentDir)) {
282
- fs.mkdirSync(parentDir, { recursive: true });
283
- }
284
- fs.writeFileSync(targetPath, AGENT_SECTION.trim() + '\n');
285
- results.copied.push(label);
286
- }
287
- }
288
-
289
- // Cursor rules — dedicated file, no merging needed.
290
- const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
291
- const cursorRulesFile = path.join(cursorRulesDir, 'ship-safe.mdc');
292
-
293
- if (fs.existsSync(cursorRulesFile) && !force) {
294
- results.skipped.push('.cursor/rules/ship-safe.mdc (already exists, use -f to overwrite)');
295
- } else {
296
- if (!fs.existsSync(cursorRulesDir)) {
297
- fs.mkdirSync(cursorRulesDir, { recursive: true });
298
- }
299
- fs.writeFileSync(cursorRulesFile, CURSOR_RULE_CONTENT.trim() + '\n');
300
- results.copied.push('.cursor/rules/ship-safe.mdc');
301
- }
302
- }
303
-
304
- // =============================================================================
305
- // OPENCLAW HARDENED CONFIG
306
- // =============================================================================
307
-
308
- const HARDENED_OPENCLAW = `{
309
- "// SECURITY": "Generated by ship-safe init --openclaw — hardened defaults",
310
-
311
- "// host": "Bind to localhost only — never 0.0.0.0 (CVE-2026-25253 ClawJacked)",
312
- "host": "127.0.0.1",
313
- "port": 3100,
314
-
315
- "// auth": "Always require authentication — prevents unauthorized agent takeover",
316
- "auth": {
317
- "type": "apiKey",
318
- "key": "\${OPENCLAW_API_KEY}"
319
- },
320
-
321
- "// url": "Use wss:// for all non-localhost connections (encrypted WebSocket)",
322
- "url": "wss://localhost:3100",
323
-
324
- "// safeBins": "Allowlist of binaries the agent can execute — block everything else",
325
- "safeBins": ["node", "git", "npx", "npm"],
326
-
327
- "// skills": "Only add verified skills from trusted sources — ClawHavoc had 1,184 malicious skills",
328
- "skills": [],
329
-
330
- "// logging": "Enable audit logging for security monitoring",
331
- "logging": {
332
- "level": "info",
333
- "auditLog": true
334
- }
335
- }
336
- `;
337
-
338
- async function handleOpenClawInit(targetDir, force, results) {
339
- const targetPath = path.join(targetDir, 'openclaw.json');
340
-
341
- if (fs.existsSync(targetPath) && !force) {
342
- results.skipped.push('openclaw.json (already exists, use -f to overwrite)');
343
- } else {
344
- fs.writeFileSync(targetPath, HARDENED_OPENCLAW.trim() + '\n');
345
- results.copied.push('openclaw.json (hardened template)');
346
- }
347
-
348
- printSummary(results);
349
-
350
- console.log(chalk.cyan('Important:'));
351
- console.log(chalk.white(' 1.') + ' Set the OPENCLAW_API_KEY environment variable');
352
- console.log(chalk.white(' 2.') + ' Only add verified skills from trusted sources');
353
- console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe openclaw .') + ' to verify security');
354
- console.log();
355
- }
356
-
357
- // =============================================================================
358
- // SUMMARY
359
- // =============================================================================
360
-
361
- function printSummary(results) {
362
- console.log();
363
- console.log(chalk.cyan('='.repeat(60)));
364
- console.log(chalk.cyan.bold(' Summary'));
365
- console.log(chalk.cyan('='.repeat(60)));
366
- console.log();
367
-
368
- if (results.copied.length > 0) {
369
- console.log(chalk.green.bold('Created:'));
370
- for (const file of results.copied) {
371
- console.log(chalk.green(` \u2714 ${file}`));
372
- }
373
- console.log();
374
- }
375
-
376
- if (results.merged.length > 0) {
377
- console.log(chalk.blue.bold('Merged:'));
378
- for (const file of results.merged) {
379
- console.log(chalk.blue(` \u2194 ${file} (appended ship-safe patterns)`));
380
- }
381
- console.log();
382
- }
383
-
384
- if (results.skipped.length > 0) {
385
- console.log(chalk.yellow.bold('Skipped:'));
386
- for (const file of results.skipped) {
387
- console.log(chalk.yellow(` \u2192 ${file}`));
388
- }
389
- console.log();
390
- }
391
-
392
- if (results.errors.length > 0) {
393
- console.log(chalk.red.bold('Errors:'));
394
- for (const { file, error } of results.errors) {
395
- console.log(chalk.red(` \u2718 ${file}: ${error}`));
396
- }
397
- console.log();
398
- }
399
-
400
- // Next steps
401
- console.log(chalk.cyan('Next steps:'));
402
- console.log(chalk.white(' 1.') + ' Review the copied files and customize for your project');
403
- console.log(chalk.white(' 2.') + ' Run ' + chalk.cyan('npx ship-safe scan .') + ' to check for secrets');
404
- console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe checklist') + ' before launching');
405
- console.log();
406
- console.log(chalk.cyan('='.repeat(60)));
407
- }
1
+ /**
2
+ * Init Command
3
+ * ============
4
+ *
5
+ * Initialize security configurations in the current project.
6
+ * Copies pre-configured security files from ship-safe.
7
+ *
8
+ * USAGE:
9
+ * ship-safe init Copy all security configs
10
+ * ship-safe init --gitignore Only copy .gitignore
11
+ * ship-safe init --headers Only copy security headers
12
+ * ship-safe init -f Force overwrite existing files
13
+ *
14
+ * FILES COPIED:
15
+ * - .gitignore (merged with existing if present)
16
+ * - nextjs-security-headers.js (for Next.js projects)
17
+ */
18
+
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import { fileURLToPath } from 'url';
22
+ import chalk from 'chalk';
23
+ import * as output from '../utils/output.js';
24
+
25
+ // Get the directory of this module (for finding config files)
26
+ const __filename = fileURLToPath(import.meta.url); // ship-safe-ignore — module's own path via import.meta.url, not user input
27
+ const __dirname = path.dirname(__filename);
28
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
29
+
30
+ // =============================================================================
31
+ // MAIN INIT FUNCTION
32
+ // =============================================================================
33
+
34
+ export async function initCommand(options = {}) {
35
+ const targetDir = process.cwd();
36
+
37
+ console.log();
38
+ output.header('Initializing Security Configs');
39
+ console.log();
40
+ console.log(chalk.gray(`Target directory: ${targetDir}`));
41
+ console.log();
42
+
43
+ const results = {
44
+ copied: [],
45
+ skipped: [],
46
+ merged: [],
47
+ errors: []
48
+ };
49
+
50
+ // Determine which files to copy.
51
+ // If a specific flag is set, only run that category.
52
+ // With no flags, run everything.
53
+ // Handle --openclaw flag separately
54
+ if (options.openclaw) {
55
+ return handleOpenClawInit(targetDir, options.force, results);
56
+ }
57
+
58
+ const hasSpecificFlag = options.gitignore || options.headers || options.agents;
59
+ const copyGitignore = hasSpecificFlag ? !!options.gitignore : true;
60
+ const copyHeaders = hasSpecificFlag ? !!options.headers : true;
61
+ const copyAgents = hasSpecificFlag ? !!options.agents : true;
62
+
63
+ // Copy .gitignore
64
+ if (copyGitignore) {
65
+ await handleGitignore(targetDir, options.force, results);
66
+ }
67
+
68
+ // Copy security headers
69
+ if (copyHeaders) {
70
+ await handleSecurityHeaders(targetDir, options.force, results);
71
+ }
72
+
73
+ // Append security rules to AI agent instruction files
74
+ if (copyAgents) {
75
+ await handleAgentFiles(targetDir, options.force, results);
76
+ }
77
+
78
+ // Print summary
79
+ printSummary(results);
80
+ }
81
+
82
+ // =============================================================================
83
+ // GITIGNORE HANDLING
84
+ // =============================================================================
85
+
86
+ async function handleGitignore(targetDir, force, results) {
87
+ // Note: We use 'gitignore-template' because npm excludes dotfiles from packages
88
+ const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'gitignore-template');
89
+ const targetPath = path.join(targetDir, '.gitignore');
90
+
91
+ // Check if source exists
92
+ if (!fs.existsSync(sourcePath)) {
93
+ results.errors.push({
94
+ file: '.gitignore',
95
+ error: 'Source file not found in ship-safe package'
96
+ });
97
+ return;
98
+ }
99
+
100
+ const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
101
+
102
+ // Check if target exists
103
+ if (fs.existsSync(targetPath)) {
104
+ if (force) {
105
+ // Overwrite
106
+ fs.writeFileSync(targetPath, sourceContent);
107
+ results.copied.push('.gitignore (overwritten)');
108
+ } else {
109
+ // Merge: append ship-safe patterns to existing
110
+ const existingContent = fs.readFileSync(targetPath, 'utf-8');
111
+
112
+ // Check if already has ship-safe content
113
+ if (existingContent.includes('# SHIP SAFE')) {
114
+ results.skipped.push('.gitignore (already contains ship-safe patterns)');
115
+ return;
116
+ }
117
+
118
+ // Append ship-safe section
119
+ const mergedContent = existingContent.trim() + '\n\n' +
120
+ '# =============================================================================\n' +
121
+ '# SHIP SAFE ADDITIONS\n' +
122
+ '# Added by: npx ship-safe init\n' +
123
+ '# =============================================================================\n\n' +
124
+ extractSecurityPatterns(sourceContent);
125
+
126
+ fs.writeFileSync(targetPath, mergedContent);
127
+ results.merged.push('.gitignore');
128
+ }
129
+ } else {
130
+ // Create new
131
+ fs.writeFileSync(targetPath, sourceContent);
132
+ results.copied.push('.gitignore');
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Extract the most important security patterns from our .gitignore
138
+ */
139
+ function extractSecurityPatterns(fullGitignore) {
140
+ // Extract key sections
141
+ const patterns = `
142
+ # Environment files
143
+ .env
144
+ .env.local
145
+ .env*.local
146
+ *.env
147
+
148
+ # Private keys & certificates
149
+ *.pem
150
+ *.key
151
+ *.p12
152
+ *.pfx
153
+
154
+ # Credentials
155
+ *credentials*
156
+ *.secrets.json
157
+ secrets.yml
158
+ secrets.yaml
159
+
160
+ # Service accounts
161
+ **/service-account*.json
162
+ *-firebase-adminsdk-*.json
163
+
164
+ # AWS
165
+ .aws/credentials
166
+
167
+ # Database files
168
+ *.sqlite
169
+ *.sqlite3
170
+ *.db
171
+
172
+ # Logs (may contain sensitive data)
173
+ *.log
174
+ logs/
175
+ `;
176
+
177
+ return patterns.trim();
178
+ }
179
+
180
+ // =============================================================================
181
+ // SECURITY HEADERS HANDLING
182
+ // =============================================================================
183
+
184
+ async function handleSecurityHeaders(targetDir, force, results) {
185
+ const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'nextjs-security-headers.js');
186
+ const targetPath = path.join(targetDir, 'security-headers.config.js');
187
+
188
+ // Check if source exists
189
+ if (!fs.existsSync(sourcePath)) {
190
+ results.errors.push({
191
+ file: 'security-headers.config.js',
192
+ error: 'Source file not found in ship-safe package'
193
+ });
194
+ return;
195
+ }
196
+
197
+ // Detect if this is a Next.js project
198
+ const packageJsonPath = path.join(targetDir, 'package.json');
199
+ let isNextProject = false;
200
+
201
+ if (fs.existsSync(packageJsonPath)) {
202
+ try {
203
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
204
+ isNextProject = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
205
+ } catch {
206
+ // Ignore parse errors
207
+ }
208
+ }
209
+
210
+ // Check if target exists
211
+ if (fs.existsSync(targetPath) && !force) {
212
+ results.skipped.push('security-headers.config.js (already exists, use -f to overwrite)');
213
+ return;
214
+ }
215
+
216
+ // Copy the file
217
+ const content = fs.readFileSync(sourcePath, 'utf-8');
218
+ fs.writeFileSync(targetPath, content);
219
+ results.copied.push('security-headers.config.js');
220
+
221
+ // Show integration instructions
222
+ if (isNextProject) {
223
+ console.log(chalk.cyan('\nNext.js detected! Add to your next.config.js:\n'));
224
+ console.log(chalk.gray(' const { securityHeadersConfig } = require(\'./security-headers.config.js\');'));
225
+ console.log(chalk.gray(' module.exports = { ...securityHeadersConfig, /* your config */ };'));
226
+ console.log();
227
+ }
228
+ }
229
+
230
+ // =============================================================================
231
+ // AGENT FILE HANDLING
232
+ // =============================================================================
233
+
234
+ // Marker used for idempotency — if this string is present we already added our section.
235
+ const AGENT_MARKER = '<!-- ship-safe -->';
236
+
237
+ // The security rules block appended to existing agent instruction files.
238
+ const AGENT_SECTION = `
239
+ ## Security (ship-safe)
240
+ ${AGENT_MARKER}
241
+ - Always run \`npx ship-safe scan .\` before committing code
242
+ - Never hardcode API keys, tokens, or credentials in source files — use environment variables
243
+ - If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
244
+ `;
245
+
246
+ // Standalone content for a new Cursor rules file.
247
+ const CURSOR_RULE_CONTENT = `---
248
+ description: Prevent API key leaks and credential exposure in generated code
249
+ alwaysApply: true
250
+ ---
251
+
252
+ - Always run \`npx ship-safe scan .\` before committing code
253
+ - Never hardcode API keys, tokens, or credentials in source files — use environment variables
254
+ - If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
255
+ `;
256
+
257
+ async function handleAgentFiles(targetDir, force, results) {
258
+ // Files where we append a section if they already exist, or create if they don't.
259
+ const appendTargets = [
260
+ { file: 'CLAUDE.md', label: 'CLAUDE.md' },
261
+ { file: '.windsurfrules', label: '.windsurfrules' },
262
+ { file: path.join('.github', 'copilot-instructions.md'), label: '.github/copilot-instructions.md' },
263
+ ];
264
+
265
+ for (const { file, label } of appendTargets) {
266
+ const targetPath = path.join(targetDir, file);
267
+
268
+ if (fs.existsSync(targetPath)) {
269
+ const existing = fs.readFileSync(targetPath, 'utf-8');
270
+ if (existing.includes(AGENT_MARKER)) {
271
+ results.skipped.push(`${label} (already contains ship-safe rules)`);
272
+ continue;
273
+ }
274
+ if (force || true) { // always append unless already present
275
+ fs.writeFileSync(targetPath, existing.trimEnd() + '\n' + AGENT_SECTION);
276
+ results.merged.push(label);
277
+ }
278
+ } else {
279
+ // Ensure parent directory exists (e.g. .github/)
280
+ const parentDir = path.dirname(targetPath);
281
+ if (!fs.existsSync(parentDir)) {
282
+ fs.mkdirSync(parentDir, { recursive: true });
283
+ }
284
+ fs.writeFileSync(targetPath, AGENT_SECTION.trim() + '\n');
285
+ results.copied.push(label);
286
+ }
287
+ }
288
+
289
+ // Cursor rules — dedicated file, no merging needed.
290
+ const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
291
+ const cursorRulesFile = path.join(cursorRulesDir, 'ship-safe.mdc');
292
+
293
+ if (fs.existsSync(cursorRulesFile) && !force) {
294
+ results.skipped.push('.cursor/rules/ship-safe.mdc (already exists, use -f to overwrite)');
295
+ } else {
296
+ if (!fs.existsSync(cursorRulesDir)) {
297
+ fs.mkdirSync(cursorRulesDir, { recursive: true });
298
+ }
299
+ fs.writeFileSync(cursorRulesFile, CURSOR_RULE_CONTENT.trim() + '\n');
300
+ results.copied.push('.cursor/rules/ship-safe.mdc');
301
+ }
302
+ }
303
+
304
+ // =============================================================================
305
+ // OPENCLAW HARDENED CONFIG
306
+ // =============================================================================
307
+
308
+ const HARDENED_OPENCLAW = `{
309
+ "// SECURITY": "Generated by ship-safe init --openclaw — hardened defaults",
310
+
311
+ "// host": "Bind to localhost only — never 0.0.0.0 (CVE-2026-25253 ClawJacked)",
312
+ "host": "127.0.0.1",
313
+ "port": 3100,
314
+
315
+ "// auth": "Always require authentication — prevents unauthorized agent takeover",
316
+ "auth": {
317
+ "type": "apiKey",
318
+ "key": "\${OPENCLAW_API_KEY}"
319
+ },
320
+
321
+ "// url": "Use wss:// for all non-localhost connections (encrypted WebSocket)",
322
+ "url": "wss://localhost:3100",
323
+
324
+ "// safeBins": "Allowlist of binaries the agent can execute — block everything else",
325
+ "safeBins": ["node", "git", "npx", "npm"],
326
+
327
+ "// skills": "Only add verified skills from trusted sources — ClawHavoc had 1,184 malicious skills",
328
+ "skills": [],
329
+
330
+ "// logging": "Enable audit logging for security monitoring",
331
+ "logging": {
332
+ "level": "info",
333
+ "auditLog": true
334
+ }
335
+ }
336
+ `;
337
+
338
+ async function handleOpenClawInit(targetDir, force, results) {
339
+ const targetPath = path.join(targetDir, 'openclaw.json');
340
+
341
+ if (fs.existsSync(targetPath) && !force) {
342
+ results.skipped.push('openclaw.json (already exists, use -f to overwrite)');
343
+ } else {
344
+ fs.writeFileSync(targetPath, HARDENED_OPENCLAW.trim() + '\n');
345
+ results.copied.push('openclaw.json (hardened template)');
346
+ }
347
+
348
+ printSummary(results);
349
+
350
+ console.log(chalk.cyan('Important:'));
351
+ console.log(chalk.white(' 1.') + ' Set the OPENCLAW_API_KEY environment variable');
352
+ console.log(chalk.white(' 2.') + ' Only add verified skills from trusted sources');
353
+ console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe openclaw .') + ' to verify security');
354
+ console.log();
355
+ }
356
+
357
+ // =============================================================================
358
+ // SUMMARY
359
+ // =============================================================================
360
+
361
+ function printSummary(results) {
362
+ console.log();
363
+ console.log(chalk.cyan('='.repeat(60)));
364
+ console.log(chalk.cyan.bold(' Summary'));
365
+ console.log(chalk.cyan('='.repeat(60)));
366
+ console.log();
367
+
368
+ if (results.copied.length > 0) {
369
+ console.log(chalk.green.bold('Created:'));
370
+ for (const file of results.copied) {
371
+ console.log(chalk.green(` \u2714 ${file}`));
372
+ }
373
+ console.log();
374
+ }
375
+
376
+ if (results.merged.length > 0) {
377
+ console.log(chalk.blue.bold('Merged:'));
378
+ for (const file of results.merged) {
379
+ console.log(chalk.blue(` \u2194 ${file} (appended ship-safe patterns)`));
380
+ }
381
+ console.log();
382
+ }
383
+
384
+ if (results.skipped.length > 0) {
385
+ console.log(chalk.yellow.bold('Skipped:'));
386
+ for (const file of results.skipped) {
387
+ console.log(chalk.yellow(` \u2192 ${file}`));
388
+ }
389
+ console.log();
390
+ }
391
+
392
+ if (results.errors.length > 0) {
393
+ console.log(chalk.red.bold('Errors:'));
394
+ for (const { file, error } of results.errors) {
395
+ console.log(chalk.red(` \u2718 ${file}: ${error}`));
396
+ }
397
+ console.log();
398
+ }
399
+
400
+ // Next steps
401
+ console.log(chalk.cyan('Next steps:'));
402
+ console.log(chalk.white(' 1.') + ' Review the copied files and customize for your project');
403
+ console.log(chalk.white(' 2.') + ' Run ' + chalk.cyan('npx ship-safe scan .') + ' to check for secrets');
404
+ console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe checklist') + ' before launching');
405
+ console.log();
406
+ console.log(chalk.cyan('='.repeat(60)));
407
+ }