driftdetect 0.9.30 → 0.9.31
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 +3 -1
- package/dist/bin/drift.js.map +1 -1
- package/dist/commands/approve.d.ts +6 -0
- package/dist/commands/approve.d.ts.map +1 -1
- package/dist/commands/approve.js +91 -2
- package/dist/commands/approve.js.map +1 -1
- package/dist/commands/audit.d.ts +29 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +495 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/callgraph.d.ts.map +1 -1
- package/dist/commands/callgraph.js +174 -39
- package/dist/commands/callgraph.js.map +1 -1
- package/dist/commands/coupling.d.ts.map +1 -1
- package/dist/commands/coupling.js +131 -1
- package/dist/commands/coupling.js.map +1 -1
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +80 -1
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/error-handling.d.ts.map +1 -1
- package/dist/commands/error-handling.js +126 -1
- package/dist/commands/error-handling.js.map +1 -1
- package/dist/commands/index.d.ts +1 -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/scan.d.ts +4 -0
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +490 -181
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +3 -2
- package/dist/commands/watch.js.map +1 -1
- package/dist/commands/wrappers.d.ts.map +1 -1
- package/dist/commands/wrappers.js +184 -0
- package/dist/commands/wrappers.js.map +1 -1
- package/package.json +13 -13
package/dist/commands/scan.js
CHANGED
|
@@ -11,7 +11,9 @@ import * as fs from 'node:fs/promises';
|
|
|
11
11
|
import * as path from 'node:path';
|
|
12
12
|
import * as crypto from 'node:crypto';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
-
import { PatternStore, HistoryStore, FileWalker, createDataLake, loadProjectConfig, getProjectRegistry, createTelemetryClient, createTestTopologyAnalyzer, createCallGraphAnalyzer,
|
|
14
|
+
import { PatternStore, HistoryStore, FileWalker, createDataLake, loadProjectConfig, getProjectRegistry, createTelemetryClient, createTestTopologyAnalyzer, createCallGraphAnalyzer, getDefaultIgnorePatterns, mergeIgnorePatterns,
|
|
15
|
+
// Native adapters with TypeScript fallback
|
|
16
|
+
isNativeAvailable, analyzeTestTopologyWithFallback, scanBoundariesWithFallback, analyzeConstantsWithFallback, buildCallGraph, ConstantStore, } from 'driftdetect-core';
|
|
15
17
|
import { createSpinner, status } from '../ui/spinner.js';
|
|
16
18
|
import { createPatternsTable } from '../ui/table.js';
|
|
17
19
|
import { createScannerService } from '../services/scanner-service.js';
|
|
@@ -157,87 +159,20 @@ async function isDriftInitialized(rootDir) {
|
|
|
157
159
|
}
|
|
158
160
|
/**
|
|
159
161
|
* Load ignore patterns from .driftignore
|
|
160
|
-
*
|
|
162
|
+
* Uses enterprise-grade defaults from @driftdetect/core
|
|
161
163
|
*/
|
|
162
164
|
async function loadIgnorePatterns(rootDir) {
|
|
163
|
-
const defaultIgnores = [
|
|
164
|
-
// Universal
|
|
165
|
-
'node_modules/**',
|
|
166
|
-
'.git/**',
|
|
167
|
-
'dist/**',
|
|
168
|
-
'build/**',
|
|
169
|
-
'coverage/**',
|
|
170
|
-
'.drift/**',
|
|
171
|
-
// Python
|
|
172
|
-
'__pycache__/**',
|
|
173
|
-
'.venv/**',
|
|
174
|
-
'venv/**',
|
|
175
|
-
'.eggs/**',
|
|
176
|
-
'*.egg-info/**',
|
|
177
|
-
'.tox/**',
|
|
178
|
-
'.mypy_cache/**',
|
|
179
|
-
'.pytest_cache/**',
|
|
180
|
-
// .NET / C# / MAUI / Blazor
|
|
181
|
-
'bin/**',
|
|
182
|
-
'obj/**',
|
|
183
|
-
'packages/**',
|
|
184
|
-
'.vs/**',
|
|
185
|
-
'*.dll',
|
|
186
|
-
'*.exe',
|
|
187
|
-
'*.pdb',
|
|
188
|
-
'*.nupkg',
|
|
189
|
-
'wwwroot/lib/**',
|
|
190
|
-
// Java / Spring / Gradle / Maven
|
|
191
|
-
'target/**',
|
|
192
|
-
'.gradle/**',
|
|
193
|
-
'.m2/**',
|
|
194
|
-
'*.class',
|
|
195
|
-
'*.jar',
|
|
196
|
-
'*.war',
|
|
197
|
-
// Go
|
|
198
|
-
'vendor/**',
|
|
199
|
-
// Rust (target already covered above)
|
|
200
|
-
// C++ / CMake
|
|
201
|
-
'cmake-build-*/**',
|
|
202
|
-
'out/**',
|
|
203
|
-
'*.o',
|
|
204
|
-
'*.obj',
|
|
205
|
-
'*.a',
|
|
206
|
-
'*.lib',
|
|
207
|
-
'*.so',
|
|
208
|
-
'*.dylib',
|
|
209
|
-
// Node extras
|
|
210
|
-
'.npm/**',
|
|
211
|
-
'.yarn/**',
|
|
212
|
-
'.pnpm-store/**',
|
|
213
|
-
'.next/**',
|
|
214
|
-
'.nuxt/**',
|
|
215
|
-
// IDE / Editor
|
|
216
|
-
'.idea/**',
|
|
217
|
-
'.vscode/**',
|
|
218
|
-
'*.swp',
|
|
219
|
-
'*.swo',
|
|
220
|
-
// Archives / binaries (common in enterprise)
|
|
221
|
-
'*.zip',
|
|
222
|
-
'*.rar',
|
|
223
|
-
'*.7z',
|
|
224
|
-
'*.tar',
|
|
225
|
-
'*.gz',
|
|
226
|
-
// Logs
|
|
227
|
-
'*.log',
|
|
228
|
-
'logs/**',
|
|
229
|
-
];
|
|
230
165
|
try {
|
|
231
166
|
const driftignorePath = path.join(rootDir, '.driftignore');
|
|
232
167
|
const content = await fs.readFile(driftignorePath, 'utf-8');
|
|
233
|
-
const
|
|
168
|
+
const userPatterns = content
|
|
234
169
|
.split('\n')
|
|
235
170
|
.map((line) => line.trim())
|
|
236
171
|
.filter((line) => line && !line.startsWith('#'));
|
|
237
|
-
return
|
|
172
|
+
return mergeIgnorePatterns(userPatterns);
|
|
238
173
|
}
|
|
239
174
|
catch {
|
|
240
|
-
return
|
|
175
|
+
return getDefaultIgnorePatterns();
|
|
241
176
|
}
|
|
242
177
|
}
|
|
243
178
|
/**
|
|
@@ -1054,59 +989,186 @@ async function scanSingleProject(rootDir, options, quiet = false) {
|
|
|
1054
989
|
const boundarySpinner = createSpinner('Scanning for data boundaries...');
|
|
1055
990
|
boundarySpinner.start();
|
|
1056
991
|
try {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
992
|
+
// Try native analyzer first (much faster)
|
|
993
|
+
if (isNativeAvailable()) {
|
|
994
|
+
try {
|
|
995
|
+
const nativeResult = await scanBoundariesWithFallback(rootDir, files);
|
|
996
|
+
// Convert native result to BoundaryScanResult format
|
|
997
|
+
// Note: Native types may differ slightly, so we convert carefully
|
|
998
|
+
const accessPointsMap = {};
|
|
999
|
+
for (const ap of nativeResult.accessPoints) {
|
|
1000
|
+
const id = `${ap.file}:${ap.line}:0:${ap.table}`;
|
|
1001
|
+
accessPointsMap[id] = {
|
|
1002
|
+
id,
|
|
1003
|
+
table: ap.table,
|
|
1004
|
+
fields: ap.fields,
|
|
1005
|
+
operation: ap.operation,
|
|
1006
|
+
file: ap.file,
|
|
1007
|
+
line: ap.line,
|
|
1008
|
+
column: 0,
|
|
1009
|
+
context: '',
|
|
1010
|
+
isRawSql: false,
|
|
1011
|
+
confidence: ap.confidence,
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
const sensitiveFields = nativeResult.sensitiveFields.map(sf => ({
|
|
1015
|
+
field: sf.field,
|
|
1016
|
+
table: sf.table ?? null,
|
|
1017
|
+
sensitivityType: sf.sensitivityType,
|
|
1018
|
+
file: sf.file,
|
|
1019
|
+
line: sf.line,
|
|
1020
|
+
confidence: sf.confidence,
|
|
1021
|
+
}));
|
|
1022
|
+
const models = nativeResult.models.map(m => ({
|
|
1023
|
+
name: m.name,
|
|
1024
|
+
tableName: m.tableName,
|
|
1025
|
+
fields: m.fields,
|
|
1026
|
+
file: m.file,
|
|
1027
|
+
line: m.line,
|
|
1028
|
+
framework: m.framework,
|
|
1029
|
+
confidence: m.confidence,
|
|
1030
|
+
}));
|
|
1031
|
+
// Build tables map from access points
|
|
1032
|
+
const tablesMap = {};
|
|
1033
|
+
for (const ap of nativeResult.accessPoints) {
|
|
1034
|
+
let tableInfo = tablesMap[ap.table];
|
|
1035
|
+
if (!tableInfo) {
|
|
1036
|
+
tableInfo = {
|
|
1037
|
+
name: ap.table,
|
|
1038
|
+
model: null,
|
|
1039
|
+
fields: [],
|
|
1040
|
+
sensitiveFields: [],
|
|
1041
|
+
accessedBy: [],
|
|
1042
|
+
};
|
|
1043
|
+
tablesMap[ap.table] = tableInfo;
|
|
1044
|
+
}
|
|
1045
|
+
const apId = `${ap.file}:${ap.line}:0:${ap.table}`;
|
|
1046
|
+
const fullAp = accessPointsMap[apId];
|
|
1047
|
+
if (fullAp) {
|
|
1048
|
+
tableInfo.accessedBy.push(fullAp);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
scanBoundaryResult = {
|
|
1052
|
+
accessMap: {
|
|
1053
|
+
version: '1.0',
|
|
1054
|
+
generatedAt: new Date().toISOString(),
|
|
1055
|
+
projectRoot: rootDir,
|
|
1056
|
+
tables: tablesMap,
|
|
1057
|
+
accessPoints: accessPointsMap,
|
|
1058
|
+
sensitiveFields,
|
|
1059
|
+
models,
|
|
1060
|
+
stats: {
|
|
1061
|
+
totalTables: Object.keys(tablesMap).length,
|
|
1062
|
+
totalAccessPoints: nativeResult.accessPoints.length,
|
|
1063
|
+
totalSensitiveFields: nativeResult.sensitiveFields.length,
|
|
1064
|
+
totalModels: nativeResult.models.length,
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
violations: [],
|
|
1068
|
+
stats: {
|
|
1069
|
+
filesScanned: nativeResult.filesScanned,
|
|
1070
|
+
tablesFound: Object.keys(tablesMap).length,
|
|
1071
|
+
accessPointsFound: nativeResult.accessPoints.length,
|
|
1072
|
+
sensitiveFieldsFound: nativeResult.sensitiveFields.length,
|
|
1073
|
+
violationsFound: 0,
|
|
1074
|
+
scanDurationMs: nativeResult.durationMs,
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
boundarySpinner.succeed(`Found ${scanBoundaryResult.stats.tablesFound} tables, ` +
|
|
1078
|
+
`${scanBoundaryResult.stats.accessPointsFound} access points (native)`);
|
|
1079
|
+
// Show sensitive field access warnings
|
|
1080
|
+
if (scanBoundaryResult.stats.sensitiveFieldsFound > 0) {
|
|
1081
|
+
console.log();
|
|
1082
|
+
console.log(chalk.bold.yellow(`⚠️ ${scanBoundaryResult.stats.sensitiveFieldsFound} Sensitive Field Access Detected:`));
|
|
1083
|
+
const sensFields = scanBoundaryResult.accessMap.sensitiveFields.slice(0, 5);
|
|
1084
|
+
for (const field of sensFields) {
|
|
1085
|
+
const fieldName = field.table ? `${field.table}.${field.field}` : field.field;
|
|
1086
|
+
console.log(chalk.yellow(` ${fieldName} (${field.sensitivityType}) - ${field.file}:${field.line}`));
|
|
1087
|
+
}
|
|
1088
|
+
if (scanBoundaryResult.accessMap.sensitiveFields.length > 5) {
|
|
1089
|
+
console.log(chalk.gray(` ... and ${scanBoundaryResult.accessMap.sensitiveFields.length - 5} more`));
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
console.log();
|
|
1093
|
+
console.log(chalk.gray('View data boundaries:'));
|
|
1094
|
+
console.log(chalk.cyan(' drift boundaries'));
|
|
1095
|
+
console.log(chalk.cyan(' drift boundaries table <name>'));
|
|
1072
1096
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1097
|
+
catch (nativeError) {
|
|
1098
|
+
if (verbose) {
|
|
1099
|
+
boundarySpinner.text(chalk.gray(`Native boundary scanner failed, using TypeScript fallback`));
|
|
1100
|
+
}
|
|
1101
|
+
// Fall through to TypeScript implementation
|
|
1102
|
+
throw nativeError; // Re-throw to trigger fallback
|
|
1075
1103
|
}
|
|
1076
1104
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1105
|
+
else {
|
|
1106
|
+
// TypeScript fallback
|
|
1107
|
+
const boundaryScanner = createBoundaryScanner({ rootDir, verbose });
|
|
1108
|
+
await boundaryScanner.initialize();
|
|
1109
|
+
const boundaryResult = await boundaryScanner.scanFiles(files);
|
|
1110
|
+
// Store result for materializer
|
|
1111
|
+
scanBoundaryResult = boundaryResult;
|
|
1112
|
+
boundarySpinner.succeed(`Found ${boundaryResult.stats.tablesFound} tables, ` +
|
|
1113
|
+
`${boundaryResult.stats.accessPointsFound} access points`);
|
|
1114
|
+
// Show sensitive field access warnings
|
|
1115
|
+
if (boundaryResult.stats.sensitiveFieldsFound > 0) {
|
|
1116
|
+
console.log();
|
|
1117
|
+
console.log(chalk.bold.yellow(`⚠️ ${boundaryResult.stats.sensitiveFieldsFound} Sensitive Field Access Detected:`));
|
|
1118
|
+
const sensitiveFields = boundaryResult.accessMap.sensitiveFields.slice(0, 5);
|
|
1119
|
+
for (const field of sensitiveFields) {
|
|
1120
|
+
const fieldName = field.table ? `${field.table}.${field.field}` : field.field;
|
|
1121
|
+
console.log(chalk.yellow(` ${fieldName} (${field.sensitivityType}) - ${field.file}:${field.line}`));
|
|
1122
|
+
}
|
|
1123
|
+
if (boundaryResult.accessMap.sensitiveFields.length > 5) {
|
|
1124
|
+
console.log(chalk.gray(` ... and ${boundaryResult.accessMap.sensitiveFields.length - 5} more`));
|
|
1125
|
+
}
|
|
1084
1126
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1127
|
+
// Check violations if rules exist
|
|
1128
|
+
if (boundaryResult.stats.violationsFound > 0) {
|
|
1129
|
+
console.log();
|
|
1130
|
+
console.log(chalk.bold.red(`🚫 ${boundaryResult.stats.violationsFound} Boundary Violations:`));
|
|
1131
|
+
for (const violation of boundaryResult.violations.slice(0, 5)) {
|
|
1132
|
+
const icon = violation.severity === 'error' ? '🔴' : violation.severity === 'warning' ? '🟡' : '🔵';
|
|
1133
|
+
console.log(chalk.red(` ${icon} ${violation.file}:${violation.line} - ${violation.message}`));
|
|
1134
|
+
}
|
|
1135
|
+
if (boundaryResult.violations.length > 5) {
|
|
1136
|
+
console.log(chalk.gray(` ... and ${boundaryResult.violations.length - 5} more`));
|
|
1137
|
+
}
|
|
1087
1138
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1139
|
+
// Show top accessed tables in verbose mode
|
|
1140
|
+
if (verbose && boundaryResult.stats.tablesFound > 0) {
|
|
1141
|
+
console.log();
|
|
1142
|
+
console.log(chalk.gray(' Top accessed tables:'));
|
|
1143
|
+
const tableEntries = Object.entries(boundaryResult.accessMap.tables)
|
|
1144
|
+
.map(([name, info]) => ({ name, count: info.accessedBy.length }))
|
|
1145
|
+
.sort((a, b) => b.count - a.count)
|
|
1146
|
+
.slice(0, 5);
|
|
1147
|
+
for (const table of tableEntries) {
|
|
1148
|
+
console.log(chalk.gray(` ${table.name}: ${table.count} access points`));
|
|
1149
|
+
}
|
|
1099
1150
|
}
|
|
1151
|
+
console.log();
|
|
1152
|
+
console.log(chalk.gray('View data boundaries:'));
|
|
1153
|
+
console.log(chalk.cyan(' drift boundaries'));
|
|
1154
|
+
console.log(chalk.cyan(' drift boundaries table <name>'));
|
|
1100
1155
|
}
|
|
1101
|
-
console.log();
|
|
1102
|
-
console.log(chalk.gray('View data boundaries:'));
|
|
1103
|
-
console.log(chalk.cyan(' drift boundaries'));
|
|
1104
|
-
console.log(chalk.cyan(' drift boundaries table <name>'));
|
|
1105
1156
|
}
|
|
1106
1157
|
catch (error) {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1158
|
+
// If native failed, try TypeScript fallback
|
|
1159
|
+
try {
|
|
1160
|
+
const boundaryScanner = createBoundaryScanner({ rootDir, verbose });
|
|
1161
|
+
await boundaryScanner.initialize();
|
|
1162
|
+
const boundaryResult = await boundaryScanner.scanFiles(files);
|
|
1163
|
+
scanBoundaryResult = boundaryResult;
|
|
1164
|
+
boundarySpinner.succeed(`Found ${boundaryResult.stats.tablesFound} tables, ` +
|
|
1165
|
+
`${boundaryResult.stats.accessPointsFound} access points`);
|
|
1166
|
+
}
|
|
1167
|
+
catch (fallbackError) {
|
|
1168
|
+
boundarySpinner.fail('Boundary scanning failed');
|
|
1169
|
+
if (verbose) {
|
|
1170
|
+
console.error(chalk.red(fallbackError.message));
|
|
1171
|
+
}
|
|
1110
1172
|
}
|
|
1111
1173
|
}
|
|
1112
1174
|
}
|
|
@@ -1116,82 +1178,230 @@ async function scanSingleProject(rootDir, options, quiet = false) {
|
|
|
1116
1178
|
const testTopologySpinner = createSpinner('Building test topology...');
|
|
1117
1179
|
testTopologySpinner.start();
|
|
1118
1180
|
try {
|
|
1119
|
-
//
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1181
|
+
// Try native analyzer first (much faster)
|
|
1182
|
+
if (isNativeAvailable()) {
|
|
1183
|
+
try {
|
|
1184
|
+
const nativeResult = await analyzeTestTopologyWithFallback(rootDir, files);
|
|
1185
|
+
// Save results
|
|
1186
|
+
const testTopologyDir = path.join(rootDir, DRIFT_DIR, 'test-topology');
|
|
1187
|
+
await fs.mkdir(testTopologyDir, { recursive: true });
|
|
1188
|
+
// Convert native result to summary format
|
|
1189
|
+
const summary = {
|
|
1190
|
+
testFiles: nativeResult.testFiles.length,
|
|
1191
|
+
testCases: nativeResult.totalTests,
|
|
1192
|
+
coveredFunctions: nativeResult.coverage.length,
|
|
1193
|
+
totalFunctions: nativeResult.coverage.length + nativeResult.uncoveredFiles.length,
|
|
1194
|
+
functionCoveragePercent: nativeResult.coverage.length > 0
|
|
1195
|
+
? Math.round((nativeResult.coverage.length / (nativeResult.coverage.length + nativeResult.uncoveredFiles.length)) * 100)
|
|
1196
|
+
: 0,
|
|
1197
|
+
coveragePercent: 0,
|
|
1198
|
+
avgQualityScore: 0,
|
|
1199
|
+
byFramework: {},
|
|
1200
|
+
};
|
|
1201
|
+
// Count by framework
|
|
1202
|
+
for (const tf of nativeResult.testFiles) {
|
|
1203
|
+
summary.byFramework[tf.framework] = (summary.byFramework[tf.framework] ?? 0) + tf.testCount;
|
|
1204
|
+
}
|
|
1205
|
+
const mockAnalysis = {
|
|
1206
|
+
totalMocks: nativeResult.testFiles.reduce((sum, tf) => sum + tf.mockCount, 0),
|
|
1207
|
+
externalMocks: 0,
|
|
1208
|
+
internalMocks: 0,
|
|
1209
|
+
externalPercent: 0,
|
|
1210
|
+
internalPercent: 0,
|
|
1211
|
+
avgMockRatio: 0,
|
|
1212
|
+
highMockRatioTests: [],
|
|
1213
|
+
topMockedModules: [],
|
|
1214
|
+
};
|
|
1215
|
+
await fs.writeFile(path.join(testTopologyDir, 'summary.json'), JSON.stringify({ summary, mockAnalysis, generatedAt: new Date().toISOString() }, null, 2));
|
|
1216
|
+
testTopologySpinner.succeed(`Built test topology (native): ${summary.testFiles} test files, ${summary.testCases} tests`);
|
|
1217
|
+
if (verbose) {
|
|
1218
|
+
console.log(chalk.gray(` Native analyzer used for faster analysis`));
|
|
1219
|
+
console.log(chalk.gray(` Files analyzed: ${nativeResult.filesAnalyzed}`));
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
catch (nativeError) {
|
|
1223
|
+
// Native failed, fall through to TypeScript implementation
|
|
1224
|
+
if (verbose) {
|
|
1225
|
+
console.log(chalk.gray(` Native analyzer failed, using TypeScript fallback`));
|
|
1226
|
+
}
|
|
1227
|
+
await runTypeScriptTestTopology(rootDir, files, verbose, testTopologySpinner);
|
|
1128
1228
|
}
|
|
1129
1229
|
}
|
|
1130
|
-
|
|
1131
|
-
//
|
|
1230
|
+
else {
|
|
1231
|
+
// Native not available, use TypeScript
|
|
1232
|
+
await runTypeScriptTestTopology(rootDir, files, verbose, testTopologySpinner);
|
|
1132
1233
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
/test_.*\.py$/,
|
|
1139
|
-
/Test\.java$/,
|
|
1140
|
-
/Tests\.java$/,
|
|
1141
|
-
/Test\.cs$/,
|
|
1142
|
-
/Tests\.cs$/,
|
|
1143
|
-
/Test\.php$/,
|
|
1144
|
-
];
|
|
1145
|
-
const testFiles = files.filter(f => testFilePatterns.some(p => p.test(f)));
|
|
1146
|
-
if (testFiles.length === 0) {
|
|
1147
|
-
testTopologySpinner.succeed('No test files found');
|
|
1234
|
+
}
|
|
1235
|
+
catch (error) {
|
|
1236
|
+
testTopologySpinner.fail('Test topology build failed');
|
|
1237
|
+
if (verbose) {
|
|
1238
|
+
console.error(chalk.red(error.message));
|
|
1148
1239
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
// Constants extraction (opt-in)
|
|
1243
|
+
if (options.constants) {
|
|
1244
|
+
console.log();
|
|
1245
|
+
const constantsSpinner = createSpinner('Extracting constants...');
|
|
1246
|
+
constantsSpinner.start();
|
|
1247
|
+
try {
|
|
1248
|
+
const constantsResult = await analyzeConstantsWithFallback(rootDir, files);
|
|
1249
|
+
// Save to ConstantStore
|
|
1250
|
+
const constantStore = new ConstantStore({ rootDir });
|
|
1251
|
+
await constantStore.initialize();
|
|
1252
|
+
// Group constants by file and save
|
|
1253
|
+
const constantsByFile = new Map();
|
|
1254
|
+
for (const constant of constantsResult.constants) {
|
|
1255
|
+
const existing = constantsByFile.get(constant.file) ?? [];
|
|
1256
|
+
existing.push(constant);
|
|
1257
|
+
constantsByFile.set(constant.file, existing);
|
|
1258
|
+
}
|
|
1259
|
+
// Map native language to ConstantLanguage
|
|
1260
|
+
const mapLanguage = (lang) => {
|
|
1261
|
+
const validLangs = ['typescript', 'javascript', 'python', 'java', 'csharp', 'php', 'go', 'rust', 'cpp'];
|
|
1262
|
+
return validLangs.includes(lang) ? lang : 'typescript';
|
|
1263
|
+
};
|
|
1264
|
+
// Map native kind to ConstantKind
|
|
1265
|
+
const mapKind = (kind) => {
|
|
1266
|
+
const kindMap = {
|
|
1267
|
+
'const': 'primitive',
|
|
1268
|
+
'let': 'primitive',
|
|
1269
|
+
'var': 'primitive',
|
|
1270
|
+
'readonly': 'primitive',
|
|
1271
|
+
'static': 'class_constant',
|
|
1272
|
+
'final': 'class_constant',
|
|
1273
|
+
'define': 'primitive',
|
|
1274
|
+
'enum_member': 'enum_member',
|
|
1275
|
+
'primitive': 'primitive',
|
|
1276
|
+
'enum': 'enum',
|
|
1277
|
+
'object': 'object',
|
|
1278
|
+
'array': 'array',
|
|
1279
|
+
'computed': 'computed',
|
|
1280
|
+
'class_constant': 'class_constant',
|
|
1281
|
+
'interface_constant': 'interface_constant',
|
|
1282
|
+
};
|
|
1283
|
+
return kindMap[kind] ?? 'primitive';
|
|
1284
|
+
};
|
|
1285
|
+
// Map native category to ConstantCategory
|
|
1286
|
+
const mapCategory = (cat) => {
|
|
1287
|
+
const validCats = ['config', 'api', 'status', 'error', 'feature_flag', 'limit', 'regex', 'path', 'env', 'security', 'uncategorized'];
|
|
1288
|
+
return validCats.includes(cat) ? cat : 'uncategorized';
|
|
1289
|
+
};
|
|
1290
|
+
// Save each file's constants
|
|
1291
|
+
for (const [file, fileConstants] of constantsByFile) {
|
|
1292
|
+
const firstLang = fileConstants[0]?.language;
|
|
1293
|
+
await constantStore.saveFileResult({
|
|
1294
|
+
file,
|
|
1295
|
+
language: mapLanguage(firstLang ?? 'typescript'),
|
|
1296
|
+
constants: fileConstants.map(c => ({
|
|
1297
|
+
id: `${file}:${c.line}:${c.name}`,
|
|
1298
|
+
name: c.name,
|
|
1299
|
+
qualifiedName: c.name,
|
|
1300
|
+
file,
|
|
1301
|
+
line: c.line,
|
|
1302
|
+
column: 0,
|
|
1303
|
+
endLine: c.line,
|
|
1304
|
+
language: mapLanguage(c.language),
|
|
1305
|
+
kind: mapKind(c.declarationType ?? 'const'),
|
|
1306
|
+
category: mapCategory(c.category),
|
|
1307
|
+
value: c.value,
|
|
1308
|
+
isExported: c.isExported,
|
|
1309
|
+
decorators: [],
|
|
1310
|
+
modifiers: [],
|
|
1311
|
+
confidence: 0.9,
|
|
1312
|
+
})),
|
|
1313
|
+
enums: [],
|
|
1314
|
+
references: [],
|
|
1315
|
+
errors: [],
|
|
1316
|
+
quality: {
|
|
1317
|
+
method: 'regex',
|
|
1318
|
+
confidence: 0.9,
|
|
1319
|
+
coveragePercent: 100,
|
|
1320
|
+
itemsExtracted: fileConstants.length,
|
|
1321
|
+
parseErrors: 0,
|
|
1322
|
+
warnings: [],
|
|
1323
|
+
usedFallback: !isNativeAvailable(),
|
|
1324
|
+
extractionTimeMs: constantsResult.stats.durationMs,
|
|
1325
|
+
},
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
// Rebuild index
|
|
1329
|
+
await constantStore.rebuildIndex();
|
|
1330
|
+
const nativeIndicator = isNativeAvailable() ? ' (native)' : '';
|
|
1331
|
+
constantsSpinner.succeed(`Extracted ${constantsResult.stats.totalConstants} constants from ${constantsResult.stats.filesAnalyzed} files${nativeIndicator}`);
|
|
1332
|
+
// Show secrets warning if any found
|
|
1333
|
+
if (constantsResult.secrets.length > 0) {
|
|
1334
|
+
console.log();
|
|
1335
|
+
console.log(chalk.bold.red(`🔐 ${constantsResult.secrets.length} Potential Hardcoded Secrets Detected!`));
|
|
1336
|
+
const critical = constantsResult.secrets.filter(s => s.severity === 'critical' || s.severity === 'high');
|
|
1337
|
+
if (critical.length > 0) {
|
|
1338
|
+
for (const secret of critical.slice(0, 3)) {
|
|
1339
|
+
console.log(chalk.red(` ${secret.name} (${secret.secretType}) - ${secret.file}:${secret.line}`));
|
|
1158
1340
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1341
|
+
if (critical.length > 3) {
|
|
1342
|
+
console.log(chalk.gray(` ... and ${critical.length - 3} more`));
|
|
1161
1343
|
}
|
|
1162
1344
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1345
|
+
console.log();
|
|
1346
|
+
console.log(chalk.yellow("Run 'drift constants secrets' to review all potential secrets"));
|
|
1347
|
+
}
|
|
1348
|
+
if (verbose) {
|
|
1349
|
+
console.log(chalk.gray(` Duration: ${constantsResult.stats.durationMs}ms`));
|
|
1350
|
+
console.log(chalk.gray(` Exported: ${constantsResult.stats.exportedCount}`));
|
|
1351
|
+
}
|
|
1352
|
+
console.log();
|
|
1353
|
+
console.log(chalk.gray('View constants:'));
|
|
1354
|
+
console.log(chalk.cyan(' drift constants'));
|
|
1355
|
+
console.log(chalk.cyan(' drift constants list'));
|
|
1356
|
+
}
|
|
1357
|
+
catch (error) {
|
|
1358
|
+
constantsSpinner.fail('Constants extraction failed');
|
|
1359
|
+
if (verbose) {
|
|
1360
|
+
console.error(chalk.red(error.message));
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
// Call graph building (opt-in) - uses native Rust for memory safety
|
|
1365
|
+
if (options.callgraph) {
|
|
1366
|
+
console.log();
|
|
1367
|
+
const callgraphSpinner = createSpinner('Building call graph...');
|
|
1368
|
+
callgraphSpinner.start();
|
|
1369
|
+
try {
|
|
1370
|
+
if (isNativeAvailable()) {
|
|
1371
|
+
const callgraphConfig = {
|
|
1372
|
+
root: rootDir,
|
|
1373
|
+
patterns: [
|
|
1374
|
+
'**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx',
|
|
1375
|
+
'**/*.py', '**/*.cs', '**/*.java', '**/*.php',
|
|
1376
|
+
],
|
|
1377
|
+
resolutionBatchSize: 50,
|
|
1378
|
+
};
|
|
1379
|
+
const callgraphResult = await buildCallGraph(callgraphConfig);
|
|
1380
|
+
callgraphSpinner.succeed(`Built call graph (native): ${callgraphResult.filesProcessed} files, ` +
|
|
1381
|
+
`${callgraphResult.totalFunctions.toLocaleString()} functions, ` +
|
|
1382
|
+
`${callgraphResult.resolvedCalls.toLocaleString()}/${callgraphResult.totalCalls.toLocaleString()} calls resolved`);
|
|
1174
1383
|
if (verbose) {
|
|
1175
|
-
console.log(chalk.gray(`
|
|
1176
|
-
console.log(chalk.gray(`
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1384
|
+
console.log(chalk.gray(` Entry points: ${callgraphResult.entryPoints}`));
|
|
1385
|
+
console.log(chalk.gray(` Data accessors: ${callgraphResult.dataAccessors}`));
|
|
1386
|
+
console.log(chalk.gray(` Resolution rate: ${Math.round(callgraphResult.resolutionRate * 100)}%`));
|
|
1387
|
+
console.log(chalk.gray(` Duration: ${(callgraphResult.durationMs / 1000).toFixed(2)}s`));
|
|
1180
1388
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
console.log();
|
|
1184
|
-
console.log(chalk.yellow(`⚠️ Low test coverage: ${summary.functionCoveragePercent}% of functions covered`));
|
|
1185
|
-
console.log(chalk.gray(' Run `drift test-topology uncovered` to find untested code'));
|
|
1389
|
+
if (callgraphResult.errors.length > 0 && verbose) {
|
|
1390
|
+
console.log(chalk.yellow(` ⚠️ ${callgraphResult.errors.length} files had parse errors`));
|
|
1186
1391
|
}
|
|
1187
1392
|
console.log();
|
|
1188
|
-
console.log(chalk.gray('
|
|
1189
|
-
console.log(chalk.cyan(' drift
|
|
1190
|
-
console.log(chalk.cyan(' drift
|
|
1393
|
+
console.log(chalk.gray('Query the call graph:'));
|
|
1394
|
+
console.log(chalk.cyan(' drift callgraph status'));
|
|
1395
|
+
console.log(chalk.cyan(' drift callgraph reach <function>'));
|
|
1396
|
+
console.log(chalk.cyan(' drift callgraph inverse <table>'));
|
|
1397
|
+
}
|
|
1398
|
+
else {
|
|
1399
|
+
callgraphSpinner.fail('Call graph requires native module (not available)');
|
|
1400
|
+
console.log(chalk.gray(' Run `drift callgraph build` for TypeScript fallback (may OOM on large codebases)'));
|
|
1191
1401
|
}
|
|
1192
1402
|
}
|
|
1193
1403
|
catch (error) {
|
|
1194
|
-
|
|
1404
|
+
callgraphSpinner.fail('Call graph build failed');
|
|
1195
1405
|
if (verbose) {
|
|
1196
1406
|
console.error(chalk.red(error.message));
|
|
1197
1407
|
}
|
|
@@ -1254,6 +1464,8 @@ async function scanSingleProject(rootDir, options, quiet = false) {
|
|
|
1254
1464
|
if (stats.byStatus.discovered > 0) {
|
|
1255
1465
|
const discovered = store.getDiscovered();
|
|
1256
1466
|
const highConfidence = discovered.filter((p) => p.confidence.level === 'high');
|
|
1467
|
+
// Count auto-approve eligible (≥90% confidence)
|
|
1468
|
+
const autoApproveEligible = discovered.filter((p) => p.confidence.score >= 0.90).length;
|
|
1257
1469
|
if (highConfidence.length > 0) {
|
|
1258
1470
|
console.log(chalk.bold('High Confidence Patterns (ready for approval):'));
|
|
1259
1471
|
console.log();
|
|
@@ -1271,12 +1483,107 @@ async function scanSingleProject(rootDir, options, quiet = false) {
|
|
|
1271
1483
|
}
|
|
1272
1484
|
console.log();
|
|
1273
1485
|
}
|
|
1274
|
-
|
|
1275
|
-
console.log(chalk.
|
|
1276
|
-
console.log(chalk.
|
|
1486
|
+
// Post-scan summary with agent assistance prompt
|
|
1487
|
+
console.log(chalk.bold('📊 Pattern Review'));
|
|
1488
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
1489
|
+
console.log(` Patterns discovered: ${chalk.cyan(stats.byStatus.discovered)}`);
|
|
1490
|
+
console.log(` Auto-approve eligible: ${chalk.green(autoApproveEligible)} (≥90% confidence)`);
|
|
1491
|
+
console.log(` Needs review: ${chalk.yellow(stats.byStatus.discovered - autoApproveEligible)}`);
|
|
1492
|
+
console.log();
|
|
1493
|
+
console.log(chalk.gray('Quick actions:'));
|
|
1494
|
+
if (autoApproveEligible > 0) {
|
|
1495
|
+
console.log(chalk.cyan(` drift approve --auto`) + chalk.gray(` - Auto-approve ${autoApproveEligible} high-confidence patterns`));
|
|
1496
|
+
}
|
|
1497
|
+
console.log(chalk.cyan(` drift audit --review`) + chalk.gray(` - Generate detailed review report`));
|
|
1498
|
+
console.log(chalk.cyan(` drift approve <id>`) + chalk.gray(` - Approve a specific pattern`));
|
|
1499
|
+
console.log();
|
|
1500
|
+
console.log(chalk.gray('For agent assistance, copy this to your AI assistant:'));
|
|
1501
|
+
console.log(chalk.cyan('┌─────────────────────────────────────────────────────────────┐'));
|
|
1502
|
+
console.log(chalk.cyan('│') + ' Run `drift audit --review` and approve high-confidence ' + chalk.cyan('│'));
|
|
1503
|
+
console.log(chalk.cyan('│') + ' patterns that match codebase conventions. Flag any that ' + chalk.cyan('│'));
|
|
1504
|
+
console.log(chalk.cyan('│') + ' look like false positives or duplicates. ' + chalk.cyan('│'));
|
|
1505
|
+
console.log(chalk.cyan('└─────────────────────────────────────────────────────────────┘'));
|
|
1277
1506
|
}
|
|
1278
1507
|
console.log();
|
|
1279
1508
|
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Run TypeScript test topology analyzer (fallback when native is unavailable)
|
|
1511
|
+
*/
|
|
1512
|
+
async function runTypeScriptTestTopology(rootDir, files, verbose, spinner) {
|
|
1513
|
+
// Initialize test topology analyzer
|
|
1514
|
+
const testAnalyzer = createTestTopologyAnalyzer({});
|
|
1515
|
+
// Try to load call graph for transitive analysis
|
|
1516
|
+
try {
|
|
1517
|
+
const callGraphAnalyzer = createCallGraphAnalyzer({ rootDir });
|
|
1518
|
+
await callGraphAnalyzer.initialize();
|
|
1519
|
+
const graph = callGraphAnalyzer.getGraph();
|
|
1520
|
+
if (graph) {
|
|
1521
|
+
testAnalyzer.setCallGraph(graph);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
catch {
|
|
1525
|
+
// No call graph available, continue with direct analysis
|
|
1526
|
+
}
|
|
1527
|
+
// Find test files from the already-discovered files
|
|
1528
|
+
const testFilePatterns = [
|
|
1529
|
+
/\.test\.[jt]sx?$/,
|
|
1530
|
+
/\.spec\.[jt]sx?$/,
|
|
1531
|
+
/_test\.py$/,
|
|
1532
|
+
/test_.*\.py$/,
|
|
1533
|
+
/Test\.java$/,
|
|
1534
|
+
/Tests\.java$/,
|
|
1535
|
+
/Test\.cs$/,
|
|
1536
|
+
/Tests\.cs$/,
|
|
1537
|
+
/Test\.php$/,
|
|
1538
|
+
];
|
|
1539
|
+
const testFiles = files.filter(f => testFilePatterns.some(p => p.test(f)));
|
|
1540
|
+
if (testFiles.length === 0) {
|
|
1541
|
+
spinner.succeed('No test files found');
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
// Extract tests from each file
|
|
1545
|
+
let extractedCount = 0;
|
|
1546
|
+
for (const testFile of testFiles) {
|
|
1547
|
+
try {
|
|
1548
|
+
const content = await fs.readFile(path.join(rootDir, testFile), 'utf-8');
|
|
1549
|
+
const extraction = testAnalyzer.extractFromFile(content, testFile);
|
|
1550
|
+
if (extraction)
|
|
1551
|
+
extractedCount++;
|
|
1552
|
+
}
|
|
1553
|
+
catch {
|
|
1554
|
+
// Skip files that can't be read
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
// Build mappings
|
|
1558
|
+
testAnalyzer.buildMappings();
|
|
1559
|
+
// Get results
|
|
1560
|
+
const summary = testAnalyzer.getSummary();
|
|
1561
|
+
const mockAnalysis = testAnalyzer.analyzeMocks();
|
|
1562
|
+
// Save results
|
|
1563
|
+
const testTopologyDir = path.join(rootDir, DRIFT_DIR, 'test-topology');
|
|
1564
|
+
await fs.mkdir(testTopologyDir, { recursive: true });
|
|
1565
|
+
await fs.writeFile(path.join(testTopologyDir, 'summary.json'), JSON.stringify({ summary, mockAnalysis, generatedAt: new Date().toISOString() }, null, 2));
|
|
1566
|
+
spinner.succeed(`Built test topology: ${summary.testFiles} test files, ${summary.testCases} tests, ` +
|
|
1567
|
+
`${summary.coveredFunctions}/${summary.totalFunctions} functions covered`);
|
|
1568
|
+
if (verbose) {
|
|
1569
|
+
console.log(chalk.gray(` Test files extracted: ${extractedCount}/${testFiles.length}`));
|
|
1570
|
+
console.log(chalk.gray(` Coverage: ${summary.coveragePercent}%`));
|
|
1571
|
+
if (mockAnalysis.totalMocks > 0) {
|
|
1572
|
+
console.log(chalk.gray(` Mocks: ${mockAnalysis.totalMocks} (${mockAnalysis.externalPercent}% external)`));
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// Show uncovered functions warning
|
|
1576
|
+
if (summary.totalFunctions > 0 && summary.functionCoveragePercent < 50) {
|
|
1577
|
+
console.log();
|
|
1578
|
+
console.log(chalk.yellow(`⚠️ Low test coverage: ${summary.functionCoveragePercent}% of functions covered`));
|
|
1579
|
+
console.log(chalk.gray(' Run `drift test-topology uncovered` to find untested code'));
|
|
1580
|
+
}
|
|
1581
|
+
console.log();
|
|
1582
|
+
console.log(chalk.gray('View test topology:'));
|
|
1583
|
+
console.log(chalk.cyan(' drift test-topology status'));
|
|
1584
|
+
console.log(chalk.cyan(' drift test-topology uncovered'));
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1280
1587
|
export const scanCommand = new Command('scan')
|
|
1281
1588
|
.description('Scan codebase for patterns using enterprise detectors')
|
|
1282
1589
|
.argument('[paths...]', 'Paths to scan (defaults to current directory)')
|
|
@@ -1289,6 +1596,8 @@ export const scanCommand = new Command('scan')
|
|
|
1289
1596
|
.option('--no-contracts', 'Skip BE↔FE contract scanning')
|
|
1290
1597
|
.option('--no-boundaries', 'Skip data boundary scanning')
|
|
1291
1598
|
.option('--test-topology', 'Build test topology (test-to-code mappings)')
|
|
1599
|
+
.option('--constants', 'Extract constants, enums, and detect hardcoded secrets')
|
|
1600
|
+
.option('--callgraph', 'Build call graph for reachability analysis (native Rust)')
|
|
1292
1601
|
.option('-p, --project <name>', 'Scan a specific registered project by name')
|
|
1293
1602
|
.option('--all-projects', 'Scan all registered projects')
|
|
1294
1603
|
.option('-t, --timeout <seconds>', 'Scan timeout in seconds (default: 300)', '300')
|