driftdetect 0.4.0 → 0.4.2
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/dist/bin/drift.js +10 -1
- package/dist/bin/drift.js.map +1 -1
- package/dist/commands/callgraph.d.ts +24 -0
- package/dist/commands/callgraph.d.ts.map +1 -0
- package/dist/commands/callgraph.js +1376 -0
- package/dist/commands/callgraph.js.map +1 -0
- package/dist/commands/index.d.ts +24 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +50 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/projects.d.ts +17 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +400 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +34 -1
- package/dist/commands/scan.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,1376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call Graph Command - drift callgraph
|
|
3
|
+
*
|
|
4
|
+
* Build and query call graphs to understand code reachability.
|
|
5
|
+
* Answers: "What data can this code access?" and "Who can reach this data?"
|
|
6
|
+
*
|
|
7
|
+
* @requirements Call Graph Feature
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import * as fs from 'node:fs/promises';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { createCallGraphAnalyzer, createBoundaryScanner, createSecurityPrioritizer, createImpactAnalyzer, createDeadCodeDetector, createCoverageAnalyzer, createSemanticDataAccessScanner, detectProjectStack, } from 'driftdetect-core';
|
|
14
|
+
import { createSpinner } from '../ui/spinner.js';
|
|
15
|
+
/** Directory name for drift configuration */
|
|
16
|
+
const DRIFT_DIR = '.drift';
|
|
17
|
+
/** Directory name for call graph data */
|
|
18
|
+
const CALLGRAPH_DIR = 'callgraph';
|
|
19
|
+
/**
|
|
20
|
+
* Check if call graph data exists
|
|
21
|
+
*/
|
|
22
|
+
async function callGraphExists(rootDir) {
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(path.join(rootDir, DRIFT_DIR, CALLGRAPH_DIR, 'graph.json'));
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Show helpful message when call graph not built
|
|
33
|
+
*/
|
|
34
|
+
function showNotBuiltMessage() {
|
|
35
|
+
console.log();
|
|
36
|
+
console.log(chalk.yellow('⚠️ No call graph built yet.'));
|
|
37
|
+
console.log();
|
|
38
|
+
console.log(chalk.gray('Build a call graph to analyze code reachability:'));
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(chalk.cyan(' drift callgraph build'));
|
|
41
|
+
console.log();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Format detected stack for display
|
|
45
|
+
*/
|
|
46
|
+
function formatDetectedStack(stack) {
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(chalk.bold('🔍 Detected Project Stack'));
|
|
49
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
50
|
+
if (stack.languages.length > 0) {
|
|
51
|
+
const langIcons = {
|
|
52
|
+
'typescript': '🟦',
|
|
53
|
+
'javascript': '🟨',
|
|
54
|
+
'python': '🐍',
|
|
55
|
+
'csharp': '🟣',
|
|
56
|
+
'java': '☕',
|
|
57
|
+
'php': '🐘',
|
|
58
|
+
};
|
|
59
|
+
const langs = stack.languages.map(l => `${langIcons[l] ?? '📄'} ${l}`).join(' ');
|
|
60
|
+
console.log(` Languages: ${langs}`);
|
|
61
|
+
}
|
|
62
|
+
if (stack.orms.length > 0) {
|
|
63
|
+
const ormColors = {
|
|
64
|
+
'supabase': chalk.green,
|
|
65
|
+
'prisma': chalk.cyan,
|
|
66
|
+
'django': chalk.green,
|
|
67
|
+
'sqlalchemy': chalk.yellow,
|
|
68
|
+
'ef-core': chalk.magenta,
|
|
69
|
+
'dapper': chalk.blue,
|
|
70
|
+
'spring-data-jpa': chalk.green,
|
|
71
|
+
'hibernate': chalk.yellow,
|
|
72
|
+
'eloquent': chalk.red,
|
|
73
|
+
'doctrine': chalk.blue,
|
|
74
|
+
};
|
|
75
|
+
const orms = stack.orms.map(o => {
|
|
76
|
+
const color = ormColors[o] ?? chalk.gray;
|
|
77
|
+
return color(o);
|
|
78
|
+
}).join(', ');
|
|
79
|
+
console.log(` ORMs/Data: ${orms}`);
|
|
80
|
+
}
|
|
81
|
+
if (stack.frameworks.length > 0) {
|
|
82
|
+
console.log(` Frameworks: ${chalk.cyan(stack.frameworks.join(', '))}`);
|
|
83
|
+
}
|
|
84
|
+
console.log();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Show tree-sitter availability warnings
|
|
88
|
+
*/
|
|
89
|
+
function showParserWarnings(detectedLanguages) {
|
|
90
|
+
// Languages with tree-sitter support
|
|
91
|
+
const treeSitterSupported = ['typescript', 'javascript', 'python', 'csharp', 'java', 'php'];
|
|
92
|
+
// Check for unsupported languages
|
|
93
|
+
const unsupported = detectedLanguages.filter(l => !treeSitterSupported.includes(l));
|
|
94
|
+
if (unsupported.length > 0) {
|
|
95
|
+
console.log(chalk.yellow('⚠️ Parser Warnings:'));
|
|
96
|
+
for (const lang of unsupported) {
|
|
97
|
+
console.log(chalk.yellow(` • ${lang}: No tree-sitter parser available, using regex fallback`));
|
|
98
|
+
}
|
|
99
|
+
console.log();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build subcommand - build the call graph
|
|
104
|
+
*/
|
|
105
|
+
async function buildAction(options) {
|
|
106
|
+
const rootDir = process.cwd();
|
|
107
|
+
const format = options.format ?? 'text';
|
|
108
|
+
const isTextFormat = format === 'text';
|
|
109
|
+
try {
|
|
110
|
+
// Step 0: Detect project stack first
|
|
111
|
+
if (isTextFormat) {
|
|
112
|
+
console.log();
|
|
113
|
+
console.log(chalk.bold('🚀 Building Call Graph'));
|
|
114
|
+
console.log(chalk.gray('═'.repeat(50)));
|
|
115
|
+
}
|
|
116
|
+
const detectedStack = await detectProjectStack(rootDir);
|
|
117
|
+
if (isTextFormat && (detectedStack.languages.length > 0 || detectedStack.orms.length > 0)) {
|
|
118
|
+
formatDetectedStack(detectedStack);
|
|
119
|
+
showParserWarnings(detectedStack.languages);
|
|
120
|
+
}
|
|
121
|
+
const spinner = isTextFormat ? createSpinner('Initializing...') : null;
|
|
122
|
+
spinner?.start();
|
|
123
|
+
// File patterns to scan - include all supported languages
|
|
124
|
+
const filePatterns = [
|
|
125
|
+
'**/*.ts',
|
|
126
|
+
'**/*.tsx',
|
|
127
|
+
'**/*.js',
|
|
128
|
+
'**/*.jsx',
|
|
129
|
+
'**/*.py',
|
|
130
|
+
'**/*.cs',
|
|
131
|
+
'**/*.java',
|
|
132
|
+
'**/*.php',
|
|
133
|
+
];
|
|
134
|
+
// Step 1: Run semantic data access scanner (tree-sitter based)
|
|
135
|
+
spinner?.text('🌳 Scanning with tree-sitter (semantic analysis)...');
|
|
136
|
+
const semanticScanner = createSemanticDataAccessScanner({
|
|
137
|
+
rootDir,
|
|
138
|
+
verbose: options.verbose ?? false,
|
|
139
|
+
autoDetect: true,
|
|
140
|
+
});
|
|
141
|
+
const semanticResult = await semanticScanner.scanDirectory({ patterns: filePatterns });
|
|
142
|
+
// Use semantic results as primary source
|
|
143
|
+
const dataAccessPoints = semanticResult.accessPoints;
|
|
144
|
+
const semanticStats = semanticResult.stats;
|
|
145
|
+
// Step 2: Fall back to boundary scanner for additional coverage (regex-based)
|
|
146
|
+
spinner?.text('🔍 Scanning for additional patterns (regex fallback)...');
|
|
147
|
+
const boundaryScanner = createBoundaryScanner({ rootDir, verbose: options.verbose ?? false });
|
|
148
|
+
await boundaryScanner.initialize();
|
|
149
|
+
const boundaryResult = await boundaryScanner.scanDirectory({ patterns: filePatterns });
|
|
150
|
+
// Merge boundary results with semantic results (semantic takes precedence)
|
|
151
|
+
let regexAdditions = 0;
|
|
152
|
+
for (const [, accessPoint] of Object.entries(boundaryResult.accessMap.accessPoints)) {
|
|
153
|
+
const existing = dataAccessPoints.get(accessPoint.file) ?? [];
|
|
154
|
+
// Only add if not already detected by semantic scanner
|
|
155
|
+
const isDuplicate = existing.some(ap => ap.line === accessPoint.line && ap.table === accessPoint.table);
|
|
156
|
+
if (!isDuplicate) {
|
|
157
|
+
existing.push(accessPoint);
|
|
158
|
+
dataAccessPoints.set(accessPoint.file, existing);
|
|
159
|
+
regexAdditions++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Step 3: Build call graph with data access points
|
|
163
|
+
spinner?.text('📊 Building call graph...');
|
|
164
|
+
const analyzer = createCallGraphAnalyzer({ rootDir });
|
|
165
|
+
await analyzer.initialize();
|
|
166
|
+
const graph = await analyzer.scan(filePatterns, dataAccessPoints);
|
|
167
|
+
// Ensure directory exists
|
|
168
|
+
const graphDir = path.join(rootDir, DRIFT_DIR, CALLGRAPH_DIR);
|
|
169
|
+
await fs.mkdir(graphDir, { recursive: true });
|
|
170
|
+
// Save graph
|
|
171
|
+
spinner?.text('💾 Saving call graph...');
|
|
172
|
+
const graphPath = path.join(graphDir, 'graph.json');
|
|
173
|
+
await fs.writeFile(graphPath, JSON.stringify({
|
|
174
|
+
version: graph.version,
|
|
175
|
+
generatedAt: graph.generatedAt,
|
|
176
|
+
projectRoot: graph.projectRoot,
|
|
177
|
+
stats: graph.stats,
|
|
178
|
+
entryPoints: graph.entryPoints,
|
|
179
|
+
dataAccessors: graph.dataAccessors,
|
|
180
|
+
functions: Object.fromEntries(graph.functions),
|
|
181
|
+
}, null, 2));
|
|
182
|
+
spinner?.stop();
|
|
183
|
+
// JSON output
|
|
184
|
+
if (format === 'json') {
|
|
185
|
+
console.log(JSON.stringify({
|
|
186
|
+
success: true,
|
|
187
|
+
detectedStack,
|
|
188
|
+
stats: graph.stats,
|
|
189
|
+
entryPoints: graph.entryPoints.length,
|
|
190
|
+
dataAccessors: graph.dataAccessors.length,
|
|
191
|
+
semanticStats: semanticStats,
|
|
192
|
+
boundaryStats: boundaryResult.stats,
|
|
193
|
+
regexAdditions,
|
|
194
|
+
}, null, 2));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Text output
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(chalk.green.bold('✓ Call graph built successfully'));
|
|
200
|
+
console.log();
|
|
201
|
+
// Main statistics box
|
|
202
|
+
console.log(chalk.bold('📊 Graph Statistics'));
|
|
203
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
204
|
+
console.log(` Functions: ${chalk.cyan.bold(graph.stats.totalFunctions.toLocaleString())}`);
|
|
205
|
+
console.log(` Call Sites: ${chalk.cyan(graph.stats.totalCallSites.toLocaleString())} (${chalk.green(Math.round(graph.stats.resolvedCallSites / Math.max(1, graph.stats.totalCallSites) * 100) + '%')} resolved)`);
|
|
206
|
+
console.log(` Entry Points: ${chalk.magenta.bold(graph.entryPoints.length.toLocaleString())} ${chalk.gray('(API routes, exports)')}`);
|
|
207
|
+
console.log(` Data Accessors: ${chalk.yellow.bold(graph.dataAccessors.length.toLocaleString())} ${chalk.gray('(functions with DB access)')}`);
|
|
208
|
+
console.log();
|
|
209
|
+
// Data access detection summary
|
|
210
|
+
const totalAccessPoints = semanticStats.accessPointsFound + regexAdditions;
|
|
211
|
+
if (totalAccessPoints > 0) {
|
|
212
|
+
console.log(chalk.bold('💾 Data Access Detection'));
|
|
213
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
214
|
+
console.log(` ${chalk.green('🌳 Tree-sitter:')} ${chalk.cyan(semanticStats.accessPointsFound)} access points ${chalk.gray(`(${semanticStats.filesScanned} files)`)}`);
|
|
215
|
+
if (regexAdditions > 0) {
|
|
216
|
+
console.log(` ${chalk.yellow('🔍 Regex fallback:')} ${chalk.cyan(regexAdditions)} additional points`);
|
|
217
|
+
}
|
|
218
|
+
// ORM breakdown
|
|
219
|
+
if (Object.keys(semanticStats.byOrm).length > 0) {
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(chalk.gray(' By ORM/Framework:'));
|
|
222
|
+
const sortedOrms = Object.entries(semanticStats.byOrm)
|
|
223
|
+
.sort((a, b) => b[1] - a[1]);
|
|
224
|
+
for (const [orm, count] of sortedOrms) {
|
|
225
|
+
const bar = '█'.repeat(Math.min(20, Math.ceil(count / Math.max(...sortedOrms.map(([, c]) => c)) * 20)));
|
|
226
|
+
console.log(` ${chalk.gray(orm.padEnd(15))} ${chalk.cyan(bar)} ${count}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
console.log();
|
|
230
|
+
}
|
|
231
|
+
// Language breakdown
|
|
232
|
+
const languages = Object.entries(graph.stats.byLanguage)
|
|
233
|
+
.filter(([, count]) => count > 0)
|
|
234
|
+
.sort((a, b) => b[1] - a[1]);
|
|
235
|
+
if (languages.length > 0) {
|
|
236
|
+
console.log(chalk.bold('📝 By Language'));
|
|
237
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
238
|
+
const langIcons = {
|
|
239
|
+
'typescript': '🟦',
|
|
240
|
+
'javascript': '🟨',
|
|
241
|
+
'python': '🐍',
|
|
242
|
+
'csharp': '🟣',
|
|
243
|
+
'java': '☕',
|
|
244
|
+
'php': '🐘',
|
|
245
|
+
};
|
|
246
|
+
for (const [lang, count] of languages) {
|
|
247
|
+
const icon = langIcons[lang] ?? '📄';
|
|
248
|
+
const bar = '█'.repeat(Math.min(20, Math.ceil(count / Math.max(...languages.map(([, c]) => c)) * 20)));
|
|
249
|
+
console.log(` ${icon} ${lang.padEnd(12)} ${chalk.cyan(bar)} ${count.toLocaleString()} functions`);
|
|
250
|
+
}
|
|
251
|
+
console.log();
|
|
252
|
+
}
|
|
253
|
+
// Errors summary
|
|
254
|
+
if (semanticStats.errors > 0) {
|
|
255
|
+
console.log(chalk.yellow(`⚠️ ${semanticStats.errors} file(s) had parsing errors (use --verbose for details)`));
|
|
256
|
+
console.log();
|
|
257
|
+
}
|
|
258
|
+
// Next steps
|
|
259
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
260
|
+
console.log(chalk.bold('📌 Next Steps:'));
|
|
261
|
+
console.log(chalk.gray(` • drift callgraph status ${chalk.white('View entry points & data accessors')}`));
|
|
262
|
+
console.log(chalk.gray(` • drift callgraph status -s ${chalk.white('Security-prioritized view (P0-P4)')}`));
|
|
263
|
+
console.log(chalk.gray(` • drift callgraph reach <fn> ${chalk.white('What data can this code access?')}`));
|
|
264
|
+
console.log(chalk.gray(` • drift callgraph coverage ${chalk.white('Test coverage for sensitive data')}`));
|
|
265
|
+
console.log();
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (format === 'json') {
|
|
269
|
+
console.log(JSON.stringify({ error: String(error) }));
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
console.log(chalk.red(`\n❌ Error: ${error}`));
|
|
273
|
+
if (options.verbose && error instanceof Error && error.stack) {
|
|
274
|
+
console.log(chalk.gray(error.stack));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Status subcommand - show call graph overview
|
|
281
|
+
*/
|
|
282
|
+
async function statusAction(options) {
|
|
283
|
+
const rootDir = process.cwd();
|
|
284
|
+
const format = options.format ?? 'text';
|
|
285
|
+
const showSecurity = options.security ?? false;
|
|
286
|
+
if (!(await callGraphExists(rootDir))) {
|
|
287
|
+
if (format === 'json') {
|
|
288
|
+
console.log(JSON.stringify({ error: 'No call graph found' }));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
showNotBuiltMessage();
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const analyzer = createCallGraphAnalyzer({ rootDir });
|
|
296
|
+
await analyzer.initialize();
|
|
297
|
+
const graph = analyzer.getGraph();
|
|
298
|
+
if (!graph) {
|
|
299
|
+
if (format === 'json') {
|
|
300
|
+
console.log(JSON.stringify({ error: 'Failed to load call graph' }));
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
console.log(chalk.red('Failed to load call graph'));
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// If security flag is set, run boundary scan and prioritize
|
|
308
|
+
if (showSecurity) {
|
|
309
|
+
await showSecurityPrioritizedStatus(rootDir, graph, format);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// JSON output
|
|
313
|
+
if (format === 'json') {
|
|
314
|
+
console.log(JSON.stringify({
|
|
315
|
+
stats: graph.stats,
|
|
316
|
+
entryPoints: graph.entryPoints.map(id => {
|
|
317
|
+
const func = graph.functions.get(id);
|
|
318
|
+
return func ? { id, name: func.qualifiedName, file: func.file, line: func.startLine } : { id };
|
|
319
|
+
}),
|
|
320
|
+
dataAccessors: graph.dataAccessors.map(id => {
|
|
321
|
+
const func = graph.functions.get(id);
|
|
322
|
+
return func ? {
|
|
323
|
+
id,
|
|
324
|
+
name: func.qualifiedName,
|
|
325
|
+
file: func.file,
|
|
326
|
+
line: func.startLine,
|
|
327
|
+
tables: [...new Set(func.dataAccess.map(d => d.table))],
|
|
328
|
+
} : { id };
|
|
329
|
+
}),
|
|
330
|
+
}, null, 2));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Text output
|
|
334
|
+
console.log();
|
|
335
|
+
console.log(chalk.bold('📊 Call Graph Status'));
|
|
336
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
337
|
+
console.log();
|
|
338
|
+
console.log(`Functions: ${chalk.cyan(graph.stats.totalFunctions)}`);
|
|
339
|
+
console.log(`Call Sites: ${chalk.cyan(graph.stats.totalCallSites)} (${chalk.green(graph.stats.resolvedCallSites)} resolved)`);
|
|
340
|
+
console.log(`Entry Points: ${chalk.cyan(graph.entryPoints.length)}`);
|
|
341
|
+
console.log(`Data Accessors: ${chalk.cyan(graph.dataAccessors.length)}`);
|
|
342
|
+
console.log();
|
|
343
|
+
// Entry points
|
|
344
|
+
if (graph.entryPoints.length > 0) {
|
|
345
|
+
console.log(chalk.bold('Entry Points (API Routes, Exports):'));
|
|
346
|
+
for (const id of graph.entryPoints.slice(0, 10)) {
|
|
347
|
+
const func = graph.functions.get(id);
|
|
348
|
+
if (func) {
|
|
349
|
+
console.log(` ${chalk.magenta('🚪')} ${chalk.white(func.qualifiedName)}`);
|
|
350
|
+
console.log(chalk.gray(` ${func.file}:${func.startLine}`));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (graph.entryPoints.length > 10) {
|
|
354
|
+
console.log(chalk.gray(` ... and ${graph.entryPoints.length - 10} more`));
|
|
355
|
+
}
|
|
356
|
+
console.log();
|
|
357
|
+
}
|
|
358
|
+
// Data accessors
|
|
359
|
+
if (graph.dataAccessors.length > 0) {
|
|
360
|
+
console.log(chalk.bold('Data Accessors (Functions with DB access):'));
|
|
361
|
+
for (const id of graph.dataAccessors.slice(0, 10)) {
|
|
362
|
+
const func = graph.functions.get(id);
|
|
363
|
+
if (func) {
|
|
364
|
+
const tables = [...new Set(func.dataAccess.map(d => d.table))];
|
|
365
|
+
console.log(` ${chalk.blue('💾')} ${chalk.white(func.qualifiedName)} → [${tables.join(', ')}]`);
|
|
366
|
+
console.log(chalk.gray(` ${func.file}:${func.startLine}`));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (graph.dataAccessors.length > 10) {
|
|
370
|
+
console.log(chalk.gray(` ... and ${graph.dataAccessors.length - 10} more`));
|
|
371
|
+
}
|
|
372
|
+
console.log();
|
|
373
|
+
}
|
|
374
|
+
console.log(chalk.gray("Tip: Use 'drift callgraph status --security' to see security-prioritized view"));
|
|
375
|
+
console.log();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Show security-prioritized status view
|
|
379
|
+
*/
|
|
380
|
+
async function showSecurityPrioritizedStatus(rootDir, _graph, format) {
|
|
381
|
+
// Only show spinner for text format
|
|
382
|
+
const isTextFormat = format === 'text';
|
|
383
|
+
const spinner = isTextFormat ? createSpinner('Analyzing security priorities...') : null;
|
|
384
|
+
spinner?.start();
|
|
385
|
+
// Run boundary scan
|
|
386
|
+
const boundaryScanner = createBoundaryScanner({ rootDir, verbose: false });
|
|
387
|
+
await boundaryScanner.initialize();
|
|
388
|
+
const filePatterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py'];
|
|
389
|
+
const boundaryResult = await boundaryScanner.scanDirectory({ patterns: filePatterns });
|
|
390
|
+
// Prioritize by security
|
|
391
|
+
const prioritizer = createSecurityPrioritizer();
|
|
392
|
+
const prioritized = prioritizer.prioritize(boundaryResult.accessMap);
|
|
393
|
+
spinner?.stop();
|
|
394
|
+
// JSON output
|
|
395
|
+
if (!isTextFormat) {
|
|
396
|
+
console.log(JSON.stringify({
|
|
397
|
+
summary: prioritized.summary,
|
|
398
|
+
critical: prioritized.critical.map(p => ({
|
|
399
|
+
table: p.accessPoint.table,
|
|
400
|
+
fields: p.accessPoint.fields,
|
|
401
|
+
operation: p.accessPoint.operation,
|
|
402
|
+
file: p.accessPoint.file,
|
|
403
|
+
line: p.accessPoint.line,
|
|
404
|
+
tier: p.security.tier,
|
|
405
|
+
riskScore: p.security.riskScore,
|
|
406
|
+
sensitivity: p.security.maxSensitivity,
|
|
407
|
+
regulations: p.security.regulations,
|
|
408
|
+
rationale: p.security.rationale,
|
|
409
|
+
})),
|
|
410
|
+
high: prioritized.high.slice(0, 20).map(p => ({
|
|
411
|
+
table: p.accessPoint.table,
|
|
412
|
+
fields: p.accessPoint.fields,
|
|
413
|
+
operation: p.accessPoint.operation,
|
|
414
|
+
file: p.accessPoint.file,
|
|
415
|
+
line: p.accessPoint.line,
|
|
416
|
+
tier: p.security.tier,
|
|
417
|
+
riskScore: p.security.riskScore,
|
|
418
|
+
sensitivity: p.security.maxSensitivity,
|
|
419
|
+
})),
|
|
420
|
+
}, null, 2));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
// Text output
|
|
424
|
+
console.log();
|
|
425
|
+
console.log(chalk.bold('🔒 Security-Prioritized Data Access'));
|
|
426
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
427
|
+
console.log();
|
|
428
|
+
// Summary
|
|
429
|
+
const { summary } = prioritized;
|
|
430
|
+
console.log(chalk.bold('Summary:'));
|
|
431
|
+
console.log(` Total Access Points: ${chalk.cyan(summary.totalAccessPoints)}`);
|
|
432
|
+
console.log(` ${chalk.red('🔴 Critical (P0/P1):')} ${chalk.red(summary.criticalCount)}`);
|
|
433
|
+
console.log(` ${chalk.yellow('🟡 High (P2):')} ${chalk.yellow(summary.highCount)}`);
|
|
434
|
+
console.log(` ${chalk.gray('⚪ Low (P3/P4):')} ${chalk.gray(summary.lowCount)}`);
|
|
435
|
+
console.log(` ${chalk.gray('📦 Noise (filtered):')} ${chalk.gray(summary.noiseCount)}`);
|
|
436
|
+
console.log();
|
|
437
|
+
// Regulations
|
|
438
|
+
if (summary.regulations.length > 0) {
|
|
439
|
+
console.log(chalk.bold('Regulatory Implications:'));
|
|
440
|
+
console.log(` ${summary.regulations.map(r => chalk.magenta(r.toUpperCase())).join(', ')}`);
|
|
441
|
+
console.log();
|
|
442
|
+
}
|
|
443
|
+
// Critical items (P0/P1)
|
|
444
|
+
if (prioritized.critical.length > 0) {
|
|
445
|
+
console.log(chalk.bold.red('🚨 Critical Security Items (P0/P1):'));
|
|
446
|
+
for (const p of prioritized.critical.slice(0, 15)) {
|
|
447
|
+
const tierColor = p.security.tier === 'P0' ? chalk.bgRed.white : chalk.red;
|
|
448
|
+
const sensitivityIcon = getSensitivityIcon(p.security.maxSensitivity);
|
|
449
|
+
const opColor = p.accessPoint.operation === 'write' ? chalk.yellow :
|
|
450
|
+
p.accessPoint.operation === 'delete' ? chalk.red : chalk.gray;
|
|
451
|
+
console.log(` ${tierColor(` ${p.security.tier} `)} ${sensitivityIcon} ${chalk.white(p.accessPoint.table)}`);
|
|
452
|
+
console.log(` ${opColor(p.accessPoint.operation)} ${p.accessPoint.fields.join(', ') || '*'}`);
|
|
453
|
+
console.log(chalk.gray(` ${p.accessPoint.file}:${p.accessPoint.line}`));
|
|
454
|
+
console.log(chalk.gray(` ${p.security.rationale}`));
|
|
455
|
+
if (p.security.regulations.length > 0) {
|
|
456
|
+
console.log(chalk.magenta(` Regulations: ${p.security.regulations.join(', ')}`));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (prioritized.critical.length > 15) {
|
|
460
|
+
console.log(chalk.gray(` ... and ${prioritized.critical.length - 15} more critical items`));
|
|
461
|
+
}
|
|
462
|
+
console.log();
|
|
463
|
+
}
|
|
464
|
+
// High priority items (P2)
|
|
465
|
+
if (prioritized.high.length > 0) {
|
|
466
|
+
console.log(chalk.bold.yellow('⚠️ High Priority Items (P2):'));
|
|
467
|
+
for (const p of prioritized.high.slice(0, 10)) {
|
|
468
|
+
const sensitivityIcon = getSensitivityIcon(p.security.maxSensitivity);
|
|
469
|
+
console.log(` ${chalk.yellow('P2')} ${sensitivityIcon} ${chalk.white(p.accessPoint.table)}.${p.accessPoint.fields.join(', ') || '*'}`);
|
|
470
|
+
console.log(chalk.gray(` ${p.accessPoint.file}:${p.accessPoint.line}`));
|
|
471
|
+
}
|
|
472
|
+
if (prioritized.high.length > 10) {
|
|
473
|
+
console.log(chalk.gray(` ... and ${prioritized.high.length - 10} more high priority items`));
|
|
474
|
+
}
|
|
475
|
+
console.log();
|
|
476
|
+
}
|
|
477
|
+
// Sensitivity breakdown
|
|
478
|
+
console.log(chalk.bold('By Sensitivity Type:'));
|
|
479
|
+
if (summary.bySensitivity.credentials > 0) {
|
|
480
|
+
console.log(` ${chalk.red('🔑 Credentials:')} ${summary.bySensitivity.credentials}`);
|
|
481
|
+
}
|
|
482
|
+
if (summary.bySensitivity.financial > 0) {
|
|
483
|
+
console.log(` ${chalk.magenta('💰 Financial:')} ${summary.bySensitivity.financial}`);
|
|
484
|
+
}
|
|
485
|
+
if (summary.bySensitivity.health > 0) {
|
|
486
|
+
console.log(` ${chalk.blue('🏥 Health:')} ${summary.bySensitivity.health}`);
|
|
487
|
+
}
|
|
488
|
+
if (summary.bySensitivity.pii > 0) {
|
|
489
|
+
console.log(` ${chalk.yellow('👤 PII:')} ${summary.bySensitivity.pii}`);
|
|
490
|
+
}
|
|
491
|
+
console.log(` ${chalk.gray('❓ Unknown:')} ${summary.bySensitivity.unknown}`);
|
|
492
|
+
console.log();
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Get icon for sensitivity type
|
|
496
|
+
*/
|
|
497
|
+
function getSensitivityIcon(sensitivity) {
|
|
498
|
+
switch (sensitivity) {
|
|
499
|
+
case 'credentials': return chalk.red('🔑');
|
|
500
|
+
case 'financial': return chalk.magenta('💰');
|
|
501
|
+
case 'health': return chalk.blue('🏥');
|
|
502
|
+
case 'pii': return chalk.yellow('👤');
|
|
503
|
+
default: return chalk.gray('❓');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Reach subcommand - what data can this code reach?
|
|
508
|
+
*/
|
|
509
|
+
async function reachAction(location, options) {
|
|
510
|
+
const rootDir = process.cwd();
|
|
511
|
+
const format = options.format ?? 'text';
|
|
512
|
+
const maxDepth = options.maxDepth ?? 10;
|
|
513
|
+
if (!(await callGraphExists(rootDir))) {
|
|
514
|
+
if (format === 'json') {
|
|
515
|
+
console.log(JSON.stringify({ error: 'No call graph found' }));
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
showNotBuiltMessage();
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// Parse location: file:line or function name
|
|
523
|
+
let file;
|
|
524
|
+
let line;
|
|
525
|
+
let functionName;
|
|
526
|
+
if (location.includes(':')) {
|
|
527
|
+
const parts = location.split(':');
|
|
528
|
+
const filePart = parts[0];
|
|
529
|
+
const linePart = parts[1];
|
|
530
|
+
if (filePart && linePart) {
|
|
531
|
+
const parsedLine = parseInt(linePart, 10);
|
|
532
|
+
if (!isNaN(parsedLine)) {
|
|
533
|
+
file = filePart;
|
|
534
|
+
line = parsedLine;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
functionName = location;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
functionName = location;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
functionName = location;
|
|
546
|
+
}
|
|
547
|
+
const analyzer = createCallGraphAnalyzer({ rootDir });
|
|
548
|
+
await analyzer.initialize();
|
|
549
|
+
let result;
|
|
550
|
+
if (file !== undefined && line !== undefined) {
|
|
551
|
+
result = analyzer.getReachableData(file, line, { maxDepth });
|
|
552
|
+
}
|
|
553
|
+
else if (functionName) {
|
|
554
|
+
// Find function by name
|
|
555
|
+
const graph = analyzer.getGraph();
|
|
556
|
+
if (!graph) {
|
|
557
|
+
console.log(format === 'json'
|
|
558
|
+
? JSON.stringify({ error: 'Failed to load call graph' })
|
|
559
|
+
: chalk.red('Failed to load call graph'));
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
let funcId;
|
|
563
|
+
for (const [id, func] of graph.functions) {
|
|
564
|
+
if (func.name === functionName || func.qualifiedName === functionName) {
|
|
565
|
+
funcId = id;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (!funcId) {
|
|
570
|
+
console.log(format === 'json'
|
|
571
|
+
? JSON.stringify({ error: `Function '${functionName}' not found` })
|
|
572
|
+
: chalk.red(`Function '${functionName}' not found`));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
result = analyzer.getReachableDataFromFunction(funcId, { maxDepth });
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
console.log(format === 'json'
|
|
579
|
+
? JSON.stringify({ error: 'Invalid location format. Use file:line or function_name' })
|
|
580
|
+
: chalk.red('Invalid location format. Use file:line or function_name'));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
// JSON output
|
|
584
|
+
if (format === 'json') {
|
|
585
|
+
console.log(JSON.stringify({
|
|
586
|
+
origin: result.origin,
|
|
587
|
+
tables: result.tables,
|
|
588
|
+
sensitiveFields: result.sensitiveFields.map(sf => ({
|
|
589
|
+
field: `${sf.field.table}.${sf.field.field}`,
|
|
590
|
+
type: sf.field.sensitivityType,
|
|
591
|
+
accessCount: sf.accessCount,
|
|
592
|
+
})),
|
|
593
|
+
maxDepth: result.maxDepth,
|
|
594
|
+
functionsTraversed: result.functionsTraversed,
|
|
595
|
+
accessPoints: result.reachableAccess.map(ra => ({
|
|
596
|
+
table: ra.access.table,
|
|
597
|
+
fields: ra.access.fields,
|
|
598
|
+
operation: ra.access.operation,
|
|
599
|
+
depth: ra.depth,
|
|
600
|
+
path: ra.path.map(p => p.functionName),
|
|
601
|
+
})),
|
|
602
|
+
}, null, 2));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
// Text output
|
|
606
|
+
console.log();
|
|
607
|
+
console.log(chalk.bold('🔎 Reachability Analysis'));
|
|
608
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
609
|
+
console.log();
|
|
610
|
+
console.log(`Origin: ${chalk.cyan(file ? `${file}:${line}` : functionName)}`);
|
|
611
|
+
console.log(`Tables Reachable: ${chalk.yellow(result.tables.join(', ') || 'none')}`);
|
|
612
|
+
console.log(`Functions Traversed: ${chalk.cyan(result.functionsTraversed)}`);
|
|
613
|
+
console.log(`Max Depth: ${chalk.cyan(result.maxDepth)}`);
|
|
614
|
+
console.log();
|
|
615
|
+
// Sensitive fields
|
|
616
|
+
if (result.sensitiveFields.length > 0) {
|
|
617
|
+
console.log(chalk.bold.yellow('⚠️ Sensitive Fields Accessible:'));
|
|
618
|
+
for (const sf of result.sensitiveFields) {
|
|
619
|
+
const typeColor = sf.field.sensitivityType === 'credentials' ? chalk.red :
|
|
620
|
+
sf.field.sensitivityType === 'pii' ? chalk.yellow :
|
|
621
|
+
sf.field.sensitivityType === 'financial' ? chalk.magenta : chalk.gray;
|
|
622
|
+
console.log(` ${typeColor('●')} ${sf.field.table}.${sf.field.field} (${sf.field.sensitivityType})`);
|
|
623
|
+
console.log(chalk.gray(` ${sf.accessCount} access point(s), ${sf.paths.length} path(s)`));
|
|
624
|
+
}
|
|
625
|
+
console.log();
|
|
626
|
+
}
|
|
627
|
+
// Access points
|
|
628
|
+
if (result.reachableAccess.length > 0) {
|
|
629
|
+
console.log(chalk.bold('Data Access Points:'));
|
|
630
|
+
for (const ra of result.reachableAccess.slice(0, 15)) {
|
|
631
|
+
const opColor = ra.access.operation === 'write' ? chalk.yellow :
|
|
632
|
+
ra.access.operation === 'delete' ? chalk.red : chalk.gray;
|
|
633
|
+
console.log(` ${opColor(ra.access.operation)} ${chalk.white(ra.access.table)}.${ra.access.fields.join(', ')}`);
|
|
634
|
+
console.log(chalk.gray(` Path: ${ra.path.map(p => p.functionName).join(' → ')}`));
|
|
635
|
+
}
|
|
636
|
+
if (result.reachableAccess.length > 15) {
|
|
637
|
+
console.log(chalk.gray(` ... and ${result.reachableAccess.length - 15} more`));
|
|
638
|
+
}
|
|
639
|
+
console.log();
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
console.log(chalk.gray('No data access points found from this location.'));
|
|
643
|
+
console.log();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Inverse subcommand - who can reach this data?
|
|
648
|
+
*/
|
|
649
|
+
async function inverseAction(target, options) {
|
|
650
|
+
const rootDir = process.cwd();
|
|
651
|
+
const format = options.format ?? 'text';
|
|
652
|
+
const maxDepth = options.maxDepth ?? 10;
|
|
653
|
+
if (!(await callGraphExists(rootDir))) {
|
|
654
|
+
if (format === 'json') {
|
|
655
|
+
console.log(JSON.stringify({ error: 'No call graph found' }));
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
showNotBuiltMessage();
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
// Parse target: table or table.field
|
|
663
|
+
const parts = target.split('.');
|
|
664
|
+
const table = parts[0] ?? '';
|
|
665
|
+
const field = parts.length > 1 ? parts.slice(1).join('.') : undefined;
|
|
666
|
+
const analyzer = createCallGraphAnalyzer({ rootDir });
|
|
667
|
+
await analyzer.initialize();
|
|
668
|
+
const result = analyzer.getCodePathsToData(field ? { table, field, maxDepth } : { table, maxDepth });
|
|
669
|
+
// JSON output
|
|
670
|
+
if (format === 'json') {
|
|
671
|
+
console.log(JSON.stringify({
|
|
672
|
+
target: result.target,
|
|
673
|
+
totalAccessors: result.totalAccessors,
|
|
674
|
+
entryPoints: result.entryPoints,
|
|
675
|
+
accessPaths: result.accessPaths.map(ap => ({
|
|
676
|
+
entryPoint: ap.entryPoint,
|
|
677
|
+
path: ap.path.map(p => p.functionName),
|
|
678
|
+
accessPoint: ap.accessPoint ? {
|
|
679
|
+
table: ap.accessPoint.table,
|
|
680
|
+
fields: ap.accessPoint.fields,
|
|
681
|
+
operation: ap.accessPoint.operation,
|
|
682
|
+
} : null,
|
|
683
|
+
})),
|
|
684
|
+
}, null, 2));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
// Text output
|
|
688
|
+
console.log();
|
|
689
|
+
console.log(chalk.bold('🔄 Inverse Reachability'));
|
|
690
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
691
|
+
console.log();
|
|
692
|
+
console.log(`Target: ${chalk.cyan(field ? `${table}.${field}` : table)}`);
|
|
693
|
+
console.log(`Direct Accessors: ${chalk.cyan(result.totalAccessors)}`);
|
|
694
|
+
console.log(`Entry Points That Can Reach: ${chalk.cyan(result.entryPoints.length)}`);
|
|
695
|
+
console.log();
|
|
696
|
+
if (result.accessPaths.length > 0) {
|
|
697
|
+
console.log(chalk.bold('Access Paths:'));
|
|
698
|
+
const graph = analyzer.getGraph();
|
|
699
|
+
for (const ap of result.accessPaths.slice(0, 10)) {
|
|
700
|
+
const entryFunc = graph?.functions.get(ap.entryPoint);
|
|
701
|
+
if (entryFunc) {
|
|
702
|
+
console.log(` ${chalk.magenta('🚪')} ${chalk.white(entryFunc.qualifiedName)}`);
|
|
703
|
+
console.log(chalk.gray(` Path: ${ap.path.map(p => p.functionName).join(' → ')}`));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (result.accessPaths.length > 10) {
|
|
707
|
+
console.log(chalk.gray(` ... and ${result.accessPaths.length - 10} more paths`));
|
|
708
|
+
}
|
|
709
|
+
console.log();
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
console.log(chalk.gray('No entry points can reach this data.'));
|
|
713
|
+
console.log();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Function subcommand - show details about a function
|
|
718
|
+
*/
|
|
719
|
+
async function functionAction(name, options) {
|
|
720
|
+
const rootDir = process.cwd();
|
|
721
|
+
const format = options.format ?? 'text';
|
|
722
|
+
if (!(await callGraphExists(rootDir))) {
|
|
723
|
+
if (format === 'json') {
|
|
724
|
+
console.log(JSON.stringify({ error: 'No call graph found' }));
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
showNotBuiltMessage();
|
|
728
|
+
}
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const analyzer = createCallGraphAnalyzer({ rootDir });
|
|
732
|
+
await analyzer.initialize();
|
|
733
|
+
const graph = analyzer.getGraph();
|
|
734
|
+
if (!graph) {
|
|
735
|
+
console.log(format === 'json'
|
|
736
|
+
? JSON.stringify({ error: 'Failed to load call graph' })
|
|
737
|
+
: chalk.red('Failed to load call graph'));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
// Find function by name
|
|
741
|
+
let func;
|
|
742
|
+
for (const [, f] of graph.functions) {
|
|
743
|
+
if (f.name === name || f.qualifiedName === name || f.id.includes(name)) {
|
|
744
|
+
func = f;
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (!func) {
|
|
749
|
+
console.log(format === 'json'
|
|
750
|
+
? JSON.stringify({ error: `Function '${name}' not found` })
|
|
751
|
+
: chalk.red(`Function '${name}' not found`));
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
// JSON output
|
|
755
|
+
if (format === 'json') {
|
|
756
|
+
console.log(JSON.stringify({
|
|
757
|
+
id: func.id,
|
|
758
|
+
name: func.name,
|
|
759
|
+
qualifiedName: func.qualifiedName,
|
|
760
|
+
file: func.file,
|
|
761
|
+
line: func.startLine,
|
|
762
|
+
language: func.language,
|
|
763
|
+
className: func.className,
|
|
764
|
+
isExported: func.isExported,
|
|
765
|
+
isAsync: func.isAsync,
|
|
766
|
+
parameters: func.parameters,
|
|
767
|
+
returnType: func.returnType,
|
|
768
|
+
calls: func.calls.map(c => ({
|
|
769
|
+
callee: c.calleeName,
|
|
770
|
+
resolved: c.resolved,
|
|
771
|
+
line: c.line,
|
|
772
|
+
})),
|
|
773
|
+
calledBy: func.calledBy.map(c => ({
|
|
774
|
+
caller: c.callerId,
|
|
775
|
+
line: c.line,
|
|
776
|
+
})),
|
|
777
|
+
dataAccess: func.dataAccess.map(d => ({
|
|
778
|
+
table: d.table,
|
|
779
|
+
fields: d.fields,
|
|
780
|
+
operation: d.operation,
|
|
781
|
+
})),
|
|
782
|
+
}, null, 2));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
// Text output
|
|
786
|
+
console.log();
|
|
787
|
+
console.log(chalk.bold(`📋 Function: ${func.qualifiedName}`));
|
|
788
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
789
|
+
console.log();
|
|
790
|
+
console.log(`File: ${chalk.cyan(func.file)}:${func.startLine}`);
|
|
791
|
+
console.log(`Language: ${chalk.cyan(func.language)}`);
|
|
792
|
+
if (func.className)
|
|
793
|
+
console.log(`Class: ${chalk.cyan(func.className)}`);
|
|
794
|
+
console.log(`Exported: ${func.isExported ? chalk.green('yes') : chalk.gray('no')}`);
|
|
795
|
+
console.log(`Async: ${func.isAsync ? chalk.green('yes') : chalk.gray('no')}`);
|
|
796
|
+
console.log();
|
|
797
|
+
// Parameters
|
|
798
|
+
if (func.parameters.length > 0) {
|
|
799
|
+
console.log(chalk.bold('Parameters:'));
|
|
800
|
+
for (const p of func.parameters) {
|
|
801
|
+
console.log(` ${chalk.white(p.name)}${p.type ? `: ${chalk.gray(p.type)}` : ''}`);
|
|
802
|
+
}
|
|
803
|
+
console.log();
|
|
804
|
+
}
|
|
805
|
+
// Calls
|
|
806
|
+
if (func.calls.length > 0) {
|
|
807
|
+
console.log(chalk.bold(`Calls (${func.calls.length}):`));
|
|
808
|
+
for (const c of func.calls.slice(0, 10)) {
|
|
809
|
+
const status = c.resolved ? chalk.green('✓') : chalk.gray('?');
|
|
810
|
+
console.log(` ${status} ${chalk.white(c.calleeName)} ${chalk.gray(`line ${c.line}`)}`);
|
|
811
|
+
}
|
|
812
|
+
if (func.calls.length > 10) {
|
|
813
|
+
console.log(chalk.gray(` ... and ${func.calls.length - 10} more`));
|
|
814
|
+
}
|
|
815
|
+
console.log();
|
|
816
|
+
}
|
|
817
|
+
// Called by
|
|
818
|
+
if (func.calledBy.length > 0) {
|
|
819
|
+
console.log(chalk.bold(`Called By (${func.calledBy.length}):`));
|
|
820
|
+
for (const c of func.calledBy.slice(0, 10)) {
|
|
821
|
+
const caller = graph.functions.get(c.callerId);
|
|
822
|
+
console.log(` ${chalk.white(caller?.qualifiedName ?? c.callerId)}`);
|
|
823
|
+
}
|
|
824
|
+
if (func.calledBy.length > 10) {
|
|
825
|
+
console.log(chalk.gray(` ... and ${func.calledBy.length - 10} more`));
|
|
826
|
+
}
|
|
827
|
+
console.log();
|
|
828
|
+
}
|
|
829
|
+
// Data access
|
|
830
|
+
if (func.dataAccess.length > 0) {
|
|
831
|
+
console.log(chalk.bold('Data Access:'));
|
|
832
|
+
for (const d of func.dataAccess) {
|
|
833
|
+
const opColor = d.operation === 'write' ? chalk.yellow :
|
|
834
|
+
d.operation === 'delete' ? chalk.red : chalk.gray;
|
|
835
|
+
console.log(` ${opColor(d.operation)} ${chalk.white(d.table)}.${d.fields.join(', ')}`);
|
|
836
|
+
}
|
|
837
|
+
console.log();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Impact subcommand - what breaks if I change this?
|
|
842
|
+
*/
|
|
843
|
+
async function impactAction(target, options) {
|
|
844
|
+
const rootDir = process.cwd();
|
|
845
|
+
const format = options.format ?? 'text';
|
|
846
|
+
if (!(await callGraphExists(rootDir))) {
|
|
847
|
+
if (format === 'json') {
|
|
848
|
+
console.log(JSON.stringify({ error: 'No call graph found' }));
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
showNotBuiltMessage();
|
|
852
|
+
}
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const spinner = format === 'text' ? createSpinner('Analyzing impact...') : null;
|
|
856
|
+
spinner?.start();
|
|
857
|
+
const analyzer = createCallGraphAnalyzer({ rootDir });
|
|
858
|
+
await analyzer.initialize();
|
|
859
|
+
const graph = analyzer.getGraph();
|
|
860
|
+
if (!graph) {
|
|
861
|
+
spinner?.stop();
|
|
862
|
+
console.log(format === 'json'
|
|
863
|
+
? JSON.stringify({ error: 'Failed to load call graph' })
|
|
864
|
+
: chalk.red('Failed to load call graph'));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const impactAnalyzer = createImpactAnalyzer(graph);
|
|
868
|
+
let result;
|
|
869
|
+
// Determine if target is a file or function
|
|
870
|
+
if (target.includes('/') || target.includes('.py') || target.includes('.ts') || target.includes('.js')) {
|
|
871
|
+
// It's a file path
|
|
872
|
+
result = impactAnalyzer.analyzeFile(target);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
// It's a function name
|
|
876
|
+
result = impactAnalyzer.analyzeFunctionByName(target);
|
|
877
|
+
}
|
|
878
|
+
spinner?.stop();
|
|
879
|
+
// JSON output
|
|
880
|
+
if (format === 'json') {
|
|
881
|
+
console.log(JSON.stringify({
|
|
882
|
+
target: result.target,
|
|
883
|
+
risk: result.risk,
|
|
884
|
+
riskScore: result.riskScore,
|
|
885
|
+
summary: result.summary,
|
|
886
|
+
affected: result.affected.slice(0, 50).map(a => ({
|
|
887
|
+
name: a.qualifiedName,
|
|
888
|
+
file: a.file,
|
|
889
|
+
line: a.line,
|
|
890
|
+
depth: a.depth,
|
|
891
|
+
isEntryPoint: a.isEntryPoint,
|
|
892
|
+
accessesSensitiveData: a.accessesSensitiveData,
|
|
893
|
+
path: a.pathToChange.map(p => p.functionName),
|
|
894
|
+
})),
|
|
895
|
+
entryPoints: result.entryPoints.map(e => ({
|
|
896
|
+
name: e.qualifiedName,
|
|
897
|
+
file: e.file,
|
|
898
|
+
line: e.line,
|
|
899
|
+
path: e.pathToChange.map(p => p.functionName),
|
|
900
|
+
})),
|
|
901
|
+
sensitiveDataPaths: result.sensitiveDataPaths.map(p => ({
|
|
902
|
+
table: p.table,
|
|
903
|
+
fields: p.fields,
|
|
904
|
+
operation: p.operation,
|
|
905
|
+
sensitivity: p.sensitivity,
|
|
906
|
+
entryPoint: p.entryPoint,
|
|
907
|
+
path: p.fullPath.map(n => n.functionName),
|
|
908
|
+
})),
|
|
909
|
+
}, null, 2));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
// Text output
|
|
913
|
+
console.log();
|
|
914
|
+
console.log(chalk.bold('💥 Impact Analysis'));
|
|
915
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
916
|
+
console.log();
|
|
917
|
+
// Target info
|
|
918
|
+
if (result.target.type === 'file') {
|
|
919
|
+
console.log(`Target: ${chalk.cyan(result.target.file)} (${result.changedFunctions.length} functions)`);
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
console.log(`Target: ${chalk.cyan(result.target.functionName ?? result.target.functionId ?? 'unknown')}`);
|
|
923
|
+
}
|
|
924
|
+
console.log();
|
|
925
|
+
// Risk assessment
|
|
926
|
+
const riskColor = result.risk === 'critical' ? chalk.bgRed.white :
|
|
927
|
+
result.risk === 'high' ? chalk.red :
|
|
928
|
+
result.risk === 'medium' ? chalk.yellow : chalk.green;
|
|
929
|
+
console.log(`Risk Level: ${riskColor(` ${result.risk.toUpperCase()} `)} (score: ${result.riskScore}/100)`);
|
|
930
|
+
console.log();
|
|
931
|
+
// Summary
|
|
932
|
+
console.log(chalk.bold('Summary:'));
|
|
933
|
+
console.log(` Direct Callers: ${chalk.cyan(result.summary.directCallers)}`);
|
|
934
|
+
console.log(` Transitive Callers: ${chalk.cyan(result.summary.transitiveCallers)}`);
|
|
935
|
+
console.log(` Affected Entry Points: ${chalk.yellow(result.summary.affectedEntryPoints)}`);
|
|
936
|
+
console.log(` Sensitive Data Paths: ${chalk.red(result.summary.affectedDataPaths)}`);
|
|
937
|
+
console.log(` Max Call Depth: ${chalk.gray(result.summary.maxDepth)}`);
|
|
938
|
+
console.log();
|
|
939
|
+
// Entry points affected
|
|
940
|
+
if (result.entryPoints.length > 0) {
|
|
941
|
+
console.log(chalk.bold.yellow('🚪 Affected Entry Points (User-Facing Impact):'));
|
|
942
|
+
for (const ep of result.entryPoints.slice(0, 10)) {
|
|
943
|
+
console.log(` ${chalk.magenta('●')} ${chalk.white(ep.qualifiedName)}`);
|
|
944
|
+
console.log(chalk.gray(` ${ep.file}:${ep.line}`));
|
|
945
|
+
console.log(chalk.gray(` Path: ${ep.pathToChange.map(p => p.functionName).join(' → ')}`));
|
|
946
|
+
}
|
|
947
|
+
if (result.entryPoints.length > 10) {
|
|
948
|
+
console.log(chalk.gray(` ... and ${result.entryPoints.length - 10} more entry points`));
|
|
949
|
+
}
|
|
950
|
+
console.log();
|
|
951
|
+
}
|
|
952
|
+
// Sensitive data paths
|
|
953
|
+
if (result.sensitiveDataPaths.length > 0) {
|
|
954
|
+
console.log(chalk.bold.red('🔒 Sensitive Data Paths Affected:'));
|
|
955
|
+
for (const dp of result.sensitiveDataPaths.slice(0, 10)) {
|
|
956
|
+
const sensitivityIcon = dp.sensitivity === 'credentials' ? '🔑' :
|
|
957
|
+
dp.sensitivity === 'financial' ? '💰' :
|
|
958
|
+
dp.sensitivity === 'health' ? '🏥' : '👤';
|
|
959
|
+
const sensitivityColor = dp.sensitivity === 'credentials' ? chalk.red :
|
|
960
|
+
dp.sensitivity === 'financial' ? chalk.magenta :
|
|
961
|
+
dp.sensitivity === 'health' ? chalk.blue : chalk.yellow;
|
|
962
|
+
console.log(` ${sensitivityIcon} ${sensitivityColor(dp.sensitivity)} ${chalk.white(dp.table)}.${dp.fields.join(', ')}`);
|
|
963
|
+
console.log(chalk.gray(` Entry: ${dp.entryPoint}`));
|
|
964
|
+
console.log(chalk.gray(` Path: ${dp.fullPath.map(n => n.functionName).join(' → ')}`));
|
|
965
|
+
}
|
|
966
|
+
if (result.sensitiveDataPaths.length > 10) {
|
|
967
|
+
console.log(chalk.gray(` ... and ${result.sensitiveDataPaths.length - 10} more sensitive paths`));
|
|
968
|
+
}
|
|
969
|
+
console.log();
|
|
970
|
+
}
|
|
971
|
+
// Direct callers
|
|
972
|
+
const directCallers = result.affected.filter(a => a.depth === 1);
|
|
973
|
+
if (directCallers.length > 0) {
|
|
974
|
+
console.log(chalk.bold('📞 Direct Callers (Immediate Impact):'));
|
|
975
|
+
for (const caller of directCallers.slice(0, 10)) {
|
|
976
|
+
const icon = caller.accessesSensitiveData ? chalk.red('●') : chalk.gray('○');
|
|
977
|
+
console.log(` ${icon} ${chalk.white(caller.qualifiedName)}`);
|
|
978
|
+
console.log(chalk.gray(` ${caller.file}:${caller.line}`));
|
|
979
|
+
}
|
|
980
|
+
if (directCallers.length > 10) {
|
|
981
|
+
console.log(chalk.gray(` ... and ${directCallers.length - 10} more direct callers`));
|
|
982
|
+
}
|
|
983
|
+
console.log();
|
|
984
|
+
}
|
|
985
|
+
// Transitive callers (depth > 1)
|
|
986
|
+
const transitiveCallers = result.affected.filter(a => a.depth > 1);
|
|
987
|
+
if (transitiveCallers.length > 0) {
|
|
988
|
+
console.log(chalk.bold('🔗 Transitive Callers (Ripple Effect):'));
|
|
989
|
+
for (const caller of transitiveCallers.slice(0, 8)) {
|
|
990
|
+
const depthIndicator = chalk.gray(`[depth ${caller.depth}]`);
|
|
991
|
+
console.log(` ${depthIndicator} ${chalk.white(caller.qualifiedName)}`);
|
|
992
|
+
}
|
|
993
|
+
if (transitiveCallers.length > 8) {
|
|
994
|
+
console.log(chalk.gray(` ... and ${transitiveCallers.length - 8} more transitive callers`));
|
|
995
|
+
}
|
|
996
|
+
console.log();
|
|
997
|
+
}
|
|
998
|
+
// Recommendations
|
|
999
|
+
if (result.risk === 'critical' || result.risk === 'high') {
|
|
1000
|
+
console.log(chalk.bold('⚠️ Recommendations:'));
|
|
1001
|
+
if (result.sensitiveDataPaths.length > 0) {
|
|
1002
|
+
console.log(chalk.yellow(' • Review all sensitive data paths before merging'));
|
|
1003
|
+
}
|
|
1004
|
+
if (result.entryPoints.length > 5) {
|
|
1005
|
+
console.log(chalk.yellow(' • Consider incremental rollout - many entry points affected'));
|
|
1006
|
+
}
|
|
1007
|
+
if (result.summary.maxDepth > 5) {
|
|
1008
|
+
console.log(chalk.yellow(' • Deep call chain - test thoroughly for regressions'));
|
|
1009
|
+
}
|
|
1010
|
+
console.log();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Dead code subcommand - find unused functions
|
|
1015
|
+
*/
|
|
1016
|
+
async function deadCodeAction(options) {
|
|
1017
|
+
const rootDir = process.cwd();
|
|
1018
|
+
const format = options.format ?? 'text';
|
|
1019
|
+
const minConfidence = (options.confidence ?? 'low');
|
|
1020
|
+
const includeExported = options.includeExported ?? false;
|
|
1021
|
+
const includeTests = options.includeTests ?? false;
|
|
1022
|
+
if (!(await callGraphExists(rootDir))) {
|
|
1023
|
+
if (format === 'json') {
|
|
1024
|
+
console.log(JSON.stringify({ error: 'No call graph found' }));
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
showNotBuiltMessage();
|
|
1028
|
+
}
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const spinner = format === 'text' ? createSpinner('Detecting dead code...') : null;
|
|
1032
|
+
spinner?.start();
|
|
1033
|
+
const analyzer = createCallGraphAnalyzer({ rootDir });
|
|
1034
|
+
await analyzer.initialize();
|
|
1035
|
+
const graph = analyzer.getGraph();
|
|
1036
|
+
if (!graph) {
|
|
1037
|
+
spinner?.stop();
|
|
1038
|
+
console.log(format === 'json'
|
|
1039
|
+
? JSON.stringify({ error: 'Failed to load call graph' })
|
|
1040
|
+
: chalk.red('Failed to load call graph'));
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const detector = createDeadCodeDetector(graph);
|
|
1044
|
+
const result = detector.detect({
|
|
1045
|
+
minConfidence,
|
|
1046
|
+
includeExported,
|
|
1047
|
+
includeTests,
|
|
1048
|
+
});
|
|
1049
|
+
spinner?.stop();
|
|
1050
|
+
// JSON output
|
|
1051
|
+
if (format === 'json') {
|
|
1052
|
+
console.log(JSON.stringify({
|
|
1053
|
+
summary: result.summary,
|
|
1054
|
+
candidates: result.candidates.slice(0, 100).map(c => ({
|
|
1055
|
+
name: c.qualifiedName,
|
|
1056
|
+
file: c.file,
|
|
1057
|
+
line: c.line,
|
|
1058
|
+
confidence: c.confidence,
|
|
1059
|
+
linesOfCode: c.linesOfCode,
|
|
1060
|
+
possibleFalsePositives: c.possibleFalsePositives,
|
|
1061
|
+
hasDataAccess: c.hasDataAccess,
|
|
1062
|
+
})),
|
|
1063
|
+
excluded: result.excluded,
|
|
1064
|
+
}, null, 2));
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
// Text output
|
|
1068
|
+
console.log();
|
|
1069
|
+
console.log(chalk.bold('🗑️ Dead Code Detection'));
|
|
1070
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
1071
|
+
console.log();
|
|
1072
|
+
// Summary
|
|
1073
|
+
const { summary } = result;
|
|
1074
|
+
console.log(chalk.bold('Summary:'));
|
|
1075
|
+
console.log(` Total Functions: ${chalk.cyan(summary.totalFunctions)}`);
|
|
1076
|
+
console.log(` Dead Code Candidates: ${chalk.yellow(summary.deadCandidates)}`);
|
|
1077
|
+
console.log(` Estimated Dead Lines: ${chalk.red(summary.estimatedDeadLines)}`);
|
|
1078
|
+
console.log();
|
|
1079
|
+
// By confidence
|
|
1080
|
+
console.log(chalk.bold('By Confidence:'));
|
|
1081
|
+
console.log(` ${chalk.red('🔴 High:')} ${summary.highConfidence} (safe to remove)`);
|
|
1082
|
+
console.log(` ${chalk.yellow('🟡 Medium:')} ${summary.mediumConfidence} (review first)`);
|
|
1083
|
+
console.log(` ${chalk.gray('⚪ Low:')} ${summary.lowConfidence} (might be false positive)`);
|
|
1084
|
+
console.log();
|
|
1085
|
+
// Excluded
|
|
1086
|
+
console.log(chalk.bold('Excluded from Analysis:'));
|
|
1087
|
+
console.log(` Entry Points: ${chalk.gray(result.excluded.entryPoints)}`);
|
|
1088
|
+
console.log(` Functions with Callers: ${chalk.gray(result.excluded.withCallers)}`);
|
|
1089
|
+
console.log(` Framework Hooks: ${chalk.gray(result.excluded.frameworkHooks)}`);
|
|
1090
|
+
console.log();
|
|
1091
|
+
// High confidence candidates
|
|
1092
|
+
const highConf = result.candidates.filter(c => c.confidence === 'high');
|
|
1093
|
+
if (highConf.length > 0) {
|
|
1094
|
+
console.log(chalk.bold.red('🔴 High Confidence (Safe to Remove):'));
|
|
1095
|
+
for (const c of highConf.slice(0, 15)) {
|
|
1096
|
+
console.log(` ${chalk.white(c.qualifiedName)} ${chalk.gray(`(${c.linesOfCode} lines)`)}`);
|
|
1097
|
+
console.log(chalk.gray(` ${c.file}:${c.line}`));
|
|
1098
|
+
}
|
|
1099
|
+
if (highConf.length > 15) {
|
|
1100
|
+
console.log(chalk.gray(` ... and ${highConf.length - 15} more`));
|
|
1101
|
+
}
|
|
1102
|
+
console.log();
|
|
1103
|
+
}
|
|
1104
|
+
// Medium confidence candidates
|
|
1105
|
+
const medConf = result.candidates.filter(c => c.confidence === 'medium');
|
|
1106
|
+
if (medConf.length > 0) {
|
|
1107
|
+
console.log(chalk.bold.yellow('🟡 Medium Confidence (Review First):'));
|
|
1108
|
+
for (const c of medConf.slice(0, 10)) {
|
|
1109
|
+
const reasons = c.possibleFalsePositives.slice(0, 2).join(', ');
|
|
1110
|
+
console.log(` ${chalk.white(c.qualifiedName)} ${chalk.gray(`(${c.linesOfCode} lines)`)}`);
|
|
1111
|
+
console.log(chalk.gray(` ${c.file}:${c.line}`));
|
|
1112
|
+
if (reasons) {
|
|
1113
|
+
console.log(chalk.gray(` Might be: ${reasons}`));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (medConf.length > 10) {
|
|
1117
|
+
console.log(chalk.gray(` ... and ${medConf.length - 10} more`));
|
|
1118
|
+
}
|
|
1119
|
+
console.log();
|
|
1120
|
+
}
|
|
1121
|
+
// Files with most dead code
|
|
1122
|
+
if (summary.byFile.length > 0) {
|
|
1123
|
+
console.log(chalk.bold('Files with Most Dead Code:'));
|
|
1124
|
+
for (const f of summary.byFile.slice(0, 10)) {
|
|
1125
|
+
console.log(` ${chalk.cyan(f.count)} functions (${f.lines} lines): ${chalk.white(f.file)}`);
|
|
1126
|
+
}
|
|
1127
|
+
console.log();
|
|
1128
|
+
}
|
|
1129
|
+
// Recommendations
|
|
1130
|
+
if (summary.highConfidence > 0) {
|
|
1131
|
+
console.log(chalk.bold('💡 Recommendations:'));
|
|
1132
|
+
console.log(chalk.green(` • ${summary.highConfidence} functions can likely be safely removed`));
|
|
1133
|
+
console.log(chalk.green(` • This would remove ~${summary.estimatedDeadLines} lines of code`));
|
|
1134
|
+
if (summary.mediumConfidence > 0) {
|
|
1135
|
+
console.log(chalk.yellow(` • Review ${summary.mediumConfidence} medium-confidence candidates before removing`));
|
|
1136
|
+
}
|
|
1137
|
+
console.log();
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Coverage subcommand - analyze test coverage for sensitive data access
|
|
1142
|
+
*/
|
|
1143
|
+
async function coverageAction(options) {
|
|
1144
|
+
const rootDir = process.cwd();
|
|
1145
|
+
const format = options.format ?? 'text';
|
|
1146
|
+
const showSensitive = options.sensitive ?? true;
|
|
1147
|
+
if (!(await callGraphExists(rootDir))) {
|
|
1148
|
+
if (format === 'json') {
|
|
1149
|
+
console.log(JSON.stringify({ error: 'No call graph found' }));
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
showNotBuiltMessage();
|
|
1153
|
+
}
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const spinner = format === 'text' ? createSpinner('Analyzing test coverage for sensitive data...') : null;
|
|
1157
|
+
spinner?.start();
|
|
1158
|
+
const analyzer = createCallGraphAnalyzer({ rootDir });
|
|
1159
|
+
await analyzer.initialize();
|
|
1160
|
+
const graph = analyzer.getGraph();
|
|
1161
|
+
if (!graph) {
|
|
1162
|
+
spinner?.stop();
|
|
1163
|
+
console.log(format === 'json'
|
|
1164
|
+
? JSON.stringify({ error: 'Failed to load call graph' })
|
|
1165
|
+
: chalk.red('Failed to load call graph'));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const coverageAnalyzer = createCoverageAnalyzer(graph);
|
|
1169
|
+
const result = coverageAnalyzer.analyze();
|
|
1170
|
+
spinner?.stop();
|
|
1171
|
+
// JSON output
|
|
1172
|
+
if (format === 'json') {
|
|
1173
|
+
console.log(JSON.stringify({
|
|
1174
|
+
summary: result.summary,
|
|
1175
|
+
fields: result.fields.map(f => ({
|
|
1176
|
+
field: f.fullName,
|
|
1177
|
+
sensitivity: f.sensitivity,
|
|
1178
|
+
totalPaths: f.totalPaths,
|
|
1179
|
+
testedPaths: f.testedPaths,
|
|
1180
|
+
coveragePercent: f.coveragePercent,
|
|
1181
|
+
status: f.status,
|
|
1182
|
+
})),
|
|
1183
|
+
uncoveredPaths: result.uncoveredPaths.slice(0, 50).map(p => ({
|
|
1184
|
+
field: `${p.table}.${p.field}`,
|
|
1185
|
+
sensitivity: p.sensitivity,
|
|
1186
|
+
entryPoint: p.entryPoint.name,
|
|
1187
|
+
accessor: p.accessor.name,
|
|
1188
|
+
depth: p.depth,
|
|
1189
|
+
})),
|
|
1190
|
+
testFiles: result.testFiles,
|
|
1191
|
+
testFunctions: result.testFunctions,
|
|
1192
|
+
}, null, 2));
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
// Text output
|
|
1196
|
+
console.log();
|
|
1197
|
+
console.log(chalk.bold('🧪 Sensitive Data Test Coverage'));
|
|
1198
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
1199
|
+
console.log();
|
|
1200
|
+
// Summary
|
|
1201
|
+
const { summary } = result;
|
|
1202
|
+
console.log(chalk.bold('Summary:'));
|
|
1203
|
+
console.log(` Sensitive Fields: ${chalk.cyan(summary.totalSensitiveFields)}`);
|
|
1204
|
+
console.log(` Access Paths: ${chalk.cyan(summary.totalAccessPaths)}`);
|
|
1205
|
+
console.log(` Tested Paths: ${chalk.green(summary.testedAccessPaths)}`);
|
|
1206
|
+
console.log(` Coverage: ${getCoverageColor(summary.coveragePercent)(`${summary.coveragePercent}%`)}`);
|
|
1207
|
+
console.log(` Test Files: ${chalk.gray(result.testFiles.length)}`);
|
|
1208
|
+
console.log(` Test Functions: ${chalk.gray(result.testFunctions)}`);
|
|
1209
|
+
console.log();
|
|
1210
|
+
// By sensitivity
|
|
1211
|
+
console.log(chalk.bold('Coverage by Sensitivity:'));
|
|
1212
|
+
const sensOrder = ['credentials', 'financial', 'health', 'pii'];
|
|
1213
|
+
for (const sens of sensOrder) {
|
|
1214
|
+
const s = summary.bySensitivity[sens];
|
|
1215
|
+
if (s.fields > 0) {
|
|
1216
|
+
const icon = sens === 'credentials' ? '🔑' :
|
|
1217
|
+
sens === 'financial' ? '💰' :
|
|
1218
|
+
sens === 'health' ? '🏥' : '👤';
|
|
1219
|
+
const color = getCoverageColor(s.coveragePercent);
|
|
1220
|
+
console.log(` ${icon} ${chalk.white(sens)}: ${color(`${s.coveragePercent}%`)} (${s.testedPaths}/${s.paths} paths)`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
console.log();
|
|
1224
|
+
// Field coverage
|
|
1225
|
+
if (result.fields.length > 0 && showSensitive) {
|
|
1226
|
+
console.log(chalk.bold('Field Coverage:'));
|
|
1227
|
+
for (const f of result.fields.slice(0, 20)) {
|
|
1228
|
+
const statusIcon = f.status === 'covered' ? chalk.green('✓') :
|
|
1229
|
+
f.status === 'partial' ? chalk.yellow('◐') :
|
|
1230
|
+
chalk.red('✗');
|
|
1231
|
+
const coverageColor = getCoverageColor(f.coveragePercent);
|
|
1232
|
+
const sensIcon = f.sensitivity === 'credentials' ? '🔑' :
|
|
1233
|
+
f.sensitivity === 'financial' ? '💰' :
|
|
1234
|
+
f.sensitivity === 'health' ? '🏥' : '👤';
|
|
1235
|
+
console.log(` ${statusIcon} ${sensIcon} ${chalk.white(f.fullName)}: ${coverageColor(`${f.testedPaths}/${f.totalPaths}`)} paths tested`);
|
|
1236
|
+
}
|
|
1237
|
+
if (result.fields.length > 20) {
|
|
1238
|
+
console.log(chalk.gray(` ... and ${result.fields.length - 20} more fields`));
|
|
1239
|
+
}
|
|
1240
|
+
console.log();
|
|
1241
|
+
}
|
|
1242
|
+
// Uncovered paths (highest priority)
|
|
1243
|
+
const uncoveredByCredentials = result.uncoveredPaths.filter(p => p.sensitivity === 'credentials');
|
|
1244
|
+
const uncoveredByFinancial = result.uncoveredPaths.filter(p => p.sensitivity === 'financial');
|
|
1245
|
+
const uncoveredByHealth = result.uncoveredPaths.filter(p => p.sensitivity === 'health');
|
|
1246
|
+
const uncoveredByPii = result.uncoveredPaths.filter(p => p.sensitivity === 'pii');
|
|
1247
|
+
if (uncoveredByCredentials.length > 0) {
|
|
1248
|
+
console.log(chalk.bold.red('🔑 Untested Credential Access Paths:'));
|
|
1249
|
+
for (const p of uncoveredByCredentials.slice(0, 5)) {
|
|
1250
|
+
console.log(` ${chalk.white(`${p.table}.${p.field}`)}`);
|
|
1251
|
+
console.log(chalk.gray(` Entry: ${p.entryPoint.name} → Accessor: ${p.accessor.name}`));
|
|
1252
|
+
console.log(chalk.gray(` ${p.entryPoint.file}:${p.entryPoint.line}`));
|
|
1253
|
+
}
|
|
1254
|
+
if (uncoveredByCredentials.length > 5) {
|
|
1255
|
+
console.log(chalk.gray(` ... and ${uncoveredByCredentials.length - 5} more`));
|
|
1256
|
+
}
|
|
1257
|
+
console.log();
|
|
1258
|
+
}
|
|
1259
|
+
if (uncoveredByFinancial.length > 0) {
|
|
1260
|
+
console.log(chalk.bold.magenta('💰 Untested Financial Data Paths:'));
|
|
1261
|
+
for (const p of uncoveredByFinancial.slice(0, 5)) {
|
|
1262
|
+
console.log(` ${chalk.white(`${p.table}.${p.field}`)}`);
|
|
1263
|
+
console.log(chalk.gray(` Entry: ${p.entryPoint.name} → Accessor: ${p.accessor.name}`));
|
|
1264
|
+
}
|
|
1265
|
+
if (uncoveredByFinancial.length > 5) {
|
|
1266
|
+
console.log(chalk.gray(` ... and ${uncoveredByFinancial.length - 5} more`));
|
|
1267
|
+
}
|
|
1268
|
+
console.log();
|
|
1269
|
+
}
|
|
1270
|
+
if (uncoveredByHealth.length > 0) {
|
|
1271
|
+
console.log(chalk.bold.blue('🏥 Untested Health Data Paths:'));
|
|
1272
|
+
for (const p of uncoveredByHealth.slice(0, 3)) {
|
|
1273
|
+
console.log(` ${chalk.white(`${p.table}.${p.field}`)}`);
|
|
1274
|
+
console.log(chalk.gray(` Entry: ${p.entryPoint.name} → Accessor: ${p.accessor.name}`));
|
|
1275
|
+
}
|
|
1276
|
+
if (uncoveredByHealth.length > 3) {
|
|
1277
|
+
console.log(chalk.gray(` ... and ${uncoveredByHealth.length - 3} more`));
|
|
1278
|
+
}
|
|
1279
|
+
console.log();
|
|
1280
|
+
}
|
|
1281
|
+
if (uncoveredByPii.length > 0) {
|
|
1282
|
+
console.log(chalk.bold.yellow('👤 Untested PII Access Paths:'));
|
|
1283
|
+
for (const p of uncoveredByPii.slice(0, 3)) {
|
|
1284
|
+
console.log(` ${chalk.white(`${p.table}.${p.field}`)}`);
|
|
1285
|
+
console.log(chalk.gray(` Entry: ${p.entryPoint.name} → Accessor: ${p.accessor.name}`));
|
|
1286
|
+
}
|
|
1287
|
+
if (uncoveredByPii.length > 3) {
|
|
1288
|
+
console.log(chalk.gray(` ... and ${uncoveredByPii.length - 3} more`));
|
|
1289
|
+
}
|
|
1290
|
+
console.log();
|
|
1291
|
+
}
|
|
1292
|
+
// Recommendations
|
|
1293
|
+
if (result.uncoveredPaths.length > 0) {
|
|
1294
|
+
console.log(chalk.bold('💡 Recommendations:'));
|
|
1295
|
+
if (uncoveredByCredentials.length > 0) {
|
|
1296
|
+
console.log(chalk.red(` • ${uncoveredByCredentials.length} credential access paths need tests (highest priority)`));
|
|
1297
|
+
}
|
|
1298
|
+
if (uncoveredByFinancial.length > 0) {
|
|
1299
|
+
console.log(chalk.magenta(` • ${uncoveredByFinancial.length} financial data paths need tests`));
|
|
1300
|
+
}
|
|
1301
|
+
if (summary.coveragePercent < 50) {
|
|
1302
|
+
console.log(chalk.yellow(` • Overall coverage is ${summary.coveragePercent}% - consider adding integration tests`));
|
|
1303
|
+
}
|
|
1304
|
+
console.log();
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
console.log(chalk.green('✓ All sensitive data access paths are covered by tests!'));
|
|
1308
|
+
console.log();
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Get color function based on coverage percentage
|
|
1313
|
+
*/
|
|
1314
|
+
function getCoverageColor(percent) {
|
|
1315
|
+
if (percent >= 80)
|
|
1316
|
+
return chalk.green;
|
|
1317
|
+
if (percent >= 50)
|
|
1318
|
+
return chalk.yellow;
|
|
1319
|
+
return chalk.red;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Create the callgraph command with subcommands
|
|
1323
|
+
*/
|
|
1324
|
+
export const callgraphCommand = new Command('callgraph')
|
|
1325
|
+
.description('Build and query call graphs for code reachability analysis')
|
|
1326
|
+
.option('--verbose', 'Enable verbose output')
|
|
1327
|
+
.action(statusAction);
|
|
1328
|
+
// Subcommands
|
|
1329
|
+
callgraphCommand
|
|
1330
|
+
.command('build')
|
|
1331
|
+
.description('Build the call graph from source files')
|
|
1332
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
1333
|
+
.action(buildAction);
|
|
1334
|
+
callgraphCommand
|
|
1335
|
+
.command('status')
|
|
1336
|
+
.description('Show call graph overview and statistics')
|
|
1337
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
1338
|
+
.option('-s, --security', 'Show security-prioritized view (P0-P4 tiers)')
|
|
1339
|
+
.action(statusAction);
|
|
1340
|
+
callgraphCommand
|
|
1341
|
+
.command('reach <location>')
|
|
1342
|
+
.description('What data can this code reach? (file:line or function_name)')
|
|
1343
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
1344
|
+
.option('-d, --max-depth <depth>', 'Maximum traversal depth', '10')
|
|
1345
|
+
.action((location, opts) => reachAction(location, { ...opts, maxDepth: parseInt(opts.maxDepth, 10) }));
|
|
1346
|
+
callgraphCommand
|
|
1347
|
+
.command('inverse <target>')
|
|
1348
|
+
.description('Who can reach this data? (table or table.field)')
|
|
1349
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
1350
|
+
.option('-d, --max-depth <depth>', 'Maximum traversal depth', '10')
|
|
1351
|
+
.action((target, opts) => inverseAction(target, { ...opts, maxDepth: parseInt(opts.maxDepth, 10) }));
|
|
1352
|
+
callgraphCommand
|
|
1353
|
+
.command('function <name>')
|
|
1354
|
+
.description('Show details about a specific function')
|
|
1355
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
1356
|
+
.action(functionAction);
|
|
1357
|
+
callgraphCommand
|
|
1358
|
+
.command('impact <target>')
|
|
1359
|
+
.description('What breaks if I change this? (file path or function name)')
|
|
1360
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
1361
|
+
.action(impactAction);
|
|
1362
|
+
callgraphCommand
|
|
1363
|
+
.command('dead')
|
|
1364
|
+
.description('Find dead code (functions never called)')
|
|
1365
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
1366
|
+
.option('-c, --confidence <level>', 'Minimum confidence (high, medium, low)', 'low')
|
|
1367
|
+
.option('--include-exported', 'Include exported functions (might be used externally)')
|
|
1368
|
+
.option('--include-tests', 'Include test files')
|
|
1369
|
+
.action(deadCodeAction);
|
|
1370
|
+
callgraphCommand
|
|
1371
|
+
.command('coverage')
|
|
1372
|
+
.description('Analyze test coverage for sensitive data access paths')
|
|
1373
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
1374
|
+
.option('--sensitive', 'Show sensitive field details (default: true)')
|
|
1375
|
+
.action(coverageAction);
|
|
1376
|
+
//# sourceMappingURL=callgraph.js.map
|