ship-safe 2.0.0 → 3.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 +109 -7
- package/cli/bin/ship-safe.js +36 -1
- package/cli/commands/fix.js +216 -0
- package/cli/commands/guard.js +297 -0
- package/cli/commands/mcp.js +303 -0
- package/cli/commands/scan.js +231 -39
- package/cli/utils/entropy.js +126 -0
- package/cli/utils/output.js +10 -1
- package/cli/utils/patterns.js +32 -1
- package/configs/ship-safeignore-template +50 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/assets/logo%20ship%20safe.png" alt="Ship Safe Logo" width="180" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">Ship Safe</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center"><strong>Don't let vibe coding leak your API keys.</strong></p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/ship-safe"><img src="https://badge.fury.io/js/ship-safe.svg" alt="npm version" /></a>
|
|
11
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
4
15
|
|
|
5
16
|
You're shipping fast. You're using AI to write code. You're one `git push` away from exposing your database credentials to the world.
|
|
6
17
|
|
|
7
18
|
**Ship Safe** is a security toolkit for indie hackers and vibe coders who want to secure their MVP in 5 minutes, not 5 days.
|
|
8
19
|
|
|
9
|
-
[](https://www.npmjs.com/package/ship-safe)
|
|
10
|
-
[](https://opensource.org/licenses/MIT)
|
|
11
|
-
|
|
12
20
|
---
|
|
13
21
|
|
|
14
22
|
## Quick Start
|
|
15
23
|
|
|
16
24
|
```bash
|
|
17
|
-
# Scan
|
|
25
|
+
# Scan for leaked secrets (no install required!)
|
|
18
26
|
npx ship-safe scan .
|
|
19
27
|
|
|
28
|
+
# Auto-generate .env.example from found secrets
|
|
29
|
+
npx ship-safe fix
|
|
30
|
+
|
|
31
|
+
# Block git push if secrets are found
|
|
32
|
+
npx ship-safe guard
|
|
33
|
+
|
|
20
34
|
# Run the launch-day security checklist
|
|
21
35
|
npx ship-safe checklist
|
|
22
36
|
|
|
@@ -24,7 +38,9 @@ npx ship-safe checklist
|
|
|
24
38
|
npx ship-safe init
|
|
25
39
|
```
|
|
26
40
|
|
|
27
|
-
That's it.
|
|
41
|
+
That's it. Five commands to secure your MVP.
|
|
42
|
+
|
|
43
|
+

|
|
28
44
|
|
|
29
45
|
### Let AI Do It For You
|
|
30
46
|
|
|
@@ -74,6 +90,32 @@ npx ship-safe scan . -v
|
|
|
74
90
|
|
|
75
91
|
**Exit codes:** Returns `1` if secrets found (useful for CI), `0` if clean.
|
|
76
92
|
|
|
93
|
+
**Flags:**
|
|
94
|
+
- `--json` — structured JSON output for CI pipelines
|
|
95
|
+
- `--sarif` — SARIF format for GitHub Code Scanning
|
|
96
|
+
- `--include-tests` — also scan test/spec/fixture files (excluded by default)
|
|
97
|
+
- `-v` — verbose mode
|
|
98
|
+
|
|
99
|
+
**Suppress false positives:**
|
|
100
|
+
```bash
|
|
101
|
+
const apiKey = 'example-key'; // ship-safe-ignore
|
|
102
|
+
```
|
|
103
|
+
Or exclude paths with `.ship-safeignore` (gitignore syntax).
|
|
104
|
+
|
|
105
|
+
**Custom patterns** — create `.ship-safe.json` in your project root:
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"patterns": [
|
|
109
|
+
{
|
|
110
|
+
"name": "My Internal API Key",
|
|
111
|
+
"pattern": "MYAPP_[A-Z0-9]{32}",
|
|
112
|
+
"severity": "high",
|
|
113
|
+
"description": "Internal key for myapp services."
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
77
119
|
**Detects 50+ secret patterns:**
|
|
78
120
|
- **AI/ML:** OpenAI, Anthropic, Google AI, Cohere, Replicate, Hugging Face
|
|
79
121
|
- **Auth:** Clerk, Auth0, Supabase Auth
|
|
@@ -125,6 +167,66 @@ npx ship-safe init -f
|
|
|
125
167
|
|
|
126
168
|
---
|
|
127
169
|
|
|
170
|
+
### `npx ship-safe fix`
|
|
171
|
+
|
|
172
|
+
Scan for secrets and auto-generate a `.env.example` file.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Scan and generate .env.example
|
|
176
|
+
npx ship-safe fix
|
|
177
|
+
|
|
178
|
+
# Preview what would be generated without writing it
|
|
179
|
+
npx ship-safe fix --dry-run
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### `npx ship-safe guard`
|
|
185
|
+
|
|
186
|
+
Install a git hook that blocks pushes if secrets are found. Works with or without Husky.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Install pre-push hook (runs scan before every git push)
|
|
190
|
+
npx ship-safe guard
|
|
191
|
+
|
|
192
|
+
# Install pre-commit hook instead
|
|
193
|
+
npx ship-safe guard --pre-commit
|
|
194
|
+
|
|
195
|
+
# Remove installed hooks
|
|
196
|
+
npx ship-safe guard remove
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Suppress false positives:**
|
|
200
|
+
- Add `# ship-safe-ignore` as a comment on a line to skip it
|
|
201
|
+
- Create `.ship-safeignore` (gitignore syntax) to exclude paths
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### `npx ship-safe mcp`
|
|
206
|
+
|
|
207
|
+
Start ship-safe as an MCP server so AI editors can call it directly.
|
|
208
|
+
|
|
209
|
+
**Setup (Claude Desktop)** — add to `claude_desktop_config.json`:
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"mcpServers": {
|
|
213
|
+
"ship-safe": {
|
|
214
|
+
"command": "npx",
|
|
215
|
+
"args": ["ship-safe", "mcp"]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Works with Claude Desktop, Cursor, Windsurf, Zed, and any MCP-compatible editor.
|
|
222
|
+
|
|
223
|
+
**Available tools:**
|
|
224
|
+
- `scan_secrets` — scan a directory for leaked secrets
|
|
225
|
+
- `get_checklist` — return the security checklist as structured data
|
|
226
|
+
- `analyze_file` — analyze a single file for issues
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
128
230
|
## What's Inside
|
|
129
231
|
|
|
130
232
|
### [`/checklists`](./checklists)
|
package/cli/bin/ship-safe.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* npx ship-safe scan [path] Scan for secrets in your codebase
|
|
11
11
|
* npx ship-safe checklist Run the launch-day security checklist
|
|
12
12
|
* npx ship-safe init Initialize security configs in your project
|
|
13
|
+
* npx ship-safe fix Generate .env.example from found secrets
|
|
14
|
+
* npx ship-safe guard Install pre-push git hook
|
|
13
15
|
* npx ship-safe --help Show all commands
|
|
14
16
|
*/
|
|
15
17
|
|
|
@@ -21,6 +23,9 @@ import { dirname, join } from 'path';
|
|
|
21
23
|
import { scanCommand } from '../commands/scan.js';
|
|
22
24
|
import { checklistCommand } from '../commands/checklist.js';
|
|
23
25
|
import { initCommand } from '../commands/init.js';
|
|
26
|
+
import { fixCommand } from '../commands/fix.js';
|
|
27
|
+
import { guardCommand } from '../commands/guard.js';
|
|
28
|
+
import { mcpCommand } from '../commands/mcp.js';
|
|
24
29
|
|
|
25
30
|
// =============================================================================
|
|
26
31
|
// CLI CONFIGURATION
|
|
@@ -63,6 +68,8 @@ program
|
|
|
63
68
|
.option('-v, --verbose', 'Show all files being scanned')
|
|
64
69
|
.option('--no-color', 'Disable colored output')
|
|
65
70
|
.option('--json', 'Output results as JSON (useful for CI)')
|
|
71
|
+
.option('--sarif', 'Output results in SARIF format (for GitHub Code Scanning)')
|
|
72
|
+
.option('--include-tests', 'Also scan test files (excluded by default to reduce false positives)')
|
|
66
73
|
.action(scanCommand);
|
|
67
74
|
|
|
68
75
|
// -----------------------------------------------------------------------------
|
|
@@ -85,6 +92,32 @@ program
|
|
|
85
92
|
.option('--headers', 'Only copy security headers config')
|
|
86
93
|
.action(initCommand);
|
|
87
94
|
|
|
95
|
+
// -----------------------------------------------------------------------------
|
|
96
|
+
// FIX COMMAND
|
|
97
|
+
// -----------------------------------------------------------------------------
|
|
98
|
+
program
|
|
99
|
+
.command('fix')
|
|
100
|
+
.description('Scan for secrets and generate a .env.example with placeholder values')
|
|
101
|
+
.option('--dry-run', 'Preview generated .env.example without writing it')
|
|
102
|
+
.action(fixCommand);
|
|
103
|
+
|
|
104
|
+
// -----------------------------------------------------------------------------
|
|
105
|
+
// GUARD COMMAND
|
|
106
|
+
// -----------------------------------------------------------------------------
|
|
107
|
+
program
|
|
108
|
+
.command('guard [action]')
|
|
109
|
+
.description('Install a git hook to block pushes if secrets are found')
|
|
110
|
+
.option('--pre-commit', 'Install as pre-commit hook instead of pre-push')
|
|
111
|
+
.action(guardCommand);
|
|
112
|
+
|
|
113
|
+
// -----------------------------------------------------------------------------
|
|
114
|
+
// MCP SERVER COMMAND
|
|
115
|
+
// -----------------------------------------------------------------------------
|
|
116
|
+
program
|
|
117
|
+
.command('mcp')
|
|
118
|
+
.description('Start ship-safe as an MCP server (for Claude Desktop, Cursor, Windsurf, etc.)')
|
|
119
|
+
.action(mcpCommand);
|
|
120
|
+
|
|
88
121
|
// -----------------------------------------------------------------------------
|
|
89
122
|
// PARSE AND RUN
|
|
90
123
|
// -----------------------------------------------------------------------------
|
|
@@ -93,7 +126,9 @@ program
|
|
|
93
126
|
if (process.argv.length === 2) {
|
|
94
127
|
console.log(banner);
|
|
95
128
|
console.log(chalk.yellow('\nQuick start:\n'));
|
|
96
|
-
console.log(chalk.white(' npx ship-safe scan . ') + chalk.gray('# Scan
|
|
129
|
+
console.log(chalk.white(' npx ship-safe scan . ') + chalk.gray('# Scan for secrets'));
|
|
130
|
+
console.log(chalk.white(' npx ship-safe fix ') + chalk.gray('# Generate .env.example from secrets'));
|
|
131
|
+
console.log(chalk.white(' npx ship-safe guard ') + chalk.gray('# Block git push if secrets found'));
|
|
97
132
|
console.log(chalk.white(' npx ship-safe checklist ') + chalk.gray('# Run security checklist'));
|
|
98
133
|
console.log(chalk.white(' npx ship-safe init ') + chalk.gray('# Add security configs to your project'));
|
|
99
134
|
console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix Command
|
|
3
|
+
* ===========
|
|
4
|
+
*
|
|
5
|
+
* Scans for secrets and generates a .env.example file with placeholder values.
|
|
6
|
+
* Also shows a summary of what to move to environment variables.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* ship-safe fix Scan and generate .env.example
|
|
10
|
+
* ship-safe fix --dry-run Preview what would be generated (don't write file)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import ora from 'ora';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import {
|
|
18
|
+
SECRET_PATTERNS,
|
|
19
|
+
SKIP_DIRS,
|
|
20
|
+
SKIP_EXTENSIONS,
|
|
21
|
+
TEST_FILE_PATTERNS,
|
|
22
|
+
MAX_FILE_SIZE
|
|
23
|
+
} from '../utils/patterns.js';
|
|
24
|
+
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
25
|
+
import { glob } from 'glob';
|
|
26
|
+
import * as output from '../utils/output.js';
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// MAIN COMMAND
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
export async function fixCommand(options = {}) {
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
|
|
35
|
+
const spinner = ora({ text: 'Scanning for secrets...', color: 'cyan' }).start();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const files = await findFiles(cwd);
|
|
39
|
+
const results = [];
|
|
40
|
+
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const findings = await scanFile(file);
|
|
43
|
+
if (findings.length > 0) {
|
|
44
|
+
results.push({ file, findings });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
spinner.stop();
|
|
49
|
+
|
|
50
|
+
if (results.length === 0) {
|
|
51
|
+
output.success('No secrets found — nothing to fix!');
|
|
52
|
+
console.log(chalk.gray('\nYour codebase looks clean. Keep it that way with:'));
|
|
53
|
+
console.log(chalk.gray(' npx ship-safe guard # Block pushes if secrets are found'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build env var suggestions from findings
|
|
58
|
+
const envVars = buildEnvVarSuggestions(results);
|
|
59
|
+
|
|
60
|
+
output.header('Fix Report');
|
|
61
|
+
printFindings(results, cwd);
|
|
62
|
+
printEnvExample(envVars, options.dryRun);
|
|
63
|
+
|
|
64
|
+
} catch (err) {
|
|
65
|
+
spinner.fail('Fix scan failed');
|
|
66
|
+
output.error(err.message);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// SCAN (same logic as scan command, reused here)
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
async function findFiles(rootPath) {
|
|
76
|
+
const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
|
|
77
|
+
const files = await glob('**/*', {
|
|
78
|
+
cwd: rootPath, absolute: true, nodir: true, ignore: globIgnore, dot: true
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const filtered = [];
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const ext = path.extname(file).toLowerCase();
|
|
84
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
85
|
+
const basename = path.basename(file);
|
|
86
|
+
if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) continue;
|
|
87
|
+
if (TEST_FILE_PATTERNS.some(p => p.test(file))) continue;
|
|
88
|
+
if (basename === '.env.example') continue; // Don't scan example files
|
|
89
|
+
try {
|
|
90
|
+
const stats = fs.statSync(file);
|
|
91
|
+
if (stats.size > MAX_FILE_SIZE) continue;
|
|
92
|
+
} catch { continue; }
|
|
93
|
+
filtered.push(file);
|
|
94
|
+
}
|
|
95
|
+
return filtered;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function scanFile(filePath) {
|
|
99
|
+
const findings = [];
|
|
100
|
+
try {
|
|
101
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
102
|
+
const lines = content.split('\n');
|
|
103
|
+
|
|
104
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
105
|
+
const line = lines[lineNum];
|
|
106
|
+
if (/ship-safe-ignore/i.test(line)) continue;
|
|
107
|
+
|
|
108
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
109
|
+
pattern.pattern.lastIndex = 0;
|
|
110
|
+
let match;
|
|
111
|
+
while ((match = pattern.pattern.exec(line)) !== null) {
|
|
112
|
+
if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
|
|
113
|
+
findings.push({
|
|
114
|
+
line: lineNum + 1,
|
|
115
|
+
matched: match[0],
|
|
116
|
+
patternName: pattern.name,
|
|
117
|
+
severity: pattern.severity,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch {}
|
|
123
|
+
return findings;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// ENV VAR GENERATION
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
function buildEnvVarSuggestions(results) {
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const vars = [];
|
|
133
|
+
|
|
134
|
+
for (const { findings } of results) {
|
|
135
|
+
for (const f of findings) {
|
|
136
|
+
const varName = patternToEnvVar(f.patternName);
|
|
137
|
+
if (!seen.has(varName)) {
|
|
138
|
+
seen.add(varName);
|
|
139
|
+
vars.push({ name: varName, comment: f.patternName });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return vars;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Convert a pattern name to a sensible env var name.
|
|
149
|
+
* e.g. "OpenAI API Key" → "OPENAI_API_KEY"
|
|
150
|
+
*/
|
|
151
|
+
function patternToEnvVar(patternName) {
|
|
152
|
+
return patternName
|
|
153
|
+
.toUpperCase()
|
|
154
|
+
.replace(/[^A-Z0-9\s]/g, '')
|
|
155
|
+
.trim()
|
|
156
|
+
.replace(/\s+/g, '_');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// =============================================================================
|
|
160
|
+
// OUTPUT
|
|
161
|
+
// =============================================================================
|
|
162
|
+
|
|
163
|
+
function printFindings(results, rootPath) {
|
|
164
|
+
const total = results.reduce((sum, r) => sum + r.findings.length, 0);
|
|
165
|
+
console.log(chalk.red.bold(`\n Found ${total} secret(s) across ${results.length} file(s)\n`));
|
|
166
|
+
|
|
167
|
+
for (const { file, findings } of results) {
|
|
168
|
+
const relPath = path.relative(rootPath, file);
|
|
169
|
+
console.log(chalk.white.bold(` ${relPath}`));
|
|
170
|
+
for (const f of findings) {
|
|
171
|
+
console.log(chalk.gray(` Line ${f.line}: `) + chalk.yellow(f.patternName));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function printEnvExample(envVars, dryRun) {
|
|
177
|
+
const lines = [
|
|
178
|
+
'# .env.example',
|
|
179
|
+
'# Generated by ship-safe — replace placeholder values with your actual secrets.',
|
|
180
|
+
'# Copy this file to .env and fill in the values.',
|
|
181
|
+
'# NEVER commit .env — only commit .env.example',
|
|
182
|
+
'',
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
for (const { name, comment } of envVars) {
|
|
186
|
+
lines.push(`# ${comment}`);
|
|
187
|
+
lines.push(`${name}=your_${name.toLowerCase()}_here`);
|
|
188
|
+
lines.push('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const content = lines.join('\n');
|
|
192
|
+
|
|
193
|
+
output.header(dryRun ? '.env.example Preview (dry run)' : 'Generated .env.example');
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(chalk.gray(content));
|
|
196
|
+
|
|
197
|
+
if (!dryRun) {
|
|
198
|
+
const envExamplePath = path.join(process.cwd(), '.env.example');
|
|
199
|
+
|
|
200
|
+
if (fs.existsSync(envExamplePath)) {
|
|
201
|
+
output.warning('.env.example already exists — skipping. Use --force to overwrite.');
|
|
202
|
+
} else {
|
|
203
|
+
fs.writeFileSync(envExamplePath, content);
|
|
204
|
+
output.success('Created .env.example');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log();
|
|
208
|
+
console.log(chalk.cyan.bold('Next steps:'));
|
|
209
|
+
console.log(chalk.white('1.') + chalk.gray(' Copy .env.example to .env'));
|
|
210
|
+
console.log(chalk.white('2.') + chalk.gray(' Replace placeholder values with your real secrets'));
|
|
211
|
+
console.log(chalk.white('3.') + chalk.gray(' Remove the hardcoded values from your source code'));
|
|
212
|
+
console.log(chalk.white('4.') + chalk.gray(' Verify .env is in your .gitignore'));
|
|
213
|
+
console.log(chalk.white('5.') + chalk.gray(' Run npx ship-safe scan . to confirm clean'));
|
|
214
|
+
console.log();
|
|
215
|
+
}
|
|
216
|
+
}
|