entkapp 5.5.0 ā 5.6.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 -1
- package/bin/cli.js +2 -2
- package/bin/cli.mjs +175 -0
- package/index.cjs +18 -0
- package/index.mjs +51 -0
- package/package.json +7 -6
- package/src/EngineContext.mjs +428 -0
- package/src/Initializer.mjs +82 -0
- package/src/analyzers/CodeSmellAnalyzer.mjs +106 -0
- package/src/analyzers/OxcAnalyzer.mjs +11 -0
- package/src/api/HeadlessAPI.js +31 -16
- package/src/api/HeadlessAPI.mjs +369 -0
- package/src/api/PluginSDK.mjs +135 -0
- package/src/ast/ASTAnalyzer.js +17 -3
- package/src/ast/ASTAnalyzer.mjs +742 -0
- package/src/ast/AdvancedAnalysis.mjs +586 -0
- package/src/ast/BarrelParser.mjs +230 -0
- package/src/ast/DeadCodeDetector.js +7 -5
- package/src/ast/DeadCodeDetector.mjs +92 -0
- package/src/ast/MagicDetector.mjs +203 -0
- package/src/ast/OxcAnalyzer.mjs +188 -0
- package/src/ast/SecretScanner.mjs +374 -0
- package/src/healing/GitSandbox.mjs +82 -0
- package/src/healing/SelfHealer.mjs +48 -0
- package/src/index.js +37 -1
- package/src/index.mjs +1176 -0
- package/src/performance/GraphCache.mjs +108 -0
- package/src/performance/SupplyChainGuard.mjs +92 -0
- package/src/performance/WorkerPool.mjs +132 -0
- package/src/performance/WorkerTaskRunner.mjs +144 -0
- package/src/plugins/BasePlugin.mjs +240 -0
- package/src/plugins/PluginRegistry.mjs +203 -0
- package/src/plugins/ecosystems/BackendServices.mjs +197 -0
- package/src/plugins/ecosystems/GenericPlugins.mjs +142 -0
- package/src/plugins/ecosystems/ModernFrameworks.mjs +162 -0
- package/src/plugins/ecosystems/MorePlugins.mjs +562 -0
- package/src/plugins/ecosystems/NewPlugins.mjs +526 -0
- package/src/plugins/ecosystems/NextJsPlugin.mjs +45 -0
- package/src/plugins/ecosystems/PluginLoader.mjs +193 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.mjs +56 -0
- package/src/plugins/ecosystems/UltimateBundle.mjs +1182 -0
- package/src/refractor/ImpactAnalyzer.mjs +92 -0
- package/src/refractor/SourceRewriter.mjs +86 -0
- package/src/refractor/TransactionManager.mjs +5 -0
- package/src/refractor/TypeIntegrity.mjs +4 -0
- package/src/resolution/BuildOrchestrator.mjs +46 -0
- package/src/resolution/CircularDetector.mjs +91 -0
- package/src/resolution/ConfigGenerator.mjs +83 -0
- package/src/resolution/ConfigLoader.mjs +94 -0
- package/src/resolution/DepencyResolver.mjs +66 -0
- package/src/resolution/DependencyFixer.mjs +88 -0
- package/src/resolution/DependencyProfiler.mjs +286 -0
- package/src/resolution/EntryPointDetector.mjs +134 -0
- package/src/resolution/FrameworkConfigParser.mjs +119 -0
- package/src/resolution/GraphAnalyzer.mjs +80 -0
- package/src/resolution/MigrationAnalyzer.mjs +60 -0
- package/src/resolution/PathMapper.mjs +82 -0
- package/src/resolution/TSConfigLoader.mjs +162 -0
- package/src/resolution/WorkSpaceGraph.mjs +183 -0
- package/src/resolution/WorkspaceDiagnostic.mjs +139 -0
- package/test.js +76 -0
- package/wrangler.toml +6 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Production-Grade Native Rust AST Parser Bridge (OXC)
|
|
5
|
+
* Fixed for Windows environments to prevent path-based "Unexpected token" errors.
|
|
6
|
+
*/
|
|
7
|
+
export class OxcAnalyzer {
|
|
8
|
+
constructor(context) {
|
|
9
|
+
this.context = context;
|
|
10
|
+
this.oxc = null;
|
|
11
|
+
this.isAvailable = false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async init() {
|
|
15
|
+
if (this.context.usedExternalPackages) this.context.usedExternalPackages.add("oxc-parser");
|
|
16
|
+
if (this.isAvailable) return true;
|
|
17
|
+
try {
|
|
18
|
+
const oxc = await import("oxc-parser");
|
|
19
|
+
this.oxc = oxc;
|
|
20
|
+
this.isAvailable = true;
|
|
21
|
+
return true;
|
|
22
|
+
} catch (e) {
|
|
23
|
+
try {
|
|
24
|
+
const { createRequire } = await import('module');
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
this.oxc = require("oxc-parser");
|
|
27
|
+
this.isAvailable = true;
|
|
28
|
+
return true;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
this.isAvailable = false;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* WINDOWS FIX: Robust path normalization for OXC.
|
|
38
|
+
* Replaces backslashes with forward slashes to prevent escape sequence errors.
|
|
39
|
+
*/
|
|
40
|
+
normalizePath(filePath) {
|
|
41
|
+
if (!filePath) return filePath;
|
|
42
|
+
let normalized = filePath.replace(/\\/g, '/');
|
|
43
|
+
if (/^[a-z]:\//i.test(normalized)) {
|
|
44
|
+
normalized = normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
45
|
+
}
|
|
46
|
+
return normalized.replace(/\/+/g, '/');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async parseFile(filePath, content, fileNode) {
|
|
50
|
+
if (!this.isAvailable) {
|
|
51
|
+
const initialized = await this.init();
|
|
52
|
+
if (!initialized) return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const cleanContent = content.startsWith('\uFEFF') ? content.slice(1) : content;
|
|
57
|
+
const normalizedPath = this.normalizePath(filePath);
|
|
58
|
+
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = this.oxc.parseSync(normalizedPath, cleanContent, {
|
|
62
|
+
sourceType: "module",
|
|
63
|
+
sourceFilename: normalizedPath,
|
|
64
|
+
lang: "typescript"
|
|
65
|
+
});
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// Fallback with normalized path if the options object fails
|
|
68
|
+
try {
|
|
69
|
+
result = this.oxc.parseSync(normalizedPath, cleanContent);
|
|
70
|
+
} catch (innerErr) {
|
|
71
|
+
if (this.context.verbose) {
|
|
72
|
+
console.error(`[OXC-ERROR] Native parse failed for: ${normalizedPath}`);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let parsedResult;
|
|
79
|
+
try {
|
|
80
|
+
parsedResult = typeof result === 'string' ? JSON.parse(result) : JSON.parse(JSON.stringify(result));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (result && typeof result === 'object') {
|
|
83
|
+
parsedResult = result;
|
|
84
|
+
} else {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let programRoot = null;
|
|
90
|
+
if (parsedResult && typeof parsedResult === 'object') {
|
|
91
|
+
if (parsedResult.program) programRoot = parsedResult.program;
|
|
92
|
+
else if (parsedResult.ast) programRoot = parsedResult.ast;
|
|
93
|
+
else if (parsedResult.type === 'Program' || parsedResult.body) programRoot = parsedResult;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- VALIDATION CHECK ---
|
|
97
|
+
// If the file has content but OXC returns an empty body, it's a silent failure.
|
|
98
|
+
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) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fileNode.ast = programRoot;
|
|
110
|
+
fileNode.symbolTable = new Map();
|
|
111
|
+
|
|
112
|
+
this.walkOxcAst(programRoot, fileNode, cleanContent, 1);
|
|
113
|
+
this.walkOxcAst(programRoot, fileNode, cleanContent, 2);
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
walkOxcAst(node, fileNode, content, pass) {
|
|
122
|
+
if (!node) return;
|
|
123
|
+
|
|
124
|
+
// Pass 1: Handle Imports and Exports
|
|
125
|
+
if (pass === 1) {
|
|
126
|
+
if (node.type === 'ImportDeclaration') {
|
|
127
|
+
const specifier = node.source.value;
|
|
128
|
+
fileNode.explicitImports.add(specifier);
|
|
129
|
+
} else if (node.type === 'ExportNamedDeclaration') {
|
|
130
|
+
if (node.source) {
|
|
131
|
+
fileNode.explicitImports.add(node.source.value);
|
|
132
|
+
}
|
|
133
|
+
if (node.declaration) {
|
|
134
|
+
this.extractExportFromDeclaration(node.declaration, fileNode);
|
|
135
|
+
}
|
|
136
|
+
if (node.specifiers) {
|
|
137
|
+
node.specifiers.forEach(spec => {
|
|
138
|
+
fileNode.internalExports.set(spec.exported.name || spec.exported.value, { type: 'export' });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} else if (node.type === 'ExportDefaultDeclaration') {
|
|
142
|
+
fileNode.internalExports.set('default', { type: 'default' });
|
|
143
|
+
} else if (node.type === 'ExportAllDeclaration') {
|
|
144
|
+
fileNode.explicitImports.add(node.source.value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Pass 2: Handle Identifiers and Calls
|
|
148
|
+
else if (pass === 2) {
|
|
149
|
+
if (node.type === 'IdentifierReference' || node.type === 'Identifier') {
|
|
150
|
+
fileNode.instantiatedIdentifiers.add(node.name);
|
|
151
|
+
} else if (node.type === 'CallExpression') {
|
|
152
|
+
if (node.callee.type === 'Import') {
|
|
153
|
+
const arg = node.arguments[0];
|
|
154
|
+
if (arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')) {
|
|
155
|
+
fileNode.dynamicImports.add(arg.value);
|
|
156
|
+
}
|
|
157
|
+
} else if (node.callee.name === 'require') {
|
|
158
|
+
const arg = node.arguments[0];
|
|
159
|
+
if (arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')) {
|
|
160
|
+
fileNode.explicitImports.add(arg.value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Recursive traversal
|
|
167
|
+
for (const key in node) {
|
|
168
|
+
const child = node[key];
|
|
169
|
+
if (Array.isArray(child)) {
|
|
170
|
+
child.forEach(c => c && typeof c === 'object' && this.walkOxcAst(c, fileNode, content, pass));
|
|
171
|
+
} else if (child && typeof child === 'object') {
|
|
172
|
+
this.walkOxcAst(child, fileNode, content, pass);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
extractExportFromDeclaration(decl, fileNode) {
|
|
178
|
+
if (decl.type === 'VariableDeclaration') {
|
|
179
|
+
decl.declarations.forEach(d => {
|
|
180
|
+
if (d.id.type === 'Identifier') {
|
|
181
|
+
fileNode.internalExports.set(d.id.name, { type: 'variable' });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
} else if (decl.id && decl.id.name) {
|
|
185
|
+
fileNode.internalExports.set(decl.id.name, { type: decl.type.toLowerCase() });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
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
|
+
/security[_-]?token/i,
|
|
39
|
+
/master[_-]?key/i,
|
|
40
|
+
/root[_-]?password/i,
|
|
41
|
+
|
|
42
|
+
// Database credentials
|
|
43
|
+
/db[_-]?pass(word)?/i,
|
|
44
|
+
/database[_-]?pass(word)?/i,
|
|
45
|
+
/db[_-]?url/i,
|
|
46
|
+
/database[_-]?url/i,
|
|
47
|
+
/connection[_-]?string/i,
|
|
48
|
+
/postgres(ql)?[_-]?url/i,
|
|
49
|
+
/mongo(db)?[_-]?url/i,
|
|
50
|
+
/redis[_-]?url/i,
|
|
51
|
+
|
|
52
|
+
// Passwords & Pins
|
|
53
|
+
/^password$/i,
|
|
54
|
+
/^passwd$/i,
|
|
55
|
+
/^pwd$/i,
|
|
56
|
+
/[_-]password$/i,
|
|
57
|
+
/[_-]pass$/i,
|
|
58
|
+
/pass(phrase|code)/i,
|
|
59
|
+
/admin[_-]?pass/i,
|
|
60
|
+
/pin[_-]?number/i,
|
|
61
|
+
|
|
62
|
+
// Tokens & Sessions
|
|
63
|
+
/[_-]token$/i,
|
|
64
|
+
/^token$/i,
|
|
65
|
+
/jwt[_-]?(secret|token)/i,
|
|
66
|
+
/session[_-]?(secret|token|id)/i,
|
|
67
|
+
/cookie[_-]?secret/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,
|
|
80
|
+
/anthropic[_-]?key/i,
|
|
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,
|
|
110
|
+
/signing[_-]?key/i,
|
|
111
|
+
/hmac[_-]?(key|secret)/i,
|
|
112
|
+
/salt$/i,
|
|
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,
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Patterns that flag a *string literal value* as likely being a secret.
|
|
125
|
+
* These are matched against the raw string content.
|
|
126
|
+
*/
|
|
127
|
+
const SENSITIVE_VALUE_PATTERNS = [
|
|
128
|
+
// AWS Access Key IDs
|
|
129
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, label: 'AWS_ACCESS_KEY_ID', severity: SecretSeverity.CRITICAL },
|
|
130
|
+
// AWS Secret Access Keys (40-char base64-ish)
|
|
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
|
+
|
|
138
|
+
// JWT tokens
|
|
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
|
+
|
|
146
|
+
// Stripe keys
|
|
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
|
+
|
|
162
|
+
// SendGrid API key
|
|
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 },
|
|
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
|
+
];
|
|
180
|
+
/**
|
|
181
|
+
* Minimum length a string value must have to be considered a potential secret.
|
|
182
|
+
* Short strings like "test", "dev", "localhost" are excluded.
|
|
183
|
+
*/
|
|
184
|
+
const MIN_SECRET_VALUE_LENGTH = 8;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Values that are obviously not secrets (common placeholders / env-var references).
|
|
188
|
+
*/
|
|
189
|
+
const SAFE_VALUE_ALLOWLIST = new Set([
|
|
190
|
+
'your_api_key_here',
|
|
191
|
+
'your-api-key',
|
|
192
|
+
'YOUR_API_KEY',
|
|
193
|
+
'YOUR_SECRET',
|
|
194
|
+
'REPLACE_ME',
|
|
195
|
+
'changeme',
|
|
196
|
+
'placeholder',
|
|
197
|
+
'example',
|
|
198
|
+
'test',
|
|
199
|
+
'demo',
|
|
200
|
+
'localhost',
|
|
201
|
+
'127.0.0.1',
|
|
202
|
+
'process.env',
|
|
203
|
+
'',
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Checks whether a string value looks like an environment-variable reference
|
|
208
|
+
* (e.g. `process.env.SECRET` or `import.meta.env.SECRET`).
|
|
209
|
+
*/
|
|
210
|
+
function isEnvReference(value) {
|
|
211
|
+
return (
|
|
212
|
+
value.startsWith('process.env') ||
|
|
213
|
+
value.startsWith('import.meta.env') ||
|
|
214
|
+
value.startsWith('${') ||
|
|
215
|
+
/^[A-Z_][A-Z0-9_]*$/.test(value) // ALL_CAPS env-var placeholder
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Determines whether a string value is high-entropy enough to be a real secret.
|
|
221
|
+
* Uses Shannon entropy as a heuristic.
|
|
222
|
+
*/
|
|
223
|
+
function shannonEntropy(str) {
|
|
224
|
+
const freq = {};
|
|
225
|
+
for (const ch of str) freq[ch] = (freq[ch] || 0) + 1;
|
|
226
|
+
let entropy = 0;
|
|
227
|
+
for (const count of Object.values(freq)) {
|
|
228
|
+
const p = count / str.length;
|
|
229
|
+
entropy -= p * Math.log2(p);
|
|
230
|
+
}
|
|
231
|
+
return entropy;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Main scanner class. Operates on pre-parsed AST nodes produced by either
|
|
236
|
+
* ASTAnalyzer (TypeScript compiler API) or OxcAnalyzer (oxc-parser).
|
|
237
|
+
*/
|
|
238
|
+
export class SecretScanner {
|
|
239
|
+
/**
|
|
240
|
+
* Scans a source file's text for hardcoded secrets.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} filePath - Absolute path to the file being scanned.
|
|
243
|
+
* @param {string} fileContent - Raw source text of the file.
|
|
244
|
+
* @returns {Array<SecretFinding>} - Array of detected secret findings.
|
|
245
|
+
*/
|
|
246
|
+
scanFileContent(filePath, fileContent) {
|
|
247
|
+
const findings = [];
|
|
248
|
+
const lines = fileContent.split('\n');
|
|
249
|
+
|
|
250
|
+
lines.forEach((line, lineIndex) => {
|
|
251
|
+
const lineNumber = lineIndex + 1;
|
|
252
|
+
|
|
253
|
+
// Skip comment-only lines and import statements
|
|
254
|
+
const trimmed = line.trim();
|
|
255
|
+
if (
|
|
256
|
+
trimmed.startsWith('//') ||
|
|
257
|
+
trimmed.startsWith('*') ||
|
|
258
|
+
trimmed.startsWith('/*') ||
|
|
259
|
+
trimmed.startsWith('import ') ||
|
|
260
|
+
trimmed.startsWith('export { ')
|
|
261
|
+
) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// āā Strategy 1: Name-based heuristic āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
266
|
+
// Look for variable/property assignments where the name matches a
|
|
267
|
+
// sensitive pattern and the value is a non-trivial string literal.
|
|
268
|
+
const assignmentMatch = line.match(
|
|
269
|
+
/(?:const|let|var|readonly)?\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*[=:]\s*["'`]([^"'`]+)["'`]/
|
|
270
|
+
);
|
|
271
|
+
if (assignmentMatch) {
|
|
272
|
+
const [, varName, value] = assignmentMatch;
|
|
273
|
+
if (
|
|
274
|
+
value.length >= MIN_SECRET_VALUE_LENGTH &&
|
|
275
|
+
!SAFE_VALUE_ALLOWLIST.has(value) &&
|
|
276
|
+
!isEnvReference(value) &&
|
|
277
|
+
SENSITIVE_NAME_PATTERNS.some(p => p.test(varName))
|
|
278
|
+
) {
|
|
279
|
+
findings.push({
|
|
280
|
+
file: filePath,
|
|
281
|
+
line: lineNumber,
|
|
282
|
+
column: line.indexOf(value),
|
|
283
|
+
variableName: varName,
|
|
284
|
+
valueSnippet: this._redact(value),
|
|
285
|
+
label: this._labelFromName(varName),
|
|
286
|
+
severity: SecretSeverity.CRITICAL,
|
|
287
|
+
});
|
|
288
|
+
return; // Don't double-report the same line
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// āā Strategy 2: Value-pattern matching āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
293
|
+
// Extract all string literals on this line and test them against known
|
|
294
|
+
// secret value patterns.
|
|
295
|
+
const stringLiterals = [...line.matchAll(/["'`]([^"'`\n]{8,})["'`]/g)];
|
|
296
|
+
for (const match of stringLiterals) {
|
|
297
|
+
const value = match[1];
|
|
298
|
+
if (SAFE_VALUE_ALLOWLIST.has(value) || isEnvReference(value)) continue;
|
|
299
|
+
|
|
300
|
+
for (const { pattern, label, severity } of SENSITIVE_VALUE_PATTERNS) {
|
|
301
|
+
if (pattern.test(value)) {
|
|
302
|
+
// Avoid duplicate findings for the same position
|
|
303
|
+
const alreadyReported = findings.some(
|
|
304
|
+
f => f.line === lineNumber && f.valueSnippet === this._redact(value)
|
|
305
|
+
);
|
|
306
|
+
if (!alreadyReported) {
|
|
307
|
+
findings.push({
|
|
308
|
+
file: filePath,
|
|
309
|
+
line: lineNumber,
|
|
310
|
+
column: line.indexOf(match[0]),
|
|
311
|
+
variableName: null,
|
|
312
|
+
valueSnippet: this._redact(value),
|
|
313
|
+
label,
|
|
314
|
+
severity,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// āā Strategy 3: High-entropy string heuristic āāāāāāāāāāāāāāāāāāāāāāāāā
|
|
323
|
+
// Any string literal with Shannon entropy > 4.5 and length >= 20 that
|
|
324
|
+
// is assigned to a sensitive-named variable is flagged.
|
|
325
|
+
if (assignmentMatch) {
|
|
326
|
+
const [, varName, value] = assignmentMatch;
|
|
327
|
+
if (
|
|
328
|
+
value.length >= 20 &&
|
|
329
|
+
shannonEntropy(value) > 4.5 &&
|
|
330
|
+
!SAFE_VALUE_ALLOWLIST.has(value) &&
|
|
331
|
+
!isEnvReference(value) &&
|
|
332
|
+
SENSITIVE_NAME_PATTERNS.some(p => p.test(varName))
|
|
333
|
+
) {
|
|
334
|
+
const alreadyReported = findings.some(f => f.line === lineNumber);
|
|
335
|
+
if (!alreadyReported) {
|
|
336
|
+
findings.push({
|
|
337
|
+
file: filePath,
|
|
338
|
+
line: lineNumber,
|
|
339
|
+
column: line.indexOf(value),
|
|
340
|
+
variableName: varName,
|
|
341
|
+
valueSnippet: this._redact(value),
|
|
342
|
+
label: 'HIGH_ENTROPY_SECRET',
|
|
343
|
+
severity: SecretSeverity.HIGH,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return findings;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Redacts a secret value for safe display in reports.
|
|
355
|
+
* Shows the first 4 characters followed by asterisks.
|
|
356
|
+
*/
|
|
357
|
+
_redact(value) {
|
|
358
|
+
if (value.length <= 4) return '****';
|
|
359
|
+
return value.slice(0, 4) + '*'.repeat(Math.min(value.length - 4, 8));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Derives a human-readable label from a variable name.
|
|
364
|
+
*/
|
|
365
|
+
_labelFromName(name) {
|
|
366
|
+
const upper = name.toUpperCase();
|
|
367
|
+
if (/API[_-]?KEY/.test(upper)) return 'API_KEY';
|
|
368
|
+
if (/TOKEN/.test(upper)) return 'AUTH_TOKEN';
|
|
369
|
+
if (/PASSWORD|PASSWD|PWD/.test(upper)) return 'PASSWORD';
|
|
370
|
+
if (/SECRET/.test(upper)) return 'SECRET';
|
|
371
|
+
if (/DATABASE|DB/.test(upper)) return 'DATABASE_CREDENTIAL';
|
|
372
|
+
return 'SENSITIVE_VALUE';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic Version Control Guard for Structural Healing Operations.
|
|
6
|
+
* Manages atomic state rollbacks when automated refactoring breaks the build.
|
|
7
|
+
*/
|
|
8
|
+
export class GitSandbox {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.initialBranch = '';
|
|
12
|
+
this.healingBranch = `scaffold-healing-${Date.now()}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Captures the current repository state before applying structural modifications.
|
|
17
|
+
*/
|
|
18
|
+
async captureState() {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: this.context.cwd });
|
|
21
|
+
this.initialBranch = stdout.trim();
|
|
22
|
+
|
|
23
|
+
// Create a temporary recovery branch
|
|
24
|
+
await execa('git', ['checkout', '-b', this.healingBranch], { cwd: this.context.cwd });
|
|
25
|
+
if (this.context.verbose) {
|
|
26
|
+
console.log(`[Git] State captured in temporary branch: ${this.healingBranch}`);
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
throw new Error(`Git state capture failed: Ensure the directory is a git repository. (${e.message})`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reverts all changes applied during the healing cycle if verification fails.
|
|
35
|
+
*/
|
|
36
|
+
async rollback() {
|
|
37
|
+
try {
|
|
38
|
+
console.log(`[Git] Rolling back structural modifications...`);
|
|
39
|
+
await execa('git', ['reset', '--hard', 'HEAD'], { cwd: this.context.cwd });
|
|
40
|
+
await execa('git', ['checkout', this.initialBranch], { cwd: this.context.cwd });
|
|
41
|
+
await execa('git', ['branch', '-D', this.healingBranch], { cwd: this.context.cwd });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error(`[Git] Critical rollback failure: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Finalizes the healing cycle by merging changes back to the original branch.
|
|
49
|
+
*/
|
|
50
|
+
async commit() {
|
|
51
|
+
try {
|
|
52
|
+
await execa('git', ['add', '.'], { cwd: this.context.cwd });
|
|
53
|
+
await execa('git', ['commit', '-m', 'chore: automated structural healing (entkapp)'], { cwd: this.context.cwd });
|
|
54
|
+
|
|
55
|
+
await execa('git', ['checkout', this.initialBranch], { cwd: this.context.cwd });
|
|
56
|
+
await execa('git', ['merge', this.healingBranch], { cwd: this.context.cwd });
|
|
57
|
+
await execa('git', ['branch', '-D', this.healingBranch], { cwd: this.context.cwd });
|
|
58
|
+
|
|
59
|
+
if (this.context.verbose) {
|
|
60
|
+
console.log(`[Git] Structural modifications successfully merged into ${this.initialBranch}`);
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(`[Git] Commit failed: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Runs a verification command (e.g., npm test) to ensure structural integrity.
|
|
69
|
+
*/
|
|
70
|
+
async verifyIntegrity() {
|
|
71
|
+
try {
|
|
72
|
+
const [cmd, ...args] = this.context.testCommand.split(' ');
|
|
73
|
+
await execa(cmd, args, { cwd: this.context.cwd });
|
|
74
|
+
return true;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (this.context.verbose) {
|
|
77
|
+
console.warn(`[Git] Integrity verification failed: ${e.message}`);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import ansis from 'ansis';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Automated Structural Healing Orchestrator.
|
|
5
|
+
* Manages the lifecycle of applying structural fixes and verifying codebase health.
|
|
6
|
+
* This is a deterministic engine and does not use AI/LLMs.
|
|
7
|
+
*/
|
|
8
|
+
export class SelfHealer {
|
|
9
|
+
constructor(context, txManager, gitSandbox) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.txManager = txManager;
|
|
12
|
+
this.gitSandbox = gitSandbox;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Executes a structural healing cycle with automatic rollback on failure.
|
|
17
|
+
* @param {Function} refactorLogic - Async function that stages structural changes
|
|
18
|
+
*/
|
|
19
|
+
async runSelfHealingLifecycle(refactorLogic) {
|
|
20
|
+
console.log(ansis.bold.blue('\n𩹠Initiating Automated Structural Healing Cycle...'));
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// --- FIXED: Skip git state capture if it fails ---
|
|
24
|
+
try {
|
|
25
|
+
await this.gitSandbox.captureState();
|
|
26
|
+
} catch (e) {
|
|
27
|
+
if (this.context.verbose) console.log(`[SelfHealer] Skipping git state capture: ${e.message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. Execute the provided refactoring logic
|
|
31
|
+
await refactorLogic();
|
|
32
|
+
|
|
33
|
+
// 4. Verify structural integrity
|
|
34
|
+
console.log(ansis.dim('š§Ŗ Verifying codebase integrity...'));
|
|
35
|
+
const isHealthy = await this.gitSandbox.verifyIntegrity();
|
|
36
|
+
|
|
37
|
+
if (isHealthy) {
|
|
38
|
+
console.log(ansis.bold.green('ā
Structural integrity verified. Finalizing changes.'));
|
|
39
|
+
try { await this.gitSandbox.commit(); } catch (e) {}
|
|
40
|
+
} else {
|
|
41
|
+
console.log(ansis.bold.red('ā Structural integrity compromised. Rolling back changes.'));
|
|
42
|
+
try { await this.gitSandbox.rollback(); } catch (e) {}
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(ansis.bold.red(`\nšØ Healing Cycle Aborted: ${error.message}`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|