driftdetect 0.6.1 → 0.7.1
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/LICENSE +21 -0
- package/dist/bin/drift.js +20 -1
- package/dist/bin/drift.js.map +1 -1
- package/dist/commands/constants.d.ts +35 -0
- package/dist/commands/constants.d.ts.map +1 -0
- package/dist/commands/constants.js +659 -0
- package/dist/commands/constants.js.map +1 -0
- package/dist/commands/constraints.d.ts.map +1 -1
- package/dist/commands/constraints.js +8 -7
- package/dist/commands/constraints.js.map +1 -1
- package/dist/commands/env.d.ts +23 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +497 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/index.js.map +1 -1
- package/package.json +16 -16
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants Command - drift constants
|
|
3
|
+
*
|
|
4
|
+
* Analyze constants, enums, and exported values across the codebase.
|
|
5
|
+
* Detects hardcoded secrets, inconsistent values, and magic numbers.
|
|
6
|
+
*
|
|
7
|
+
* @requirements Constant & Enum Extraction 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 { ConstantStore, ConstantSecurityScanner, ConsistencyAnalyzer, } from 'driftdetect-core';
|
|
14
|
+
/** Directory name for drift configuration */
|
|
15
|
+
const DRIFT_DIR = '.drift';
|
|
16
|
+
/** Directory name for constants data */
|
|
17
|
+
const CONSTANTS_DIR = 'constants';
|
|
18
|
+
/**
|
|
19
|
+
* Check if constants data exists
|
|
20
|
+
*/
|
|
21
|
+
async function constantsDataExists(rootDir) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(path.join(rootDir, DRIFT_DIR, CONSTANTS_DIR));
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Show helpful message when constants data not initialized
|
|
32
|
+
*/
|
|
33
|
+
function showNotInitializedMessage() {
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(chalk.yellow('⚠️ No constant data discovered yet.'));
|
|
36
|
+
console.log();
|
|
37
|
+
console.log(chalk.gray('Constant tracking extracts constants, enums, and exported values.'));
|
|
38
|
+
console.log(chalk.gray('Run a scan to discover constants:'));
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(chalk.cyan(' drift scan'));
|
|
41
|
+
console.log();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get color for severity level
|
|
45
|
+
*/
|
|
46
|
+
function getSeverityColor(severity) {
|
|
47
|
+
switch (severity) {
|
|
48
|
+
case 'critical':
|
|
49
|
+
return chalk.red;
|
|
50
|
+
case 'high':
|
|
51
|
+
return chalk.redBright;
|
|
52
|
+
case 'medium':
|
|
53
|
+
return chalk.yellow;
|
|
54
|
+
case 'low':
|
|
55
|
+
return chalk.blue;
|
|
56
|
+
default:
|
|
57
|
+
return chalk.gray;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get color for category
|
|
62
|
+
*/
|
|
63
|
+
function getCategoryColor(category) {
|
|
64
|
+
switch (category) {
|
|
65
|
+
case 'security':
|
|
66
|
+
return chalk.red;
|
|
67
|
+
case 'api':
|
|
68
|
+
return chalk.blue;
|
|
69
|
+
case 'config':
|
|
70
|
+
return chalk.green;
|
|
71
|
+
case 'error':
|
|
72
|
+
return chalk.yellow;
|
|
73
|
+
case 'feature_flag':
|
|
74
|
+
return chalk.magenta;
|
|
75
|
+
case 'limit':
|
|
76
|
+
return chalk.cyan;
|
|
77
|
+
default:
|
|
78
|
+
return chalk.gray;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Overview action - default view showing summary
|
|
83
|
+
*/
|
|
84
|
+
async function overviewAction(options) {
|
|
85
|
+
const rootDir = process.cwd();
|
|
86
|
+
const format = options.format ?? 'text';
|
|
87
|
+
if (!(await constantsDataExists(rootDir))) {
|
|
88
|
+
if (format === 'json') {
|
|
89
|
+
console.log(JSON.stringify({ error: 'No constants data found' }));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
showNotInitializedMessage();
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const store = new ConstantStore({ rootDir });
|
|
97
|
+
try {
|
|
98
|
+
const stats = await store.getStats();
|
|
99
|
+
const index = await store.getIndex();
|
|
100
|
+
// JSON output
|
|
101
|
+
if (format === 'json') {
|
|
102
|
+
console.log(JSON.stringify({
|
|
103
|
+
totalConstants: stats.totalConstants,
|
|
104
|
+
totalEnums: stats.totalEnums,
|
|
105
|
+
byLanguage: stats.byLanguage,
|
|
106
|
+
byCategory: stats.byCategory,
|
|
107
|
+
issues: stats.issues,
|
|
108
|
+
lastScanAt: index.generatedAt,
|
|
109
|
+
}, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Text output
|
|
113
|
+
console.log();
|
|
114
|
+
console.log(chalk.bold('📊 Constants Overview'));
|
|
115
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
116
|
+
console.log();
|
|
117
|
+
console.log(`Total Constants: ${chalk.cyan(stats.totalConstants)}`);
|
|
118
|
+
console.log(`Total Enums: ${chalk.cyan(stats.totalEnums)}`);
|
|
119
|
+
console.log();
|
|
120
|
+
// By language
|
|
121
|
+
if (Object.keys(stats.byLanguage).length > 0) {
|
|
122
|
+
console.log(chalk.bold('By Language:'));
|
|
123
|
+
for (const [lang, count] of Object.entries(stats.byLanguage)) {
|
|
124
|
+
console.log(` ${chalk.white(lang.padEnd(12))} ${chalk.cyan(count)}`);
|
|
125
|
+
}
|
|
126
|
+
console.log();
|
|
127
|
+
}
|
|
128
|
+
// By category
|
|
129
|
+
if (Object.keys(stats.byCategory).length > 0) {
|
|
130
|
+
console.log(chalk.bold('By Category:'));
|
|
131
|
+
for (const [cat, count] of Object.entries(stats.byCategory)) {
|
|
132
|
+
const color = getCategoryColor(cat);
|
|
133
|
+
console.log(` ${color(cat.padEnd(16))} ${chalk.cyan(count)}`);
|
|
134
|
+
}
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
137
|
+
// Issues
|
|
138
|
+
const totalIssues = stats.issues.magicValues + stats.issues.deadConstants +
|
|
139
|
+
stats.issues.potentialSecrets + stats.issues.inconsistentValues;
|
|
140
|
+
if (totalIssues > 0) {
|
|
141
|
+
console.log(chalk.bold('Issues:'));
|
|
142
|
+
if (stats.issues.potentialSecrets > 0) {
|
|
143
|
+
console.log(` ${chalk.red('●')} Potential Secrets: ${chalk.red(stats.issues.potentialSecrets)}`);
|
|
144
|
+
}
|
|
145
|
+
if (stats.issues.inconsistentValues > 0) {
|
|
146
|
+
console.log(` ${chalk.yellow('●')} Inconsistent Values: ${chalk.yellow(stats.issues.inconsistentValues)}`);
|
|
147
|
+
}
|
|
148
|
+
if (stats.issues.deadConstants > 0) {
|
|
149
|
+
console.log(` ${chalk.gray('●')} Dead Constants: ${chalk.gray(stats.issues.deadConstants)}`);
|
|
150
|
+
}
|
|
151
|
+
if (stats.issues.magicValues > 0) {
|
|
152
|
+
console.log(` ${chalk.blue('●')} Magic Values: ${chalk.blue(stats.issues.magicValues)}`);
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
// Quick actions
|
|
157
|
+
console.log(chalk.gray("Run 'drift constants list' to browse constants"));
|
|
158
|
+
if (stats.issues.potentialSecrets > 0) {
|
|
159
|
+
console.log(chalk.yellow("Run 'drift constants secrets' to review potential secrets"));
|
|
160
|
+
}
|
|
161
|
+
console.log();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
if (format === 'json') {
|
|
165
|
+
console.log(JSON.stringify({ error: 'Failed to load constants data' }));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
showNotInitializedMessage();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* List action - list all constants with filtering
|
|
174
|
+
*/
|
|
175
|
+
async function listAction(options) {
|
|
176
|
+
const rootDir = process.cwd();
|
|
177
|
+
const format = options.format ?? 'text';
|
|
178
|
+
const limit = options.limit ?? 50;
|
|
179
|
+
if (!(await constantsDataExists(rootDir))) {
|
|
180
|
+
if (format === 'json') {
|
|
181
|
+
console.log(JSON.stringify({ error: 'No constants data found', constants: [] }));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
showNotInitializedMessage();
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const store = new ConstantStore({ rootDir });
|
|
189
|
+
let constants = await store.getAllConstants();
|
|
190
|
+
let enums = await store.getAllEnums();
|
|
191
|
+
// Apply filters
|
|
192
|
+
if (options.category) {
|
|
193
|
+
constants = constants.filter(c => c.category === options.category);
|
|
194
|
+
}
|
|
195
|
+
if (options.language) {
|
|
196
|
+
constants = constants.filter(c => c.language === options.language);
|
|
197
|
+
enums = enums.filter(e => e.language === options.language);
|
|
198
|
+
}
|
|
199
|
+
if (options.file) {
|
|
200
|
+
constants = constants.filter(c => c.file.includes(options.file));
|
|
201
|
+
enums = enums.filter(e => e.file.includes(options.file));
|
|
202
|
+
}
|
|
203
|
+
if (options.search) {
|
|
204
|
+
const searchLower = options.search.toLowerCase();
|
|
205
|
+
constants = constants.filter(c => c.name.toLowerCase().includes(searchLower) ||
|
|
206
|
+
c.qualifiedName.toLowerCase().includes(searchLower));
|
|
207
|
+
enums = enums.filter(e => e.name.toLowerCase().includes(searchLower));
|
|
208
|
+
}
|
|
209
|
+
if (options.exported !== undefined) {
|
|
210
|
+
constants = constants.filter(c => c.isExported === options.exported);
|
|
211
|
+
enums = enums.filter(e => e.isExported === options.exported);
|
|
212
|
+
}
|
|
213
|
+
// Limit results
|
|
214
|
+
const paginatedConstants = constants.slice(0, limit);
|
|
215
|
+
const paginatedEnums = enums.slice(0, Math.max(0, limit - paginatedConstants.length));
|
|
216
|
+
// JSON output
|
|
217
|
+
if (format === 'json') {
|
|
218
|
+
console.log(JSON.stringify({
|
|
219
|
+
constants: paginatedConstants.map(c => ({
|
|
220
|
+
id: c.id,
|
|
221
|
+
name: c.name,
|
|
222
|
+
qualifiedName: c.qualifiedName,
|
|
223
|
+
file: c.file,
|
|
224
|
+
line: c.line,
|
|
225
|
+
language: c.language,
|
|
226
|
+
kind: c.kind,
|
|
227
|
+
category: c.category,
|
|
228
|
+
value: c.value,
|
|
229
|
+
isExported: c.isExported,
|
|
230
|
+
})),
|
|
231
|
+
enums: paginatedEnums.map(e => ({
|
|
232
|
+
id: e.id,
|
|
233
|
+
name: e.name,
|
|
234
|
+
file: e.file,
|
|
235
|
+
line: e.line,
|
|
236
|
+
memberCount: e.members.length,
|
|
237
|
+
})),
|
|
238
|
+
total: constants.length + enums.length,
|
|
239
|
+
}, null, 2));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// CSV output
|
|
243
|
+
if (format === 'csv') {
|
|
244
|
+
console.log('id,name,file,line,language,kind,category,value,exported');
|
|
245
|
+
for (const c of paginatedConstants) {
|
|
246
|
+
const value = c.value !== undefined ? String(c.value).replace(/,/g, ';') : '';
|
|
247
|
+
console.log(`${c.id},${c.name},${c.file},${c.line},${c.language},${c.kind},${c.category},"${value}",${c.isExported}`);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Text output
|
|
252
|
+
console.log();
|
|
253
|
+
console.log(chalk.bold('📋 Constants'));
|
|
254
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
255
|
+
console.log();
|
|
256
|
+
if (paginatedConstants.length === 0 && paginatedEnums.length === 0) {
|
|
257
|
+
console.log(chalk.gray(' No constants found matching filters.'));
|
|
258
|
+
console.log();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Constants
|
|
262
|
+
for (const c of paginatedConstants) {
|
|
263
|
+
const categoryColor = getCategoryColor(c.category);
|
|
264
|
+
const name = chalk.white(c.name.padEnd(30));
|
|
265
|
+
const category = categoryColor(c.category.padEnd(12));
|
|
266
|
+
const exported = c.isExported ? chalk.green('✓') : chalk.gray('·');
|
|
267
|
+
const value = c.value !== undefined
|
|
268
|
+
? chalk.gray(String(c.value).slice(0, 30) + (String(c.value).length > 30 ? '...' : ''))
|
|
269
|
+
: '';
|
|
270
|
+
console.log(` ${exported} ${name} ${category} ${value}`);
|
|
271
|
+
console.log(chalk.gray(` ${c.file}:${c.line}`));
|
|
272
|
+
}
|
|
273
|
+
// Enums
|
|
274
|
+
if (paginatedEnums.length > 0) {
|
|
275
|
+
console.log();
|
|
276
|
+
console.log(chalk.bold('📦 Enums'));
|
|
277
|
+
console.log();
|
|
278
|
+
for (const e of paginatedEnums) {
|
|
279
|
+
const name = chalk.white(e.name.padEnd(30));
|
|
280
|
+
const members = chalk.cyan(`${e.members.length} members`);
|
|
281
|
+
console.log(` ${name} ${members}`);
|
|
282
|
+
console.log(chalk.gray(` ${e.file}:${e.line}`));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
console.log();
|
|
286
|
+
console.log(chalk.gray(`Showing ${paginatedConstants.length + paginatedEnums.length} of ${constants.length + enums.length} items`));
|
|
287
|
+
console.log();
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Get action - show details for a specific constant
|
|
291
|
+
*/
|
|
292
|
+
async function getAction(nameOrId, options) {
|
|
293
|
+
const rootDir = process.cwd();
|
|
294
|
+
const format = options.format ?? 'text';
|
|
295
|
+
if (!(await constantsDataExists(rootDir))) {
|
|
296
|
+
if (format === 'json') {
|
|
297
|
+
console.log(JSON.stringify({ error: 'No constants data found' }));
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
showNotInitializedMessage();
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const store = new ConstantStore({ rootDir });
|
|
305
|
+
// Try to find by ID first, then by name
|
|
306
|
+
let constant = await store.getConstantById(nameOrId);
|
|
307
|
+
if (!constant) {
|
|
308
|
+
const results = await store.searchByName(nameOrId);
|
|
309
|
+
constant = results[0] ?? null;
|
|
310
|
+
}
|
|
311
|
+
let enumDef = null;
|
|
312
|
+
if (!constant) {
|
|
313
|
+
enumDef = await store.getEnumById(nameOrId);
|
|
314
|
+
if (!enumDef) {
|
|
315
|
+
const enums = await store.getAllEnums();
|
|
316
|
+
enumDef = enums.find(e => e.name === nameOrId) ?? null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!constant && !enumDef) {
|
|
320
|
+
if (format === 'json') {
|
|
321
|
+
console.log(JSON.stringify({ error: `Constant or enum not found: ${nameOrId}` }));
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
console.log();
|
|
325
|
+
console.log(chalk.red(`Constant or enum '${nameOrId}' not found.`));
|
|
326
|
+
console.log(chalk.gray("Run 'drift constants list' to see available constants."));
|
|
327
|
+
console.log();
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// JSON output
|
|
332
|
+
if (format === 'json') {
|
|
333
|
+
console.log(JSON.stringify({
|
|
334
|
+
constant: constant ?? undefined,
|
|
335
|
+
enum: enumDef ?? undefined,
|
|
336
|
+
}, null, 2));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Text output
|
|
340
|
+
console.log();
|
|
341
|
+
if (constant) {
|
|
342
|
+
console.log(chalk.bold(`📌 Constant: ${constant.name}`));
|
|
343
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
344
|
+
console.log();
|
|
345
|
+
console.log(`Qualified Name: ${chalk.cyan(constant.qualifiedName)}`);
|
|
346
|
+
console.log(`File: ${chalk.white(constant.file)}:${constant.line}`);
|
|
347
|
+
console.log(`Language: ${chalk.white(constant.language)}`);
|
|
348
|
+
console.log(`Kind: ${chalk.white(constant.kind)}`);
|
|
349
|
+
console.log(`Category: ${getCategoryColor(constant.category)(constant.category)}`);
|
|
350
|
+
console.log(`Exported: ${constant.isExported ? chalk.green('yes') : chalk.gray('no')}`);
|
|
351
|
+
if (constant.value !== undefined) {
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(chalk.bold('Value:'));
|
|
354
|
+
console.log(` ${chalk.cyan(String(constant.value))}`);
|
|
355
|
+
}
|
|
356
|
+
if (constant.docComment) {
|
|
357
|
+
console.log();
|
|
358
|
+
console.log(chalk.bold('Documentation:'));
|
|
359
|
+
console.log(` ${chalk.gray(constant.docComment)}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (enumDef) {
|
|
363
|
+
console.log(chalk.bold(`📦 Enum: ${enumDef.name}`));
|
|
364
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
365
|
+
console.log();
|
|
366
|
+
console.log(`File: ${chalk.white(enumDef.file)}:${enumDef.line}`);
|
|
367
|
+
console.log(`Language: ${chalk.white(enumDef.language)}`);
|
|
368
|
+
console.log(`Exported: ${enumDef.isExported ? chalk.green('yes') : chalk.gray('no')}`);
|
|
369
|
+
console.log(`Members: ${chalk.cyan(enumDef.members.length)}`);
|
|
370
|
+
if (enumDef.members.length > 0) {
|
|
371
|
+
console.log();
|
|
372
|
+
console.log(chalk.bold('Members:'));
|
|
373
|
+
for (const member of enumDef.members.slice(0, 20)) {
|
|
374
|
+
const value = member.value !== undefined ? ` = ${chalk.cyan(String(member.value))}` : '';
|
|
375
|
+
console.log(` ${chalk.white(member.name)}${value}`);
|
|
376
|
+
}
|
|
377
|
+
if (enumDef.members.length > 20) {
|
|
378
|
+
console.log(chalk.gray(` ... and ${enumDef.members.length - 20} more`));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
console.log();
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Secrets action - show potential hardcoded secrets
|
|
386
|
+
*/
|
|
387
|
+
async function secretsAction(options) {
|
|
388
|
+
const rootDir = process.cwd();
|
|
389
|
+
const format = options.format ?? 'text';
|
|
390
|
+
const limit = options.limit ?? 50;
|
|
391
|
+
const minSeverity = options.severity;
|
|
392
|
+
if (!(await constantsDataExists(rootDir))) {
|
|
393
|
+
if (format === 'json') {
|
|
394
|
+
console.log(JSON.stringify({ error: 'No constants data found', secrets: [] }));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
showNotInitializedMessage();
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const store = new ConstantStore({ rootDir });
|
|
402
|
+
const scanner = new ConstantSecurityScanner();
|
|
403
|
+
const constants = await store.getAllConstants();
|
|
404
|
+
const result = scanner.scan(constants);
|
|
405
|
+
// Filter by severity
|
|
406
|
+
let secrets = result.secrets;
|
|
407
|
+
if (minSeverity) {
|
|
408
|
+
const severityOrder = ['info', 'low', 'medium', 'high', 'critical'];
|
|
409
|
+
const minIndex = severityOrder.indexOf(minSeverity);
|
|
410
|
+
secrets = secrets.filter(s => severityOrder.indexOf(s.severity) >= minIndex);
|
|
411
|
+
}
|
|
412
|
+
const paginatedSecrets = secrets.slice(0, limit);
|
|
413
|
+
// JSON output
|
|
414
|
+
if (format === 'json') {
|
|
415
|
+
console.log(JSON.stringify({
|
|
416
|
+
secrets: paginatedSecrets,
|
|
417
|
+
total: secrets.length,
|
|
418
|
+
bySeverity: {
|
|
419
|
+
critical: secrets.filter(s => s.severity === 'critical').length,
|
|
420
|
+
high: secrets.filter(s => s.severity === 'high').length,
|
|
421
|
+
medium: secrets.filter(s => s.severity === 'medium').length,
|
|
422
|
+
low: secrets.filter(s => s.severity === 'low').length,
|
|
423
|
+
info: secrets.filter(s => s.severity === 'info').length,
|
|
424
|
+
},
|
|
425
|
+
}, null, 2));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// Text output
|
|
429
|
+
console.log();
|
|
430
|
+
console.log(chalk.bold('🔐 Potential Hardcoded Secrets'));
|
|
431
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
432
|
+
console.log();
|
|
433
|
+
if (secrets.length === 0) {
|
|
434
|
+
console.log(chalk.green(' ✓ No hardcoded secrets detected.'));
|
|
435
|
+
console.log();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Summary
|
|
439
|
+
const critical = secrets.filter(s => s.severity === 'critical').length;
|
|
440
|
+
const high = secrets.filter(s => s.severity === 'high').length;
|
|
441
|
+
if (critical > 0) {
|
|
442
|
+
console.log(chalk.red(` 🔴 ${critical} CRITICAL secrets found!`));
|
|
443
|
+
}
|
|
444
|
+
if (high > 0) {
|
|
445
|
+
console.log(chalk.redBright(` 🟠 ${high} HIGH severity secrets found`));
|
|
446
|
+
}
|
|
447
|
+
console.log();
|
|
448
|
+
// List secrets
|
|
449
|
+
for (const secret of paginatedSecrets) {
|
|
450
|
+
const severityColor = getSeverityColor(secret.severity);
|
|
451
|
+
const severity = severityColor(secret.severity.toUpperCase().padEnd(8));
|
|
452
|
+
const name = chalk.white(secret.name.padEnd(30));
|
|
453
|
+
console.log(` ${severity} ${name}`);
|
|
454
|
+
console.log(chalk.gray(` ${secret.file}:${secret.line}`));
|
|
455
|
+
console.log(chalk.gray(` Type: ${secret.secretType}`));
|
|
456
|
+
console.log();
|
|
457
|
+
}
|
|
458
|
+
console.log(chalk.gray(`Showing ${paginatedSecrets.length} of ${secrets.length} potential secrets`));
|
|
459
|
+
console.log();
|
|
460
|
+
console.log(chalk.yellow('⚠️ Move secrets to environment variables or a secrets manager'));
|
|
461
|
+
console.log();
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Inconsistent action - show constants with inconsistent values
|
|
465
|
+
*/
|
|
466
|
+
async function inconsistentAction(options) {
|
|
467
|
+
const rootDir = process.cwd();
|
|
468
|
+
const format = options.format ?? 'text';
|
|
469
|
+
const limit = options.limit ?? 50;
|
|
470
|
+
if (!(await constantsDataExists(rootDir))) {
|
|
471
|
+
if (format === 'json') {
|
|
472
|
+
console.log(JSON.stringify({ error: 'No constants data found', inconsistencies: [] }));
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
showNotInitializedMessage();
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const store = new ConstantStore({ rootDir });
|
|
480
|
+
const analyzer = new ConsistencyAnalyzer();
|
|
481
|
+
const constants = await store.getAllConstants();
|
|
482
|
+
const result = analyzer.analyze(constants);
|
|
483
|
+
const paginatedInconsistencies = result.inconsistencies.slice(0, limit);
|
|
484
|
+
// JSON output
|
|
485
|
+
if (format === 'json') {
|
|
486
|
+
console.log(JSON.stringify({
|
|
487
|
+
inconsistencies: paginatedInconsistencies,
|
|
488
|
+
total: result.inconsistencies.length,
|
|
489
|
+
}, null, 2));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// Text output
|
|
493
|
+
console.log();
|
|
494
|
+
console.log(chalk.bold('⚡ Inconsistent Constants'));
|
|
495
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
496
|
+
console.log();
|
|
497
|
+
if (result.inconsistencies.length === 0) {
|
|
498
|
+
console.log(chalk.green(' ✓ No inconsistent constants found.'));
|
|
499
|
+
console.log();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
for (const inc of paginatedInconsistencies) {
|
|
503
|
+
console.log(chalk.yellow(` ${inc.name}`));
|
|
504
|
+
console.log(chalk.gray(` Found ${inc.instances.length} different values:`));
|
|
505
|
+
for (const inst of inc.instances.slice(0, 5)) {
|
|
506
|
+
console.log(` ${chalk.cyan(String(inst.value))} in ${chalk.gray(inst.file)}:${inst.line}`);
|
|
507
|
+
}
|
|
508
|
+
if (inc.instances.length > 5) {
|
|
509
|
+
console.log(chalk.gray(` ... and ${inc.instances.length - 5} more`));
|
|
510
|
+
}
|
|
511
|
+
console.log();
|
|
512
|
+
}
|
|
513
|
+
console.log(chalk.gray(`Showing ${paginatedInconsistencies.length} of ${result.inconsistencies.length} inconsistencies`));
|
|
514
|
+
console.log();
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Dead action - show potentially unused constants
|
|
518
|
+
*/
|
|
519
|
+
async function deadAction(options) {
|
|
520
|
+
const rootDir = process.cwd();
|
|
521
|
+
const format = options.format ?? 'text';
|
|
522
|
+
const limit = options.limit ?? 50;
|
|
523
|
+
if (!(await constantsDataExists(rootDir))) {
|
|
524
|
+
if (format === 'json') {
|
|
525
|
+
console.log(JSON.stringify({ error: 'No constants data found', dead: [] }));
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
showNotInitializedMessage();
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const store = new ConstantStore({ rootDir });
|
|
533
|
+
// Get constants that are not exported and uncategorized (likely unused)
|
|
534
|
+
const constants = await store.getAllConstants();
|
|
535
|
+
const potentiallyDead = constants
|
|
536
|
+
.filter(c => !c.isExported && c.category === 'uncategorized')
|
|
537
|
+
.slice(0, limit);
|
|
538
|
+
// JSON output
|
|
539
|
+
if (format === 'json') {
|
|
540
|
+
console.log(JSON.stringify({
|
|
541
|
+
dead: potentiallyDead.map(c => ({
|
|
542
|
+
id: c.id,
|
|
543
|
+
name: c.name,
|
|
544
|
+
file: c.file,
|
|
545
|
+
line: c.line,
|
|
546
|
+
confidence: 0.5,
|
|
547
|
+
reason: 'not_exported_uncategorized',
|
|
548
|
+
})),
|
|
549
|
+
total: potentiallyDead.length,
|
|
550
|
+
}, null, 2));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// Text output
|
|
554
|
+
console.log();
|
|
555
|
+
console.log(chalk.bold('💀 Potentially Unused Constants'));
|
|
556
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
557
|
+
console.log();
|
|
558
|
+
if (potentiallyDead.length === 0) {
|
|
559
|
+
console.log(chalk.green(' ✓ No obvious dead constants found.'));
|
|
560
|
+
console.log();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
console.log(chalk.gray(' Note: Full dead code detection requires reference analysis.'));
|
|
564
|
+
console.log();
|
|
565
|
+
for (const c of potentiallyDead) {
|
|
566
|
+
console.log(` ${chalk.gray('●')} ${chalk.white(c.name)}`);
|
|
567
|
+
console.log(chalk.gray(` ${c.file}:${c.line}`));
|
|
568
|
+
}
|
|
569
|
+
console.log();
|
|
570
|
+
console.log(chalk.gray(`Found ${potentiallyDead.length} potentially unused constants`));
|
|
571
|
+
console.log();
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Export action - export constants to file
|
|
575
|
+
*/
|
|
576
|
+
async function exportAction(outputPath, options) {
|
|
577
|
+
const rootDir = process.cwd();
|
|
578
|
+
const format = options.format ?? 'json';
|
|
579
|
+
if (!(await constantsDataExists(rootDir))) {
|
|
580
|
+
console.log(chalk.red('No constants data found. Run drift scan first.'));
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
const store = new ConstantStore({ rootDir });
|
|
584
|
+
let constants = await store.getAllConstants();
|
|
585
|
+
const enums = await store.getAllEnums();
|
|
586
|
+
// Apply filters
|
|
587
|
+
if (options.category) {
|
|
588
|
+
constants = constants.filter(c => c.category === options.category);
|
|
589
|
+
}
|
|
590
|
+
if (options.language) {
|
|
591
|
+
constants = constants.filter(c => c.language === options.language);
|
|
592
|
+
}
|
|
593
|
+
let content;
|
|
594
|
+
if (format === 'csv') {
|
|
595
|
+
const lines = ['id,name,qualifiedName,file,line,language,kind,category,value,exported'];
|
|
596
|
+
for (const c of constants) {
|
|
597
|
+
const value = c.value !== undefined ? String(c.value).replace(/"/g, '""') : '';
|
|
598
|
+
lines.push(`"${c.id}","${c.name}","${c.qualifiedName}","${c.file}",${c.line},"${c.language}","${c.kind}","${c.category}","${value}",${c.isExported}`);
|
|
599
|
+
}
|
|
600
|
+
content = lines.join('\n');
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
content = JSON.stringify({ constants, enums }, null, 2);
|
|
604
|
+
}
|
|
605
|
+
await fs.writeFile(outputPath, content, 'utf-8');
|
|
606
|
+
console.log(chalk.green(`✓ Exported ${constants.length} constants to ${outputPath}`));
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Create the constants command with subcommands
|
|
610
|
+
*/
|
|
611
|
+
export const constantsCommand = new Command('constants')
|
|
612
|
+
.description('Analyze constants, enums, and exported values')
|
|
613
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
614
|
+
.option('--verbose', 'Enable verbose output')
|
|
615
|
+
.action(overviewAction);
|
|
616
|
+
// Subcommands
|
|
617
|
+
constantsCommand
|
|
618
|
+
.command('list')
|
|
619
|
+
.description('List all constants with filtering')
|
|
620
|
+
.option('-f, --format <format>', 'Output format (text, json, csv)', 'text')
|
|
621
|
+
.option('-c, --category <category>', 'Filter by category')
|
|
622
|
+
.option('-l, --language <language>', 'Filter by language')
|
|
623
|
+
.option('--file <path>', 'Filter by file path')
|
|
624
|
+
.option('-s, --search <query>', 'Search by name')
|
|
625
|
+
.option('--exported', 'Show only exported constants')
|
|
626
|
+
.option('--limit <n>', 'Limit results', parseInt)
|
|
627
|
+
.action(listAction);
|
|
628
|
+
constantsCommand
|
|
629
|
+
.command('get <nameOrId>')
|
|
630
|
+
.description('Show details for a specific constant or enum')
|
|
631
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
632
|
+
.action(getAction);
|
|
633
|
+
constantsCommand
|
|
634
|
+
.command('secrets')
|
|
635
|
+
.description('Show potential hardcoded secrets')
|
|
636
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
637
|
+
.option('--severity <level>', 'Minimum severity (info, low, medium, high, critical)')
|
|
638
|
+
.option('--limit <n>', 'Limit results', parseInt)
|
|
639
|
+
.action(secretsAction);
|
|
640
|
+
constantsCommand
|
|
641
|
+
.command('inconsistent')
|
|
642
|
+
.description('Show constants with inconsistent values across files')
|
|
643
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
644
|
+
.option('--limit <n>', 'Limit results', parseInt)
|
|
645
|
+
.action(inconsistentAction);
|
|
646
|
+
constantsCommand
|
|
647
|
+
.command('dead')
|
|
648
|
+
.description('Show potentially unused constants')
|
|
649
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
650
|
+
.option('--limit <n>', 'Limit results', parseInt)
|
|
651
|
+
.action(deadAction);
|
|
652
|
+
constantsCommand
|
|
653
|
+
.command('export <output>')
|
|
654
|
+
.description('Export constants to file')
|
|
655
|
+
.option('-f, --format <format>', 'Output format (json, csv)', 'json')
|
|
656
|
+
.option('-c, --category <category>', 'Filter by category')
|
|
657
|
+
.option('-l, --language <language>', 'Filter by language')
|
|
658
|
+
.action(exportAction);
|
|
659
|
+
//# sourceMappingURL=constants.js.map
|