ship-safe 6.1.1 → 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 (47) hide show
  1. package/README.md +735 -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 +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 -980
  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 -569
  34. package/cli/commands/score.js +449 -449
  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 +230 -230
  44. package/cli/utils/patterns.js +1121 -1121
  45. package/cli/utils/pdf-generator.js +94 -94
  46. package/package.json +69 -69
  47. 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
+ }