ship-safe 6.1.1 → 6.3.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.
Files changed (49) hide show
  1. package/README.md +748 -641
  2. package/cli/agents/api-fuzzer.js +345 -345
  3. package/cli/agents/auth-bypass-agent.js +348 -348
  4. package/cli/agents/base-agent.js +272 -272
  5. package/cli/agents/cicd-scanner.js +236 -201
  6. package/cli/agents/config-auditor.js +521 -521
  7. package/cli/agents/deep-analyzer.js +6 -2
  8. package/cli/agents/git-history-scanner.js +170 -170
  9. package/cli/agents/html-reporter.js +568 -568
  10. package/cli/agents/index.js +85 -84
  11. package/cli/agents/injection-tester.js +500 -500
  12. package/cli/agents/legal-risk-agent.js +302 -0
  13. package/cli/agents/llm-redteam.js +251 -251
  14. package/cli/agents/mobile-scanner.js +231 -231
  15. package/cli/agents/orchestrator.js +322 -322
  16. package/cli/agents/pii-compliance-agent.js +301 -301
  17. package/cli/agents/scoring-engine.js +248 -248
  18. package/cli/agents/supabase-rls-agent.js +154 -154
  19. package/cli/agents/supply-chain-agent.js +650 -507
  20. package/cli/bin/ship-safe.js +464 -426
  21. package/cli/commands/agent.js +608 -608
  22. package/cli/commands/audit.js +1006 -980
  23. package/cli/commands/baseline.js +193 -193
  24. package/cli/commands/ci.js +342 -342
  25. package/cli/commands/deps.js +516 -516
  26. package/cli/commands/doctor.js +159 -159
  27. package/cli/commands/fix.js +218 -218
  28. package/cli/commands/hooks.js +268 -0
  29. package/cli/commands/init.js +407 -407
  30. package/cli/commands/legal.js +158 -0
  31. package/cli/commands/mcp.js +304 -304
  32. package/cli/commands/red-team.js +7 -1
  33. package/cli/commands/remediate.js +798 -798
  34. package/cli/commands/rotate.js +571 -571
  35. package/cli/commands/scan.js +569 -569
  36. package/cli/commands/score.js +449 -449
  37. package/cli/commands/watch.js +281 -281
  38. package/cli/hooks/patterns.js +313 -0
  39. package/cli/hooks/post-tool-use.js +140 -0
  40. package/cli/hooks/pre-tool-use.js +186 -0
  41. package/cli/index.js +73 -69
  42. package/cli/providers/llm-provider.js +397 -287
  43. package/cli/utils/autofix-rules.js +74 -74
  44. package/cli/utils/cache-manager.js +311 -311
  45. package/cli/utils/output.js +230 -230
  46. package/cli/utils/patterns.js +1121 -1121
  47. package/cli/utils/pdf-generator.js +94 -94
  48. package/package.json +69 -69
  49. package/configs/supabase/rls-templates.sql +0 -242
@@ -1,281 +1,281 @@
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
- // Agent config files to watch
21
- const AGENT_CONFIG_PATTERNS = [
22
- '.cursorrules', '.windsurfrules', 'CLAUDE.md', 'AGENTS.md',
23
- '.github/copilot-instructions.md', '.aider.conf.yml',
24
- '.continue/config.json', 'openclaw.json', 'openclaw.config.json',
25
- 'clawhub.json', 'mcp.json', '.claude/settings.json',
26
- '.cursor/mcp.json', '.vscode/mcp.json',
27
- ];
28
-
29
- export async function watchCommand(targetPath = '.', options = {}) {
30
- const absolutePath = path.resolve(targetPath);
31
-
32
- if (!fs.existsSync(absolutePath)) {
33
- output.error(`Path does not exist: ${absolutePath}`);
34
- process.exit(1);
35
- }
36
-
37
- // Config-only watch mode
38
- if (options.configs) {
39
- return watchConfigs(absolutePath);
40
- }
41
-
42
- console.log();
43
- output.header('Ship Safe — Watch Mode');
44
- console.log();
45
- console.log(chalk.cyan(' Watching for file changes...'));
46
- console.log(chalk.gray(' Press Ctrl+C to stop'));
47
- console.log();
48
-
49
- const allPatterns = [...SECRET_PATTERNS, ...SECURITY_PATTERNS];
50
- const skipDirSet = SKIP_DIRS;
51
- let debounceTimer = null;
52
- const pendingFiles = new Set();
53
-
54
- // Use fs.watch recursively
55
- try {
56
- const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => { // ship-safe-ignore — filename from fs.watch OS event, not user input
57
- if (!filename) return; // ship-safe-ignore
58
-
59
- const fullPath = path.join(absolutePath, filename); // ship-safe-ignore — filename from fs.watch, not user input
60
- const relPath = filename.replace(/\\/g, '/');
61
-
62
- // Skip directories we don't care about
63
- for (const skipDir of skipDirSet) {
64
- if (relPath.includes(`${skipDir}/`) || relPath.startsWith(`${skipDir}/`)) return;
65
- }
66
-
67
- // Skip non-code files
68
- const ext = path.extname(filename).toLowerCase(); // ship-safe-ignore — filename from fs.watch OS event
69
- if (SKIP_EXTENSIONS.has(ext)) return;
70
- if (SKIP_FILENAMES.has(path.basename(filename))) return; // ship-safe-ignore
71
- if (filename.endsWith('.min.js') || filename.endsWith('.min.css')) return;
72
-
73
- // Add to pending and debounce
74
- pendingFiles.add(fullPath);
75
-
76
- if (debounceTimer) clearTimeout(debounceTimer);
77
- debounceTimer = setTimeout(() => {
78
- const filesToScan = [...pendingFiles];
79
- pendingFiles.clear();
80
- scanChangedFiles(filesToScan, allPatterns, absolutePath);
81
- }, 300);
82
- });
83
-
84
- // Keep the process alive
85
- process.on('SIGINT', () => {
86
- watcher.close();
87
- console.log();
88
- output.info('Watch mode stopped.');
89
- process.exit(0);
90
- });
91
-
92
- // Prevent Node from exiting
93
- setInterval(() => {}, 1000 * 60 * 60);
94
-
95
- } catch (err) {
96
- output.error(`Watch failed: ${err.message}`);
97
- console.log(chalk.gray(' Try: npx ship-safe watch . --poll'));
98
- process.exit(1);
99
- }
100
- }
101
-
102
- function scanChangedFiles(files, patterns, rootPath) {
103
- const timestamp = new Date().toLocaleTimeString();
104
- let totalFindings = 0;
105
-
106
- for (const filePath of files) {
107
- if (!fs.existsSync(filePath)) continue;
108
-
109
- try {
110
- const stats = fs.statSync(filePath);
111
- if (stats.size > 1_000_000) continue;
112
- } catch {
113
- continue;
114
- }
115
-
116
- const findings = scanFile(filePath, patterns);
117
- if (findings.length > 0) {
118
- totalFindings += findings.length;
119
- const relPath = path.relative(rootPath, filePath);
120
-
121
- for (const f of findings) {
122
- const sevColor = f.severity === 'critical' ? chalk.red.bold
123
- : f.severity === 'high' ? chalk.yellow
124
- : chalk.blue;
125
-
126
- console.log(
127
- chalk.gray(` [${timestamp}] `) +
128
- sevColor(`[${f.severity.toUpperCase()}]`) +
129
- chalk.white(` ${relPath}:${f.line} `) +
130
- chalk.gray(f.patternName)
131
- );
132
- }
133
- }
134
- }
135
-
136
- if (totalFindings === 0 && files.length > 0) {
137
- console.log(chalk.gray(` [${timestamp}] ${files.length} file(s) scanned — clean`));
138
- }
139
- }
140
-
141
- function scanFile(filePath, patterns) {
142
- const findings = [];
143
- try {
144
- const content = fs.readFileSync(filePath, 'utf-8');
145
- const lines = content.split('\n');
146
-
147
- for (let i = 0; i < lines.length; i++) {
148
- const line = lines[i];
149
- if (/ship-safe-ignore/i.test(line)) continue;
150
-
151
- for (const pattern of patterns) {
152
- pattern.pattern.lastIndex = 0;
153
- let match;
154
- while ((match = pattern.pattern.exec(line)) !== null) {
155
- if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
156
- findings.push({
157
- line: i + 1,
158
- patternName: pattern.name,
159
- severity: pattern.severity,
160
- matched: match[0],
161
- category: pattern.category || 'secret',
162
- });
163
- }
164
- }
165
- }
166
- } catch { /* skip */ }
167
-
168
- const seen = new Set();
169
- return findings.filter(f => {
170
- const key = `${f.line}:${f.matched}`;
171
- if (seen.has(key)) return false;
172
- seen.add(key);
173
- return true;
174
- });
175
- }
176
-
177
- // =============================================================================
178
- // CONFIG-ONLY WATCH MODE
179
- // =============================================================================
180
-
181
- async function watchConfigs(absolutePath) {
182
- console.log();
183
- output.header('Ship Safe — Agent Config Watch');
184
- console.log();
185
- console.log(chalk.cyan(' Watching agent config files for changes...'));
186
- console.log(chalk.gray(' Monitors: .cursorrules, CLAUDE.md, openclaw.json, mcp.json, .claude/settings.json, ...'));
187
- console.log(chalk.gray(' Press Ctrl+C to stop'));
188
- console.log();
189
-
190
- let debounceTimer = null;
191
- const pendingFiles = new Set();
192
-
193
- try {
194
- const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
195
- if (!filename) return;
196
-
197
- // Check if this is an agent config file
198
- const relPath = filename.replace(/\\/g, '/');
199
- const isConfig = AGENT_CONFIG_PATTERNS.some(p => relPath === p || relPath.endsWith('/' + p));
200
- const isGlobMatch = relPath.match(/\.cursor\/rules\/.*\.mdc$/) ||
201
- relPath.match(/\.openclaw\/.*\.json$/) ||
202
- relPath.match(/\.claude\/commands\/.*\.md$/) ||
203
- relPath.match(/\.claude\/memory\//);
204
-
205
- if (!isConfig && !isGlobMatch) return;
206
-
207
- const fullPath = path.join(absolutePath, filename);
208
- pendingFiles.add(fullPath);
209
-
210
- if (debounceTimer) clearTimeout(debounceTimer);
211
- debounceTimer = setTimeout(async () => {
212
- const filesToScan = [...pendingFiles];
213
- pendingFiles.clear();
214
- await scanConfigFiles(filesToScan, absolutePath);
215
- }, 300);
216
- });
217
-
218
- process.on('SIGINT', () => {
219
- watcher.close();
220
- console.log();
221
- output.info('Config watch stopped.');
222
- process.exit(0);
223
- });
224
-
225
- setInterval(() => {}, 1000 * 60 * 60);
226
-
227
- } catch (err) {
228
- output.error(`Watch failed: ${err.message}`);
229
- process.exit(1);
230
- }
231
- }
232
-
233
- async function scanConfigFiles(files, rootPath) {
234
- // Dynamic import to avoid circular dependency
235
- const { AgentConfigScanner } = await import('../agents/agent-config-scanner.js');
236
- const { MCPSecurityAgent } = await import('../agents/mcp-security-agent.js');
237
-
238
- const timestamp = new Date().toLocaleTimeString();
239
- const scanner = new AgentConfigScanner();
240
- const mcpScanner = new MCPSecurityAgent();
241
-
242
- for (const filePath of files) {
243
- if (!fs.existsSync(filePath)) {
244
- console.log(chalk.gray(` [${timestamp}] ${path.relative(rootPath, filePath)} — deleted`));
245
- continue;
246
- }
247
-
248
- const relPath = path.relative(rootPath, filePath).replace(/\\/g, '/');
249
- console.log(chalk.cyan(` [${timestamp}] Changed: ${relPath}`));
250
-
251
- // Git blame (best-effort)
252
- try {
253
- const { execFileSync } = await import('child_process');
254
- const blame = execFileSync('git', ['log', '-1', '--format=%an (%ar)', '--', filePath], { cwd: rootPath, encoding: 'utf-8', timeout: 5000 }).trim();
255
- if (blame) console.log(chalk.gray(` Last modified by: ${blame}`));
256
- } catch { /* not a git repo or git not available */ }
257
-
258
- // Run agent config scanner
259
- const context = { rootPath, files: [] };
260
- const [configFindings, mcpFindings] = await Promise.all([
261
- scanner.analyze(context),
262
- mcpScanner.analyze(context),
263
- ]);
264
-
265
- const findings = [...configFindings, ...mcpFindings].filter(f =>
266
- f.file && path.resolve(f.file) === path.resolve(filePath)
267
- );
268
-
269
- if (findings.length > 0) {
270
- for (const f of findings) {
271
- const sevColor = f.severity === 'critical' ? chalk.red.bold
272
- : f.severity === 'high' ? chalk.yellow
273
- : chalk.blue;
274
- console.log(` ${sevColor(`[${f.severity.toUpperCase()}]`)} ${f.title || f.rule}`);
275
- }
276
- } else {
277
- console.log(chalk.green(' ✔ Clean'));
278
- }
279
- console.log();
280
- }
281
- }
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
+ // Agent config files to watch
21
+ const AGENT_CONFIG_PATTERNS = [
22
+ '.cursorrules', '.windsurfrules', 'CLAUDE.md', 'AGENTS.md',
23
+ '.github/copilot-instructions.md', '.aider.conf.yml',
24
+ '.continue/config.json', 'openclaw.json', 'openclaw.config.json',
25
+ 'clawhub.json', 'mcp.json', '.claude/settings.json',
26
+ '.cursor/mcp.json', '.vscode/mcp.json',
27
+ ];
28
+
29
+ export async function watchCommand(targetPath = '.', options = {}) {
30
+ const absolutePath = path.resolve(targetPath);
31
+
32
+ if (!fs.existsSync(absolutePath)) {
33
+ output.error(`Path does not exist: ${absolutePath}`);
34
+ process.exit(1);
35
+ }
36
+
37
+ // Config-only watch mode
38
+ if (options.configs) {
39
+ return watchConfigs(absolutePath);
40
+ }
41
+
42
+ console.log();
43
+ output.header('Ship Safe — Watch Mode');
44
+ console.log();
45
+ console.log(chalk.cyan(' Watching for file changes...'));
46
+ console.log(chalk.gray(' Press Ctrl+C to stop'));
47
+ console.log();
48
+
49
+ const allPatterns = [...SECRET_PATTERNS, ...SECURITY_PATTERNS];
50
+ const skipDirSet = SKIP_DIRS;
51
+ let debounceTimer = null;
52
+ const pendingFiles = new Set();
53
+
54
+ // Use fs.watch recursively
55
+ try {
56
+ const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => { // ship-safe-ignore — filename from fs.watch OS event, not user input
57
+ if (!filename) return; // ship-safe-ignore
58
+
59
+ const fullPath = path.join(absolutePath, filename); // ship-safe-ignore — filename from fs.watch, not user input
60
+ const relPath = filename.replace(/\\/g, '/');
61
+
62
+ // Skip directories we don't care about
63
+ for (const skipDir of skipDirSet) {
64
+ if (relPath.includes(`${skipDir}/`) || relPath.startsWith(`${skipDir}/`)) return;
65
+ }
66
+
67
+ // Skip non-code files
68
+ const ext = path.extname(filename).toLowerCase(); // ship-safe-ignore — filename from fs.watch OS event
69
+ if (SKIP_EXTENSIONS.has(ext)) return;
70
+ if (SKIP_FILENAMES.has(path.basename(filename))) return; // ship-safe-ignore
71
+ if (filename.endsWith('.min.js') || filename.endsWith('.min.css')) return;
72
+
73
+ // Add to pending and debounce
74
+ pendingFiles.add(fullPath);
75
+
76
+ if (debounceTimer) clearTimeout(debounceTimer);
77
+ debounceTimer = setTimeout(() => {
78
+ const filesToScan = [...pendingFiles];
79
+ pendingFiles.clear();
80
+ scanChangedFiles(filesToScan, allPatterns, absolutePath);
81
+ }, 300);
82
+ });
83
+
84
+ // Keep the process alive
85
+ process.on('SIGINT', () => {
86
+ watcher.close();
87
+ console.log();
88
+ output.info('Watch mode stopped.');
89
+ process.exit(0);
90
+ });
91
+
92
+ // Prevent Node from exiting
93
+ setInterval(() => {}, 1000 * 60 * 60);
94
+
95
+ } catch (err) {
96
+ output.error(`Watch failed: ${err.message}`);
97
+ console.log(chalk.gray(' Try: npx ship-safe watch . --poll'));
98
+ process.exit(1);
99
+ }
100
+ }
101
+
102
+ function scanChangedFiles(files, patterns, rootPath) {
103
+ const timestamp = new Date().toLocaleTimeString();
104
+ let totalFindings = 0;
105
+
106
+ for (const filePath of files) {
107
+ if (!fs.existsSync(filePath)) continue;
108
+
109
+ try {
110
+ const stats = fs.statSync(filePath);
111
+ if (stats.size > 1_000_000) continue;
112
+ } catch {
113
+ continue;
114
+ }
115
+
116
+ const findings = scanFile(filePath, patterns);
117
+ if (findings.length > 0) {
118
+ totalFindings += findings.length;
119
+ const relPath = path.relative(rootPath, filePath);
120
+
121
+ for (const f of findings) {
122
+ const sevColor = f.severity === 'critical' ? chalk.red.bold
123
+ : f.severity === 'high' ? chalk.yellow
124
+ : chalk.blue;
125
+
126
+ console.log(
127
+ chalk.gray(` [${timestamp}] `) +
128
+ sevColor(`[${f.severity.toUpperCase()}]`) +
129
+ chalk.white(` ${relPath}:${f.line} `) +
130
+ chalk.gray(f.patternName)
131
+ );
132
+ }
133
+ }
134
+ }
135
+
136
+ if (totalFindings === 0 && files.length > 0) {
137
+ console.log(chalk.gray(` [${timestamp}] ${files.length} file(s) scanned — clean`));
138
+ }
139
+ }
140
+
141
+ function scanFile(filePath, patterns) {
142
+ const findings = [];
143
+ try {
144
+ const content = fs.readFileSync(filePath, 'utf-8');
145
+ const lines = content.split('\n');
146
+
147
+ for (let i = 0; i < lines.length; i++) {
148
+ const line = lines[i];
149
+ if (/ship-safe-ignore/i.test(line)) continue;
150
+
151
+ for (const pattern of patterns) {
152
+ pattern.pattern.lastIndex = 0;
153
+ let match;
154
+ while ((match = pattern.pattern.exec(line)) !== null) {
155
+ if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
156
+ findings.push({
157
+ line: i + 1,
158
+ patternName: pattern.name,
159
+ severity: pattern.severity,
160
+ matched: match[0],
161
+ category: pattern.category || 'secret',
162
+ });
163
+ }
164
+ }
165
+ }
166
+ } catch { /* skip */ }
167
+
168
+ const seen = new Set();
169
+ return findings.filter(f => {
170
+ const key = `${f.line}:${f.matched}`;
171
+ if (seen.has(key)) return false;
172
+ seen.add(key);
173
+ return true;
174
+ });
175
+ }
176
+
177
+ // =============================================================================
178
+ // CONFIG-ONLY WATCH MODE
179
+ // =============================================================================
180
+
181
+ async function watchConfigs(absolutePath) {
182
+ console.log();
183
+ output.header('Ship Safe — Agent Config Watch');
184
+ console.log();
185
+ console.log(chalk.cyan(' Watching agent config files for changes...'));
186
+ console.log(chalk.gray(' Monitors: .cursorrules, CLAUDE.md, openclaw.json, mcp.json, .claude/settings.json, ...'));
187
+ console.log(chalk.gray(' Press Ctrl+C to stop'));
188
+ console.log();
189
+
190
+ let debounceTimer = null;
191
+ const pendingFiles = new Set();
192
+
193
+ try {
194
+ const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
195
+ if (!filename) return;
196
+
197
+ // Check if this is an agent config file
198
+ const relPath = filename.replace(/\\/g, '/');
199
+ const isConfig = AGENT_CONFIG_PATTERNS.some(p => relPath === p || relPath.endsWith('/' + p));
200
+ const isGlobMatch = relPath.match(/\.cursor\/rules\/.*\.mdc$/) ||
201
+ relPath.match(/\.openclaw\/.*\.json$/) ||
202
+ relPath.match(/\.claude\/commands\/.*\.md$/) ||
203
+ relPath.match(/\.claude\/memory\//);
204
+
205
+ if (!isConfig && !isGlobMatch) return;
206
+
207
+ const fullPath = path.join(absolutePath, filename);
208
+ pendingFiles.add(fullPath);
209
+
210
+ if (debounceTimer) clearTimeout(debounceTimer);
211
+ debounceTimer = setTimeout(async () => {
212
+ const filesToScan = [...pendingFiles];
213
+ pendingFiles.clear();
214
+ await scanConfigFiles(filesToScan, absolutePath);
215
+ }, 300);
216
+ });
217
+
218
+ process.on('SIGINT', () => {
219
+ watcher.close();
220
+ console.log();
221
+ output.info('Config watch stopped.');
222
+ process.exit(0);
223
+ });
224
+
225
+ setInterval(() => {}, 1000 * 60 * 60);
226
+
227
+ } catch (err) {
228
+ output.error(`Watch failed: ${err.message}`);
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ async function scanConfigFiles(files, rootPath) {
234
+ // Dynamic import to avoid circular dependency
235
+ const { AgentConfigScanner } = await import('../agents/agent-config-scanner.js');
236
+ const { MCPSecurityAgent } = await import('../agents/mcp-security-agent.js');
237
+
238
+ const timestamp = new Date().toLocaleTimeString();
239
+ const scanner = new AgentConfigScanner();
240
+ const mcpScanner = new MCPSecurityAgent();
241
+
242
+ for (const filePath of files) {
243
+ if (!fs.existsSync(filePath)) {
244
+ console.log(chalk.gray(` [${timestamp}] ${path.relative(rootPath, filePath)} — deleted`));
245
+ continue;
246
+ }
247
+
248
+ const relPath = path.relative(rootPath, filePath).replace(/\\/g, '/');
249
+ console.log(chalk.cyan(` [${timestamp}] Changed: ${relPath}`));
250
+
251
+ // Git blame (best-effort)
252
+ try {
253
+ const { execFileSync } = await import('child_process');
254
+ const blame = execFileSync('git', ['log', '-1', '--format=%an (%ar)', '--', filePath], { cwd: rootPath, encoding: 'utf-8', timeout: 5000 }).trim();
255
+ if (blame) console.log(chalk.gray(` Last modified by: ${blame}`));
256
+ } catch { /* not a git repo or git not available */ }
257
+
258
+ // Run agent config scanner
259
+ const context = { rootPath, files: [] };
260
+ const [configFindings, mcpFindings] = await Promise.all([
261
+ scanner.analyze(context),
262
+ mcpScanner.analyze(context),
263
+ ]);
264
+
265
+ const findings = [...configFindings, ...mcpFindings].filter(f =>
266
+ f.file && path.resolve(f.file) === path.resolve(filePath)
267
+ );
268
+
269
+ if (findings.length > 0) {
270
+ for (const f of findings) {
271
+ const sevColor = f.severity === 'critical' ? chalk.red.bold
272
+ : f.severity === 'high' ? chalk.yellow
273
+ : chalk.blue;
274
+ console.log(` ${sevColor(`[${f.severity.toUpperCase()}]`)} ${f.title || f.rule}`);
275
+ }
276
+ } else {
277
+ console.log(chalk.green(' ✔ Clean'));
278
+ }
279
+ console.log();
280
+ }
281
+ }