entkapp 4.1.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.
Files changed (46) hide show
  1. package/.scaffold-ignore +22 -0
  2. package/LICENSE +211 -0
  3. package/NOTICE +13 -0
  4. package/README.md +33 -0
  5. package/bin/cli.js +174 -0
  6. package/entkapp/config.json +42 -0
  7. package/entkapp/plugins/README.md +19 -0
  8. package/index.js +2254 -0
  9. package/logo.png +0 -0
  10. package/package.json +96 -0
  11. package/src/EngineContext.js +185 -0
  12. package/src/api/HeadlessAPI.js +376 -0
  13. package/src/api/PluginSDK.js +299 -0
  14. package/src/ast/ASTAnalyzer.js +492 -0
  15. package/src/ast/BarrelParser.js +221 -0
  16. package/src/ast/DeadCodeDetector.js +73 -0
  17. package/src/ast/MagicDetector.js +203 -0
  18. package/src/ast/OxcAnalyzer.js +337 -0
  19. package/src/ast/SecretScanner.js +304 -0
  20. package/src/healing/GitSandbox.js +82 -0
  21. package/src/healing/SelfHealer.js +52 -0
  22. package/src/index.js +723 -0
  23. package/src/performance/GraphCache.js +87 -0
  24. package/src/performance/SupplyChainGuard.js +92 -0
  25. package/src/performance/WorkerPool.js +109 -0
  26. package/src/plugins/BasePlugin.js +71 -0
  27. package/src/plugins/KnipAdapter.js +106 -0
  28. package/src/plugins/PluginRegistry.js +197 -0
  29. package/src/plugins/ecosystems/BackendServices.js +61 -0
  30. package/src/plugins/ecosystems/GenericPlugins.js +64 -0
  31. package/src/plugins/ecosystems/ModernFrameworks.js +159 -0
  32. package/src/plugins/ecosystems/MorePlugins.js +184 -0
  33. package/src/plugins/ecosystems/NextJsPlugin.js +33 -0
  34. package/src/plugins/ecosystems/PluginLoader.js +20 -0
  35. package/src/plugins/ecosystems/TypeScriptPlugin.js +56 -0
  36. package/src/refractor/ImpactAnalyzer.js +92 -0
  37. package/src/refractor/SourceRewriter.js +86 -0
  38. package/src/refractor/TransactionManager.js +138 -0
  39. package/src/refractor/TypeIntegrity.js +75 -0
  40. package/src/resolution/CircularDetector.js +91 -0
  41. package/src/resolution/ConfigLoader.js +87 -0
  42. package/src/resolution/DepencyResolver.js +133 -0
  43. package/src/resolution/DependencyProfiler.js +268 -0
  44. package/src/resolution/PathMapper.js +125 -0
  45. package/src/resolution/WorkSpaceGraph.js +474 -0
  46. package/tsconfig.json +26 -0
@@ -0,0 +1,337 @@
1
+ export class OxcAnalyzer {
2
+ constructor(context) {
3
+ this.context = context;
4
+ this.oxc = null;
5
+ this.isAvailable = false;
6
+ // Initialization is handled via init() or lazily during parseFile
7
+ }
8
+
9
+ async init() {
10
+ if (this.isAvailable) return true;
11
+ try {
12
+ // In ESM, we use dynamic import()
13
+ const oxc = await import("oxc-parser");
14
+ this.oxc = oxc;
15
+ this.isAvailable = true;
16
+ return true;
17
+ } catch (e) {
18
+ try {
19
+ // Fallback for older node versions or specific bundling setups
20
+ const { createRequire } = await import('module');
21
+ const require = createRequire(import.meta.url);
22
+ this.oxc = require("oxc-parser");
23
+ this.isAvailable = true;
24
+ return true;
25
+ } catch (err) {
26
+ this.isAvailable = false;
27
+ if (this.context.verbose) {
28
+ console.warn("[OxcAnalyzer] oxc-parser not found or failed to load, falling back to TypeScript compiler API.");
29
+ }
30
+ return false;
31
+ }
32
+ }
33
+ }
34
+
35
+ async parseFile(filePath, content, fileNode) {
36
+ if (!this.isAvailable) {
37
+ const initialized = await this.init();
38
+ if (!initialized) return false;
39
+ }
40
+
41
+ try {
42
+ if (this.context.verbose) {
43
+ console.log(`[OXC] Fast-parsing: ${filePath}`);
44
+ }
45
+
46
+ const ast = this.oxc.parseSync(content, {
47
+ sourceType: "module",
48
+ sourceFilename: filePath,
49
+ ecmaVersion: "latest",
50
+ });
51
+
52
+ // Initialize new properties for JSX and Decorator analysis
53
+ fileNode.jsxComponents = new Set();
54
+ fileNode.jsxProps = new Set();
55
+ fileNode.decorators = new Set();
56
+
57
+ this.walkOxcAst(ast.program, fileNode, content);
58
+
59
+ // Compute line/column for each export start position
60
+ const lines = content.split('\n');
61
+ const getLineCol = (pos) => {
62
+ let count = 0;
63
+ for (let i = 0; i < lines.length; i++) {
64
+ if (count + lines[i].length + 1 > pos) {
65
+ return { line: i + 1, column: pos - count + 1 };
66
+ }
67
+ count += lines[i].length + 1;
68
+ }
69
+ return { line: 1, column: 1 };
70
+ };
71
+
72
+ for (const [name, meta] of fileNode.internalExports.entries()) {
73
+ if (meta.start !== undefined) {
74
+ fileNode.symbolSourceLocations.set(name, getLineCol(meta.start));
75
+ }
76
+ }
77
+
78
+ return true;
79
+ } catch (e) {
80
+ if (this.context.verbose) {
81
+ console.warn(`[OXC] Failed to parse ${filePath}: ${e.message}`);
82
+ }
83
+ return false;
84
+ }
85
+ }
86
+
87
+ walkOxcAst(node, fileNode, content) {
88
+ if (!node) return;
89
+
90
+ switch (node.type) {
91
+ case "ImportDeclaration":
92
+ this.handleImportDeclaration(node, fileNode);
93
+ break;
94
+ case "ExportNamedDeclaration":
95
+ case "ExportDefaultDeclaration":
96
+ case "ExportAllDeclaration":
97
+ this.handleExportDeclaration(node, fileNode, content);
98
+ break;
99
+ case "CallExpression":
100
+ this.handleCallExpression(node, fileNode);
101
+ break;
102
+ case "JSXElement":
103
+ case "JSXFragment": // Consider fragments as well
104
+ this.handleJsxElement(node, fileNode);
105
+ break;
106
+ case "Decorator":
107
+ this.handleDecorator(node, fileNode);
108
+ break;
109
+ case "StringLiteral":
110
+ // Track for secret scanning if it looks like a secret
111
+ fileNode.rawStringReferences.add(node.value);
112
+ break;
113
+ }
114
+
115
+ // Traverse children
116
+ for (const key in node) {
117
+ if (node[key] && typeof node[key] === "object") {
118
+ if (Array.isArray(node[key])) {
119
+ node[key].forEach((child) => this.walkOxcAst(child, fileNode, content));
120
+ } else {
121
+ this.walkOxcAst(node[key], fileNode, content);
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ handleImportDeclaration(node, fileNode) {
128
+ const specifier = node.source.value;
129
+ fileNode.explicitImports.add(specifier);
130
+
131
+ // Track external package usage for dependency analysis
132
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
133
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
134
+ }
135
+
136
+ if (node.specifiers) {
137
+ node.specifiers.forEach((spec) => {
138
+ if (spec.type === "ImportSpecifier") {
139
+ const importedName = spec.imported.name;
140
+ fileNode.importedSymbols.add(`${specifier}:${importedName}`);
141
+ } else if (spec.type === "ImportDefaultSpecifier") {
142
+ fileNode.importedSymbols.add(`${specifier}:default`);
143
+ } else if (spec.type === "ImportNamespaceSpecifier") {
144
+ fileNode.importedSymbols.add(`${specifier}:*`);
145
+ }
146
+ });
147
+ }
148
+ }
149
+
150
+ handleExportDeclaration(node, fileNode, content) {
151
+ if (node.type === "ExportDefaultDeclaration") {
152
+ fileNode.internalExports.set("default", { type: "default", start: node.start, end: node.end });
153
+ return;
154
+ }
155
+
156
+ if (node.type === "ExportAllDeclaration") {
157
+ const sourceSpecifier = node.source ? node.source.value : null;
158
+ if (sourceSpecifier) {
159
+ // Register re-export source as an explicit import so the graph linker
160
+ // creates an incomingEdge on the re-exported file.
161
+ fileNode.explicitImports.add(sourceSpecifier);
162
+
163
+ // Track external package usage from re-exports
164
+ if (!sourceSpecifier.startsWith('.') && !sourceSpecifier.startsWith('/')) {
165
+ fileNode.externalPackageUsage.add(this._extractPackageName(sourceSpecifier));
166
+ }
167
+
168
+ if (node.exported) {
169
+ // export * as name from 'module'
170
+ const name = node.exported.name || (node.exported.type === "Identifier" ? node.exported.name : null);
171
+ if (name) {
172
+ fileNode.internalExports.set(name, { type: "re-export-namespace", source: sourceSpecifier, originalName: "*", start: node.start, end: node.end });
173
+ fileNode.importedSymbols.add(`${sourceSpecifier}:*`);
174
+ }
175
+ } else {
176
+ // export * from 'module'
177
+ fileNode.internalExports.set("*", { type: "re-export-all", source: sourceSpecifier });
178
+ // Register as wildcard importedSymbol so graph linker creates incomingEdge
179
+ fileNode.importedSymbols.add(`${sourceSpecifier}:*`);
180
+ }
181
+ }
182
+ return;
183
+ }
184
+
185
+ if (node.source) {
186
+ // Re-export with source: export { x } from 'module'
187
+ const specifier = node.source.value;
188
+ // Register re-export source as an explicit import
189
+ fileNode.explicitImports.add(specifier);
190
+
191
+ // Track external package usage from re-exports
192
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
193
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
194
+ }
195
+
196
+ if (node.specifiers) {
197
+ node.specifiers.forEach((spec) => {
198
+ const exportedName = spec.exported.name;
199
+ const localName = spec.local.name;
200
+ fileNode.internalExports.set(exportedName, {
201
+ type: "re-export",
202
+ source: specifier,
203
+ originalName: localName,
204
+ start: node.start,
205
+ end: node.end,
206
+ });
207
+ // Register as importedSymbol so barrel-tracer can resolve origin file
208
+ fileNode.importedSymbols.add(`${specifier}:${localName}`);
209
+ });
210
+ }
211
+ } else if (node.declaration) {
212
+ // Direct export
213
+ const decl = node.declaration;
214
+ if (decl.type === "VariableDeclaration") {
215
+ decl.declarations.forEach((d) => {
216
+ if (d.id.type === "Identifier") {
217
+ fileNode.internalExports.set(d.id.name, { type: "variable", start: d.start, end: d.end });
218
+ } else if (d.id.type === "ObjectPattern") {
219
+ d.id.properties.forEach((p) => {
220
+ if (p.type === "Property" && p.value.type === "Identifier") {
221
+ fileNode.internalExports.set(p.value.name, { type: "variable", start: p.start, end: p.end });
222
+ }
223
+ });
224
+ } else if (d.id.type === "ArrayPattern") {
225
+ d.id.elements.forEach((e) => {
226
+ if (e && e.type === "Identifier") {
227
+ fileNode.internalExports.set(e.name, { type: "variable", start: e.start, end: e.end });
228
+ }
229
+ });
230
+ }
231
+ });
232
+ } else if (decl.id && decl.id.name) {
233
+ let type = "unknown";
234
+ if (decl.type === "FunctionDeclaration") type = "function";
235
+ else if (decl.type === "ClassDeclaration") type = "class";
236
+ else if (decl.type === "TSEnumDeclaration") type = "enum";
237
+ else if (decl.type === "TSInterfaceDeclaration") type = "interface";
238
+ else if (decl.type === "TSTypeAliasDeclaration") type = "type";
239
+ else if (decl.type === "TSModuleDeclaration") type = "namespace";
240
+
241
+ const exportInfo = { type, start: decl.start, end: decl.end };
242
+ fileNode.internalExports.set(decl.id.name, exportInfo);
243
+
244
+ if (decl.type === "TSEnumDeclaration") {
245
+ exportInfo.members = decl.members.map((m) => m.id.name || (m.id.type === "Identifier" ? m.id.name : ""));
246
+ } else if (decl.type === "TSInterfaceDeclaration" || decl.type === "ClassDeclaration") {
247
+ exportInfo.members = decl.body.body.filter((m) => m.key && m.key.name).map((m) => m.key.name);
248
+ }
249
+ }
250
+ } else if (node.specifiers) {
251
+ node.specifiers.forEach((spec) => {
252
+ const exportedName = spec.exported.name;
253
+ const localName = spec.local.name;
254
+ fileNode.internalExports.set(exportedName, {
255
+ type: "export",
256
+ originalName: localName,
257
+ start: node.start,
258
+ end: node.end,
259
+ });
260
+ });
261
+ }
262
+ }
263
+
264
+ handleCallExpression(node, fileNode) {
265
+ // Dynamic import(): import('./module')
266
+ if (node.callee.type === "Import" && node.arguments.length > 0) {
267
+ const arg = node.arguments[0];
268
+ if (arg.type === "StringLiteral") {
269
+ const specifier = arg.value;
270
+ fileNode.explicitImports.add(specifier);
271
+ fileNode.dynamicImports.add(specifier);
272
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
273
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
274
+ }
275
+ } else {
276
+ // Non-literal dynamic import: record as calculated
277
+ if (fileNode.calculatedDynamicImports) {
278
+ fileNode.calculatedDynamicImports.push({ kind: arg.type, start: arg.start });
279
+ }
280
+ }
281
+ } else if (node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments.length > 0 && node.arguments[0].type === "StringLiteral") {
282
+ const specifier = node.arguments[0].value;
283
+ fileNode.explicitImports.add(specifier);
284
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
285
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
286
+ }
287
+ }
288
+ }
289
+
290
+ handleJsxElement(node, fileNode) {
291
+ const getElementName = (nameNode) => {
292
+ if (nameNode.type === "JSXIdentifier") return nameNode.name;
293
+ if (nameNode.type === "JSXMemberExpression") return `${getElementName(nameNode.object)}.${nameNode.property.name}`;
294
+ return "unknown";
295
+ };
296
+
297
+ if (node.openingElement) {
298
+ const tagName = getElementName(node.openingElement.name);
299
+ fileNode.jsxComponents.add(tagName);
300
+
301
+ if (node.openingElement.attributes) {
302
+ node.openingElement.attributes.forEach(attr => {
303
+ if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier") {
304
+ fileNode.jsxProps.add(`${tagName}:${attr.name.name}`);
305
+ }
306
+ });
307
+ }
308
+ }
309
+ }
310
+
311
+ handleDecorator(node, fileNode) {
312
+ const getDecoratorName = (expr) => {
313
+ if (expr.type === "Identifier") return expr.name;
314
+ if (expr.type === "CallExpression" && expr.callee.type === "Identifier") return expr.callee.name;
315
+ if (expr.type === "CallExpression" && expr.callee.type === "MemberExpression" && expr.callee.property.type === "Identifier") return expr.callee.property.name;
316
+ return "unknown";
317
+ };
318
+
319
+ const decoratorName = getDecoratorName(node.expression);
320
+ fileNode.decorators.add(decoratorName);
321
+
322
+ // Optionally, extract decorator arguments
323
+ if (node.expression.type === "CallExpression") {
324
+ node.expression.arguments.forEach(arg => {
325
+ // Further analysis of arguments can be done here if needed
326
+ });
327
+ }
328
+ }
329
+
330
+ _extractPackageName(specifier) {
331
+ if (specifier.startsWith('@')) {
332
+ const parts = specifier.split('/');
333
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
334
+ }
335
+ return specifier.split('/')[0];
336
+ }
337
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * ============================================================================
3
+ * 🔐 SecretScanner – Hardcoded Credential & API Key Detector
4
+ * ============================================================================
5
+ * Scans source files for hardcoded secrets such as API keys, tokens,
6
+ * passwords, and other sensitive credentials using heuristic pattern matching
7
+ * on both variable names and string literal values.
8
+ *
9
+ * New in v3.3.8: integrated into the main analysis pipeline so that secrets
10
+ * are surfaced alongside dead-code and unused-dependency findings.
11
+ */
12
+
13
+ /**
14
+ * Severity levels for detected secrets.
15
+ */
16
+ export const SecretSeverity = {
17
+ CRITICAL: 'CRITICAL',
18
+ HIGH: 'HIGH',
19
+ MEDIUM: 'MEDIUM',
20
+ };
21
+
22
+ /**
23
+ * Patterns that flag a variable/property *name* as likely containing a secret.
24
+ * Matched case-insensitively against the identifier text.
25
+ */
26
+ const SENSITIVE_NAME_PATTERNS = [
27
+ // Generic credential names
28
+ /api[_-]?key/i,
29
+ /apikey/i,
30
+ /api[_-]?secret/i,
31
+ /access[_-]?token/i,
32
+ /auth[_-]?token/i,
33
+ /bearer[_-]?token/i,
34
+ /secret[_-]?key/i,
35
+ /private[_-]?key/i,
36
+ /client[_-]?secret/i,
37
+ /app[_-]?secret/i,
38
+ // Database credentials
39
+ /db[_-]?pass(word)?/i,
40
+ /database[_-]?pass(word)?/i,
41
+ /db[_-]?url/i,
42
+ /database[_-]?url/i,
43
+ /connection[_-]?string/i,
44
+ // Passwords
45
+ /^password$/i,
46
+ /^passwd$/i,
47
+ /^pwd$/i,
48
+ /[_-]password$/i,
49
+ // Tokens
50
+ /[_-]token$/i,
51
+ /^token$/i,
52
+ /jwt[_-]?secret/i,
53
+ /session[_-]?secret/i,
54
+ /cookie[_-]?secret/i,
55
+ // Cloud provider keys
56
+ /aws[_-]?(access[_-]?key|secret|session[_-]?token)/i,
57
+ /gcp[_-]?key/i,
58
+ /azure[_-]?(key|secret|connection)/i,
59
+ // Service-specific
60
+ /stripe[_-]?(key|secret)/i,
61
+ /twilio[_-]?(auth|token|sid)/i,
62
+ /sendgrid[_-]?key/i,
63
+ /github[_-]?token/i,
64
+ /slack[_-]?(token|webhook)/i,
65
+ /discord[_-]?(token|secret)/i,
66
+ /openai[_-]?(key|token)/i,
67
+ /anthropic[_-]?key/i,
68
+ /webhook[_-]?(url|secret)/i,
69
+ /encryption[_-]?key/i,
70
+ /signing[_-]?key/i,
71
+ /hmac[_-]?key/i,
72
+ /salt$/i,
73
+ ];
74
+
75
+ /**
76
+ * Patterns that flag a *string literal value* as likely being a secret.
77
+ * These are matched against the raw string content.
78
+ */
79
+ const SENSITIVE_VALUE_PATTERNS = [
80
+ // AWS Access Key IDs
81
+ { pattern: /AKIA[0-9A-Z]{16}/, label: 'AWS_ACCESS_KEY_ID', severity: SecretSeverity.CRITICAL },
82
+ // AWS Secret Access Keys (40-char base64-ish)
83
+ { pattern: /[A-Za-z0-9/+=]{40}/, label: 'AWS_SECRET_KEY_CANDIDATE', severity: SecretSeverity.MEDIUM },
84
+ // Generic high-entropy hex strings (32+ chars)
85
+ { pattern: /^[0-9a-f]{32,}$/i, label: 'HEX_SECRET', severity: SecretSeverity.HIGH },
86
+ // Generic high-entropy base64 strings (32+ chars)
87
+ { pattern: /^[A-Za-z0-9+/]{32,}={0,2}$/, label: 'BASE64_SECRET', severity: SecretSeverity.MEDIUM },
88
+ // JWT tokens
89
+ { pattern: /^ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, label: 'JWT_TOKEN', severity: SecretSeverity.CRITICAL },
90
+ // GitHub personal access tokens
91
+ { pattern: /ghp_[A-Za-z0-9]{36}/, label: 'GITHUB_PAT', severity: SecretSeverity.CRITICAL },
92
+ { pattern: /github_pat_[A-Za-z0-9_]{82}/, label: 'GITHUB_PAT_FINE', severity: SecretSeverity.CRITICAL },
93
+ // Stripe keys
94
+ { pattern: /sk_(live|test)_[A-Za-z0-9]{24,}/, label: 'STRIPE_SECRET_KEY', severity: SecretSeverity.CRITICAL },
95
+ { pattern: /pk_(live|test)_[A-Za-z0-9]{24,}/, label: 'STRIPE_PUBLIC_KEY', severity: SecretSeverity.HIGH },
96
+ // Slack tokens
97
+ { pattern: /xox[baprs]-[A-Za-z0-9-]{10,}/, label: 'SLACK_TOKEN', severity: SecretSeverity.CRITICAL },
98
+ // Discord tokens
99
+ { pattern: /[MN][A-Za-z0-9]{23}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27}/, label: 'DISCORD_TOKEN', severity: SecretSeverity.CRITICAL },
100
+ // Twilio SID
101
+ { pattern: /AC[a-f0-9]{32}/, label: 'TWILIO_SID', severity: SecretSeverity.HIGH },
102
+ // SendGrid API key
103
+ { pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/, label: 'SENDGRID_KEY', severity: SecretSeverity.CRITICAL },
104
+ // OpenAI API key
105
+ { pattern: /sk-[A-Za-z0-9]{32,}/, label: 'OPENAI_KEY', severity: SecretSeverity.CRITICAL },
106
+ // Generic UUID-like tokens that look like secrets (not just any UUID)
107
+ { 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 },
108
+ ];
109
+
110
+ /**
111
+ * Minimum length a string value must have to be considered a potential secret.
112
+ * Short strings like "test", "dev", "localhost" are excluded.
113
+ */
114
+ const MIN_SECRET_VALUE_LENGTH = 8;
115
+
116
+ /**
117
+ * Values that are obviously not secrets (common placeholders / env-var references).
118
+ */
119
+ const SAFE_VALUE_ALLOWLIST = new Set([
120
+ 'your_api_key_here',
121
+ 'your-api-key',
122
+ 'YOUR_API_KEY',
123
+ 'YOUR_SECRET',
124
+ 'REPLACE_ME',
125
+ 'changeme',
126
+ 'placeholder',
127
+ 'example',
128
+ 'test',
129
+ 'demo',
130
+ 'localhost',
131
+ '127.0.0.1',
132
+ 'process.env',
133
+ '',
134
+ ]);
135
+
136
+ /**
137
+ * Checks whether a string value looks like an environment-variable reference
138
+ * (e.g. `process.env.SECRET` or `import.meta.env.SECRET`).
139
+ */
140
+ function isEnvReference(value) {
141
+ return (
142
+ value.startsWith('process.env') ||
143
+ value.startsWith('import.meta.env') ||
144
+ value.startsWith('${') ||
145
+ /^[A-Z_][A-Z0-9_]*$/.test(value) // ALL_CAPS env-var placeholder
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Determines whether a string value is high-entropy enough to be a real secret.
151
+ * Uses Shannon entropy as a heuristic.
152
+ */
153
+ function shannonEntropy(str) {
154
+ const freq = {};
155
+ for (const ch of str) freq[ch] = (freq[ch] || 0) + 1;
156
+ let entropy = 0;
157
+ for (const count of Object.values(freq)) {
158
+ const p = count / str.length;
159
+ entropy -= p * Math.log2(p);
160
+ }
161
+ return entropy;
162
+ }
163
+
164
+ /**
165
+ * Main scanner class. Operates on pre-parsed AST nodes produced by either
166
+ * ASTAnalyzer (TypeScript compiler API) or OxcAnalyzer (oxc-parser).
167
+ */
168
+ export class SecretScanner {
169
+ /**
170
+ * Scans a source file's text for hardcoded secrets.
171
+ *
172
+ * @param {string} filePath - Absolute path to the file being scanned.
173
+ * @param {string} fileContent - Raw source text of the file.
174
+ * @returns {Array<SecretFinding>} - Array of detected secret findings.
175
+ */
176
+ scanFileContent(filePath, fileContent) {
177
+ const findings = [];
178
+ const lines = fileContent.split('\n');
179
+
180
+ lines.forEach((line, lineIndex) => {
181
+ const lineNumber = lineIndex + 1;
182
+
183
+ // Skip comment-only lines and import statements
184
+ const trimmed = line.trim();
185
+ if (
186
+ trimmed.startsWith('//') ||
187
+ trimmed.startsWith('*') ||
188
+ trimmed.startsWith('/*') ||
189
+ trimmed.startsWith('import ') ||
190
+ trimmed.startsWith('export { ')
191
+ ) {
192
+ return;
193
+ }
194
+
195
+ // ── Strategy 1: Name-based heuristic ──────────────────────────────────
196
+ // Look for variable/property assignments where the name matches a
197
+ // sensitive pattern and the value is a non-trivial string literal.
198
+ const assignmentMatch = line.match(
199
+ /(?:const|let|var|readonly)?\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*[=:]\s*["'`]([^"'`]+)["'`]/
200
+ );
201
+ if (assignmentMatch) {
202
+ const [, varName, value] = assignmentMatch;
203
+ if (
204
+ value.length >= MIN_SECRET_VALUE_LENGTH &&
205
+ !SAFE_VALUE_ALLOWLIST.has(value) &&
206
+ !isEnvReference(value) &&
207
+ SENSITIVE_NAME_PATTERNS.some(p => p.test(varName))
208
+ ) {
209
+ findings.push({
210
+ file: filePath,
211
+ line: lineNumber,
212
+ column: line.indexOf(value),
213
+ variableName: varName,
214
+ valueSnippet: this._redact(value),
215
+ label: this._labelFromName(varName),
216
+ severity: SecretSeverity.CRITICAL,
217
+ });
218
+ return; // Don't double-report the same line
219
+ }
220
+ }
221
+
222
+ // ── Strategy 2: Value-pattern matching ────────────────────────────────
223
+ // Extract all string literals on this line and test them against known
224
+ // secret value patterns.
225
+ const stringLiterals = [...line.matchAll(/["'`]([^"'`\n]{8,})["'`]/g)];
226
+ for (const match of stringLiterals) {
227
+ const value = match[1];
228
+ if (SAFE_VALUE_ALLOWLIST.has(value) || isEnvReference(value)) continue;
229
+
230
+ for (const { pattern, label, severity } of SENSITIVE_VALUE_PATTERNS) {
231
+ if (pattern.test(value)) {
232
+ // Avoid duplicate findings for the same position
233
+ const alreadyReported = findings.some(
234
+ f => f.line === lineNumber && f.valueSnippet === this._redact(value)
235
+ );
236
+ if (!alreadyReported) {
237
+ findings.push({
238
+ file: filePath,
239
+ line: lineNumber,
240
+ column: line.indexOf(match[0]),
241
+ variableName: null,
242
+ valueSnippet: this._redact(value),
243
+ label,
244
+ severity,
245
+ });
246
+ }
247
+ break;
248
+ }
249
+ }
250
+ }
251
+
252
+ // ── Strategy 3: High-entropy string heuristic ─────────────────────────
253
+ // Any string literal with Shannon entropy > 4.5 and length >= 20 that
254
+ // is assigned to a sensitive-named variable is flagged.
255
+ if (assignmentMatch) {
256
+ const [, varName, value] = assignmentMatch;
257
+ if (
258
+ value.length >= 20 &&
259
+ shannonEntropy(value) > 4.5 &&
260
+ !SAFE_VALUE_ALLOWLIST.has(value) &&
261
+ !isEnvReference(value) &&
262
+ SENSITIVE_NAME_PATTERNS.some(p => p.test(varName))
263
+ ) {
264
+ const alreadyReported = findings.some(f => f.line === lineNumber);
265
+ if (!alreadyReported) {
266
+ findings.push({
267
+ file: filePath,
268
+ line: lineNumber,
269
+ column: line.indexOf(value),
270
+ variableName: varName,
271
+ valueSnippet: this._redact(value),
272
+ label: 'HIGH_ENTROPY_SECRET',
273
+ severity: SecretSeverity.HIGH,
274
+ });
275
+ }
276
+ }
277
+ }
278
+ });
279
+
280
+ return findings;
281
+ }
282
+
283
+ /**
284
+ * Redacts a secret value for safe display in reports.
285
+ * Shows the first 4 characters followed by asterisks.
286
+ */
287
+ _redact(value) {
288
+ if (value.length <= 4) return '****';
289
+ return value.slice(0, 4) + '*'.repeat(Math.min(value.length - 4, 8));
290
+ }
291
+
292
+ /**
293
+ * Derives a human-readable label from a variable name.
294
+ */
295
+ _labelFromName(name) {
296
+ const upper = name.toUpperCase();
297
+ if (/API[_-]?KEY/.test(upper)) return 'API_KEY';
298
+ if (/TOKEN/.test(upper)) return 'AUTH_TOKEN';
299
+ if (/PASSWORD|PASSWD|PWD/.test(upper)) return 'PASSWORD';
300
+ if (/SECRET/.test(upper)) return 'SECRET';
301
+ if (/DATABASE|DB/.test(upper)) return 'DATABASE_CREDENTIAL';
302
+ return 'SENSITIVE_VALUE';
303
+ }
304
+ }