ship-safe 3.1.0 → 4.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 +200 -307
- package/cli/agents/api-fuzzer.js +224 -0
- package/cli/agents/auth-bypass-agent.js +326 -0
- package/cli/agents/base-agent.js +240 -0
- package/cli/agents/cicd-scanner.js +200 -0
- package/cli/agents/config-auditor.js +413 -0
- package/cli/agents/git-history-scanner.js +167 -0
- package/cli/agents/html-reporter.js +363 -0
- package/cli/agents/index.js +56 -0
- package/cli/agents/injection-tester.js +401 -0
- package/cli/agents/llm-redteam.js +251 -0
- package/cli/agents/mobile-scanner.js +225 -0
- package/cli/agents/orchestrator.js +152 -0
- package/cli/agents/policy-engine.js +149 -0
- package/cli/agents/recon-agent.js +196 -0
- package/cli/agents/sbom-generator.js +176 -0
- package/cli/agents/scoring-engine.js +207 -0
- package/cli/agents/ssrf-prober.js +130 -0
- package/cli/agents/supply-chain-agent.js +274 -0
- package/cli/bin/ship-safe.js +119 -2
- package/cli/commands/agent.js +606 -0
- package/cli/commands/audit.js +565 -0
- package/cli/commands/deps.js +447 -0
- package/cli/commands/fix.js +3 -3
- package/cli/commands/init.js +86 -3
- package/cli/commands/mcp.js +2 -2
- package/cli/commands/red-team.js +315 -0
- package/cli/commands/remediate.js +4 -4
- package/cli/commands/rotate.js +6 -6
- package/cli/commands/scan.js +64 -23
- package/cli/commands/score.js +446 -0
- package/cli/commands/watch.js +160 -0
- package/cli/index.js +40 -2
- package/cli/providers/llm-provider.js +288 -0
- package/cli/utils/entropy.js +6 -0
- package/cli/utils/output.js +42 -2
- package/cli/utils/patterns.js +393 -1
- package/package.json +19 -15
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deps Command
|
|
3
|
+
* ============
|
|
4
|
+
*
|
|
5
|
+
* Audit project dependencies for known CVEs using the package manager's
|
|
6
|
+
* built-in audit tool (npm, yarn, pnpm, pip-audit, bundler-audit).
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* npx ship-safe deps [path] Audit dependencies in the project
|
|
10
|
+
* npx ship-safe deps . --fix Also run the package manager fix command
|
|
11
|
+
*
|
|
12
|
+
* SUPPORTED PACKAGE MANAGERS:
|
|
13
|
+
* npm → npm audit --json
|
|
14
|
+
* yarn → yarn audit --json (NDJSON format)
|
|
15
|
+
* pnpm → pnpm audit --json
|
|
16
|
+
* pip → pip-audit --format json (requires: pip install pip-audit)
|
|
17
|
+
* bundler → bundle-audit check (requires: gem install bundler-audit)
|
|
18
|
+
*
|
|
19
|
+
* EXIT CODES:
|
|
20
|
+
* 0 - No vulnerabilities found (or tool not available)
|
|
21
|
+
* 1 - Vulnerabilities found
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { execSync } from 'child_process';
|
|
27
|
+
import chalk from 'chalk';
|
|
28
|
+
import ora from 'ora';
|
|
29
|
+
import * as output from '../utils/output.js';
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// MAIN COMMAND
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
export async function depsCommand(targetPath = '.', options = {}) {
|
|
36
|
+
const absolutePath = path.resolve(targetPath);
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(absolutePath)) {
|
|
39
|
+
output.error(`Path does not exist: ${absolutePath}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log();
|
|
44
|
+
output.header('Dependency Audit');
|
|
45
|
+
console.log();
|
|
46
|
+
|
|
47
|
+
// ── 1. Detect package manager ─────────────────────────────────────────────
|
|
48
|
+
const pm = detectPackageManager(absolutePath);
|
|
49
|
+
|
|
50
|
+
if (!pm) {
|
|
51
|
+
console.log(chalk.gray(' No supported package manifest found.'));
|
|
52
|
+
console.log(chalk.gray(' Supported: package.json (npm/yarn/pnpm), requirements.txt (pip), Gemfile (bundler)'));
|
|
53
|
+
console.log();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(chalk.gray(` Package manager: ${chalk.white(pm.name)} Manifest: ${chalk.white(pm.manifest)}`));
|
|
58
|
+
console.log();
|
|
59
|
+
|
|
60
|
+
// ── 2. Run audit ──────────────────────────────────────────────────────────
|
|
61
|
+
const spinner = ora({ text: `Running ${pm.name} audit...`, color: 'cyan' }).start();
|
|
62
|
+
let vulns = [];
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
vulns = runAudit(pm, absolutePath);
|
|
66
|
+
spinner.stop();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
spinner.stop();
|
|
69
|
+
if (err.code === 'ENOENT' || /not found|not recognized|command not found/i.test(err.message)) {
|
|
70
|
+
output.warning(`${pm.name} is not installed or not in PATH.`);
|
|
71
|
+
if (pm.installHint) {
|
|
72
|
+
console.log(chalk.gray(` Install it with: `) + chalk.cyan(pm.installHint));
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
output.error(`Audit failed: ${err.message}`);
|
|
78
|
+
console.log(chalk.gray(` Try running manually: `) + chalk.cyan(pm.auditCommand));
|
|
79
|
+
console.log();
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 3. No vulnerabilities ─────────────────────────────────────────────────
|
|
84
|
+
if (vulns.length === 0) {
|
|
85
|
+
output.success(`No known vulnerabilities in your ${pm.name} dependencies!`);
|
|
86
|
+
console.log();
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── 4. Display findings ───────────────────────────────────────────────────
|
|
91
|
+
printDepFindings(vulns, pm);
|
|
92
|
+
|
|
93
|
+
// ── 5. Optionally fix ─────────────────────────────────────────────────────
|
|
94
|
+
if (options.fix) {
|
|
95
|
+
console.log();
|
|
96
|
+
console.log(chalk.cyan(` Running: ${pm.fixCommand}`));
|
|
97
|
+
try {
|
|
98
|
+
execSync(pm.fixCommand, { cwd: absolutePath, stdio: 'inherit' });
|
|
99
|
+
} catch {
|
|
100
|
+
output.warning('Fix command exited with errors — some vulnerabilities may require manual updates.');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// PACKAGE MANAGER DETECTION
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
function detectPackageManager(rootPath) {
|
|
112
|
+
// Node.js — detect specific lock file first for accuracy
|
|
113
|
+
if (fs.existsSync(path.join(rootPath, 'pnpm-lock.yaml'))) {
|
|
114
|
+
return {
|
|
115
|
+
name: 'pnpm',
|
|
116
|
+
manifest: 'package.json',
|
|
117
|
+
auditCommand: 'pnpm audit --json',
|
|
118
|
+
fixCommand: 'pnpm audit --fix',
|
|
119
|
+
type: 'npm-v2'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (fs.existsSync(path.join(rootPath, 'yarn.lock'))) {
|
|
124
|
+
return {
|
|
125
|
+
name: 'yarn',
|
|
126
|
+
manifest: 'package.json',
|
|
127
|
+
auditCommand: 'yarn audit --json',
|
|
128
|
+
fixCommand: 'yarn upgrade',
|
|
129
|
+
type: 'yarn'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (fs.existsSync(path.join(rootPath, 'package.json'))) {
|
|
134
|
+
return {
|
|
135
|
+
name: 'npm',
|
|
136
|
+
manifest: 'package.json',
|
|
137
|
+
auditCommand: 'npm audit --json',
|
|
138
|
+
fixCommand: 'npm audit fix',
|
|
139
|
+
type: 'npm-v2'
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Python
|
|
144
|
+
if (fs.existsSync(path.join(rootPath, 'requirements.txt'))) {
|
|
145
|
+
return {
|
|
146
|
+
name: 'pip-audit',
|
|
147
|
+
manifest: 'requirements.txt',
|
|
148
|
+
auditCommand: 'pip-audit --format json -r requirements.txt',
|
|
149
|
+
fixCommand: 'pip-audit --fix -r requirements.txt',
|
|
150
|
+
type: 'pip',
|
|
151
|
+
installHint: 'pip install pip-audit'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Ruby
|
|
156
|
+
if (fs.existsSync(path.join(rootPath, 'Gemfile.lock'))) {
|
|
157
|
+
return {
|
|
158
|
+
name: 'bundler-audit',
|
|
159
|
+
manifest: 'Gemfile.lock',
|
|
160
|
+
auditCommand: 'bundle-audit check',
|
|
161
|
+
fixCommand: 'bundle update',
|
|
162
|
+
type: 'bundler',
|
|
163
|
+
installHint: 'gem install bundler-audit'
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// RUNNING THE AUDIT
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Run the package manager audit and return normalized vulnerability list.
|
|
176
|
+
* Catches exit code 1 (vulnerabilities found) which is NOT a real error.
|
|
177
|
+
*/
|
|
178
|
+
function runAudit(pm, cwd) {
|
|
179
|
+
let stdout;
|
|
180
|
+
try {
|
|
181
|
+
stdout = execSync(pm.auditCommand, { cwd, stdio: ['pipe', 'pipe', 'pipe'] }).toString();
|
|
182
|
+
} catch (err) {
|
|
183
|
+
// npm/yarn/pnpm exit with code 1 when vulns found — that's expected
|
|
184
|
+
if (err.stdout) {
|
|
185
|
+
stdout = err.stdout.toString();
|
|
186
|
+
} else {
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
switch (pm.type) {
|
|
192
|
+
case 'npm-v2': return parseNpmAudit(stdout);
|
|
193
|
+
case 'yarn': return parseYarnAudit(stdout);
|
|
194
|
+
case 'pip': return parsePipAudit(stdout);
|
|
195
|
+
case 'bundler': return parseBundlerAudit(stdout);
|
|
196
|
+
default: return [];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// =============================================================================
|
|
201
|
+
// AUDIT PARSERS
|
|
202
|
+
// =============================================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parse npm audit v7+ JSON (auditReportVersion: 2).
|
|
206
|
+
* Also works for pnpm which uses the same format.
|
|
207
|
+
*/
|
|
208
|
+
function parseNpmAudit(jsonStr) {
|
|
209
|
+
let data;
|
|
210
|
+
try {
|
|
211
|
+
data = JSON.parse(jsonStr);
|
|
212
|
+
} catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const vulns = [];
|
|
217
|
+
|
|
218
|
+
if (data.auditReportVersion === 2) {
|
|
219
|
+
// npm v7+ format
|
|
220
|
+
for (const [name, vuln] of Object.entries(data.vulnerabilities || {})) {
|
|
221
|
+
// Find the advisory details (via array may contain objects or strings)
|
|
222
|
+
const advisory = Array.isArray(vuln.via)
|
|
223
|
+
? vuln.via.find(v => typeof v === 'object')
|
|
224
|
+
: null;
|
|
225
|
+
|
|
226
|
+
vulns.push({
|
|
227
|
+
name,
|
|
228
|
+
range: vuln.range || 'unknown',
|
|
229
|
+
severity: vuln.severity || 'unknown',
|
|
230
|
+
title: advisory?.title || name,
|
|
231
|
+
cve: advisory?.cves?.[0] || null,
|
|
232
|
+
url: advisory?.url || null,
|
|
233
|
+
fix: vuln.fixAvailable
|
|
234
|
+
? (typeof vuln.fixAvailable === 'object'
|
|
235
|
+
? `npm install ${vuln.fixAvailable.name}@${vuln.fixAvailable.version}`
|
|
236
|
+
: 'npm audit fix')
|
|
237
|
+
: null,
|
|
238
|
+
isDirect: vuln.isDirect ?? false
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
// npm v6 format (legacy)
|
|
243
|
+
for (const [, adv] of Object.entries(data.advisories || {})) {
|
|
244
|
+
vulns.push({
|
|
245
|
+
name: adv.module_name,
|
|
246
|
+
range: adv.vulnerable_versions,
|
|
247
|
+
severity: adv.severity,
|
|
248
|
+
title: adv.title,
|
|
249
|
+
cve: adv.cves?.[0] || null,
|
|
250
|
+
url: adv.url || null,
|
|
251
|
+
fix: adv.patched_versions !== '<0.0.0'
|
|
252
|
+
? `npm install ${adv.module_name}@"${adv.patched_versions}"`
|
|
253
|
+
: null,
|
|
254
|
+
isDirect: true
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Deduplicate by package name (transitive deps appear multiple times)
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
return vulns.filter(v => {
|
|
262
|
+
const key = `${v.name}:${v.severity}`;
|
|
263
|
+
if (seen.has(key)) return false;
|
|
264
|
+
seen.add(key);
|
|
265
|
+
return true;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Parse yarn audit output (NDJSON — one JSON object per line).
|
|
271
|
+
*/
|
|
272
|
+
function parseYarnAudit(ndjsonStr) {
|
|
273
|
+
const vulns = [];
|
|
274
|
+
const seen = new Set();
|
|
275
|
+
|
|
276
|
+
for (const line of ndjsonStr.split('\n')) {
|
|
277
|
+
if (!line.trim()) continue;
|
|
278
|
+
let obj;
|
|
279
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
280
|
+
|
|
281
|
+
if (obj.type !== 'auditAdvisory') continue;
|
|
282
|
+
const adv = obj.data?.advisory;
|
|
283
|
+
if (!adv) continue;
|
|
284
|
+
|
|
285
|
+
const key = `${adv.module_name}:${adv.severity}`;
|
|
286
|
+
if (seen.has(key)) continue;
|
|
287
|
+
seen.add(key);
|
|
288
|
+
|
|
289
|
+
vulns.push({
|
|
290
|
+
name: adv.module_name,
|
|
291
|
+
range: adv.vulnerable_versions,
|
|
292
|
+
severity: adv.severity,
|
|
293
|
+
title: adv.title,
|
|
294
|
+
cve: adv.cves?.[0] || null,
|
|
295
|
+
url: adv.url || null,
|
|
296
|
+
fix: adv.patched_versions !== '<0.0.0'
|
|
297
|
+
? `yarn upgrade ${adv.module_name}`
|
|
298
|
+
: null,
|
|
299
|
+
isDirect: true
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return vulns;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Parse pip-audit JSON output.
|
|
308
|
+
* Format: [{ name, version, vulns: [{ id, fix_versions, description }] }]
|
|
309
|
+
*/
|
|
310
|
+
function parsePipAudit(jsonStr) {
|
|
311
|
+
let data;
|
|
312
|
+
try {
|
|
313
|
+
data = JSON.parse(jsonStr);
|
|
314
|
+
} catch {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const vulns = [];
|
|
319
|
+
for (const pkg of (Array.isArray(data) ? data : [])) {
|
|
320
|
+
for (const vuln of (pkg.vulns || [])) {
|
|
321
|
+
const fixVersion = vuln.fix_versions?.[0];
|
|
322
|
+
vulns.push({
|
|
323
|
+
name: pkg.name,
|
|
324
|
+
range: `==${pkg.version}`,
|
|
325
|
+
severity: 'high', // pip-audit doesn't reliably report CVSS severity
|
|
326
|
+
title: (vuln.description || vuln.id).slice(0, 100),
|
|
327
|
+
cve: vuln.id.startsWith('CVE-') ? vuln.id : null,
|
|
328
|
+
url: `https://osv.dev/vulnerability/${vuln.id}`,
|
|
329
|
+
fix: fixVersion ? `pip install "${pkg.name}>=${fixVersion}"` : null,
|
|
330
|
+
isDirect: true
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return vulns;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Parse bundler-audit text output (plain text format).
|
|
340
|
+
*/
|
|
341
|
+
function parseBundlerAudit(text) {
|
|
342
|
+
const vulns = [];
|
|
343
|
+
const blocks = text.split(/\n(?=Name:)/);
|
|
344
|
+
|
|
345
|
+
for (const block of blocks) {
|
|
346
|
+
const name = block.match(/^Name:\s*(.+)/m)?.[1]?.trim();
|
|
347
|
+
const version = block.match(/Version:\s*(.+)/m)?.[1]?.trim();
|
|
348
|
+
const cve = block.match(/CVE:\s*(.+)/m)?.[1]?.trim();
|
|
349
|
+
const title = block.match(/Title:\s*(.+)/m)?.[1]?.trim();
|
|
350
|
+
const solution = block.match(/Solution:\s*(.+)/m)?.[1]?.trim();
|
|
351
|
+
|
|
352
|
+
if (name) {
|
|
353
|
+
vulns.push({
|
|
354
|
+
name,
|
|
355
|
+
range: version ? `==${version}` : 'unknown',
|
|
356
|
+
severity: 'high',
|
|
357
|
+
title: title || cve || name,
|
|
358
|
+
cve: cve || null,
|
|
359
|
+
url: cve ? `https://www.cve.org/CVERecord?id=${cve}` : null,
|
|
360
|
+
fix: solution ? `bundle update ${name}` : null,
|
|
361
|
+
isDirect: true
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return vulns;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// =============================================================================
|
|
370
|
+
// OUTPUT
|
|
371
|
+
// =============================================================================
|
|
372
|
+
|
|
373
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, moderate: 2, medium: 2, low: 3, unknown: 4 };
|
|
374
|
+
const SEVERITY_COLOR = {
|
|
375
|
+
critical: chalk.red.bold,
|
|
376
|
+
high: chalk.red,
|
|
377
|
+
moderate: chalk.yellow,
|
|
378
|
+
medium: chalk.yellow,
|
|
379
|
+
low: chalk.gray,
|
|
380
|
+
unknown: chalk.gray
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
function printDepFindings(vulns, pm) {
|
|
384
|
+
// Sort by severity
|
|
385
|
+
const sorted = [...vulns].sort((a, b) =>
|
|
386
|
+
(SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4)
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const counts = {};
|
|
390
|
+
for (const v of vulns) {
|
|
391
|
+
counts[v.severity] = (counts[v.severity] || 0) + 1;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log(chalk.red.bold(` Dependency Vulnerabilities (${vulns.length})`));
|
|
395
|
+
console.log(chalk.red(' ' + '─'.repeat(58)));
|
|
396
|
+
console.log();
|
|
397
|
+
|
|
398
|
+
for (const v of sorted) {
|
|
399
|
+
const sevColor = SEVERITY_COLOR[v.severity] || chalk.gray;
|
|
400
|
+
const sevLabel = `[${v.severity.toUpperCase()}]`;
|
|
401
|
+
|
|
402
|
+
console.log(
|
|
403
|
+
` ${sevColor(sevLabel.padEnd(12))}` +
|
|
404
|
+
chalk.white(`${v.name}`) +
|
|
405
|
+
chalk.gray(`@${v.range}`)
|
|
406
|
+
);
|
|
407
|
+
console.log(chalk.gray(` ${v.title}`));
|
|
408
|
+
if (v.cve) {
|
|
409
|
+
console.log(chalk.gray(` ${v.cve}`) + (v.url ? chalk.gray(` ${v.url}`) : ''));
|
|
410
|
+
}
|
|
411
|
+
if (v.fix) {
|
|
412
|
+
console.log(chalk.gray(' Fix: ') + chalk.cyan(v.fix));
|
|
413
|
+
}
|
|
414
|
+
console.log();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
418
|
+
console.log(chalk.red.bold(` ⚠ Found ${vulns.length} vulnerable package(s)`));
|
|
419
|
+
for (const [sev, count] of Object.entries(counts).sort(([a], [b]) => (SEVERITY_ORDER[a] ?? 4) - (SEVERITY_ORDER[b] ?? 4))) {
|
|
420
|
+
const color = SEVERITY_COLOR[sev] || chalk.gray;
|
|
421
|
+
console.log(color(` • ${sev}: ${count}`));
|
|
422
|
+
}
|
|
423
|
+
console.log(chalk.gray(` Run: `) + chalk.cyan(pm.fixCommand) + chalk.gray(' to apply automatic fixes'));
|
|
424
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
425
|
+
console.log();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// =============================================================================
|
|
429
|
+
// INTERNAL: run audit and return normalized vulns (used by score command)
|
|
430
|
+
// =============================================================================
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Run the dependency audit for a given path and return normalized vulnerabilities.
|
|
434
|
+
* Returns { pm, vulns } or { pm: null, vulns: [] } if no manifest found.
|
|
435
|
+
* Does not print anything — used programmatically by other commands.
|
|
436
|
+
*/
|
|
437
|
+
export async function runDepsAudit(rootPath) {
|
|
438
|
+
const pm = detectPackageManager(rootPath);
|
|
439
|
+
if (!pm) return { pm: null, vulns: [] };
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const vulns = runAudit(pm, rootPath);
|
|
443
|
+
return { pm, vulns };
|
|
444
|
+
} catch {
|
|
445
|
+
return { pm, vulns: [] };
|
|
446
|
+
}
|
|
447
|
+
}
|
package/cli/commands/fix.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
MAX_FILE_SIZE
|
|
23
23
|
} from '../utils/patterns.js';
|
|
24
24
|
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
25
|
-
import
|
|
25
|
+
import fg from 'fast-glob';
|
|
26
26
|
import * as output from '../utils/output.js';
|
|
27
27
|
|
|
28
28
|
// =============================================================================
|
|
@@ -74,8 +74,8 @@ export async function fixCommand(options = {}) {
|
|
|
74
74
|
|
|
75
75
|
async function findFiles(rootPath) {
|
|
76
76
|
const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
|
|
77
|
-
const files = await
|
|
78
|
-
cwd: rootPath, absolute: true,
|
|
77
|
+
const files = await fg('**/*', {
|
|
78
|
+
cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
const filtered = [];
|
package/cli/commands/init.js
CHANGED
|
@@ -47,9 +47,13 @@ export async function initCommand(options = {}) {
|
|
|
47
47
|
errors: []
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
// Determine which files to copy
|
|
51
|
-
|
|
52
|
-
|
|
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;
|
|
53
57
|
|
|
54
58
|
// Copy .gitignore
|
|
55
59
|
if (copyGitignore) {
|
|
@@ -61,6 +65,11 @@ export async function initCommand(options = {}) {
|
|
|
61
65
|
await handleSecurityHeaders(targetDir, options.force, results);
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
// Append security rules to AI agent instruction files
|
|
69
|
+
if (copyAgents) {
|
|
70
|
+
await handleAgentFiles(targetDir, options.force, results);
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
// Print summary
|
|
65
74
|
printSummary(results);
|
|
66
75
|
}
|
|
@@ -213,6 +222,80 @@ async function handleSecurityHeaders(targetDir, force, results) {
|
|
|
213
222
|
}
|
|
214
223
|
}
|
|
215
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
|
+
|
|
216
299
|
// =============================================================================
|
|
217
300
|
// SUMMARY
|
|
218
301
|
// =============================================================================
|
package/cli/commands/mcp.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import fs from 'fs';
|
|
33
33
|
import path from 'path';
|
|
34
|
-
import
|
|
34
|
+
import fg from 'fast-glob';
|
|
35
35
|
import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, TEST_FILE_PATTERNS, MAX_FILE_SIZE } from '../utils/patterns.js';
|
|
36
36
|
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
37
37
|
|
|
@@ -169,7 +169,7 @@ async function analyzeFile({ path: filePath }) {
|
|
|
169
169
|
|
|
170
170
|
async function findFiles(rootPath, includeTests) {
|
|
171
171
|
const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
|
|
172
|
-
const files = await
|
|
172
|
+
const files = await fg('**/*', { cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true });
|
|
173
173
|
|
|
174
174
|
return files.filter(file => {
|
|
175
175
|
const ext = path.extname(file).toLowerCase();
|