ship-safe 5.0.0 → 6.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/cli/agents/agentic-security-agent.js +1 -1
- package/cli/agents/api-fuzzer.js +1 -1
- package/cli/agents/auth-bypass-agent.js +2 -2
- package/cli/agents/base-agent.js +2 -1
- package/cli/agents/config-auditor.js +3 -11
- package/cli/agents/exception-handler-agent.js +187 -0
- package/cli/agents/html-reporter.js +511 -370
- package/cli/agents/index.js +6 -0
- package/cli/agents/mcp-security-agent.js +182 -0
- package/cli/agents/pii-compliance-agent.js +301 -301
- package/cli/agents/scoring-engine.js +14 -6
- package/cli/agents/vibe-coding-agent.js +250 -0
- package/cli/bin/ship-safe.js +43 -6
- package/cli/commands/agent.js +6 -4
- package/cli/commands/audit.js +85 -14
- package/cli/commands/baseline.js +3 -2
- package/cli/commands/benchmark.js +327 -0
- package/cli/commands/ci.js +342 -260
- package/cli/commands/deps.js +73 -4
- package/cli/commands/diff.js +200 -0
- package/cli/commands/doctor.js +14 -4
- package/cli/commands/fix.js +218 -216
- package/cli/commands/init.js +349 -349
- package/cli/commands/mcp.js +304 -303
- package/cli/commands/red-team.js +2 -2
- package/cli/commands/remediate.js +155 -7
- package/cli/commands/scan.js +567 -565
- package/cli/commands/score.js +2 -0
- package/cli/commands/vibe-check.js +276 -0
- package/cli/commands/watch.js +161 -160
- package/cli/index.js +8 -1
- package/cli/utils/cache-manager.js +1 -1
- package/cli/utils/output.js +5 -2
- package/cli/utils/patterns.js +1121 -1104
- package/cli/utils/pdf-generator.js +1 -1
- package/package.json +2 -2
package/cli/commands/init.js
CHANGED
|
@@ -1,349 +1,349 @@
|
|
|
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
|
-
// If a specific flag is set, only run that category.
|
|
52
|
-
// With no flags, run everything.
|
|
53
|
-
const hasSpecificFlag = options.gitignore || options.headers || options.agents;
|
|
54
|
-
const copyGitignore = hasSpecificFlag ? !!options.gitignore : true;
|
|
55
|
-
const copyHeaders = hasSpecificFlag ? !!options.headers : true;
|
|
56
|
-
const copyAgents = hasSpecificFlag ? !!options.agents : true;
|
|
57
|
-
|
|
58
|
-
// Copy .gitignore
|
|
59
|
-
if (copyGitignore) {
|
|
60
|
-
await handleGitignore(targetDir, options.force, results);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Copy security headers
|
|
64
|
-
if (copyHeaders) {
|
|
65
|
-
await handleSecurityHeaders(targetDir, options.force, results);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Append security rules to AI agent instruction files
|
|
69
|
-
if (copyAgents) {
|
|
70
|
-
await handleAgentFiles(targetDir, options.force, results);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Print summary
|
|
74
|
-
printSummary(results);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// =============================================================================
|
|
78
|
-
// GITIGNORE HANDLING
|
|
79
|
-
// =============================================================================
|
|
80
|
-
|
|
81
|
-
async function handleGitignore(targetDir, force, results) {
|
|
82
|
-
// Note: We use 'gitignore-template' because npm excludes dotfiles from packages
|
|
83
|
-
const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'gitignore-template');
|
|
84
|
-
const targetPath = path.join(targetDir, '.gitignore');
|
|
85
|
-
|
|
86
|
-
// Check if source exists
|
|
87
|
-
if (!fs.existsSync(sourcePath)) {
|
|
88
|
-
results.errors.push({
|
|
89
|
-
file: '.gitignore',
|
|
90
|
-
error: 'Source file not found in ship-safe package'
|
|
91
|
-
});
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
|
|
96
|
-
|
|
97
|
-
// Check if target exists
|
|
98
|
-
if (fs.existsSync(targetPath)) {
|
|
99
|
-
if (force) {
|
|
100
|
-
// Overwrite
|
|
101
|
-
fs.writeFileSync(targetPath, sourceContent);
|
|
102
|
-
results.copied.push('.gitignore (overwritten)');
|
|
103
|
-
} else {
|
|
104
|
-
// Merge: append ship-safe patterns to existing
|
|
105
|
-
const existingContent = fs.readFileSync(targetPath, 'utf-8');
|
|
106
|
-
|
|
107
|
-
// Check if already has ship-safe content
|
|
108
|
-
if (existingContent.includes('# SHIP SAFE')) {
|
|
109
|
-
results.skipped.push('.gitignore (already contains ship-safe patterns)');
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Append ship-safe section
|
|
114
|
-
const mergedContent = existingContent.trim() + '\n\n' +
|
|
115
|
-
'# =============================================================================\n' +
|
|
116
|
-
'# SHIP SAFE ADDITIONS\n' +
|
|
117
|
-
'# Added by: npx ship-safe init\n' +
|
|
118
|
-
'# =============================================================================\n\n' +
|
|
119
|
-
extractSecurityPatterns(sourceContent);
|
|
120
|
-
|
|
121
|
-
fs.writeFileSync(targetPath, mergedContent);
|
|
122
|
-
results.merged.push('.gitignore');
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
// Create new
|
|
126
|
-
fs.writeFileSync(targetPath, sourceContent);
|
|
127
|
-
results.copied.push('.gitignore');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Extract the most important security patterns from our .gitignore
|
|
133
|
-
*/
|
|
134
|
-
function extractSecurityPatterns(fullGitignore) {
|
|
135
|
-
// Extract key sections
|
|
136
|
-
const patterns = `
|
|
137
|
-
# Environment files
|
|
138
|
-
.env
|
|
139
|
-
.env.local
|
|
140
|
-
.env*.local
|
|
141
|
-
*.env
|
|
142
|
-
|
|
143
|
-
# Private keys & certificates
|
|
144
|
-
*.pem
|
|
145
|
-
*.key
|
|
146
|
-
*.p12
|
|
147
|
-
*.pfx
|
|
148
|
-
|
|
149
|
-
# Credentials
|
|
150
|
-
*credentials*
|
|
151
|
-
*.secrets.json
|
|
152
|
-
secrets.yml
|
|
153
|
-
secrets.yaml
|
|
154
|
-
|
|
155
|
-
# Service accounts
|
|
156
|
-
**/service-account*.json
|
|
157
|
-
*-firebase-adminsdk-*.json
|
|
158
|
-
|
|
159
|
-
# AWS
|
|
160
|
-
.aws/credentials
|
|
161
|
-
|
|
162
|
-
# Database files
|
|
163
|
-
*.sqlite
|
|
164
|
-
*.sqlite3
|
|
165
|
-
*.db
|
|
166
|
-
|
|
167
|
-
# Logs (may contain sensitive data)
|
|
168
|
-
*.log
|
|
169
|
-
logs/
|
|
170
|
-
`;
|
|
171
|
-
|
|
172
|
-
return patterns.trim();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// =============================================================================
|
|
176
|
-
// SECURITY HEADERS HANDLING
|
|
177
|
-
// =============================================================================
|
|
178
|
-
|
|
179
|
-
async function handleSecurityHeaders(targetDir, force, results) {
|
|
180
|
-
const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'nextjs-security-headers.js');
|
|
181
|
-
const targetPath = path.join(targetDir, 'security-headers.config.js');
|
|
182
|
-
|
|
183
|
-
// Check if source exists
|
|
184
|
-
if (!fs.existsSync(sourcePath)) {
|
|
185
|
-
results.errors.push({
|
|
186
|
-
file: 'security-headers.config.js',
|
|
187
|
-
error: 'Source file not found in ship-safe package'
|
|
188
|
-
});
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Detect if this is a Next.js project
|
|
193
|
-
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
194
|
-
let isNextProject = false;
|
|
195
|
-
|
|
196
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
197
|
-
try {
|
|
198
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
199
|
-
isNextProject = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
200
|
-
} catch {
|
|
201
|
-
// Ignore parse errors
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Check if target exists
|
|
206
|
-
if (fs.existsSync(targetPath) && !force) {
|
|
207
|
-
results.skipped.push('security-headers.config.js (already exists, use -f to overwrite)');
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Copy the file
|
|
212
|
-
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
213
|
-
fs.writeFileSync(targetPath, content);
|
|
214
|
-
results.copied.push('security-headers.config.js');
|
|
215
|
-
|
|
216
|
-
// Show integration instructions
|
|
217
|
-
if (isNextProject) {
|
|
218
|
-
console.log(chalk.cyan('\nNext.js detected! Add to your next.config.js:\n'));
|
|
219
|
-
console.log(chalk.gray(' const { securityHeadersConfig } = require(\'./security-headers.config.js\');'));
|
|
220
|
-
console.log(chalk.gray(' module.exports = { ...securityHeadersConfig, /* your config */ };'));
|
|
221
|
-
console.log();
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// =============================================================================
|
|
226
|
-
// AGENT FILE HANDLING
|
|
227
|
-
// =============================================================================
|
|
228
|
-
|
|
229
|
-
// Marker used for idempotency — if this string is present we already added our section.
|
|
230
|
-
const AGENT_MARKER = '<!-- ship-safe -->';
|
|
231
|
-
|
|
232
|
-
// The security rules block appended to existing agent instruction files.
|
|
233
|
-
const AGENT_SECTION = `
|
|
234
|
-
## Security (ship-safe)
|
|
235
|
-
${AGENT_MARKER}
|
|
236
|
-
- Always run \`npx ship-safe scan .\` before committing code
|
|
237
|
-
- Never hardcode API keys, tokens, or credentials in source files — use environment variables
|
|
238
|
-
- If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
|
|
239
|
-
`;
|
|
240
|
-
|
|
241
|
-
// Standalone content for a new Cursor rules file.
|
|
242
|
-
const CURSOR_RULE_CONTENT = `---
|
|
243
|
-
description: Prevent API key leaks and credential exposure in generated code
|
|
244
|
-
alwaysApply: true
|
|
245
|
-
---
|
|
246
|
-
|
|
247
|
-
- Always run \`npx ship-safe scan .\` before committing code
|
|
248
|
-
- Never hardcode API keys, tokens, or credentials in source files — use environment variables
|
|
249
|
-
- If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
|
|
250
|
-
`;
|
|
251
|
-
|
|
252
|
-
async function handleAgentFiles(targetDir, force, results) {
|
|
253
|
-
// Files where we append a section if they already exist, or create if they don't.
|
|
254
|
-
const appendTargets = [
|
|
255
|
-
{ file: 'CLAUDE.md', label: 'CLAUDE.md' },
|
|
256
|
-
{ file: '.windsurfrules', label: '.windsurfrules' },
|
|
257
|
-
{ file: path.join('.github', 'copilot-instructions.md'), label: '.github/copilot-instructions.md' },
|
|
258
|
-
];
|
|
259
|
-
|
|
260
|
-
for (const { file, label } of appendTargets) {
|
|
261
|
-
const targetPath = path.join(targetDir, file);
|
|
262
|
-
|
|
263
|
-
if (fs.existsSync(targetPath)) {
|
|
264
|
-
const existing = fs.readFileSync(targetPath, 'utf-8');
|
|
265
|
-
if (existing.includes(AGENT_MARKER)) {
|
|
266
|
-
results.skipped.push(`${label} (already contains ship-safe rules)`);
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
if (force || true) { // always append unless already present
|
|
270
|
-
fs.writeFileSync(targetPath, existing.trimEnd() + '\n' + AGENT_SECTION);
|
|
271
|
-
results.merged.push(label);
|
|
272
|
-
}
|
|
273
|
-
} else {
|
|
274
|
-
// Ensure parent directory exists (e.g. .github/)
|
|
275
|
-
const parentDir = path.dirname(targetPath);
|
|
276
|
-
if (!fs.existsSync(parentDir)) {
|
|
277
|
-
fs.mkdirSync(parentDir, { recursive: true });
|
|
278
|
-
}
|
|
279
|
-
fs.writeFileSync(targetPath, AGENT_SECTION.trim() + '\n');
|
|
280
|
-
results.copied.push(label);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Cursor rules — dedicated file, no merging needed.
|
|
285
|
-
const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
|
|
286
|
-
const cursorRulesFile = path.join(cursorRulesDir, 'ship-safe.mdc');
|
|
287
|
-
|
|
288
|
-
if (fs.existsSync(cursorRulesFile) && !force) {
|
|
289
|
-
results.skipped.push('.cursor/rules/ship-safe.mdc (already exists, use -f to overwrite)');
|
|
290
|
-
} else {
|
|
291
|
-
if (!fs.existsSync(cursorRulesDir)) {
|
|
292
|
-
fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
293
|
-
}
|
|
294
|
-
fs.writeFileSync(cursorRulesFile, CURSOR_RULE_CONTENT.trim() + '\n');
|
|
295
|
-
results.copied.push('.cursor/rules/ship-safe.mdc');
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// =============================================================================
|
|
300
|
-
// SUMMARY
|
|
301
|
-
// =============================================================================
|
|
302
|
-
|
|
303
|
-
function printSummary(results) {
|
|
304
|
-
console.log();
|
|
305
|
-
console.log(chalk.cyan('='.repeat(60)));
|
|
306
|
-
console.log(chalk.cyan.bold(' Summary'));
|
|
307
|
-
console.log(chalk.cyan('='.repeat(60)));
|
|
308
|
-
console.log();
|
|
309
|
-
|
|
310
|
-
if (results.copied.length > 0) {
|
|
311
|
-
console.log(chalk.green.bold('Created:'));
|
|
312
|
-
for (const file of results.copied) {
|
|
313
|
-
console.log(chalk.green(` \u2714 ${file}`));
|
|
314
|
-
}
|
|
315
|
-
console.log();
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (results.merged.length > 0) {
|
|
319
|
-
console.log(chalk.blue.bold('Merged:'));
|
|
320
|
-
for (const file of results.merged) {
|
|
321
|
-
console.log(chalk.blue(` \u2194 ${file} (appended ship-safe patterns)`));
|
|
322
|
-
}
|
|
323
|
-
console.log();
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (results.skipped.length > 0) {
|
|
327
|
-
console.log(chalk.yellow.bold('Skipped:'));
|
|
328
|
-
for (const file of results.skipped) {
|
|
329
|
-
console.log(chalk.yellow(` \u2192 ${file}`));
|
|
330
|
-
}
|
|
331
|
-
console.log();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (results.errors.length > 0) {
|
|
335
|
-
console.log(chalk.red.bold('Errors:'));
|
|
336
|
-
for (const { file, error } of results.errors) {
|
|
337
|
-
console.log(chalk.red(` \u2718 ${file}: ${error}`));
|
|
338
|
-
}
|
|
339
|
-
console.log();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Next steps
|
|
343
|
-
console.log(chalk.cyan('Next steps:'));
|
|
344
|
-
console.log(chalk.white(' 1.') + ' Review the copied files and customize for your project');
|
|
345
|
-
console.log(chalk.white(' 2.') + ' Run ' + chalk.cyan('npx ship-safe scan .') + ' to check for secrets');
|
|
346
|
-
console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe checklist') + ' before launching');
|
|
347
|
-
console.log();
|
|
348
|
-
console.log(chalk.cyan('='.repeat(60)));
|
|
349
|
-
}
|
|
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
|
+
const hasSpecificFlag = options.gitignore || options.headers || options.agents;
|
|
54
|
+
const copyGitignore = hasSpecificFlag ? !!options.gitignore : true;
|
|
55
|
+
const copyHeaders = hasSpecificFlag ? !!options.headers : true;
|
|
56
|
+
const copyAgents = hasSpecificFlag ? !!options.agents : true;
|
|
57
|
+
|
|
58
|
+
// Copy .gitignore
|
|
59
|
+
if (copyGitignore) {
|
|
60
|
+
await handleGitignore(targetDir, options.force, results);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Copy security headers
|
|
64
|
+
if (copyHeaders) {
|
|
65
|
+
await handleSecurityHeaders(targetDir, options.force, results);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Append security rules to AI agent instruction files
|
|
69
|
+
if (copyAgents) {
|
|
70
|
+
await handleAgentFiles(targetDir, options.force, results);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Print summary
|
|
74
|
+
printSummary(results);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// GITIGNORE HANDLING
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
async function handleGitignore(targetDir, force, results) {
|
|
82
|
+
// Note: We use 'gitignore-template' because npm excludes dotfiles from packages
|
|
83
|
+
const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'gitignore-template');
|
|
84
|
+
const targetPath = path.join(targetDir, '.gitignore');
|
|
85
|
+
|
|
86
|
+
// Check if source exists
|
|
87
|
+
if (!fs.existsSync(sourcePath)) {
|
|
88
|
+
results.errors.push({
|
|
89
|
+
file: '.gitignore',
|
|
90
|
+
error: 'Source file not found in ship-safe package'
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
|
|
96
|
+
|
|
97
|
+
// Check if target exists
|
|
98
|
+
if (fs.existsSync(targetPath)) {
|
|
99
|
+
if (force) {
|
|
100
|
+
// Overwrite
|
|
101
|
+
fs.writeFileSync(targetPath, sourceContent);
|
|
102
|
+
results.copied.push('.gitignore (overwritten)');
|
|
103
|
+
} else {
|
|
104
|
+
// Merge: append ship-safe patterns to existing
|
|
105
|
+
const existingContent = fs.readFileSync(targetPath, 'utf-8');
|
|
106
|
+
|
|
107
|
+
// Check if already has ship-safe content
|
|
108
|
+
if (existingContent.includes('# SHIP SAFE')) {
|
|
109
|
+
results.skipped.push('.gitignore (already contains ship-safe patterns)');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Append ship-safe section
|
|
114
|
+
const mergedContent = existingContent.trim() + '\n\n' +
|
|
115
|
+
'# =============================================================================\n' +
|
|
116
|
+
'# SHIP SAFE ADDITIONS\n' +
|
|
117
|
+
'# Added by: npx ship-safe init\n' +
|
|
118
|
+
'# =============================================================================\n\n' +
|
|
119
|
+
extractSecurityPatterns(sourceContent);
|
|
120
|
+
|
|
121
|
+
fs.writeFileSync(targetPath, mergedContent);
|
|
122
|
+
results.merged.push('.gitignore');
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// Create new
|
|
126
|
+
fs.writeFileSync(targetPath, sourceContent);
|
|
127
|
+
results.copied.push('.gitignore');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract the most important security patterns from our .gitignore
|
|
133
|
+
*/
|
|
134
|
+
function extractSecurityPatterns(fullGitignore) {
|
|
135
|
+
// Extract key sections
|
|
136
|
+
const patterns = `
|
|
137
|
+
# Environment files
|
|
138
|
+
.env
|
|
139
|
+
.env.local
|
|
140
|
+
.env*.local
|
|
141
|
+
*.env
|
|
142
|
+
|
|
143
|
+
# Private keys & certificates
|
|
144
|
+
*.pem
|
|
145
|
+
*.key
|
|
146
|
+
*.p12
|
|
147
|
+
*.pfx
|
|
148
|
+
|
|
149
|
+
# Credentials
|
|
150
|
+
*credentials*
|
|
151
|
+
*.secrets.json
|
|
152
|
+
secrets.yml
|
|
153
|
+
secrets.yaml
|
|
154
|
+
|
|
155
|
+
# Service accounts
|
|
156
|
+
**/service-account*.json
|
|
157
|
+
*-firebase-adminsdk-*.json
|
|
158
|
+
|
|
159
|
+
# AWS
|
|
160
|
+
.aws/credentials
|
|
161
|
+
|
|
162
|
+
# Database files
|
|
163
|
+
*.sqlite
|
|
164
|
+
*.sqlite3
|
|
165
|
+
*.db
|
|
166
|
+
|
|
167
|
+
# Logs (may contain sensitive data)
|
|
168
|
+
*.log
|
|
169
|
+
logs/
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
return patterns.trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// =============================================================================
|
|
176
|
+
// SECURITY HEADERS HANDLING
|
|
177
|
+
// =============================================================================
|
|
178
|
+
|
|
179
|
+
async function handleSecurityHeaders(targetDir, force, results) {
|
|
180
|
+
const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'nextjs-security-headers.js');
|
|
181
|
+
const targetPath = path.join(targetDir, 'security-headers.config.js');
|
|
182
|
+
|
|
183
|
+
// Check if source exists
|
|
184
|
+
if (!fs.existsSync(sourcePath)) {
|
|
185
|
+
results.errors.push({
|
|
186
|
+
file: 'security-headers.config.js',
|
|
187
|
+
error: 'Source file not found in ship-safe package'
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Detect if this is a Next.js project
|
|
193
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
194
|
+
let isNextProject = false;
|
|
195
|
+
|
|
196
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
197
|
+
try {
|
|
198
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
199
|
+
isNextProject = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
200
|
+
} catch {
|
|
201
|
+
// Ignore parse errors
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if target exists
|
|
206
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
207
|
+
results.skipped.push('security-headers.config.js (already exists, use -f to overwrite)');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Copy the file
|
|
212
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
213
|
+
fs.writeFileSync(targetPath, content);
|
|
214
|
+
results.copied.push('security-headers.config.js');
|
|
215
|
+
|
|
216
|
+
// Show integration instructions
|
|
217
|
+
if (isNextProject) {
|
|
218
|
+
console.log(chalk.cyan('\nNext.js detected! Add to your next.config.js:\n'));
|
|
219
|
+
console.log(chalk.gray(' const { securityHeadersConfig } = require(\'./security-headers.config.js\');'));
|
|
220
|
+
console.log(chalk.gray(' module.exports = { ...securityHeadersConfig, /* your config */ };'));
|
|
221
|
+
console.log();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// =============================================================================
|
|
226
|
+
// AGENT FILE HANDLING
|
|
227
|
+
// =============================================================================
|
|
228
|
+
|
|
229
|
+
// Marker used for idempotency — if this string is present we already added our section.
|
|
230
|
+
const AGENT_MARKER = '<!-- ship-safe -->';
|
|
231
|
+
|
|
232
|
+
// The security rules block appended to existing agent instruction files.
|
|
233
|
+
const AGENT_SECTION = `
|
|
234
|
+
## Security (ship-safe)
|
|
235
|
+
${AGENT_MARKER}
|
|
236
|
+
- Always run \`npx ship-safe scan .\` before committing code
|
|
237
|
+
- Never hardcode API keys, tokens, or credentials in source files — use environment variables
|
|
238
|
+
- If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
// Standalone content for a new Cursor rules file.
|
|
242
|
+
const CURSOR_RULE_CONTENT = `---
|
|
243
|
+
description: Prevent API key leaks and credential exposure in generated code
|
|
244
|
+
alwaysApply: true
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
- Always run \`npx ship-safe scan .\` before committing code
|
|
248
|
+
- Never hardcode API keys, tokens, or credentials in source files — use environment variables
|
|
249
|
+
- If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
async function handleAgentFiles(targetDir, force, results) {
|
|
253
|
+
// Files where we append a section if they already exist, or create if they don't.
|
|
254
|
+
const appendTargets = [
|
|
255
|
+
{ file: 'CLAUDE.md', label: 'CLAUDE.md' },
|
|
256
|
+
{ file: '.windsurfrules', label: '.windsurfrules' },
|
|
257
|
+
{ file: path.join('.github', 'copilot-instructions.md'), label: '.github/copilot-instructions.md' },
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
for (const { file, label } of appendTargets) {
|
|
261
|
+
const targetPath = path.join(targetDir, file);
|
|
262
|
+
|
|
263
|
+
if (fs.existsSync(targetPath)) {
|
|
264
|
+
const existing = fs.readFileSync(targetPath, 'utf-8');
|
|
265
|
+
if (existing.includes(AGENT_MARKER)) {
|
|
266
|
+
results.skipped.push(`${label} (already contains ship-safe rules)`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (force || true) { // always append unless already present
|
|
270
|
+
fs.writeFileSync(targetPath, existing.trimEnd() + '\n' + AGENT_SECTION);
|
|
271
|
+
results.merged.push(label);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
// Ensure parent directory exists (e.g. .github/)
|
|
275
|
+
const parentDir = path.dirname(targetPath);
|
|
276
|
+
if (!fs.existsSync(parentDir)) {
|
|
277
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
278
|
+
}
|
|
279
|
+
fs.writeFileSync(targetPath, AGENT_SECTION.trim() + '\n');
|
|
280
|
+
results.copied.push(label);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Cursor rules — dedicated file, no merging needed.
|
|
285
|
+
const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
|
|
286
|
+
const cursorRulesFile = path.join(cursorRulesDir, 'ship-safe.mdc');
|
|
287
|
+
|
|
288
|
+
if (fs.existsSync(cursorRulesFile) && !force) {
|
|
289
|
+
results.skipped.push('.cursor/rules/ship-safe.mdc (already exists, use -f to overwrite)');
|
|
290
|
+
} else {
|
|
291
|
+
if (!fs.existsSync(cursorRulesDir)) {
|
|
292
|
+
fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
293
|
+
}
|
|
294
|
+
fs.writeFileSync(cursorRulesFile, CURSOR_RULE_CONTENT.trim() + '\n');
|
|
295
|
+
results.copied.push('.cursor/rules/ship-safe.mdc');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// =============================================================================
|
|
300
|
+
// SUMMARY
|
|
301
|
+
// =============================================================================
|
|
302
|
+
|
|
303
|
+
function printSummary(results) {
|
|
304
|
+
console.log();
|
|
305
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
306
|
+
console.log(chalk.cyan.bold(' Summary'));
|
|
307
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
308
|
+
console.log();
|
|
309
|
+
|
|
310
|
+
if (results.copied.length > 0) {
|
|
311
|
+
console.log(chalk.green.bold('Created:'));
|
|
312
|
+
for (const file of results.copied) {
|
|
313
|
+
console.log(chalk.green(` \u2714 ${file}`));
|
|
314
|
+
}
|
|
315
|
+
console.log();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (results.merged.length > 0) {
|
|
319
|
+
console.log(chalk.blue.bold('Merged:'));
|
|
320
|
+
for (const file of results.merged) {
|
|
321
|
+
console.log(chalk.blue(` \u2194 ${file} (appended ship-safe patterns)`));
|
|
322
|
+
}
|
|
323
|
+
console.log();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (results.skipped.length > 0) {
|
|
327
|
+
console.log(chalk.yellow.bold('Skipped:'));
|
|
328
|
+
for (const file of results.skipped) {
|
|
329
|
+
console.log(chalk.yellow(` \u2192 ${file}`));
|
|
330
|
+
}
|
|
331
|
+
console.log();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (results.errors.length > 0) {
|
|
335
|
+
console.log(chalk.red.bold('Errors:'));
|
|
336
|
+
for (const { file, error } of results.errors) {
|
|
337
|
+
console.log(chalk.red(` \u2718 ${file}: ${error}`));
|
|
338
|
+
}
|
|
339
|
+
console.log();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Next steps
|
|
343
|
+
console.log(chalk.cyan('Next steps:'));
|
|
344
|
+
console.log(chalk.white(' 1.') + ' Review the copied files and customize for your project');
|
|
345
|
+
console.log(chalk.white(' 2.') + ' Run ' + chalk.cyan('npx ship-safe scan .') + ' to check for secrets');
|
|
346
|
+
console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe checklist') + ' before launching');
|
|
347
|
+
console.log();
|
|
348
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
349
|
+
}
|