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.
@@ -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
+ }