entkapp 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -7
- package/bin/cli.js +58 -117
- package/package.json +1 -1
- package/src/EngineContext.js +6 -0
- package/src/ast/ASTAnalyzer.js +94 -6
- package/src/ast/MagicDetector.js +23 -43
- package/src/ast/OxcAnalyzer.js +190 -27
- package/src/ast/SecretScanner.js +112 -50
- package/src/index.js +87 -4
- package/src/performance/GraphCache.js +27 -0
- package/src/performance/WorkerPool.js +22 -4
- package/src/performance/WorkerTaskRunner.js +26 -0
- package/src/plugins/BasePlugin.js +16 -27
- package/src/plugins/PluginRegistry.js +44 -0
- package/src/plugins/ecosystems/UltimateBundle.js +259 -0
- package/src/resolution/ConfigLoader.js +190 -26
- package/src/resolution/FrameworkConfigParser.js +119 -0
- package/src/resolution/GraphAnalyzer.js +65 -1
- package/src/resolution/PathMapper.js +81 -0
- package/src/resolution/TSConfigLoader.js +3 -1
- package/src/resolution/WorkSpaceGraph.js +260 -89
package/src/ast/OxcAnalyzer.js
CHANGED
|
@@ -2,13 +2,18 @@ import path from 'path';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Production-Grade Native Rust AST Parser Bridge (OXC)
|
|
5
|
-
*
|
|
5
|
+
* Optimized for Monorepos and Windows environments.
|
|
6
|
+
* Implements the same semantic logic as ASTAnalyzer but using the high-performance OXC parser.
|
|
6
7
|
*/
|
|
7
8
|
export class OxcAnalyzer {
|
|
8
9
|
constructor(context) {
|
|
9
10
|
this.context = context;
|
|
10
11
|
this.oxc = null;
|
|
11
12
|
this.isAvailable = false;
|
|
13
|
+
this.scopeStack = [];
|
|
14
|
+
this.currentScope = null;
|
|
15
|
+
this.scopeCounter = 0;
|
|
16
|
+
this.pass = 1;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
async init() {
|
|
@@ -35,7 +40,6 @@ export class OxcAnalyzer {
|
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
42
|
* WINDOWS FIX: Robust path normalization for OXC.
|
|
38
|
-
* Replaces backslashes with forward slashes to prevent escape sequence errors.
|
|
39
43
|
*/
|
|
40
44
|
normalizePath(filePath) {
|
|
41
45
|
if (!filePath) return filePath;
|
|
@@ -64,13 +68,10 @@ export class OxcAnalyzer {
|
|
|
64
68
|
lang: "typescript"
|
|
65
69
|
});
|
|
66
70
|
} catch (e) {
|
|
67
|
-
// Fallback with normalized path if the options object fails
|
|
68
71
|
try {
|
|
69
72
|
result = this.oxc.parseSync(normalizedPath, cleanContent);
|
|
70
73
|
} catch (innerErr) {
|
|
71
|
-
if (this.context.verbose) {
|
|
72
|
-
console.error(`[OXC-ERROR] Native parse failed for: ${normalizedPath}`);
|
|
73
|
-
}
|
|
74
|
+
if (this.context.verbose) console.error(`[OXC-ERROR] Native parse failed for: ${normalizedPath}`);
|
|
74
75
|
return false;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
@@ -79,11 +80,8 @@ export class OxcAnalyzer {
|
|
|
79
80
|
try {
|
|
80
81
|
parsedResult = typeof result === 'string' ? JSON.parse(result) : JSON.parse(JSON.stringify(result));
|
|
81
82
|
} catch (err) {
|
|
82
|
-
if (result && typeof result === 'object')
|
|
83
|
-
|
|
84
|
-
} else {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
83
|
+
if (result && typeof result === 'object') parsedResult = result;
|
|
84
|
+
else return false;
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
let programRoot = null;
|
|
@@ -93,24 +91,35 @@ export class OxcAnalyzer {
|
|
|
93
91
|
else if (parsedResult.type === 'Program' || parsedResult.body) programRoot = parsedResult;
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
// --- VALIDATION CHECK ---
|
|
97
|
-
// If the file has content but OXC returns an empty body, it's a silent failure.
|
|
98
94
|
if (cleanContent.trim().length > 0 && (!programRoot || !programRoot.body || programRoot.body.length === 0)) {
|
|
99
|
-
if (this.context.verbose) {
|
|
100
|
-
console.warn(`[OXC-WARNING] Silent failure detected for ${normalizedPath}. Body is empty despite content. Triggering Fallback...`);
|
|
101
|
-
}
|
|
102
|
-
return false; // This triggers the fallback to TypeScript in the engine
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!programRoot || !programRoot.body) {
|
|
95
|
+
if (this.context.verbose) console.warn(`[OXC-WARNING] Silent failure detected for ${normalizedPath}. Triggering Fallback...`);
|
|
106
96
|
return false;
|
|
107
97
|
}
|
|
108
98
|
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
if (!programRoot || !programRoot.body) return false;
|
|
100
|
+
|
|
101
|
+
fileNode.ast = programRoot;
|
|
102
|
+
fileNode.symbolTable = new Map();
|
|
103
|
+
|
|
104
|
+
// --- TWO-PASS ANALYSIS ---
|
|
111
105
|
|
|
112
|
-
|
|
113
|
-
this.
|
|
106
|
+
// Pass 1: Declarations
|
|
107
|
+
this.currentScope = { symbols: new Map(), parent: null, children: [] };
|
|
108
|
+
this.scopeStack = [this.currentScope];
|
|
109
|
+
this.scopeCounter = 0;
|
|
110
|
+
this.pass = 1;
|
|
111
|
+
this.walkOxcAst(programRoot, fileNode, cleanContent);
|
|
112
|
+
|
|
113
|
+
// Pass 2: References
|
|
114
|
+
if (this.scopeStack.length > 0) {
|
|
115
|
+
this.currentScope = this.scopeStack[0];
|
|
116
|
+
this.scopeCounter = 0;
|
|
117
|
+
this.pass = 2;
|
|
118
|
+
this.walkOxcAst(programRoot, fileNode, cleanContent);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.scopeStack = [];
|
|
122
|
+
this.currentScope = null;
|
|
114
123
|
|
|
115
124
|
return true;
|
|
116
125
|
} catch (e) {
|
|
@@ -118,9 +127,163 @@ export class OxcAnalyzer {
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
pushScope() {
|
|
131
|
+
if (this.pass === 1) {
|
|
132
|
+
const newScope = { symbols: new Map(), parent: this.currentScope, children: [] };
|
|
133
|
+
if (this.currentScope) this.currentScope.children.push(newScope);
|
|
134
|
+
this.scopeStack.push(newScope);
|
|
135
|
+
this.currentScope = newScope;
|
|
136
|
+
} else {
|
|
137
|
+
if (this.currentScope && this.currentScope.children) {
|
|
138
|
+
const nextScope = this.currentScope.children[this.scopeCounter++];
|
|
139
|
+
if (nextScope) {
|
|
140
|
+
this.scopeStack.push(nextScope);
|
|
141
|
+
this.currentScope = nextScope;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
popScope() {
|
|
148
|
+
this.scopeStack.pop();
|
|
149
|
+
this.currentScope = this.scopeStack[this.scopeStack.length - 1];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
walkOxcAst(node, fileNode, content) {
|
|
123
153
|
if (!node) return;
|
|
124
|
-
|
|
154
|
+
|
|
155
|
+
const isScopeNode = node.type === 'BlockStatement' || node.type === 'FunctionDeclaration' ||
|
|
156
|
+
node.type === 'ClassDeclaration' || node.type === 'ArrowFunctionExpression';
|
|
157
|
+
|
|
158
|
+
let previousCounter = 0;
|
|
159
|
+
if (isScopeNode) {
|
|
160
|
+
if (this.pass === 2) previousCounter = this.scopeCounter;
|
|
161
|
+
this.scopeCounter = 0;
|
|
162
|
+
this.pushScope();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.pass === 1) {
|
|
166
|
+
this.handleNodePass1(node, fileNode, content);
|
|
167
|
+
} else {
|
|
168
|
+
this.handleNodePass2(node, fileNode, content);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Traverse children
|
|
172
|
+
for (const key in node) {
|
|
173
|
+
const child = node[key];
|
|
174
|
+
if (Array.isArray(child)) {
|
|
175
|
+
child.forEach(c => this.walkOxcAst(c, fileNode, content));
|
|
176
|
+
} else if (child && typeof child === 'object' && child.type) {
|
|
177
|
+
this.walkOxcAst(child, fileNode, content);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (isScopeNode) {
|
|
182
|
+
this.popScope();
|
|
183
|
+
if (this.pass === 2) this.scopeCounter = previousCounter + 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
handleNodePass1(node, fileNode, content) {
|
|
188
|
+
switch (node.type) {
|
|
189
|
+
case 'ImportDeclaration':
|
|
190
|
+
this.handleImport(node, fileNode);
|
|
191
|
+
break;
|
|
192
|
+
case 'ExportNamedDeclaration':
|
|
193
|
+
this.handleExportNamed(node, fileNode);
|
|
194
|
+
break;
|
|
195
|
+
case 'ExportDefaultDeclaration':
|
|
196
|
+
fileNode.internalExports.set('default', { type: 'default', start: node.start, end: node.end });
|
|
197
|
+
break;
|
|
198
|
+
case 'ExportAllDeclaration':
|
|
199
|
+
if (node.source) {
|
|
200
|
+
const specifier = node.source.value;
|
|
201
|
+
fileNode.explicitImports.add(specifier);
|
|
202
|
+
fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
handleNodePass2(node, fileNode, content) {
|
|
209
|
+
switch (node.type) {
|
|
210
|
+
case 'Identifier':
|
|
211
|
+
if (!this.isLocalShadowing(node.name)) {
|
|
212
|
+
fileNode.instantiatedIdentifiers.add(node.name);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
case 'CallExpression':
|
|
216
|
+
this.handleCall(node, fileNode);
|
|
217
|
+
break;
|
|
218
|
+
case 'MemberExpression':
|
|
219
|
+
if (node.property && node.property.name) {
|
|
220
|
+
fileNode.instantiatedIdentifiers.add(node.property.name);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
handleImport(node, fileNode) {
|
|
227
|
+
if (!node.source) return;
|
|
228
|
+
const specifier = node.source.value;
|
|
229
|
+
fileNode.explicitImports.add(specifier);
|
|
230
|
+
|
|
231
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
232
|
+
const pkgName = this._extractPackageName(specifier);
|
|
233
|
+
let isInternal = false;
|
|
234
|
+
if (this.context.pathMapper?.isTsconfigAlias?.(specifier)) isInternal = true;
|
|
235
|
+
if (!isInternal && this.context.workspaceGraph?.isLocalWorkspaceSpecifier?.(specifier)) isInternal = true;
|
|
236
|
+
if (!isInternal) fileNode.externalPackageUsage.add(pkgName);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
handleExportNamed(node, fileNode) {
|
|
241
|
+
if (node.declaration) {
|
|
242
|
+
const decl = node.declaration;
|
|
243
|
+
const name = decl.id?.name || (decl.declarations?.[0]?.id?.name);
|
|
244
|
+
if (name) {
|
|
245
|
+
fileNode.internalExports.set(name, { type: 'export', start: node.start, end: node.end });
|
|
246
|
+
}
|
|
247
|
+
} else if (node.specifiers) {
|
|
248
|
+
node.specifiers.forEach(spec => {
|
|
249
|
+
const name = spec.exported.name || spec.exported.value;
|
|
250
|
+
fileNode.internalExports.set(name, { type: 'export', start: spec.start, end: spec.end });
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
handleCall(node, fileNode) {
|
|
256
|
+
const callee = node.callee;
|
|
257
|
+
if (callee.type === 'Identifier' && callee.name === 'require') {
|
|
258
|
+
const arg = node.arguments[0];
|
|
259
|
+
if (arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')) {
|
|
260
|
+
const specifier = arg.value;
|
|
261
|
+
fileNode.explicitImports.add(specifier);
|
|
262
|
+
}
|
|
263
|
+
} else if (callee.type === 'Import') {
|
|
264
|
+
const arg = node.arguments[0];
|
|
265
|
+
if (arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')) {
|
|
266
|
+
const specifier = arg.value;
|
|
267
|
+
fileNode.explicitImports.add(specifier);
|
|
268
|
+
fileNode.dynamicImports.add(specifier);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
isLocalShadowing(name) {
|
|
274
|
+
let scope = this.currentScope;
|
|
275
|
+
while (scope) {
|
|
276
|
+
if (scope.symbols.has(name)) return scope.parent !== null;
|
|
277
|
+
scope = scope.parent;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_extractPackageName(specifier) {
|
|
283
|
+
if (specifier.startsWith('@')) {
|
|
284
|
+
const parts = specifier.split('/');
|
|
285
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
286
|
+
}
|
|
287
|
+
return specifier.split('/')[0];
|
|
125
288
|
}
|
|
126
289
|
}
|
package/src/ast/SecretScanner.js
CHANGED
|
@@ -35,45 +35,89 @@ const SENSITIVE_NAME_PATTERNS = [
|
|
|
35
35
|
/private[_-]?key/i,
|
|
36
36
|
/client[_-]?secret/i,
|
|
37
37
|
/app[_-]?secret/i,
|
|
38
|
+
/security[_-]?token/i,
|
|
39
|
+
/master[_-]?key/i,
|
|
40
|
+
/root[_-]?password/i,
|
|
41
|
+
|
|
38
42
|
// Database credentials
|
|
39
43
|
/db[_-]?pass(word)?/i,
|
|
40
44
|
/database[_-]?pass(word)?/i,
|
|
41
45
|
/db[_-]?url/i,
|
|
42
46
|
/database[_-]?url/i,
|
|
43
47
|
/connection[_-]?string/i,
|
|
44
|
-
|
|
48
|
+
/postgres(ql)?[_-]?url/i,
|
|
49
|
+
/mongo(db)?[_-]?url/i,
|
|
50
|
+
/redis[_-]?url/i,
|
|
51
|
+
|
|
52
|
+
// Passwords & Pins
|
|
45
53
|
/^password$/i,
|
|
46
54
|
/^passwd$/i,
|
|
47
55
|
/^pwd$/i,
|
|
48
56
|
/[_-]password$/i,
|
|
49
|
-
|
|
57
|
+
/[_-]pass$/i,
|
|
58
|
+
/pass(phrase|code)/i,
|
|
59
|
+
/admin[_-]?pass/i,
|
|
60
|
+
/pin[_-]?number/i,
|
|
61
|
+
|
|
62
|
+
// Tokens & Sessions
|
|
50
63
|
/[_-]token$/i,
|
|
51
64
|
/^token$/i,
|
|
52
|
-
/jwt[_-]?secret/i,
|
|
53
|
-
/session[_-]?secret/i,
|
|
65
|
+
/jwt[_-]?(secret|token)/i,
|
|
66
|
+
/session[_-]?(secret|token|id)/i,
|
|
54
67
|
/cookie[_-]?secret/i,
|
|
55
|
-
|
|
56
|
-
/
|
|
57
|
-
/
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
/
|
|
61
|
-
/
|
|
62
|
-
/
|
|
63
|
-
/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/openai[_-]?(key|token)/i,
|
|
68
|
+
/oauth[_-]?(token|secret|client)/i,
|
|
69
|
+
/refresh[_-]?token/i,
|
|
70
|
+
/csrf[_-]?token/i,
|
|
71
|
+
|
|
72
|
+
// Cloud Provider Keys (AWS, GCP, Azure)
|
|
73
|
+
/aws[_-]?(access[_-]?key|secret|session[_-]?token|key[_-]?id)/i,
|
|
74
|
+
/gcp[_-]?(key|secret|token|sa[_-]?key)/i,
|
|
75
|
+
/google[_-]?(api[_-]?key|credentials|client[_-]?secret)/i,
|
|
76
|
+
/azure[_-]?(key|secret|connection|token|tenant[_-]?id)/i,
|
|
77
|
+
|
|
78
|
+
// AI & Machine Learning Platforms
|
|
79
|
+
/openai[_-]?(key|token|secret)/i,
|
|
67
80
|
/anthropic[_-]?key/i,
|
|
68
|
-
/
|
|
69
|
-
/
|
|
81
|
+
/cohere[_-]?key/i,
|
|
82
|
+
/huggingface[_-]?token/i,
|
|
83
|
+
/replicate[_-]?api/i,
|
|
84
|
+
/langchain[_-]?api/i,
|
|
85
|
+
/pinecone[_-]?api/i,
|
|
86
|
+
/gemini[_-]?key/i,
|
|
87
|
+
|
|
88
|
+
// Service-Specific (CI/CD, Dev Tools, Payment, Comms)
|
|
89
|
+
/stripe[_-]?(key|secret|webhook)/i,
|
|
90
|
+
/twilio[_-]?(auth|token|sid)/i,
|
|
91
|
+
/sendgrid[_-]?(key|api)/i,
|
|
92
|
+
/github[_-]?(token|pat|secret|app[_-]?id)/i,
|
|
93
|
+
/gitlab[_-]?(token|pat|secret)/i,
|
|
94
|
+
/slack[_-]?(token|webhook|secret)/i,
|
|
95
|
+
/discord[_-]?(token|secret|webhook)/i,
|
|
96
|
+
/pagerduty[_-]?(token|key)/i,
|
|
97
|
+
/datadog[_-]?(api[_-]?key|app[_-]?key)/i,
|
|
98
|
+
/sentry[_-]?(dsn|auth[_-]?token)/i,
|
|
99
|
+
/heroku[_-]?(api[_-]?key|oauth)/i,
|
|
100
|
+
/atlassian[_-]?(token|secret)/i,
|
|
101
|
+
/jira[_-]?(token|password)/i,
|
|
102
|
+
/npm[_-]?auth[_-]?token/i,
|
|
103
|
+
/jfrog[_-]?(token|password|api)/i,
|
|
104
|
+
/firebase[_-]?(key|secret|token)/i,
|
|
105
|
+
/supabase[_-]?(key|secret|service[_-]?role)/i,
|
|
106
|
+
|
|
107
|
+
// Webhooks, Crypto, and Infrastructure
|
|
108
|
+
/webhook[_-]?(url|secret|token)/i,
|
|
109
|
+
/encryption[_-]?(key|secret|iv)/i,
|
|
70
110
|
/signing[_-]?key/i,
|
|
71
|
-
/hmac[_-]?key/i,
|
|
111
|
+
/hmac[_-]?(key|secret)/i,
|
|
72
112
|
/salt$/i,
|
|
73
|
-
/ssh[_-]?key/i,
|
|
74
|
-
/
|
|
75
|
-
/
|
|
76
|
-
/
|
|
113
|
+
/ssh[_-]?(key|private|public)/i,
|
|
114
|
+
/cert(ificate)?(s)?/i,
|
|
115
|
+
/credential(s)?/i,
|
|
116
|
+
/tls[_-]?(key|cert)/i,
|
|
117
|
+
/ssl[_-]?(key|cert)/i,
|
|
118
|
+
/kube(config)?[_-]?(token|secret)/i,
|
|
119
|
+
/vault[_-]?(token|secret|key)/i,
|
|
120
|
+
/bitcoind?[_-]?(rpc)?password/i,
|
|
77
121
|
];
|
|
78
122
|
|
|
79
123
|
/**
|
|
@@ -84,37 +128,55 @@ const SENSITIVE_VALUE_PATTERNS = [
|
|
|
84
128
|
// AWS Access Key IDs
|
|
85
129
|
{ pattern: /AKIA[0-9A-Z]{16}/, label: 'AWS_ACCESS_KEY_ID', severity: SecretSeverity.CRITICAL },
|
|
86
130
|
// AWS Secret Access Keys (40-char base64-ish)
|
|
87
|
-
{ pattern: /[A-Za-z0-9/+=]{40}/, label: 'AWS_SECRET_KEY_CANDIDATE', severity: SecretSeverity.MEDIUM },
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
131
|
+
{ pattern: /(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/, label: 'AWS_SECRET_KEY_CANDIDATE', severity: SecretSeverity.MEDIUM },
|
|
132
|
+
|
|
133
|
+
// Generic high-entropy hex strings (32+ chars) - Added word boundaries to reduce false positives
|
|
134
|
+
{ pattern: /\b[0-9a-f]{32,}\b/i, label: 'HEX_SECRET', severity: SecretSeverity.HIGH },
|
|
135
|
+
// Generic high-entropy base64 strings (32+ chars) - Replaced rigid anchors with lookarounds
|
|
136
|
+
{ pattern: /(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{32,}={0,2}(?![A-Za-z0-9+/])/, label: 'BASE64_SECRET', severity: SecretSeverity.MEDIUM },
|
|
137
|
+
|
|
92
138
|
// JWT tokens
|
|
93
|
-
{ pattern:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{ pattern:
|
|
139
|
+
{ pattern: /\bey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/, label: 'JWT_TOKEN', severity: SecretSeverity.CRITICAL },
|
|
140
|
+
|
|
141
|
+
// GitHub access tokens (Updated for all token formats: ghp, gho, ghu, ghs, ghr, fine-grained)
|
|
142
|
+
{ pattern: /\bgh[pousr]_[A-Za-z0-9]{36}\b/, label: 'GITHUB_TOKEN_CLASSIC', severity: SecretSeverity.CRITICAL },
|
|
143
|
+
{ pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/, label: 'GITHUB_PAT_FINE', severity: SecretSeverity.CRITICAL },
|
|
144
|
+
{ pattern: /\bghs_[A-Za-z0-9\.\-_]{36,}\b/, label: 'GITHUB_APP_STATELESS_TOKEN', severity: SecretSeverity.CRITICAL }, // Supports the modern 2026 ghs_ JWT format
|
|
145
|
+
|
|
97
146
|
// Stripe keys
|
|
98
|
-
{ pattern:
|
|
99
|
-
{ pattern:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{ pattern: /[
|
|
104
|
-
|
|
105
|
-
|
|
147
|
+
{ pattern: /\bsk_(live|test)_[A-Za-z0-9]{24,}\b/, label: 'STRIPE_SECRET_KEY', severity: SecretSeverity.CRITICAL },
|
|
148
|
+
{ pattern: /\bpk_(live|test)_[A-Za-z0-9]{24,}\b/, label: 'STRIPE_PUBLIC_KEY', severity: SecretSeverity.HIGH },
|
|
149
|
+
|
|
150
|
+
// Slack tokens & Webhooks
|
|
151
|
+
{ pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, label: 'SLACK_TOKEN', severity: SecretSeverity.CRITICAL },
|
|
152
|
+
{ pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9_]{8}\/B[A-Z0-9_]{8}\/[A-Za-z0-9_]{24}/, label: 'SLACK_WEBHOOK', severity: SecretSeverity.CRITICAL },
|
|
153
|
+
|
|
154
|
+
// Discord tokens & Webhooks
|
|
155
|
+
{ pattern: /\b[MN][A-Za-z0-9]{23}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27}\b/, label: 'DISCORD_TOKEN', severity: SecretSeverity.CRITICAL },
|
|
156
|
+
{ pattern: /https:\/\/discord\.com\/api\/webhooks\/[0-9]{18,20}\/[A-Za-z0-9_-]{68,}/, label: 'DISCORD_WEBHOOK', severity: SecretSeverity.CRITICAL },
|
|
157
|
+
|
|
158
|
+
// Twilio SID & Auth Token
|
|
159
|
+
{ pattern: /\bAC[a-f0-9]{32}\b/, label: 'TWILIO_SID', severity: SecretSeverity.HIGH },
|
|
160
|
+
{ pattern: /\b[a-f0-9]{32}\b/, label: 'TWILIO_AUTH_TOKEN_CANDIDATE', severity: SecretSeverity.MEDIUM }, // Twilio tokens are raw 32-char hex strings often found near SIDs
|
|
161
|
+
|
|
106
162
|
// SendGrid API key
|
|
107
|
-
{ pattern:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{ pattern:
|
|
112
|
-
|
|
113
|
-
{ pattern: /AIza[0-9A-Za-z\\-_]{35}/, label: 'GOOGLE_API_KEY', severity: SecretSeverity.CRITICAL },
|
|
114
|
-
// Firebase Web API Key
|
|
115
|
-
{ pattern: /AIzaSy[0-9A-Za-z\\-_]{33}/, label: 'FIREBASE_API_KEY', severity: SecretSeverity.CRITICAL },
|
|
116
|
-
];
|
|
163
|
+
{ pattern: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/, label: 'SENDGRID_KEY', severity: SecretSeverity.CRITICAL },
|
|
164
|
+
|
|
165
|
+
// OpenAI & Anthropic API keys (Updated for Legacy, Project, and Claude token formats)
|
|
166
|
+
{ pattern: /\bsk-[A-Za-z0-9]{32,}\b/, label: 'OPENAI_KEY_LEGACY', severity: SecretSeverity.CRITICAL },
|
|
167
|
+
{ pattern: /\bsk-proj-[A-Za-z0-9-_]{40,}\b/, label: 'OPENAI_PROJECT_KEY', severity: SecretSeverity.CRITICAL },
|
|
168
|
+
{ pattern: /\bsk-ant-sid01-[A-Za-z0-9-_]{93}\b/, label: 'ANTHROPIC_CLAUDE_KEY', severity: SecretSeverity.CRITICAL },
|
|
117
169
|
|
|
170
|
+
// Generic UUID-like tokens
|
|
171
|
+
{ pattern: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, label: 'UUID_SECRET_CANDIDATE', severity: SecretSeverity.MEDIUM },
|
|
172
|
+
|
|
173
|
+
// Google & Firebase Key
|
|
174
|
+
{ pattern: /\bAIza[0-9A-Za-z\\-_]{35}\b/, label: 'GOOGLE_API_KEY', severity: SecretSeverity.CRITICAL },
|
|
175
|
+
{ pattern: /\bAIzaSy[0-9A-Za-z\\-_]{33}\b/, label: 'FIREBASE_API_KEY', severity: SecretSeverity.CRITICAL },
|
|
176
|
+
|
|
177
|
+
// Private Key Files (Detects multi-line PEM blocks often found inside JSON/Env variables)
|
|
178
|
+
{ pattern: /-----BEGIN [A-Z ]+ PRIVATE KEY-----/, label: 'PRIVATE_KEY_BLOCK', severity: SecretSeverity.CRITICAL },
|
|
179
|
+
];
|
|
118
180
|
/**
|
|
119
181
|
* Minimum length a string value must have to be considered a potential secret.
|
|
120
182
|
* Short strings like "test", "dev", "localhost" are excluded.
|
package/src/index.js
CHANGED
|
@@ -113,22 +113,81 @@ export class RefactoringEngine {
|
|
|
113
113
|
|
|
114
114
|
// Pass 1: Boot environment contexts and load alias configuration maps
|
|
115
115
|
await this.oxcAnalyzer.init();
|
|
116
|
+
// UPGRADE: Wire pathMapper onto context so ASTAnalyzer alias-filtering works in all code paths
|
|
117
|
+
this.context.pathMapper = this.pathMapper;
|
|
116
118
|
await this.pathMapper.loadMappings(this.context.tsconfigFilename);
|
|
117
119
|
|
|
118
120
|
// Always attempt workspace mesh initialization
|
|
119
121
|
console.log(ansis.dim('🌐 Probing for monorepo workspace configuration...'));
|
|
120
122
|
await this.workspaceGraph.initializeWorkspaceMesh();
|
|
123
|
+
// UPGRADE: Always expose workspaceGraph on context (not only when workspace is enabled)
|
|
124
|
+
this.context.workspaceGraph = this.workspaceGraph;
|
|
125
|
+
|
|
126
|
+
// UPGRADE: Scan all discovered framework configs across the workspace EARLY
|
|
127
|
+
const discoveredConfigs = await this.workspaceGraph.findFrameworkConfigs();
|
|
128
|
+
if (discoveredConfigs.length > 0) {
|
|
129
|
+
console.log(ansis.dim(`⚙️ Detected ${discoveredConfigs.length} framework configurations...`));
|
|
130
|
+
const { FrameworkConfigParser } = await import('./resolution/FrameworkConfigParser.js');
|
|
131
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
132
|
+
|
|
133
|
+
for (const configPath of discoveredConfigs) {
|
|
134
|
+
try {
|
|
135
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
136
|
+
|
|
137
|
+
// 1. Core Parsing (Aliases)
|
|
138
|
+
const { aliases } = parser.parse(content, configPath);
|
|
139
|
+
for (const [key, target] of aliases.entries()) {
|
|
140
|
+
this.pathMapper.addAlias(key, target);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 2. Plugin-based Entry Detection
|
|
144
|
+
const detectedEntries = await this.magicDetector.detectEntryPointsFromPlugins(content, configPath);
|
|
145
|
+
for (const entry of detectedEntries) {
|
|
146
|
+
const absEntry = path.resolve(path.dirname(configPath), entry).replace(/\\/g, '/');
|
|
147
|
+
if (!this.context.entryPoints.includes(absEntry)) {
|
|
148
|
+
this.context.entryPoints.push(absEntry);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Register config file itself as entry
|
|
153
|
+
if (!this.context.entryPoints.includes(configPath)) {
|
|
154
|
+
this.context.entryPoints.push(configPath);
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
121
160
|
if (this.context.isWorkspaceEnabled) {
|
|
122
161
|
console.log(ansis.dim('🌐 Monorepo workspace detected – mapping package mesh layers...'));
|
|
123
|
-
|
|
124
|
-
|
|
162
|
+
|
|
163
|
+
// NEW: Register all workspace package entry points
|
|
164
|
+
for (const [dir, manifest] of this.workspaceGraph.packageManifests.entries()) {
|
|
165
|
+
if (manifest.entryPoints && manifest.entryPoints.length > 0) {
|
|
166
|
+
for (const ep of manifest.entryPoints) {
|
|
167
|
+
if (!this.context.entryPoints.includes(ep)) {
|
|
168
|
+
this.context.entryPoints.push(ep);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
125
174
|
// Reload PathMapper aliases now that workspace roots are known
|
|
126
175
|
await this.pathMapper.loadMappings(this.context.tsconfigFilename);
|
|
127
176
|
if (this.context.verbose) {
|
|
128
|
-
|
|
177
|
+
const wsSummary = this.workspaceGraph.getSummary();
|
|
178
|
+
console.log(`[Workspace] Found ${wsSummary.packages} workspace packages:`);
|
|
129
179
|
for (const [dir, manifest] of this.workspaceGraph.packageManifests.entries()) {
|
|
130
180
|
console.log(ansis.dim(` • ${manifest.name || dir}`));
|
|
131
181
|
}
|
|
182
|
+
if (wsSummary.tsconfigsLoaded > 0) {
|
|
183
|
+
console.log(ansis.dim(`[Workspace] Loaded ${wsSummary.tsconfigsLoaded} tsconfig.json file(s) from workspace packages`));
|
|
184
|
+
}
|
|
185
|
+
if (wsSummary.configFilesLoaded > 0) {
|
|
186
|
+
console.log(ansis.dim(`[Workspace] Loaded ${wsSummary.configFilesLoaded} *.config.ts/js file(s) from workspace packages`));
|
|
187
|
+
}
|
|
188
|
+
if (wsSummary.monorepoConfigs && wsSummary.monorepoConfigs.length > 0) {
|
|
189
|
+
console.log(ansis.dim(`[Workspace] Detected global configs: ${wsSummary.monorepoConfigs.join(', ')}`));
|
|
190
|
+
}
|
|
132
191
|
}
|
|
133
192
|
}
|
|
134
193
|
|
|
@@ -200,6 +259,14 @@ export class RefactoringEngine {
|
|
|
200
259
|
for (const filePath of sourceCodeFilesList) {
|
|
201
260
|
const absFilePath = slashifyInternal(filePath);
|
|
202
261
|
const node = this.context.getOrCreateNode(absFilePath);
|
|
262
|
+
|
|
263
|
+
// Config data is now handled early in the workspace mesh initialization phase.
|
|
264
|
+
// We still mark the rawCode for plugin analysis.
|
|
265
|
+
if (absFilePath.includes('.config.')) {
|
|
266
|
+
try {
|
|
267
|
+
node.rawCode = await fs.readFile(absFilePath, 'utf8');
|
|
268
|
+
} catch (e) {}
|
|
269
|
+
}
|
|
203
270
|
const currentHash = await this.cacheManager.computeHash(absFilePath);
|
|
204
271
|
node.contentHash = currentHash;
|
|
205
272
|
|
|
@@ -227,6 +294,7 @@ export class RefactoringEngine {
|
|
|
227
294
|
} else if (!parallelParseCompleted) {
|
|
228
295
|
this.context.metrics.cacheMisses++;
|
|
229
296
|
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
297
|
+
node.rawCode = fileContent; // UPGRADE: Store raw code for plugin analysis
|
|
230
298
|
|
|
231
299
|
let success = false;
|
|
232
300
|
if (this.oxcAnalyzer.isAvailable) {
|
|
@@ -248,6 +316,10 @@ export class RefactoringEngine {
|
|
|
248
316
|
if (!success || (oxcFailedToFindDependencies && (hasImportExportKeywords || hasCommonJSKeywords))) {
|
|
249
317
|
await this.analyzer.parseFile(filePath, fileContent, node);
|
|
250
318
|
}
|
|
319
|
+
|
|
320
|
+
// UPGRADE 5.3.0: Run plugin-specific content analysis
|
|
321
|
+
await this.magicDetector.runPluginContentAnalysis(node, absFilePath);
|
|
322
|
+
|
|
251
323
|
// Secret scan on freshly parsed content
|
|
252
324
|
const secretFindings = this.secretScanner.scanFileContent(filePath, fileContent);
|
|
253
325
|
if (secretFindings.length > 0) {
|
|
@@ -562,6 +634,17 @@ export class RefactoringEngine {
|
|
|
562
634
|
const nodeBuiltins = ['assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib'];
|
|
563
635
|
if (nodeBuiltins.includes(basePkg) || nodeBuiltins.includes(basePkg.replace('node:', ''))) return;
|
|
564
636
|
|
|
637
|
+
// UPGRADE: Skip tsconfig path aliases (e.g. @shared/*, @idk/*, ~/*)
|
|
638
|
+
// Check full specifier AND basePkg against alias patterns
|
|
639
|
+
if (this.pathMapper && typeof this.pathMapper.isTsconfigAlias === 'function') {
|
|
640
|
+
if (this.pathMapper.isTsconfigAlias(specifier) || this.pathMapper.isTsconfigAlias(basePkg)) return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// UPGRADE: Skip workspace packages detected via pnpm-workspace.yaml / package.json workspaces
|
|
644
|
+
if (this.workspaceGraph && typeof this.workspaceGraph.isLocalWorkspaceSpecifier === 'function') {
|
|
645
|
+
if (this.workspaceGraph.isLocalWorkspaceSpecifier(specifier) || this.workspaceGraph.isLocalWorkspaceSpecifier(basePkg)) return;
|
|
646
|
+
}
|
|
647
|
+
|
|
565
648
|
if (!localDeps.has(basePkg)) {
|
|
566
649
|
const alreadyFlagged = this.context.unlistedDependencies.some(u => u.package === basePkg && u.file === filePath);
|
|
567
650
|
if (!alreadyFlagged) {
|
|
@@ -574,7 +657,7 @@ export class RefactoringEngine {
|
|
|
574
657
|
}
|
|
575
658
|
});
|
|
576
659
|
} catch (error) {
|
|
577
|
-
if (this.context.options.verbose) {
|
|
660
|
+
if (this.context.options.verbose && error.code !== 'ENOENT') {
|
|
578
661
|
console.error(ansis.red(` ❌ Manifest Parsing Exception: ${error.message}`));
|
|
579
662
|
}
|
|
580
663
|
}
|
|
@@ -59,6 +59,33 @@ export class IncrementalCacheManager {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* UPGRADE 5.4.3: Affected-Only Analysis
|
|
64
|
+
* Identifies which files need re-analysis based on Git changes or cache misses.
|
|
65
|
+
*/
|
|
66
|
+
async getAffectedFiles(allFiles) {
|
|
67
|
+
const manifest = await this.loadCacheManifest();
|
|
68
|
+
const affected = [];
|
|
69
|
+
const unchanged = [];
|
|
70
|
+
|
|
71
|
+
for (const file of allFiles) {
|
|
72
|
+
const cached = manifest[file];
|
|
73
|
+
if (!cached) {
|
|
74
|
+
affected.push(file);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const currentHash = await this.computeHash(file);
|
|
79
|
+
if (currentHash !== cached.hash) {
|
|
80
|
+
affected.push(file);
|
|
81
|
+
} else {
|
|
82
|
+
unchanged.push({ path: file, data: cached });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { affected, unchanged };
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
/**
|
|
63
90
|
* Serializes the current active dependency graph, translating Maps and Sets into JSON schemas.
|
|
64
91
|
* @param {Map<string, Object>} currentGraphState - In-memory structural project state map
|