ship-safe 1.0.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.
- package/README.md +201 -0
- package/ai-defense/system-prompt-armor.md +327 -0
- package/checklists/launch-day.md +168 -0
- package/cli/bin/ship-safe.js +97 -0
- package/cli/commands/checklist.js +223 -0
- package/cli/commands/init.js +265 -0
- package/cli/commands/scan.js +261 -0
- package/cli/index.js +12 -0
- package/cli/utils/output.js +177 -0
- package/cli/utils/patterns.js +265 -0
- package/configs/nextjs-security-headers.js +220 -0
- package/package.json +54 -0
- package/snippets/README.md +58 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ship Safe CLI
|
|
5
|
+
* =============
|
|
6
|
+
*
|
|
7
|
+
* Security toolkit for vibe coders and indie hackers.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* npx ship-safe scan [path] Scan for secrets in your codebase
|
|
11
|
+
* npx ship-safe checklist Run the launch-day security checklist
|
|
12
|
+
* npx ship-safe init Initialize security configs in your project
|
|
13
|
+
* npx ship-safe --help Show all commands
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { program } from 'commander';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { scanCommand } from '../commands/scan.js';
|
|
19
|
+
import { checklistCommand } from '../commands/checklist.js';
|
|
20
|
+
import { initCommand } from '../commands/init.js';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// CLI CONFIGURATION
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
const VERSION = '1.0.0';
|
|
27
|
+
|
|
28
|
+
// Banner shown on help
|
|
29
|
+
const banner = `
|
|
30
|
+
${chalk.cyan('███████╗██╗ ██╗██╗██████╗ ███████╗ █████╗ ███████╗███████╗')}
|
|
31
|
+
${chalk.cyan('██╔════╝██║ ██║██║██╔══██╗ ██╔════╝██╔══██╗██╔════╝██╔════╝')}
|
|
32
|
+
${chalk.cyan('███████╗███████║██║██████╔╝ ███████╗███████║█████╗ █████╗ ')}
|
|
33
|
+
${chalk.cyan('╚════██║██╔══██║██║██╔═══╝ ╚════██║██╔══██║██╔══╝ ██╔══╝ ')}
|
|
34
|
+
${chalk.cyan('███████║██║ ██║██║██║ ███████║██║ ██║██║ ███████╗')}
|
|
35
|
+
${chalk.cyan('╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝')}
|
|
36
|
+
|
|
37
|
+
${chalk.gray('Security toolkit for vibe coders. Secure your MVP in 5 minutes.')}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// PROGRAM SETUP
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.name('ship-safe')
|
|
46
|
+
.description('Security toolkit for vibe coders and indie hackers')
|
|
47
|
+
.version(VERSION)
|
|
48
|
+
.addHelpText('before', banner);
|
|
49
|
+
|
|
50
|
+
// -----------------------------------------------------------------------------
|
|
51
|
+
// SCAN COMMAND
|
|
52
|
+
// -----------------------------------------------------------------------------
|
|
53
|
+
program
|
|
54
|
+
.command('scan [path]')
|
|
55
|
+
.description('Scan your codebase for leaked secrets (API keys, passwords, etc.)')
|
|
56
|
+
.option('-v, --verbose', 'Show all files being scanned')
|
|
57
|
+
.option('--no-color', 'Disable colored output')
|
|
58
|
+
.option('--json', 'Output results as JSON (useful for CI)')
|
|
59
|
+
.action(scanCommand);
|
|
60
|
+
|
|
61
|
+
// -----------------------------------------------------------------------------
|
|
62
|
+
// CHECKLIST COMMAND
|
|
63
|
+
// -----------------------------------------------------------------------------
|
|
64
|
+
program
|
|
65
|
+
.command('checklist')
|
|
66
|
+
.description('Run through the launch-day security checklist interactively')
|
|
67
|
+
.option('--no-interactive', 'Print checklist without prompts')
|
|
68
|
+
.action(checklistCommand);
|
|
69
|
+
|
|
70
|
+
// -----------------------------------------------------------------------------
|
|
71
|
+
// INIT COMMAND
|
|
72
|
+
// -----------------------------------------------------------------------------
|
|
73
|
+
program
|
|
74
|
+
.command('init')
|
|
75
|
+
.description('Initialize security configs in your project')
|
|
76
|
+
.option('-f, --force', 'Overwrite existing files')
|
|
77
|
+
.option('--gitignore', 'Only copy .gitignore')
|
|
78
|
+
.option('--headers', 'Only copy security headers config')
|
|
79
|
+
.action(initCommand);
|
|
80
|
+
|
|
81
|
+
// -----------------------------------------------------------------------------
|
|
82
|
+
// PARSE AND RUN
|
|
83
|
+
// -----------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
// Show help if no command provided
|
|
86
|
+
if (process.argv.length === 2) {
|
|
87
|
+
console.log(banner);
|
|
88
|
+
console.log(chalk.yellow('\nQuick start:\n'));
|
|
89
|
+
console.log(chalk.white(' npx ship-safe scan . ') + chalk.gray('# Scan current directory for secrets'));
|
|
90
|
+
console.log(chalk.white(' npx ship-safe checklist ') + chalk.gray('# Run security checklist'));
|
|
91
|
+
console.log(chalk.white(' npx ship-safe init ') + chalk.gray('# Add security configs to your project'));
|
|
92
|
+
console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
|
|
93
|
+
console.log();
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
program.parse();
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checklist Command
|
|
3
|
+
* =================
|
|
4
|
+
*
|
|
5
|
+
* Interactive launch-day security checklist.
|
|
6
|
+
*
|
|
7
|
+
* USAGE:
|
|
8
|
+
* ship-safe checklist Interactive mode (prompts for each item)
|
|
9
|
+
* ship-safe checklist --no-interactive Print checklist without prompts
|
|
10
|
+
*
|
|
11
|
+
* This walks you through the 10-point security checklist before launch.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import readline from 'readline';
|
|
16
|
+
import * as output from '../utils/output.js';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// CHECKLIST ITEMS
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
const CHECKLIST_ITEMS = [
|
|
23
|
+
{
|
|
24
|
+
title: 'No exposed .git folder',
|
|
25
|
+
check: 'curl -I https://yoursite.com/.git/config (should return 404)',
|
|
26
|
+
risk: 'Attackers can download your entire codebase including commit history with secrets.',
|
|
27
|
+
fix: 'Configure web server to block .git access. Vercel/Netlify do this by default.'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: 'Debug mode disabled',
|
|
31
|
+
check: 'Verify NODE_ENV=production, DEBUG=false in your deployment.',
|
|
32
|
+
risk: 'Debug mode exposes stack traces, environment variables, and internal paths.',
|
|
33
|
+
fix: 'Set production environment variables in your hosting platform.'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: 'Database RLS/Security rules enabled',
|
|
37
|
+
check: 'Supabase: Check Policies tab. Firebase: Check Rules tab.',
|
|
38
|
+
risk: 'Without row-level security, any user can read/write any data.',
|
|
39
|
+
fix: 'Define explicit RLS policies for each table. Never use "allow all" rules.'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: 'No hardcoded API keys in frontend',
|
|
43
|
+
check: 'Run: npx ship-safe scan ./src',
|
|
44
|
+
risk: 'Anyone viewing source code can steal your API keys.',
|
|
45
|
+
fix: 'Move secrets to server-side environment variables. Use API routes to proxy.'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: 'HTTPS enforced',
|
|
49
|
+
check: 'Visit http://yoursite.com - should redirect to https://',
|
|
50
|
+
risk: 'HTTP traffic can be intercepted and modified (MITM attacks).',
|
|
51
|
+
fix: 'Enable "Force HTTPS" in your hosting platform settings.'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: 'Security headers configured',
|
|
55
|
+
check: 'Visit securityheaders.com and enter your URL.',
|
|
56
|
+
risk: 'Missing headers enable clickjacking, XSS, and data sniffing.',
|
|
57
|
+
fix: 'Use ship-safe init --headers to add security headers config.'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: 'Rate limiting on auth endpoints',
|
|
61
|
+
check: 'Try hitting /login 100 times quickly. Should block you.',
|
|
62
|
+
risk: 'Without rate limiting, attackers can brute-force passwords.',
|
|
63
|
+
fix: 'Add rate limiting middleware or use auth providers with built-in protection.'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: 'No sensitive data in URLs',
|
|
67
|
+
check: 'Search codebase for: ?token=, ?api_key=, ?password=',
|
|
68
|
+
risk: 'URLs are logged everywhere. Tokens in URLs get leaked.',
|
|
69
|
+
fix: 'Send sensitive data in headers or POST body, never in URLs.'
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
title: 'Error messages don\'t leak info',
|
|
73
|
+
check: 'Trigger errors intentionally. Check for stack traces in response.',
|
|
74
|
+
risk: 'Detailed errors help attackers understand your system.',
|
|
75
|
+
fix: 'Show generic errors to users. Log details server-side only.'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
title: 'Admin routes protected',
|
|
79
|
+
check: 'Try accessing /admin, /api/admin, /dashboard without auth.',
|
|
80
|
+
risk: 'Exposed admin panels are the #1 target for attackers.',
|
|
81
|
+
fix: 'Add auth middleware. Consider IP whitelisting for admin routes.'
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// MAIN CHECKLIST FUNCTION
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
export async function checklistCommand(options = {}) {
|
|
90
|
+
console.log();
|
|
91
|
+
console.log(chalk.cyan.bold('='.repeat(60)));
|
|
92
|
+
console.log(chalk.cyan.bold(' Launch Day Security Checklist'));
|
|
93
|
+
console.log(chalk.cyan.bold('='.repeat(60)));
|
|
94
|
+
console.log();
|
|
95
|
+
console.log(chalk.gray('Complete these 10 checks before going live.'));
|
|
96
|
+
console.log(chalk.gray('Each one takes under 1 minute to verify.'));
|
|
97
|
+
console.log();
|
|
98
|
+
|
|
99
|
+
if (options.interactive === false) {
|
|
100
|
+
// Non-interactive: just print the checklist
|
|
101
|
+
printChecklist();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Interactive mode
|
|
106
|
+
await runInteractiveChecklist();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// NON-INTERACTIVE MODE
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
function printChecklist() {
|
|
114
|
+
for (let i = 0; i < CHECKLIST_ITEMS.length; i++) {
|
|
115
|
+
const item = CHECKLIST_ITEMS[i];
|
|
116
|
+
const num = i + 1;
|
|
117
|
+
|
|
118
|
+
console.log(chalk.white.bold(`${num}. [ ] ${item.title}`));
|
|
119
|
+
console.log(chalk.gray(` Check: ${item.check}`));
|
|
120
|
+
console.log(chalk.yellow(` Risk: ${item.risk}`));
|
|
121
|
+
console.log(chalk.green(` Fix: ${item.fix}`));
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
126
|
+
console.log(chalk.gray('Copy this checklist or run with interactive mode:'));
|
|
127
|
+
console.log(chalk.white(' npx ship-safe checklist'));
|
|
128
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// INTERACTIVE MODE
|
|
133
|
+
// =============================================================================
|
|
134
|
+
|
|
135
|
+
async function runInteractiveChecklist() {
|
|
136
|
+
const rl = readline.createInterface({
|
|
137
|
+
input: process.stdin,
|
|
138
|
+
output: process.stdout
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const results = [];
|
|
142
|
+
|
|
143
|
+
console.log(chalk.gray('For each item, press:'));
|
|
144
|
+
console.log(chalk.green(' y') + chalk.gray(' = Done/Verified'));
|
|
145
|
+
console.log(chalk.yellow(' s') + chalk.gray(' = Skip for now'));
|
|
146
|
+
console.log(chalk.red(' n') + chalk.gray(' = Not done (will show fix)'));
|
|
147
|
+
console.log(chalk.gray(' q = Quit'));
|
|
148
|
+
console.log();
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < CHECKLIST_ITEMS.length; i++) {
|
|
151
|
+
const item = CHECKLIST_ITEMS[i];
|
|
152
|
+
const num = i + 1;
|
|
153
|
+
|
|
154
|
+
console.log(chalk.cyan('-'.repeat(60)));
|
|
155
|
+
console.log(chalk.white.bold(`\n${num}/${CHECKLIST_ITEMS.length}: ${item.title}\n`));
|
|
156
|
+
console.log(chalk.gray(`How to check: ${item.check}`));
|
|
157
|
+
console.log();
|
|
158
|
+
|
|
159
|
+
const answer = await askQuestion(rl, chalk.white('Status? [y/s/n/q]: '));
|
|
160
|
+
|
|
161
|
+
if (answer.toLowerCase() === 'q') {
|
|
162
|
+
console.log(chalk.yellow('\nChecklist paused. Run again to continue.'));
|
|
163
|
+
rl.close();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (answer.toLowerCase() === 'y') {
|
|
168
|
+
results.push({ item, status: 'done' });
|
|
169
|
+
console.log(chalk.green('\u2714 Marked as complete\n'));
|
|
170
|
+
} else if (answer.toLowerCase() === 's') {
|
|
171
|
+
results.push({ item, status: 'skipped' });
|
|
172
|
+
console.log(chalk.yellow('\u2192 Skipped\n'));
|
|
173
|
+
} else {
|
|
174
|
+
results.push({ item, status: 'todo' });
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(chalk.red('\u26a0 Risk: ') + item.risk);
|
|
177
|
+
console.log(chalk.green('\u2192 Fix: ') + item.fix);
|
|
178
|
+
console.log();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
rl.close();
|
|
183
|
+
|
|
184
|
+
// Print summary
|
|
185
|
+
printSummary(results);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function askQuestion(rl, question) {
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
rl.question(question, (answer) => {
|
|
191
|
+
resolve(answer);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function printSummary(results) {
|
|
197
|
+
const done = results.filter(r => r.status === 'done').length;
|
|
198
|
+
const skipped = results.filter(r => r.status === 'skipped').length;
|
|
199
|
+
const todo = results.filter(r => r.status === 'todo').length;
|
|
200
|
+
|
|
201
|
+
console.log();
|
|
202
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
203
|
+
console.log(chalk.cyan.bold(' Summary'));
|
|
204
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
205
|
+
console.log();
|
|
206
|
+
console.log(chalk.green(` \u2714 Completed: ${done}`));
|
|
207
|
+
console.log(chalk.yellow(` \u2192 Skipped: ${skipped}`));
|
|
208
|
+
console.log(chalk.red(` \u2718 Todo: ${todo}`));
|
|
209
|
+
console.log();
|
|
210
|
+
|
|
211
|
+
if (todo === 0 && skipped === 0) {
|
|
212
|
+
console.log(chalk.green.bold(' \ud83d\ude80 You\'re ready to ship safely!'));
|
|
213
|
+
} else if (todo > 0) {
|
|
214
|
+
console.log(chalk.yellow(' Items still need attention:'));
|
|
215
|
+
for (const r of results.filter(r => r.status === 'todo')) {
|
|
216
|
+
console.log(chalk.red(` \u2022 ${r.item.title}`));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(chalk.gray(' Tip: Security is ongoing. Schedule monthly reviews.'));
|
|
222
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
223
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
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);
|
|
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
|
+
const copyGitignore = !options.headers || options.gitignore;
|
|
52
|
+
const copyHeaders = !options.gitignore || options.headers;
|
|
53
|
+
|
|
54
|
+
// Copy .gitignore
|
|
55
|
+
if (copyGitignore) {
|
|
56
|
+
await handleGitignore(targetDir, options.force, results);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Copy security headers
|
|
60
|
+
if (copyHeaders) {
|
|
61
|
+
await handleSecurityHeaders(targetDir, options.force, results);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Print summary
|
|
65
|
+
printSummary(results);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// GITIGNORE HANDLING
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
async function handleGitignore(targetDir, force, results) {
|
|
73
|
+
const sourcePath = path.join(PACKAGE_ROOT, '.gitignore');
|
|
74
|
+
const targetPath = path.join(targetDir, '.gitignore');
|
|
75
|
+
|
|
76
|
+
// Check if source exists
|
|
77
|
+
if (!fs.existsSync(sourcePath)) {
|
|
78
|
+
results.errors.push({
|
|
79
|
+
file: '.gitignore',
|
|
80
|
+
error: 'Source file not found in ship-safe package'
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
|
|
86
|
+
|
|
87
|
+
// Check if target exists
|
|
88
|
+
if (fs.existsSync(targetPath)) {
|
|
89
|
+
if (force) {
|
|
90
|
+
// Overwrite
|
|
91
|
+
fs.writeFileSync(targetPath, sourceContent);
|
|
92
|
+
results.copied.push('.gitignore (overwritten)');
|
|
93
|
+
} else {
|
|
94
|
+
// Merge: append ship-safe patterns to existing
|
|
95
|
+
const existingContent = fs.readFileSync(targetPath, 'utf-8');
|
|
96
|
+
|
|
97
|
+
// Check if already has ship-safe content
|
|
98
|
+
if (existingContent.includes('# SHIP SAFE')) {
|
|
99
|
+
results.skipped.push('.gitignore (already contains ship-safe patterns)');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Append ship-safe section
|
|
104
|
+
const mergedContent = existingContent.trim() + '\n\n' +
|
|
105
|
+
'# =============================================================================\n' +
|
|
106
|
+
'# SHIP SAFE ADDITIONS\n' +
|
|
107
|
+
'# Added by: npx ship-safe init\n' +
|
|
108
|
+
'# =============================================================================\n\n' +
|
|
109
|
+
extractSecurityPatterns(sourceContent);
|
|
110
|
+
|
|
111
|
+
fs.writeFileSync(targetPath, mergedContent);
|
|
112
|
+
results.merged.push('.gitignore');
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// Create new
|
|
116
|
+
fs.writeFileSync(targetPath, sourceContent);
|
|
117
|
+
results.copied.push('.gitignore');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Extract the most important security patterns from our .gitignore
|
|
123
|
+
*/
|
|
124
|
+
function extractSecurityPatterns(fullGitignore) {
|
|
125
|
+
// Extract key sections
|
|
126
|
+
const patterns = `
|
|
127
|
+
# Environment files
|
|
128
|
+
.env
|
|
129
|
+
.env.local
|
|
130
|
+
.env*.local
|
|
131
|
+
*.env
|
|
132
|
+
|
|
133
|
+
# Private keys & certificates
|
|
134
|
+
*.pem
|
|
135
|
+
*.key
|
|
136
|
+
*.p12
|
|
137
|
+
*.pfx
|
|
138
|
+
|
|
139
|
+
# Credentials
|
|
140
|
+
*credentials*
|
|
141
|
+
*.secrets.json
|
|
142
|
+
secrets.yml
|
|
143
|
+
secrets.yaml
|
|
144
|
+
|
|
145
|
+
# Service accounts
|
|
146
|
+
**/service-account*.json
|
|
147
|
+
*-firebase-adminsdk-*.json
|
|
148
|
+
|
|
149
|
+
# AWS
|
|
150
|
+
.aws/credentials
|
|
151
|
+
|
|
152
|
+
# Database files
|
|
153
|
+
*.sqlite
|
|
154
|
+
*.sqlite3
|
|
155
|
+
*.db
|
|
156
|
+
|
|
157
|
+
# Logs (may contain sensitive data)
|
|
158
|
+
*.log
|
|
159
|
+
logs/
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
return patterns.trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// SECURITY HEADERS HANDLING
|
|
167
|
+
// =============================================================================
|
|
168
|
+
|
|
169
|
+
async function handleSecurityHeaders(targetDir, force, results) {
|
|
170
|
+
const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'nextjs-security-headers.js');
|
|
171
|
+
const targetPath = path.join(targetDir, 'security-headers.config.js');
|
|
172
|
+
|
|
173
|
+
// Check if source exists
|
|
174
|
+
if (!fs.existsSync(sourcePath)) {
|
|
175
|
+
results.errors.push({
|
|
176
|
+
file: 'security-headers.config.js',
|
|
177
|
+
error: 'Source file not found in ship-safe package'
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Detect if this is a Next.js project
|
|
183
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
184
|
+
let isNextProject = false;
|
|
185
|
+
|
|
186
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
187
|
+
try {
|
|
188
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
189
|
+
isNextProject = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
190
|
+
} catch {
|
|
191
|
+
// Ignore parse errors
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check if target exists
|
|
196
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
197
|
+
results.skipped.push('security-headers.config.js (already exists, use -f to overwrite)');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Copy the file
|
|
202
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
203
|
+
fs.writeFileSync(targetPath, content);
|
|
204
|
+
results.copied.push('security-headers.config.js');
|
|
205
|
+
|
|
206
|
+
// Show integration instructions
|
|
207
|
+
if (isNextProject) {
|
|
208
|
+
console.log(chalk.cyan('\nNext.js detected! Add to your next.config.js:\n'));
|
|
209
|
+
console.log(chalk.gray(' const { securityHeadersConfig } = require(\'./security-headers.config.js\');'));
|
|
210
|
+
console.log(chalk.gray(' module.exports = { ...securityHeadersConfig, /* your config */ };'));
|
|
211
|
+
console.log();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// =============================================================================
|
|
216
|
+
// SUMMARY
|
|
217
|
+
// =============================================================================
|
|
218
|
+
|
|
219
|
+
function printSummary(results) {
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
222
|
+
console.log(chalk.cyan.bold(' Summary'));
|
|
223
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
224
|
+
console.log();
|
|
225
|
+
|
|
226
|
+
if (results.copied.length > 0) {
|
|
227
|
+
console.log(chalk.green.bold('Created:'));
|
|
228
|
+
for (const file of results.copied) {
|
|
229
|
+
console.log(chalk.green(` \u2714 ${file}`));
|
|
230
|
+
}
|
|
231
|
+
console.log();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (results.merged.length > 0) {
|
|
235
|
+
console.log(chalk.blue.bold('Merged:'));
|
|
236
|
+
for (const file of results.merged) {
|
|
237
|
+
console.log(chalk.blue(` \u2194 ${file} (appended ship-safe patterns)`));
|
|
238
|
+
}
|
|
239
|
+
console.log();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (results.skipped.length > 0) {
|
|
243
|
+
console.log(chalk.yellow.bold('Skipped:'));
|
|
244
|
+
for (const file of results.skipped) {
|
|
245
|
+
console.log(chalk.yellow(` \u2192 ${file}`));
|
|
246
|
+
}
|
|
247
|
+
console.log();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (results.errors.length > 0) {
|
|
251
|
+
console.log(chalk.red.bold('Errors:'));
|
|
252
|
+
for (const { file, error } of results.errors) {
|
|
253
|
+
console.log(chalk.red(` \u2718 ${file}: ${error}`));
|
|
254
|
+
}
|
|
255
|
+
console.log();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Next steps
|
|
259
|
+
console.log(chalk.cyan('Next steps:'));
|
|
260
|
+
console.log(chalk.white(' 1.') + ' Review the copied files and customize for your project');
|
|
261
|
+
console.log(chalk.white(' 2.') + ' Run ' + chalk.cyan('npx ship-safe scan .') + ' to check for secrets');
|
|
262
|
+
console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe checklist') + ' before launching');
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
265
|
+
}
|