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/score.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
SECURITY_PATTERNS,
|
|
37
37
|
SKIP_DIRS,
|
|
38
38
|
SKIP_EXTENSIONS,
|
|
39
|
+
SKIP_FILENAMES,
|
|
39
40
|
TEST_FILE_PATTERNS,
|
|
40
41
|
MAX_FILE_SIZE
|
|
41
42
|
} from '../utils/patterns.js';
|
|
@@ -335,6 +336,7 @@ async function findFiles(rootPath) {
|
|
|
335
336
|
for (const file of files) {
|
|
336
337
|
const ext = path.extname(file).toLowerCase();
|
|
337
338
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
339
|
+
if (SKIP_FILENAMES.has(path.basename(file))) continue;
|
|
338
340
|
|
|
339
341
|
const basename = path.basename(file);
|
|
340
342
|
if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) continue;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibe Check Command
|
|
3
|
+
* ==================
|
|
4
|
+
*
|
|
5
|
+
* Fun, emoji-rich security check with shareable results.
|
|
6
|
+
* Same security scan as `audit`, but with personality.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* npx ship-safe vibe-check [path] Run a vibe check
|
|
10
|
+
* npx ship-safe vibe-check . --badge Generate a markdown badge
|
|
11
|
+
*
|
|
12
|
+
* OUTPUT:
|
|
13
|
+
* Big ASCII art grade, emoji severity indicators,
|
|
14
|
+
* "vibes" rating, and a shareable one-liner.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import chalk from 'chalk';
|
|
20
|
+
import ora from 'ora';
|
|
21
|
+
import { buildOrchestrator } from '../agents/index.js';
|
|
22
|
+
import { ScoringEngine } from '../agents/scoring-engine.js';
|
|
23
|
+
import { runDepsAudit } from './deps.js';
|
|
24
|
+
import {
|
|
25
|
+
SECRET_PATTERNS,
|
|
26
|
+
SKIP_DIRS,
|
|
27
|
+
SKIP_EXTENSIONS,
|
|
28
|
+
SKIP_FILENAMES,
|
|
29
|
+
MAX_FILE_SIZE,
|
|
30
|
+
loadGitignorePatterns
|
|
31
|
+
} from '../utils/patterns.js';
|
|
32
|
+
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
33
|
+
import fg from 'fast-glob';
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// VIBES DATA
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
const VIBE_GRADES = {
|
|
40
|
+
A: {
|
|
41
|
+
emoji: '🛡️',
|
|
42
|
+
vibe: 'immaculate',
|
|
43
|
+
ascii: `
|
|
44
|
+
╔═══╗
|
|
45
|
+
║ A ║
|
|
46
|
+
╚═══╝`,
|
|
47
|
+
message: 'Your security vibes are IMMACULATE. Ship it! 🚀',
|
|
48
|
+
color: chalk.green.bold,
|
|
49
|
+
},
|
|
50
|
+
B: {
|
|
51
|
+
emoji: '✅',
|
|
52
|
+
vibe: 'solid',
|
|
53
|
+
ascii: `
|
|
54
|
+
╔═══╗
|
|
55
|
+
║ B ║
|
|
56
|
+
╚═══╝`,
|
|
57
|
+
message: 'Solid vibes. A few things to tighten up, but you\'re in good shape. 💪',
|
|
58
|
+
color: chalk.cyan.bold,
|
|
59
|
+
},
|
|
60
|
+
C: {
|
|
61
|
+
emoji: '⚠️',
|
|
62
|
+
vibe: 'mid',
|
|
63
|
+
ascii: `
|
|
64
|
+
╔═══╗
|
|
65
|
+
║ C ║
|
|
66
|
+
╚═══╝`,
|
|
67
|
+
message: 'Mid vibes. Some security gaps need attention before you ship. 🔧',
|
|
68
|
+
color: chalk.yellow.bold,
|
|
69
|
+
},
|
|
70
|
+
D: {
|
|
71
|
+
emoji: '🚨',
|
|
72
|
+
vibe: 'sketchy',
|
|
73
|
+
ascii: `
|
|
74
|
+
╔═══╗
|
|
75
|
+
║ D ║
|
|
76
|
+
╚═══╝`,
|
|
77
|
+
message: 'Sketchy vibes. Serious issues found — fix these before deploying. 🛑',
|
|
78
|
+
color: chalk.red.bold,
|
|
79
|
+
},
|
|
80
|
+
F: {
|
|
81
|
+
emoji: '💀',
|
|
82
|
+
vibe: 'cooked',
|
|
83
|
+
ascii: `
|
|
84
|
+
╔═══╗
|
|
85
|
+
║ F ║
|
|
86
|
+
╚═══╝`,
|
|
87
|
+
message: 'You are cooked. Critical vulnerabilities everywhere. DO NOT SHIP. 🔥',
|
|
88
|
+
color: chalk.red.bold,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const SEV_EMOJI = {
|
|
93
|
+
critical: '💀',
|
|
94
|
+
high: '🔴',
|
|
95
|
+
medium: '🟡',
|
|
96
|
+
low: '🔵',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// MAIN COMMAND
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
export async function vibeCheckCommand(targetPath = '.', options = {}) {
|
|
104
|
+
const absolutePath = path.resolve(targetPath);
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(absolutePath)) {
|
|
107
|
+
console.error(chalk.red(`Path does not exist: ${absolutePath}`));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const projectName = path.basename(absolutePath);
|
|
112
|
+
|
|
113
|
+
console.log();
|
|
114
|
+
console.log(chalk.cyan.bold(' 🎵 VIBE CHECK 🎵'));
|
|
115
|
+
console.log(chalk.gray(` Scanning ${projectName}...`));
|
|
116
|
+
console.log();
|
|
117
|
+
|
|
118
|
+
const startTime = Date.now();
|
|
119
|
+
|
|
120
|
+
// ── Secret Scan ──────────────────────────────────────────────────────────
|
|
121
|
+
const spinner = ora({ text: 'Checking the vibes...', color: 'magenta' }).start();
|
|
122
|
+
|
|
123
|
+
const allFiles = await findFiles(absolutePath);
|
|
124
|
+
const secretFindings = [];
|
|
125
|
+
|
|
126
|
+
for (const file of allFiles) {
|
|
127
|
+
try {
|
|
128
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
129
|
+
const lines = content.split('\n');
|
|
130
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
131
|
+
const line = lines[lineNum];
|
|
132
|
+
if (/ship-safe-ignore/i.test(line)) continue;
|
|
133
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
134
|
+
pattern.pattern.lastIndex = 0;
|
|
135
|
+
let match;
|
|
136
|
+
while ((match = pattern.pattern.exec(line)) !== null) {
|
|
137
|
+
if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
|
|
138
|
+
secretFindings.push({
|
|
139
|
+
file, line: lineNum + 1, column: match.index + 1,
|
|
140
|
+
matched: match[0], severity: pattern.severity,
|
|
141
|
+
category: pattern.category || 'secrets',
|
|
142
|
+
rule: pattern.name, title: pattern.name.replace(/_/g, ' '),
|
|
143
|
+
description: pattern.description,
|
|
144
|
+
confidence: getConfidence(pattern, match[0]),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch { /* skip */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Agent Scan ──────────────────────────────────────────────────────────
|
|
153
|
+
const orchestrator = buildOrchestrator();
|
|
154
|
+
const results = await orchestrator.runAll(absolutePath, { quiet: true });
|
|
155
|
+
|
|
156
|
+
// ── Dependency Audit ─────────────────────────────────────────────────────
|
|
157
|
+
let depVulns = [];
|
|
158
|
+
try {
|
|
159
|
+
const depResult = await runDepsAudit(absolutePath);
|
|
160
|
+
depVulns = depResult.vulns || [];
|
|
161
|
+
} catch { /* skip */ }
|
|
162
|
+
|
|
163
|
+
spinner.stop();
|
|
164
|
+
|
|
165
|
+
// ── Merge & Score ─────────────────────────────────────────────────────────
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
const allFindings = [...secretFindings, ...results.findings].filter(f => {
|
|
168
|
+
const key = `${f.file}:${f.line}:${f.rule}`;
|
|
169
|
+
if (seen.has(key)) return false;
|
|
170
|
+
seen.add(key);
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const scoringEngine = new ScoringEngine();
|
|
175
|
+
const scoreResult = scoringEngine.compute(allFindings, depVulns);
|
|
176
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
177
|
+
|
|
178
|
+
// ── Display ──────────────────────────────────────────────────────────────
|
|
179
|
+
const grade = VIBE_GRADES[scoreResult.grade.letter] || VIBE_GRADES.F;
|
|
180
|
+
const score = Math.round(scoreResult.score * 10) / 10;
|
|
181
|
+
|
|
182
|
+
const critical = allFindings.filter(f => f.severity === 'critical').length;
|
|
183
|
+
const high = allFindings.filter(f => f.severity === 'high').length;
|
|
184
|
+
const medium = allFindings.filter(f => f.severity === 'medium').length;
|
|
185
|
+
const low = allFindings.filter(f => f.severity === 'low').length;
|
|
186
|
+
|
|
187
|
+
// Big grade display
|
|
188
|
+
console.log(grade.color(grade.ascii));
|
|
189
|
+
console.log();
|
|
190
|
+
console.log(grade.color(` ${grade.emoji} Score: ${score}/100 | Vibes: ${grade.vibe.toUpperCase()}`));
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(grade.color(` ${grade.message}`));
|
|
193
|
+
console.log();
|
|
194
|
+
|
|
195
|
+
// Severity breakdown
|
|
196
|
+
console.log(chalk.white.bold(' Breakdown:'));
|
|
197
|
+
if (critical > 0) console.log(` ${SEV_EMOJI.critical} Critical: ${critical}`);
|
|
198
|
+
if (high > 0) console.log(` ${SEV_EMOJI.high} High: ${high}`);
|
|
199
|
+
if (medium > 0) console.log(` ${SEV_EMOJI.medium} Medium: ${medium}`);
|
|
200
|
+
if (low > 0) console.log(` ${SEV_EMOJI.low} Low: ${low}`);
|
|
201
|
+
if (depVulns.length > 0) console.log(` 📦 Dep CVEs: ${depVulns.length}`);
|
|
202
|
+
if (allFindings.length === 0 && depVulns.length === 0) {
|
|
203
|
+
console.log(` ✨ Zero issues found!`);
|
|
204
|
+
}
|
|
205
|
+
console.log(chalk.gray(` ⏱️ ${duration}s`));
|
|
206
|
+
console.log();
|
|
207
|
+
|
|
208
|
+
// Top 3 issues
|
|
209
|
+
if (allFindings.length > 0) {
|
|
210
|
+
console.log(chalk.white.bold(' Top issues to fix:'));
|
|
211
|
+
const top = allFindings
|
|
212
|
+
.sort((a, b) => {
|
|
213
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
214
|
+
return (order[a.severity] ?? 4) - (order[b.severity] ?? 4);
|
|
215
|
+
})
|
|
216
|
+
.slice(0, 3);
|
|
217
|
+
for (const f of top) {
|
|
218
|
+
const rel = path.relative(absolutePath, f.file).replace(/\\/g, '/');
|
|
219
|
+
console.log(` ${SEV_EMOJI[f.severity] || '⚪'} ${f.title || f.rule} ${chalk.gray(`(${rel}:${f.line})`)}`);
|
|
220
|
+
}
|
|
221
|
+
console.log();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Shareable one-liner ──────────────────────────────────────────────────
|
|
225
|
+
const shareLine = `${grade.emoji} ${projectName}: ${score}/100 (${scoreResult.grade.letter}) — ${grade.vibe} vibes | ${allFindings.length} findings | Scanned with Ship Safe`;
|
|
226
|
+
console.log(chalk.gray(' Share your vibes:'));
|
|
227
|
+
console.log(chalk.cyan(` ${shareLine}`));
|
|
228
|
+
console.log();
|
|
229
|
+
|
|
230
|
+
// ── Badge ─────────────────────────────────────────────────────────────────
|
|
231
|
+
if (options.badge) {
|
|
232
|
+
const badgeColor = {
|
|
233
|
+
A: 'brightgreen', B: 'blue', C: 'yellow', D: 'orange', F: 'red',
|
|
234
|
+
}[scoreResult.grade.letter] || 'lightgrey';
|
|
235
|
+
const badgeUrl = `https://img.shields.io/badge/ship--safe-${score}%2F100_${scoreResult.grade.letter}-${badgeColor}`;
|
|
236
|
+
const badgeMd = `[](https://shipsafecli.com)`;
|
|
237
|
+
|
|
238
|
+
console.log(chalk.white.bold(' Markdown badge:'));
|
|
239
|
+
console.log(chalk.cyan(` ${badgeMd}`));
|
|
240
|
+
console.log();
|
|
241
|
+
|
|
242
|
+
// Write badge to README if it exists and doesn't have one already
|
|
243
|
+
const readmePath = path.join(absolutePath, 'README.md');
|
|
244
|
+
if (fs.existsSync(readmePath)) {
|
|
245
|
+
const readme = fs.readFileSync(readmePath, 'utf-8');
|
|
246
|
+
if (!readme.includes('ship--safe')) {
|
|
247
|
+
console.log(chalk.gray(' Add this badge to your README.md to show off your security score!'));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
process.exit(allFindings.length > 0 || depVulns.length > 0 ? 1 : 0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// =============================================================================
|
|
256
|
+
// FILE FINDER (reused from CI)
|
|
257
|
+
// =============================================================================
|
|
258
|
+
|
|
259
|
+
async function findFiles(rootPath) {
|
|
260
|
+
const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
|
|
261
|
+
const gitignoreGlobs = loadGitignorePatterns(rootPath);
|
|
262
|
+
globIgnore.push(...gitignoreGlobs);
|
|
263
|
+
|
|
264
|
+
const files = await fg('**/*', {
|
|
265
|
+
cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return files.filter(file => {
|
|
269
|
+
const ext = path.extname(file).toLowerCase();
|
|
270
|
+
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
271
|
+
if (SKIP_FILENAMES.has(path.basename(file))) return false;
|
|
272
|
+
if (path.basename(file).endsWith('.min.js') || path.basename(file).endsWith('.min.css')) return false;
|
|
273
|
+
try { if (fs.statSync(file).size > MAX_FILE_SIZE) return false; } catch { return false; }
|
|
274
|
+
return true;
|
|
275
|
+
});
|
|
276
|
+
}
|
package/cli/commands/watch.js
CHANGED
|
@@ -1,160 +1,161 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Watch Command
|
|
3
|
-
* ==============
|
|
4
|
-
*
|
|
5
|
-
* Continuous file monitoring mode. Watches for file changes
|
|
6
|
-
* and incrementally scans modified files.
|
|
7
|
-
*
|
|
8
|
-
* USAGE:
|
|
9
|
-
* npx ship-safe watch [path] Start watching for changes
|
|
10
|
-
* npx ship-safe watch . --poll Use polling (for network drives)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import fs from 'fs';
|
|
14
|
-
import path from 'path';
|
|
15
|
-
import chalk from 'chalk';
|
|
16
|
-
import { SKIP_DIRS, SKIP_EXTENSIONS, SECRET_PATTERNS, SECURITY_PATTERNS } from '../utils/patterns.js';
|
|
17
|
-
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
18
|
-
import * as output from '../utils/output.js';
|
|
19
|
-
|
|
20
|
-
export async function watchCommand(targetPath = '.', options = {}) {
|
|
21
|
-
const absolutePath = path.resolve(targetPath);
|
|
22
|
-
|
|
23
|
-
if (!fs.existsSync(absolutePath)) {
|
|
24
|
-
output.error(`Path does not exist: ${absolutePath}`);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
console.log();
|
|
29
|
-
output.header('Ship Safe — Watch Mode');
|
|
30
|
-
console.log();
|
|
31
|
-
console.log(chalk.cyan(' Watching for file changes...'));
|
|
32
|
-
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
33
|
-
console.log();
|
|
34
|
-
|
|
35
|
-
const allPatterns = [...SECRET_PATTERNS, ...SECURITY_PATTERNS];
|
|
36
|
-
const skipDirSet = SKIP_DIRS;
|
|
37
|
-
let debounceTimer = null;
|
|
38
|
-
const pendingFiles = new Set();
|
|
39
|
-
|
|
40
|
-
// Use fs.watch recursively
|
|
41
|
-
try {
|
|
42
|
-
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
43
|
-
if (!filename) return;
|
|
44
|
-
|
|
45
|
-
const fullPath = path.join(absolutePath, filename); // ship-safe-ignore — filename from fs.watch, not user input
|
|
46
|
-
const relPath = filename.replace(/\\/g, '/');
|
|
47
|
-
|
|
48
|
-
// Skip directories we don't care about
|
|
49
|
-
for (const skipDir of skipDirSet) {
|
|
50
|
-
if (relPath.includes(`${skipDir}/`) || relPath.startsWith(`${skipDir}/`)) return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Skip non-code files
|
|
54
|
-
const ext = path.extname(filename).toLowerCase();
|
|
55
|
-
if (SKIP_EXTENSIONS.has(ext)) return;
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
debounceTimer
|
|
63
|
-
|
|
64
|
-
pendingFiles
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
: chalk.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
chalk.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
seen.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Watch Command
|
|
3
|
+
* ==============
|
|
4
|
+
*
|
|
5
|
+
* Continuous file monitoring mode. Watches for file changes
|
|
6
|
+
* and incrementally scans modified files.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* npx ship-safe watch [path] Start watching for changes
|
|
10
|
+
* npx ship-safe watch . --poll Use polling (for network drives)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, SECRET_PATTERNS, SECURITY_PATTERNS } from '../utils/patterns.js';
|
|
17
|
+
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
18
|
+
import * as output from '../utils/output.js';
|
|
19
|
+
|
|
20
|
+
export async function watchCommand(targetPath = '.', options = {}) {
|
|
21
|
+
const absolutePath = path.resolve(targetPath);
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(absolutePath)) {
|
|
24
|
+
output.error(`Path does not exist: ${absolutePath}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log();
|
|
29
|
+
output.header('Ship Safe — Watch Mode');
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(chalk.cyan(' Watching for file changes...'));
|
|
32
|
+
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
33
|
+
console.log();
|
|
34
|
+
|
|
35
|
+
const allPatterns = [...SECRET_PATTERNS, ...SECURITY_PATTERNS];
|
|
36
|
+
const skipDirSet = SKIP_DIRS;
|
|
37
|
+
let debounceTimer = null;
|
|
38
|
+
const pendingFiles = new Set();
|
|
39
|
+
|
|
40
|
+
// Use fs.watch recursively
|
|
41
|
+
try {
|
|
42
|
+
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => { // ship-safe-ignore — filename from fs.watch OS event, not user input
|
|
43
|
+
if (!filename) return; // ship-safe-ignore
|
|
44
|
+
|
|
45
|
+
const fullPath = path.join(absolutePath, filename); // ship-safe-ignore — filename from fs.watch, not user input
|
|
46
|
+
const relPath = filename.replace(/\\/g, '/');
|
|
47
|
+
|
|
48
|
+
// Skip directories we don't care about
|
|
49
|
+
for (const skipDir of skipDirSet) {
|
|
50
|
+
if (relPath.includes(`${skipDir}/`) || relPath.startsWith(`${skipDir}/`)) return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Skip non-code files
|
|
54
|
+
const ext = path.extname(filename).toLowerCase(); // ship-safe-ignore — filename from fs.watch OS event
|
|
55
|
+
if (SKIP_EXTENSIONS.has(ext)) return;
|
|
56
|
+
if (SKIP_FILENAMES.has(path.basename(filename))) return; // ship-safe-ignore
|
|
57
|
+
if (filename.endsWith('.min.js') || filename.endsWith('.min.css')) return;
|
|
58
|
+
|
|
59
|
+
// Add to pending and debounce
|
|
60
|
+
pendingFiles.add(fullPath);
|
|
61
|
+
|
|
62
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
63
|
+
debounceTimer = setTimeout(() => {
|
|
64
|
+
const filesToScan = [...pendingFiles];
|
|
65
|
+
pendingFiles.clear();
|
|
66
|
+
scanChangedFiles(filesToScan, allPatterns, absolutePath);
|
|
67
|
+
}, 300);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Keep the process alive
|
|
71
|
+
process.on('SIGINT', () => {
|
|
72
|
+
watcher.close();
|
|
73
|
+
console.log();
|
|
74
|
+
output.info('Watch mode stopped.');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Prevent Node from exiting
|
|
79
|
+
setInterval(() => {}, 1000 * 60 * 60);
|
|
80
|
+
|
|
81
|
+
} catch (err) {
|
|
82
|
+
output.error(`Watch failed: ${err.message}`);
|
|
83
|
+
console.log(chalk.gray(' Try: npx ship-safe watch . --poll'));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function scanChangedFiles(files, patterns, rootPath) {
|
|
89
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
90
|
+
let totalFindings = 0;
|
|
91
|
+
|
|
92
|
+
for (const filePath of files) {
|
|
93
|
+
if (!fs.existsSync(filePath)) continue;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const stats = fs.statSync(filePath);
|
|
97
|
+
if (stats.size > 1_000_000) continue;
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const findings = scanFile(filePath, patterns);
|
|
103
|
+
if (findings.length > 0) {
|
|
104
|
+
totalFindings += findings.length;
|
|
105
|
+
const relPath = path.relative(rootPath, filePath);
|
|
106
|
+
|
|
107
|
+
for (const f of findings) {
|
|
108
|
+
const sevColor = f.severity === 'critical' ? chalk.red.bold
|
|
109
|
+
: f.severity === 'high' ? chalk.yellow
|
|
110
|
+
: chalk.blue;
|
|
111
|
+
|
|
112
|
+
console.log(
|
|
113
|
+
chalk.gray(` [${timestamp}] `) +
|
|
114
|
+
sevColor(`[${f.severity.toUpperCase()}]`) +
|
|
115
|
+
chalk.white(` ${relPath}:${f.line} `) +
|
|
116
|
+
chalk.gray(f.patternName)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (totalFindings === 0 && files.length > 0) {
|
|
123
|
+
console.log(chalk.gray(` [${timestamp}] ${files.length} file(s) scanned — clean`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function scanFile(filePath, patterns) {
|
|
128
|
+
const findings = [];
|
|
129
|
+
try {
|
|
130
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < lines.length; i++) {
|
|
134
|
+
const line = lines[i];
|
|
135
|
+
if (/ship-safe-ignore/i.test(line)) continue;
|
|
136
|
+
|
|
137
|
+
for (const pattern of patterns) {
|
|
138
|
+
pattern.pattern.lastIndex = 0;
|
|
139
|
+
let match;
|
|
140
|
+
while ((match = pattern.pattern.exec(line)) !== null) {
|
|
141
|
+
if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
|
|
142
|
+
findings.push({
|
|
143
|
+
line: i + 1,
|
|
144
|
+
patternName: pattern.name,
|
|
145
|
+
severity: pattern.severity,
|
|
146
|
+
matched: match[0],
|
|
147
|
+
category: pattern.category || 'secret',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch { /* skip */ }
|
|
153
|
+
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
return findings.filter(f => {
|
|
156
|
+
const key = `${f.line}:${f.matched}`;
|
|
157
|
+
if (seen.has(key)) return false;
|
|
158
|
+
seen.add(key);
|
|
159
|
+
return true;
|
|
160
|
+
});
|
|
161
|
+
}
|
package/cli/index.js
CHANGED
|
@@ -25,8 +25,13 @@ export { doctorCommand } from './commands/doctor.js';
|
|
|
25
25
|
// ── v4.3 Commands ─────────────────────────────────────────────────────────────
|
|
26
26
|
export { baselineCommand } from './commands/baseline.js';
|
|
27
27
|
|
|
28
|
+
// ── v6.0 Commands ─────────────────────────────────────────────────────────────
|
|
29
|
+
export { diffCommand } from './commands/diff.js';
|
|
30
|
+
export { vibeCheckCommand } from './commands/vibe-check.js';
|
|
31
|
+
export { benchmarkCommand } from './commands/benchmark.js';
|
|
32
|
+
|
|
28
33
|
// ── Patterns ──────────────────────────────────────────────────────────────────
|
|
29
|
-
export { SECRET_PATTERNS, SECURITY_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS } from './utils/patterns.js';
|
|
34
|
+
export { SECRET_PATTERNS, SECURITY_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES } from './utils/patterns.js';
|
|
30
35
|
|
|
31
36
|
// ── Agent Framework ───────────────────────────────────────────────────────────
|
|
32
37
|
export { BaseAgent, createFinding } from './agents/base-agent.js';
|
|
@@ -46,6 +51,8 @@ export { GitHistoryScanner } from './agents/git-history-scanner.js';
|
|
|
46
51
|
export { CICDScanner } from './agents/cicd-scanner.js';
|
|
47
52
|
export { APIFuzzer } from './agents/api-fuzzer.js';
|
|
48
53
|
export { SupabaseRLSAgent } from './agents/supabase-rls-agent.js';
|
|
54
|
+
export { VibeCodingAgent } from './agents/vibe-coding-agent.js';
|
|
55
|
+
export { ExceptionHandlerAgent } from './agents/exception-handler-agent.js';
|
|
49
56
|
|
|
50
57
|
// ── Supporting Modules ────────────────────────────────────────────────────────
|
|
51
58
|
export { ScoringEngine, GRADES, CATEGORIES } from './agents/scoring-engine.js';
|
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'url';
|
|
|
23
23
|
import { dirname, join } from 'path';
|
|
24
24
|
|
|
25
25
|
// Read version from package.json
|
|
26
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url); // ship-safe-ignore — module's own path via import.meta.url, not user input
|
|
27
27
|
const __dirname = dirname(__filename);
|
|
28
28
|
const PACKAGE_VERSION = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8')).version;
|
|
29
29
|
|
package/cli/utils/output.js
CHANGED
|
@@ -120,10 +120,13 @@ export function vulnerabilityFinding(file, line, patternName, severity, matched,
|
|
|
120
120
|
* Mask the middle of a secret for safe display
|
|
121
121
|
*/
|
|
122
122
|
export function maskSecret(secret) {
|
|
123
|
-
if (secret.length <=
|
|
123
|
+
if (secret.length <= 6) {
|
|
124
|
+
return '***';
|
|
125
|
+
}
|
|
126
|
+
if (secret.length <= 12) {
|
|
124
127
|
return secret.substring(0, 3) + '***';
|
|
125
128
|
}
|
|
126
|
-
return secret.substring(0,
|
|
129
|
+
return secret.substring(0, 4) + '***' + secret.substring(secret.length - 4);
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
/**
|