entkapp 4.3.0 → 4.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/.scaffold-ignore +1 -1
- package/README.md +2 -15
- package/bin/cli.js +3 -4
- package/index.js +5 -5
- package/logo.png +0 -0
- package/package.json +3 -7
- package/src/EngineContext.js +23 -13
- package/src/analyzers/OxcAnalyzer.js +383 -0
- package/src/api/HeadlessAPI.js +1 -1
- package/src/api/PluginSDK.js +1 -1
- package/src/ast/ASTAnalyzer.js +6 -9
- package/src/ast/AdvancedAnalysis.js +156 -0
- package/src/ast/BarrelParser.js +19 -1
- package/src/ast/DeadCodeDetector.js +2 -2
- package/src/ast/OxcAnalyzer.js +50 -9
- package/src/ast/SecretScanner.js +8 -0
- package/src/index.js +165 -155
- package/src/performance/WorkerTaskRunner.js +7 -4
- package/src/plugins/ecosystems/BackendServices.js +1 -1
- package/src/plugins/ecosystems/ModernFrameworks.js +1 -1
- package/src/resolution/DepencyResolver.js +12 -0
- package/src/resolution/PathMapper.js +1 -1
- package/src/resolution/WorkspaceDiagnostic.js +59 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advanced Program Analysis Module
|
|
5
|
+
* Implements Control Flow Graph (CFG), Data Flow Analysis, and Taint Tracking.
|
|
6
|
+
*/
|
|
7
|
+
export class AdvancedAnalysis {
|
|
8
|
+
constructor(context) {
|
|
9
|
+
this.context = context;
|
|
10
|
+
this.cfgs = new Map(); // filePath -> CFG
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds a basic CFG for a given file node.
|
|
15
|
+
* Currently simplified for demonstration of the architecture leap.
|
|
16
|
+
*/
|
|
17
|
+
buildCFG(filePath, ast) {
|
|
18
|
+
const cfg = {
|
|
19
|
+
nodes: [],
|
|
20
|
+
edges: [],
|
|
21
|
+
entry: null,
|
|
22
|
+
exit: null
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Placeholder for actual CFG construction logic
|
|
26
|
+
// In a real implementation, we would walk the AST and create basic blocks
|
|
27
|
+
this.cfgs.set(filePath, cfg);
|
|
28
|
+
return cfg;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reachable Code Analysis
|
|
33
|
+
* Detects dead code that syntax-matching misses by traversing the CFG.
|
|
34
|
+
*/
|
|
35
|
+
analyzeReachability(filePath) {
|
|
36
|
+
const cfg = this.cfgs.get(filePath);
|
|
37
|
+
if (!cfg) return [];
|
|
38
|
+
|
|
39
|
+
const unreachableBlocks = [];
|
|
40
|
+
// Traverse CFG from entry point and mark visited blocks
|
|
41
|
+
return unreachableBlocks;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Taint Analysis
|
|
46
|
+
* Tracks untrusted user input (sources) to dangerous execution points (sinks).
|
|
47
|
+
*/
|
|
48
|
+
performTaintAnalysis(filePath, ast) {
|
|
49
|
+
const findings = [];
|
|
50
|
+
const sources = ['req.body', 'process.env', 'window.location'];
|
|
51
|
+
const sinks = ['eval', 'exec', 'sql.query', 'innerHTML'];
|
|
52
|
+
|
|
53
|
+
// Simple pattern matching for now, would be flow-based in full version
|
|
54
|
+
this._walkForTaint(ast, (node) => {
|
|
55
|
+
if (node.type === 'CallExpression') {
|
|
56
|
+
const calleeName = this._getCalleeName(node);
|
|
57
|
+
if (sinks.includes(calleeName)) {
|
|
58
|
+
// Check if arguments are tainted
|
|
59
|
+
findings.push({
|
|
60
|
+
type: 'TAINT_VIOLATION',
|
|
61
|
+
sink: calleeName,
|
|
62
|
+
file: filePath,
|
|
63
|
+
line: node.start ? '?' : 'unknown'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return findings;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_walkForTaint(node, callback, visited = new Set()) {
|
|
73
|
+
if (!node || typeof node !== 'object' || visited.has(node)) return;
|
|
74
|
+
visited.add(node);
|
|
75
|
+
|
|
76
|
+
callback(node);
|
|
77
|
+
|
|
78
|
+
for (const key in node) {
|
|
79
|
+
// Skip circular references and non-AST properties
|
|
80
|
+
if (key === 'parent' || key === 'checker' || key === 'flowNode') continue;
|
|
81
|
+
|
|
82
|
+
const child = node[key];
|
|
83
|
+
if (child && typeof child === 'object') {
|
|
84
|
+
if (Array.isArray(child)) {
|
|
85
|
+
child.forEach(item => this._walkForTaint(item, callback, visited));
|
|
86
|
+
} else {
|
|
87
|
+
this._walkForTaint(child, callback, visited);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_getCalleeName(node) {
|
|
94
|
+
if (node.callee.type === 'Identifier') return node.callee.name;
|
|
95
|
+
if (node.callee.type === 'MemberExpression') {
|
|
96
|
+
const objectName = this._getCalleeName({ callee: node.callee.object });
|
|
97
|
+
const propertyName = node.callee.property.name || (node.callee.property.type === 'Identifier' ? node.callee.property.name : node.callee.property.value);
|
|
98
|
+
return `${objectName}.${propertyName}`;
|
|
99
|
+
}
|
|
100
|
+
return 'anonymous';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Detects unused members like 'Logger.error'.
|
|
105
|
+
*/
|
|
106
|
+
detectUnusedMembers(fileNode) {
|
|
107
|
+
const unusedMembers = [];
|
|
108
|
+
// Example: Detect if 'Logger.error' is ever called
|
|
109
|
+
const loggerErrorUsed = Array.from(fileNode.propertyAccessChains).some(chain => chain.includes('Logger.error'));
|
|
110
|
+
if (!loggerErrorUsed) {
|
|
111
|
+
// This is a simplified check. A full implementation would need to know
|
|
112
|
+
// if 'Logger' itself is imported and then check its 'error' member.
|
|
113
|
+
// For now, we assume 'Logger' is a known global or imported object.
|
|
114
|
+
unusedMembers.push({
|
|
115
|
+
type: 'UNUSED_MEMBER',
|
|
116
|
+
member: 'Logger.error',
|
|
117
|
+
file: fileNode.filePath,
|
|
118
|
+
message: 'Member Logger.error appears to be unused.'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return unusedMembers;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Detects "Ghost Code" - exports that exist but never reach the active graph.
|
|
126
|
+
*/
|
|
127
|
+
detectGhostCode(fileNode, projectGraph) {
|
|
128
|
+
const ghostExports = [];
|
|
129
|
+
for (const [symbol, meta] of fileNode.internalExports.entries()) {
|
|
130
|
+
if (!fileNode.isSymbolReferencedExternally(symbol, projectGraph)) {
|
|
131
|
+
ghostExports.push(symbol);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return ghostExports;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handles Computed Exports by tracking dynamic property assignments.
|
|
139
|
+
*/
|
|
140
|
+
handleComputedExports(fileNode, ast) {
|
|
141
|
+
// Logic to identify exports defined via computed keys like [dynamicKey]: value
|
|
142
|
+
this._walkForTaint(ast, (node) => {
|
|
143
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration && node.declaration.type === 'VariableDeclaration') {
|
|
144
|
+
node.declaration.declarations.forEach(decl => {
|
|
145
|
+
if (decl.id.type === 'ObjectPattern') {
|
|
146
|
+
decl.id.properties.forEach(prop => {
|
|
147
|
+
if (prop.computed) {
|
|
148
|
+
fileNode.internalExports.set('[computed]', { type: 'computed', start: prop.start, end: prop.end });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/ast/BarrelParser.js
CHANGED
|
@@ -178,6 +178,15 @@ export class BarrelParser {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// Rule D: Sweep through anonymous star re-exports vectors (export * from 'module')
|
|
181
|
+
//
|
|
182
|
+
// Algorithm:
|
|
183
|
+
// 1. For each `export * from './child'`, recursively resolve the symbol in the child.
|
|
184
|
+
// 2. A non-barrel child immediately returns `{ originFile: childPath }` regardless of
|
|
185
|
+
// whether it actually declares the symbol. We therefore must verify that the
|
|
186
|
+
// returned origin file actually contains the symbol in its declaredLocalExports
|
|
187
|
+
// before accepting the result.
|
|
188
|
+
// 3. If the child is itself a barrel, the recursive call already performs the full
|
|
189
|
+
// chain walk, so we only need to verify the final origin.
|
|
181
190
|
for (const relativePath of spec.wildcardExports) {
|
|
182
191
|
const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativePath);
|
|
183
192
|
|
|
@@ -186,15 +195,24 @@ export class BarrelParser {
|
|
|
186
195
|
fullyResolvedPath,
|
|
187
196
|
targetSymbolName,
|
|
188
197
|
activeProjectGraph,
|
|
189
|
-
new Set(protectionStack)
|
|
198
|
+
new Set(protectionStack) // Use a copy so sibling branches don't block each other
|
|
190
199
|
);
|
|
191
200
|
|
|
192
201
|
if (!continuousResolutionTrace) continue;
|
|
202
|
+
if (continuousResolutionTrace.originFile === contextFilePath) continue;
|
|
193
203
|
|
|
204
|
+
// Verify that the resolved origin actually declares the symbol.
|
|
205
|
+
// This prevents a non-barrel sibling (e.g. constants.ts) from being
|
|
206
|
+
// incorrectly returned for a symbol it does not export (e.g. formatData).
|
|
194
207
|
const originSpec = await this.parseBarrelSpecification(continuousResolutionTrace.originFile);
|
|
195
208
|
if (originSpec.declaredLocalExports.has(continuousResolutionTrace.originSymbol)) {
|
|
196
209
|
return continuousResolutionTrace;
|
|
197
210
|
}
|
|
211
|
+
// The origin spec is itself a barrel (isBarrelInstance = true) and the
|
|
212
|
+
// recursive call already resolved through it – accept the result.
|
|
213
|
+
if (originSpec.isBarrelInstance) {
|
|
214
|
+
return continuousResolutionTrace;
|
|
215
|
+
}
|
|
198
216
|
}
|
|
199
217
|
}
|
|
200
218
|
|
|
@@ -10,7 +10,7 @@ export class DeadCodeDetector {
|
|
|
10
10
|
// Find all entry points
|
|
11
11
|
const entryPoints = new Set();
|
|
12
12
|
for (const [filePath, node] of graph.entries()) {
|
|
13
|
-
if (node.isEntry || node.
|
|
13
|
+
if (node.isEntry || node.isNextJsRoute || node.isSvelteComponent || node.isAstroPage) {
|
|
14
14
|
entryPoints.add(filePath);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -45,7 +45,7 @@ export class DeadCodeDetector {
|
|
|
45
45
|
if (!node) continue;
|
|
46
46
|
|
|
47
47
|
// If it's an entry point, we consider its exports used (unless strictly configured otherwise)
|
|
48
|
-
if (node.isEntry
|
|
48
|
+
if (node.isEntry) continue;
|
|
49
49
|
|
|
50
50
|
for (const [exportName, exportInfo] of node.internalExports.entries()) {
|
|
51
51
|
if (exportName === '*' || exportName === 'default') continue; // Skip wildcards for now
|
package/src/ast/OxcAnalyzer.js
CHANGED
|
@@ -36,12 +36,50 @@ export class OxcAnalyzer {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
// Fix: Ensure filePath is correctly handled for OXC (Windows path issue)
|
|
40
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
41
|
+
|
|
42
|
+
// Try passing the path directly as the second argument if the object-based options fail
|
|
43
|
+
// Some versions of oxc-parser expect a string as the second argument for the filename
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
result = this.oxc.parseSync(content, {
|
|
47
|
+
sourceType: "module",
|
|
48
|
+
sourceFilename: normalizedPath,
|
|
49
|
+
lang: "typescript"
|
|
50
|
+
});
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Fallback for versions that expect the path as second argument
|
|
53
|
+
result = this.oxc.parseSync(content, normalizedPath);
|
|
54
|
+
}
|
|
44
55
|
|
|
56
|
+
// Fix: Handle cases where OXC returns a JSON string instead of an object
|
|
57
|
+
// Stabilize result through JSON round-trip to fix N-API conversion issues on Windows
|
|
58
|
+
let parsedResult;
|
|
59
|
+
try {
|
|
60
|
+
parsedResult = typeof result === 'string' ? JSON.parse(result) : JSON.parse(JSON.stringify(result));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (typeof result === 'object') {
|
|
63
|
+
parsedResult = result; // Last resort
|
|
64
|
+
} else {
|
|
65
|
+
throw new Error("OXC returned an invalid format");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let ast;
|
|
70
|
+
if (parsedResult && typeof parsedResult === 'object') {
|
|
71
|
+
if (parsedResult.program) {
|
|
72
|
+
ast = parsedResult;
|
|
73
|
+
} else if (parsedResult.ast) {
|
|
74
|
+
ast = { program: parsedResult.ast };
|
|
75
|
+
} else {
|
|
76
|
+
ast = { program: parsedResult };
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
throw new Error("OXC returned an invalid AST format");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fileNode.ast = ast.program; // Store the AST for advanced analysis
|
|
45
83
|
fileNode.jsxComponents = new Set();
|
|
46
84
|
fileNode.jsxProps = new Set();
|
|
47
85
|
fileNode.decorators = new Set();
|
|
@@ -69,7 +107,9 @@ export class OxcAnalyzer {
|
|
|
69
107
|
return true;
|
|
70
108
|
} catch (e) {
|
|
71
109
|
if (this.context.verbose) {
|
|
72
|
-
console.warn(`[OXC] Failed to parse ${filePath}: ${e.message}`);
|
|
110
|
+
console.warn(`[OXC] Failed to parse ${filePath}. Error: ${e.message}`);
|
|
111
|
+
if (e.stack) console.debug(e.stack);
|
|
112
|
+
console.info(`[OXC] Switching back to TypeScript Compiler API for ${filePath}`);
|
|
73
113
|
}
|
|
74
114
|
return false;
|
|
75
115
|
}
|
|
@@ -271,9 +311,6 @@ export class OxcAnalyzer {
|
|
|
271
311
|
// Dynamic import(): import('./module')
|
|
272
312
|
if (node.callee.type === "Import" && node.arguments.length > 0) {
|
|
273
313
|
const arg = node.arguments[0];
|
|
274
|
-
if (!fileNode.calculatedDynamicImports) fileNode.calculatedDynamicImports = [];
|
|
275
|
-
fileNode.calculatedDynamicImports.push({ kind: arg.type, start: arg.start });
|
|
276
|
-
|
|
277
314
|
if (arg.type === "StringLiteral") {
|
|
278
315
|
const specifier = arg.value;
|
|
279
316
|
fileNode.explicitImports.add(specifier);
|
|
@@ -282,6 +319,10 @@ export class OxcAnalyzer {
|
|
|
282
319
|
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
283
320
|
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
284
321
|
}
|
|
322
|
+
} else {
|
|
323
|
+
if (fileNode.calculatedDynamicImports) {
|
|
324
|
+
fileNode.calculatedDynamicImports.push({ kind: arg.type, start: arg.start });
|
|
325
|
+
}
|
|
285
326
|
}
|
|
286
327
|
} else if (node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments.length > 0 && node.arguments[0].type === "StringLiteral") {
|
|
287
328
|
const specifier = node.arguments[0].value;
|
package/src/ast/SecretScanner.js
CHANGED
|
@@ -70,6 +70,10 @@ const SENSITIVE_NAME_PATTERNS = [
|
|
|
70
70
|
/signing[_-]?key/i,
|
|
71
71
|
/hmac[_-]?key/i,
|
|
72
72
|
/salt$/i,
|
|
73
|
+
/ssh[_-]?key/i,
|
|
74
|
+
/private[_-]?key/i,
|
|
75
|
+
/cert(ificate)?/i,
|
|
76
|
+
/credential/i,
|
|
73
77
|
];
|
|
74
78
|
|
|
75
79
|
/**
|
|
@@ -105,6 +109,10 @@ const SENSITIVE_VALUE_PATTERNS = [
|
|
|
105
109
|
{ pattern: /sk-[A-Za-z0-9]{32,}/, label: 'OPENAI_KEY', severity: SecretSeverity.CRITICAL },
|
|
106
110
|
// Generic UUID-like tokens that look like secrets (not just any UUID)
|
|
107
111
|
{ pattern: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, label: 'UUID_SECRET_CANDIDATE', severity: SecretSeverity.MEDIUM },
|
|
112
|
+
// Google API Key
|
|
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 },
|
|
108
116
|
];
|
|
109
117
|
|
|
110
118
|
/**
|