entkapp 4.3.0 → 4.4.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.
@@ -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
+ }
@@ -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.isLibraryEntry || node.isNextJsRoute || node.isSvelteComponent || node.isAstroPage) {
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 || node.isLibraryEntry) continue;
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
@@ -36,12 +36,50 @@ export class OxcAnalyzer {
36
36
  }
37
37
 
38
38
  try {
39
- const ast = this.oxc.parseSync(content, {
40
- sourceType: "module",
41
- sourceFilename: filePath,
42
- ecmaVersion: "latest",
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;
@@ -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
  /**