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.
@@ -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 = `[![Ship Safe Score](${badgeUrl})](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
+ }
@@ -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 (filename.endsWith('.min.js') || filename.endsWith('.min.css')) return;
57
-
58
- // Add to pending and debounce
59
- pendingFiles.add(fullPath);
60
-
61
- if (debounceTimer) clearTimeout(debounceTimer);
62
- debounceTimer = setTimeout(() => {
63
- const filesToScan = [...pendingFiles];
64
- pendingFiles.clear();
65
- scanChangedFiles(filesToScan, allPatterns, absolutePath);
66
- }, 300);
67
- });
68
-
69
- // Keep the process alive
70
- process.on('SIGINT', () => {
71
- watcher.close();
72
- console.log();
73
- output.info('Watch mode stopped.');
74
- process.exit(0);
75
- });
76
-
77
- // Prevent Node from exiting
78
- setInterval(() => {}, 1000 * 60 * 60);
79
-
80
- } catch (err) {
81
- output.error(`Watch failed: ${err.message}`);
82
- console.log(chalk.gray(' Try: npx ship-safe watch . --poll'));
83
- process.exit(1);
84
- }
85
- }
86
-
87
- function scanChangedFiles(files, patterns, rootPath) {
88
- const timestamp = new Date().toLocaleTimeString();
89
- let totalFindings = 0;
90
-
91
- for (const filePath of files) {
92
- if (!fs.existsSync(filePath)) continue;
93
-
94
- try {
95
- const stats = fs.statSync(filePath);
96
- if (stats.size > 1_000_000) continue;
97
- } catch {
98
- continue;
99
- }
100
-
101
- const findings = scanFile(filePath, patterns);
102
- if (findings.length > 0) {
103
- totalFindings += findings.length;
104
- const relPath = path.relative(rootPath, filePath);
105
-
106
- for (const f of findings) {
107
- const sevColor = f.severity === 'critical' ? chalk.red.bold
108
- : f.severity === 'high' ? chalk.yellow
109
- : chalk.blue;
110
-
111
- console.log(
112
- chalk.gray(` [${timestamp}] `) +
113
- sevColor(`[${f.severity.toUpperCase()}]`) +
114
- chalk.white(` ${relPath}:${f.line} `) +
115
- chalk.gray(f.patternName)
116
- );
117
- }
118
- }
119
- }
120
-
121
- if (totalFindings === 0 && files.length > 0) {
122
- console.log(chalk.gray(` [${timestamp}] ${files.length} file(s) scanned — clean`));
123
- }
124
- }
125
-
126
- function scanFile(filePath, patterns) {
127
- const findings = [];
128
- try {
129
- const content = fs.readFileSync(filePath, 'utf-8');
130
- const lines = content.split('\n');
131
-
132
- for (let i = 0; i < lines.length; i++) {
133
- const line = lines[i];
134
- if (/ship-safe-ignore/i.test(line)) continue;
135
-
136
- for (const pattern of patterns) {
137
- pattern.pattern.lastIndex = 0;
138
- let match;
139
- while ((match = pattern.pattern.exec(line)) !== null) {
140
- if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
141
- findings.push({
142
- line: i + 1,
143
- patternName: pattern.name,
144
- severity: pattern.severity,
145
- matched: match[0],
146
- category: pattern.category || 'secret',
147
- });
148
- }
149
- }
150
- }
151
- } catch { /* skip */ }
152
-
153
- const seen = new Set();
154
- return findings.filter(f => {
155
- const key = `${f.line}:${f.matched}`;
156
- if (seen.has(key)) return false;
157
- seen.add(key);
158
- return true;
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
 
@@ -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 <= 10) {
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, 6) + '***' + secret.substring(secret.length - 4);
129
+ return secret.substring(0, 4) + '***' + secret.substring(secret.length - 4);
127
130
  }
128
131
 
129
132
  /**