pkg-scaffold 2.2.0 → 2.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 +22 -0
- package/bin/cli.js +91 -0
- package/index.js +2254 -2544
- package/package.json +19 -7
- package/src/EngineContext.js +282 -0
- package/src/ast/ASTAnalyzer.js +313 -0
- package/src/ast/BarrelParser.js +177 -0
- package/src/ast/MagicDetector.js +154 -0
- package/src/healing/GitSandbox.js +160 -0
- package/src/healing/SelfHealer.js +150 -0
- package/src/index.js +343 -0
- package/src/performance/GraphCache.js +82 -0
- package/src/performance/SupplyChainGuard.js +106 -0
- package/src/performance/WorkerPool.js +89 -0
- package/src/performance/WorkerTaskRunner.js +64 -0
- package/src/refractor/ImpactAnalyzer.js +92 -0
- package/src/refractor/SourceRewriter.js +86 -0
- package/src/refractor/TransactionManager.js +131 -0
- package/src/refractor/TypeIntegrity.js +75 -0
- package/src/resolution/DepencyResolver.js +120 -0
- package/src/resolution/PathMapper.js +115 -0
- package/src/resolution/WorkSpaceGraph.js +171 -0
- package/tsconfig.json +26 -0
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkg-scaffold",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "An advanced, AST-driven dependency resolution, refactoring, and self-healing engine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"pkg-scaffold": "./
|
|
8
|
+
"pkg-scaffold": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/cli.js",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 0",
|
|
13
|
+
"test:stability": "npm run test"
|
|
9
14
|
},
|
|
10
15
|
"keywords": [
|
|
11
16
|
"scaffold",
|
|
@@ -35,8 +40,15 @@
|
|
|
35
40
|
},
|
|
36
41
|
"homepage": "https://github.com/DreamLongYT/pkg-scaffold#readme",
|
|
37
42
|
"dependencies": {
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
43
|
+
"ansis": "^3.0.0",
|
|
44
|
+
"commander": "^12.0.0",
|
|
45
|
+
"enhanced-resolve": "^5.16.0",
|
|
46
|
+
"execa": "^8.0.1",
|
|
47
|
+
"ramda": "^0.29.1",
|
|
48
|
+
"typescript": "^5.4.5",
|
|
49
|
+
"yocto-spinner": "^0.1.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
41
53
|
}
|
|
42
|
-
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* 📦 pkg-scaffold v3.0.0: Enterprise In-Memory Codebase State Manifest
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Implements a high-density, centralized graph database context for tracking
|
|
6
|
+
* software engineering debt, dependencies, types, and vulnerabilities.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* High-Fidelity Graph Element Node representing a single file asset boundary.
|
|
14
|
+
*/
|
|
15
|
+
export class GraphNode {
|
|
16
|
+
constructor(filePath) {
|
|
17
|
+
this.filePath = path.normalize(filePath);
|
|
18
|
+
this.contentHash = '';
|
|
19
|
+
this.isLibraryEntry = false;
|
|
20
|
+
this.isFrameworkContract = false;
|
|
21
|
+
this.scriptKind = 0; // Ambient enum mapping
|
|
22
|
+
|
|
23
|
+
// Explicit and Computed Dynamic Syntax Boundaries
|
|
24
|
+
this.explicitImports = new Set();
|
|
25
|
+
this.dynamicImports = new Set();
|
|
26
|
+
this.importedSymbols = new Set(); // Format: 'specifier:symbol' or 'specifier:*'
|
|
27
|
+
|
|
28
|
+
// Internal API Exposed Interfaces (Symbol Name -> ExportMetadata)
|
|
29
|
+
this.internalExports = new Map();
|
|
30
|
+
this.typeOnlyExports = new Set();
|
|
31
|
+
|
|
32
|
+
// Semantic Reference Verification Registries
|
|
33
|
+
this.instantiatedIdentifiers = new Set();
|
|
34
|
+
this.rawStringReferences = new Set();
|
|
35
|
+
this.propertyAccessChains = new Set();
|
|
36
|
+
|
|
37
|
+
// Dependency Mesh Connection Maps
|
|
38
|
+
this.incomingEdges = new Set(); // Set of absolute filePaths depending on this component
|
|
39
|
+
this.outgoingEdges = new Set(); // Set of absolute internal filePaths this component calls
|
|
40
|
+
|
|
41
|
+
// Security & Compliance Anomaly Matrices
|
|
42
|
+
this.securityThreats = [];
|
|
43
|
+
this.calculatedDynamicImports = [];
|
|
44
|
+
this.localSuppressedRules = new Set();
|
|
45
|
+
|
|
46
|
+
// Detailed AST Location Diagnostics (Symbol -> Structural Location Mapping)
|
|
47
|
+
this.symbolSourceLocations = new Map(); // Symbol -> { line: number, column: number, length: number }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Evaluates if a specific exposed symbol token is utilized by any incoming edges.
|
|
52
|
+
* Leverages precise syntax identity collections.
|
|
53
|
+
*/
|
|
54
|
+
isSymbolReferencedExternally(symbolName, projectGraph) {
|
|
55
|
+
if (this.isLibraryEntry) return true;
|
|
56
|
+
|
|
57
|
+
for (const parentPath of this.incomingEdges) {
|
|
58
|
+
const parentNode = projectGraph.get(parentPath);
|
|
59
|
+
if (!parentNode) continue;
|
|
60
|
+
|
|
61
|
+
// Direct identity reference check
|
|
62
|
+
if (parentNode.instantiatedIdentifiers.has(symbolName)) return true;
|
|
63
|
+
|
|
64
|
+
// Property lookup reference checks (e.g., config.databaseUrl)
|
|
65
|
+
for (const accessChain of parentNode.propertyAccessChains) {
|
|
66
|
+
if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Safe fallback lookup inside string reference caches (e.g., obj['databaseUrl'])
|
|
72
|
+
if (parentNode.rawStringReferences.has(symbolName)) return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compiles complete localized diagnostic telemetry tracking metrics for this node instance.
|
|
80
|
+
*/
|
|
81
|
+
compileNodeTelemetry() {
|
|
82
|
+
return {
|
|
83
|
+
path: this.filePath,
|
|
84
|
+
totalExplicitImportsCount: this.explicitImports.size,
|
|
85
|
+
totalExposedExportsCount: this.internalExports.size,
|
|
86
|
+
incomingDependenciesCount: this.incomingEdges.size,
|
|
87
|
+
outgoingDependenciesCount: this.outgoingEdges.size,
|
|
88
|
+
isDanglingOrphan: this.incomingEdges.size === 0 && !this.isLibraryEntry,
|
|
89
|
+
trackedThreatsCount: this.securityThreats.length
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Enterprise Engine Run State Registry & Suppression Context Matrix
|
|
96
|
+
*/
|
|
97
|
+
export class EngineContext {
|
|
98
|
+
constructor(options = {}) {
|
|
99
|
+
this.cwd = path.normalize(options.cwd || process.cwd());
|
|
100
|
+
this.cacheDir = path.join(this.cwd, '.scaffold-cache');
|
|
101
|
+
this.ignoreFilePath = path.join(this.cwd, '.scaffold-ignore');
|
|
102
|
+
this.tsconfigFilename = options.tsconfig || 'tsconfig.json';
|
|
103
|
+
this.testCommand = options.testCommand || 'npm test';
|
|
104
|
+
|
|
105
|
+
this.allowAutoFix = options.autoFix ?? true;
|
|
106
|
+
this.isWorkspaceEnabled = options.workspace ?? false;
|
|
107
|
+
this.verbose = options.verbose ?? false;
|
|
108
|
+
|
|
109
|
+
// Core Memory Repositories
|
|
110
|
+
this.graph = new Map(); // Absolute File Path -> GraphNode
|
|
111
|
+
this.registryHashes = new Map(); // Package Name -> Secure Lockfile Signature String
|
|
112
|
+
this.globallyIgnoredSymbols = new Set();
|
|
113
|
+
this.globallyIgnoredPaths = [];
|
|
114
|
+
this.monorepoPackageRoots = new Set();
|
|
115
|
+
|
|
116
|
+
// Structural Heuristic Verification Metrics Tracker
|
|
117
|
+
this.metrics = {
|
|
118
|
+
startTime: 0,
|
|
119
|
+
endTime: 0,
|
|
120
|
+
totalFilesScanned: 0,
|
|
121
|
+
cacheHits: 0,
|
|
122
|
+
cacheMisses: 0,
|
|
123
|
+
prunedFilesCount: 0,
|
|
124
|
+
prunedExportsCount: 0,
|
|
125
|
+
totalSymbolsAnalyzed: 0,
|
|
126
|
+
securityVulnerabilitiesMitigated: 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Initializes baseline context options, directory footprints, and suppression maps.
|
|
132
|
+
*/
|
|
133
|
+
async initialize() {
|
|
134
|
+
this.metrics.startTime = Date.now();
|
|
135
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
136
|
+
await this.compileIgnoreConfigurations();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parses .scaffold-ignore layers using precise token segment matching
|
|
141
|
+
* instead of high-risk loose regex blocks.
|
|
142
|
+
*/
|
|
143
|
+
async compileIgnoreConfigurations() {
|
|
144
|
+
try {
|
|
145
|
+
const content = await fs.readFile(this.ignoreFilePath, 'utf8');
|
|
146
|
+
const lines = content.split('\n');
|
|
147
|
+
|
|
148
|
+
for (let line of lines) {
|
|
149
|
+
line = line.trim();
|
|
150
|
+
if (!line || line.startsWith('#')) continue;
|
|
151
|
+
|
|
152
|
+
if (line.startsWith('export:')) {
|
|
153
|
+
const symbolToken = line.replace('export:', '').trim();
|
|
154
|
+
this.globallyIgnoredSymbols.add(symbolToken);
|
|
155
|
+
} else if (line.startsWith('path:')) {
|
|
156
|
+
const pathToken = line.replace('path:', '').trim();
|
|
157
|
+
this.globallyIgnoredPaths.push(path.normalize(pathToken));
|
|
158
|
+
} else {
|
|
159
|
+
// Standard structural path rule fallback
|
|
160
|
+
this.globallyIgnoredPaths.push(path.normalize(line));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// Configuration optionally omitted; proceed with default execution flags
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Allocates or resolves a unified GraphNode reference inside our memory map index.
|
|
170
|
+
*/
|
|
171
|
+
createNode(absoluteFilePath) {
|
|
172
|
+
const normalizedPath = path.normalize(absoluteFilePath);
|
|
173
|
+
if (this.graph.has(normalizedPath)) {
|
|
174
|
+
return this.graph.get(normalizedPath);
|
|
175
|
+
}
|
|
176
|
+
const node = new GraphNode(normalizedPath);
|
|
177
|
+
this.graph.set(normalizedPath, node);
|
|
178
|
+
return node;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Checks if an absolute file token path matches configuration ignore directives.
|
|
183
|
+
* Evaluates sub-path sequences exactly to prevent regular expression parsing drops.
|
|
184
|
+
*/
|
|
185
|
+
isPathIgnored(absoluteFilePath) {
|
|
186
|
+
const relativeText = path.relative(this.cwd, absoluteFilePath);
|
|
187
|
+
|
|
188
|
+
for (const ignoredTarget of this.globallyIgnoredPaths) {
|
|
189
|
+
if (relativeText === ignoredTarget || relativeText.startsWith(path.join(ignoredTarget, path.sep))) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
// Handle explicit wildcard terminal indicators
|
|
193
|
+
if (ignoredTarget.endsWith('*')) {
|
|
194
|
+
const baseSegment = ignoredTarget.slice(0, -1);
|
|
195
|
+
if (relativeText.startsWith(baseSegment)) return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Processes the entire active dependency map to compile structural issue indices.
|
|
203
|
+
* Evaluates orphaned components, dead exports, and supply-chain threats.
|
|
204
|
+
*/
|
|
205
|
+
generateSummaryReport() {
|
|
206
|
+
this.metrics.endTime = Date.now();
|
|
207
|
+
const durationSeconds = ((this.metrics.endTime - this.metrics.startTime) / 1000).toFixed(2);
|
|
208
|
+
|
|
209
|
+
const summary = {
|
|
210
|
+
executionDuration: `${durationSeconds}s`,
|
|
211
|
+
totalFilesProcessed: this.metrics.totalFilesScanned,
|
|
212
|
+
graphCacheOptimization: {
|
|
213
|
+
hits: this.metrics.cacheHits,
|
|
214
|
+
misses: this.metrics.cacheMisses,
|
|
215
|
+
ratio: this.metrics.totalFilesScanned > 0
|
|
216
|
+
? `${((this.metrics.cacheHits / this.metrics.totalFilesScanned) * 100).toFixed(1)}%`
|
|
217
|
+
: '0%'
|
|
218
|
+
},
|
|
219
|
+
structuralIssuesDetected: {
|
|
220
|
+
deadFiles: [],
|
|
221
|
+
deadExports: [],
|
|
222
|
+
securityThreats: []
|
|
223
|
+
},
|
|
224
|
+
modificationsExecuted: {
|
|
225
|
+
filesUnlinked: this.metrics.prunedFilesCount,
|
|
226
|
+
exportsStripped: this.metrics.prunedExportsCount
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
for (const [filePath, node] of this.graph.entries()) {
|
|
231
|
+
// Skip package control files from standard structural dead-code checks
|
|
232
|
+
if (filePath.endsWith('package.json')) continue;
|
|
233
|
+
if (this.isPathIgnored(filePath)) continue;
|
|
234
|
+
|
|
235
|
+
const relativePath = path.relative(this.cwd, filePath);
|
|
236
|
+
|
|
237
|
+
// Category A: Completely orphaned components (no references, not library/framework entries)
|
|
238
|
+
if (node.incomingEdges.size === 0 && !node.isLibraryEntry && !node.isFrameworkContract) {
|
|
239
|
+
summary.structuralIssuesDetected.deadFiles.push(relativePath);
|
|
240
|
+
continue; // An orphaned file implies all internal sub-exports are dead; skip sub-checks
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Category B: Dead Named Exports inside active files
|
|
244
|
+
for (const [exportName, meta] of node.internalExports.entries()) {
|
|
245
|
+
this.metrics.totalSymbolsAnalyzed++;
|
|
246
|
+
|
|
247
|
+
// Skip entry configurations, global suppresses, and type-suppressed symbols
|
|
248
|
+
if (exportName === 'default' ||
|
|
249
|
+
this.globallyIgnoredSymbols.has(exportName) ||
|
|
250
|
+
node.localSuppressedRules.has(exportName)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!node.isSymbolReferencedExternally(exportName, this.graph)) {
|
|
255
|
+
const diagnosticLocation = node.symbolSourceLocations.get(exportName) || { line: 1, column: 1 };
|
|
256
|
+
summary.structuralIssuesDetected.deadExports.push({
|
|
257
|
+
file: relativePath,
|
|
258
|
+
symbol: exportName,
|
|
259
|
+
type: meta.type || 'named',
|
|
260
|
+
line: diagnosticLocation.line,
|
|
261
|
+
column: diagnosticLocation.column
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Category C: High-Entropy Password / Key Hardcode Vulnerabilities
|
|
267
|
+
if (node.securityThreats && node.securityThreats.length > 0) {
|
|
268
|
+
node.securityThreats.forEach(threat => {
|
|
269
|
+
summary.structuralIssuesDetected.securityThreats.push({
|
|
270
|
+
file: relativePath,
|
|
271
|
+
identifier: threat.variableKey,
|
|
272
|
+
riskCode: threat.riskCode || 'HIGH_RISK_SECRET_LEAK',
|
|
273
|
+
entropy: threat.entropyValue,
|
|
274
|
+
line: threat.line || 1
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return summary;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enterprise AST Syntax Walker & Feature Extractor
|
|
7
|
+
* Utilizes the official TypeScript Compiler infrastructure to execute deeply nested
|
|
8
|
+
* node classification without falling back to high-risk regular expression approximations.
|
|
9
|
+
*/
|
|
10
|
+
export class ASTAnalyzer {
|
|
11
|
+
constructor(context) {
|
|
12
|
+
this.context = context;
|
|
13
|
+
// Standard high-entropy baseline selectors for AST variable tracking
|
|
14
|
+
this.entropyThreshold = 4.3;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parses target file data into an isolated AST representation and populates metadata structures.
|
|
19
|
+
* @param {string} filePath - Absolute path to on-disk component
|
|
20
|
+
* @param {Object} fileNode - In-memory structural graph reference node
|
|
21
|
+
*/
|
|
22
|
+
async processFile(filePath, fileNode) {
|
|
23
|
+
try {
|
|
24
|
+
const sourceText = await fs.readFile(filePath, 'utf8');
|
|
25
|
+
|
|
26
|
+
// Configure target extraction structures to parse TS, JSX, and modern TC39 specifications
|
|
27
|
+
const sourceFile = ts.createSourceFile(
|
|
28
|
+
filePath,
|
|
29
|
+
sourceText,
|
|
30
|
+
ts.ScriptTarget.Latest,
|
|
31
|
+
true, // Ensure parent pointers are bound to allow localized subtree walking
|
|
32
|
+
this.getScriptKind(filePath)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
this.walkNode(sourceFile, sourceFile, fileNode);
|
|
36
|
+
this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
} catch (parseError) {
|
|
40
|
+
if (this.context.verbose) {
|
|
41
|
+
console.error(`[AST Open Error] Failed compilation validation mapping on element: ${filePath}. Reason: ${parseError.message}`);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Primary node walker loop executing atomic switch classifications.
|
|
49
|
+
* Challenge #7: Resolves conditional/destructured references to prevent cascading breakages.
|
|
50
|
+
*/
|
|
51
|
+
walkNode(sourceFile, node, fileNode) {
|
|
52
|
+
if (!node) return;
|
|
53
|
+
|
|
54
|
+
switch (node.kind) {
|
|
55
|
+
// Handle Explicit Named or Absolute Star Namespace Imports
|
|
56
|
+
case ts.SyntaxKind.ImportDeclaration: {
|
|
57
|
+
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
58
|
+
const specifier = node.moduleSpecifier.text;
|
|
59
|
+
fileNode.explicitImports.add(specifier);
|
|
60
|
+
|
|
61
|
+
if (node.importClause) {
|
|
62
|
+
// Trace named bounds: import { activeToken } from 'module';
|
|
63
|
+
if (node.importClause.namedBindings) {
|
|
64
|
+
if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
65
|
+
node.importClause.namedBindings.elements.forEach(element => {
|
|
66
|
+
const importedName = element.name.text;
|
|
67
|
+
const propertyName = element.propertyName ? element.propertyName.text : importedName;
|
|
68
|
+
fileNode.importedSymbols.add(`${specifier}:${propertyName}`);
|
|
69
|
+
});
|
|
70
|
+
} else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
71
|
+
// Tracking total wildcard imports: import * as layout from 'module';
|
|
72
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Trace default bounds: import React from 'react';
|
|
76
|
+
if (node.importClause.name) {
|
|
77
|
+
fileNode.importedSymbols.add(`${specifier}:default`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle Explicit Namespace Requirements: import config = require('./config');
|
|
85
|
+
case ts.SyntaxKind.ImportEqualsDeclaration: {
|
|
86
|
+
if (node.moduleReference && ts.isExternalModuleReference(node.moduleReference)) {
|
|
87
|
+
if (node.moduleReference.expression && ts.isStringLiteral(node.moduleReference.expression)) {
|
|
88
|
+
fileNode.explicitImports.add(node.moduleReference.expression.text);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Challenge #1: Tracking dynamic expressions e.g., import('./chunks/' + variant)
|
|
95
|
+
case ts.SyntaxKind.CallExpression: {
|
|
96
|
+
if (node.expression && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
97
|
+
const firstArgument = node.arguments[0];
|
|
98
|
+
if (firstArgument) {
|
|
99
|
+
if (ts.isStringLiteral(firstArgument)) {
|
|
100
|
+
fileNode.explicitImports.add(firstArgument.text);
|
|
101
|
+
fileNode.dynamicImports.add(firstArgument.text);
|
|
102
|
+
} else {
|
|
103
|
+
// Deeply trace runtime calculated variables within the import parameters call
|
|
104
|
+
const stringPatterns = [];
|
|
105
|
+
this.traceStringExpressions(firstArgument, stringPatterns);
|
|
106
|
+
fileNode.calculatedDynamicImports.push({
|
|
107
|
+
rawText: firstArgument.getText(sourceFile),
|
|
108
|
+
heuristics: stringPatterns,
|
|
109
|
+
position: node.getStart(sourceFile)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle Variable Declaration Assignments & Challenge #11 (AST Secret Scanning)
|
|
118
|
+
case ts.SyntaxKind.VariableDeclaration: {
|
|
119
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
120
|
+
this.auditAssignmentSafety(node.name.text, node.initializer, fileNode, sourceFile);
|
|
121
|
+
} else if (node.name && (ts.isObjectBindingPattern(node.name) || ts.isArrayBindingPattern(node.name))) {
|
|
122
|
+
// Flatten binding properties to map destructured usage accurately
|
|
123
|
+
node.name.elements.forEach(element => {
|
|
124
|
+
if (element.name && ts.isIdentifier(element.name)) {
|
|
125
|
+
this.auditAssignmentSafety(element.name.text, null, fileNode, sourceFile);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle Named Function Node Exports Matrix Configurations
|
|
133
|
+
case ts.SyntaxKind.FunctionDeclaration: {
|
|
134
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
135
|
+
const name = node.name.text;
|
|
136
|
+
if (this.hasExportModifier(node)) {
|
|
137
|
+
fileNode.internalExports.set(name, { type: 'function', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Handle Structural Class Definitions and Class Export Signatures
|
|
144
|
+
case ts.SyntaxKind.ClassDeclaration: {
|
|
145
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
146
|
+
const name = node.name.text;
|
|
147
|
+
if (this.hasExportModifier(node)) {
|
|
148
|
+
fileNode.internalExports.set(name, { type: 'class', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Handle Interface Definitions (Crucial for Challenge #10: Type Integrity Mapping)
|
|
155
|
+
case ts.SyntaxKind.InterfaceDeclaration: {
|
|
156
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
157
|
+
const name = node.name.text;
|
|
158
|
+
if (this.hasExportModifier(node)) {
|
|
159
|
+
fileNode.internalExports.set(name, { type: 'interface', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle Type Invocations and Declarations Aliases
|
|
166
|
+
case ts.SyntaxKind.TypeAliasDeclaration: {
|
|
167
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
168
|
+
const name = node.name.text;
|
|
169
|
+
if (this.hasExportModifier(node)) {
|
|
170
|
+
fileNode.internalExports.set(name, { type: 'type-alias', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Handle Explicit Export Assignments: export default baselineConfiguration;
|
|
177
|
+
case ts.SyntaxKind.ExportAssignment: {
|
|
178
|
+
const name = node.expression ? node.expression.getText(sourceFile) : 'default';
|
|
179
|
+
fileNode.internalExports.set('default', {
|
|
180
|
+
type: 'default-assignment',
|
|
181
|
+
referencedSymbol: name,
|
|
182
|
+
start: node.getStart(sourceFile),
|
|
183
|
+
end: node.getEnd()
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handle Arbitrary String References to catch deep framework routing or dynamic keys
|
|
189
|
+
case ts.SyntaxKind.StringLiteral: {
|
|
190
|
+
const text = node.text;
|
|
191
|
+
if (text.length > 2 && text.length < 120) {
|
|
192
|
+
fileNode.rawStringReferences.add(text);
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Track general identifiers to register references to mapped import keys
|
|
198
|
+
case ts.SyntaxKind.Identifier: {
|
|
199
|
+
const idText = node.text;
|
|
200
|
+
// Avoid adding declarations to usage logs to keep verification accurate
|
|
201
|
+
if (!this.isNodeDeclarationName(node)) {
|
|
202
|
+
fileNode.instantiatedIdentifiers.add(idText);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Traverse recursively down the Node structural tree
|
|
209
|
+
ts.forEachChild(node, child => this.walkNode(sourceFile, child, fileNode));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Challenge #1: Evaluates math operations and template configurations inside dynamic imports.
|
|
214
|
+
*/
|
|
215
|
+
traceStringExpressions(node, collector) {
|
|
216
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
217
|
+
this.traceStringExpressions(node.left, collector);
|
|
218
|
+
this.traceStringExpressions(node.right, collector);
|
|
219
|
+
} else if (ts.isStringLiteral(node)) {
|
|
220
|
+
collector.push({ type: 'literal', val: node.text });
|
|
221
|
+
} else if (ts.isTemplateExpression(node)) {
|
|
222
|
+
if (node.head) collector.push({ type: 'template-slice', val: node.head.text });
|
|
223
|
+
node.templateSpans.forEach(span => {
|
|
224
|
+
collector.push({ type: 'dynamic-var', val: span.expression.getText() });
|
|
225
|
+
if (span.literal) collector.push({ type: 'template-slice', val: span.literal.text });
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
collector.push({ type: 'computed-variable', val: node.getText() });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Challenge #11: AST Secret Scanning. Evaluates entropy and patterns directly via assignments.
|
|
234
|
+
*/
|
|
235
|
+
auditAssignmentSafety(variableName, initializer, fileNode, sourceFile) {
|
|
236
|
+
// Process variable mapping target indicators first
|
|
237
|
+
if (this.hasExportModifier(variableName?.parent)) {
|
|
238
|
+
fileNode.internalExports.set(variableName, { type: 'variable', start: variableName.parent.getStart(sourceFile), end: variableName.parent.getEnd() });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!initializer || !ts.isStringLiteral(initializer)) return;
|
|
242
|
+
const value = initializer.text;
|
|
243
|
+
|
|
244
|
+
// Challenge #11 Heuristic validation parameters matching variable patterns or contents values
|
|
245
|
+
const isSuspiciousKeyName = /api_?key|secret|token|password|auth_?token|private_?key/i.test(variableName);
|
|
246
|
+
const entropy = this.calculateShannonEntropy(value);
|
|
247
|
+
|
|
248
|
+
if ((isSuspiciousKeyName && value.length > 8) || (entropy > this.entropyThreshold && value.length > 16)) {
|
|
249
|
+
fileNode.securityThreats.push({
|
|
250
|
+
identifier: variableName,
|
|
251
|
+
entropy: parseFloat(entropy.toFixed(2)),
|
|
252
|
+
position: initializer.getStart(sourceFile),
|
|
253
|
+
riskCode: 'HIGH_RISK_SECRET_LEAK'
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
calculateShannonEntropy(str) {
|
|
259
|
+
const map = {};
|
|
260
|
+
for (let i = 0; i < str.length; i++) {
|
|
261
|
+
const char = str[i];
|
|
262
|
+
map[char] = (map[char] || 0) + 1;
|
|
263
|
+
}
|
|
264
|
+
let entropy = 0;
|
|
265
|
+
for (const char in map) {
|
|
266
|
+
const p = map[char] / str.length;
|
|
267
|
+
entropy -= p * Math.log2(p);
|
|
268
|
+
}
|
|
269
|
+
return entropy;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Challenge #18 & #8: Parse JSDoc suppression blocks right out of code statements.
|
|
274
|
+
*/
|
|
275
|
+
extractTopLevelJSDocSuppreessions(sourceFile, fileNode) {
|
|
276
|
+
const fullText = sourceFile.text;
|
|
277
|
+
const commentRanges = ts.getLeadingCommentRanges(fullText, 0) || [];
|
|
278
|
+
|
|
279
|
+
for (const range of commentRanges) {
|
|
280
|
+
const comment = fullText.slice(range.pos, range.end);
|
|
281
|
+
const matches = comment.match(/@scaffold-suppress\s+([a-zA-Z0-9_\-*:]+)/g);
|
|
282
|
+
if (matches) {
|
|
283
|
+
matches.forEach(m => {
|
|
284
|
+
const directive = m.replace('@scaffold-suppress', '').trim();
|
|
285
|
+
fileNode.localSuppressedRules.add(directive);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
hasExportModifier(node) {
|
|
292
|
+
if (!node || !node.modifiers) return false;
|
|
293
|
+
return node.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
isNodeDeclarationName(node) {
|
|
297
|
+
const parent = node.parent;
|
|
298
|
+
if (!parent) return false;
|
|
299
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node) return true;
|
|
300
|
+
if (ts.isFunctionDeclaration(parent) && parent.name === node) return true;
|
|
301
|
+
if (ts.isClassDeclaration(parent) && parent.name === node) return true;
|
|
302
|
+
if (ts.isInterfaceDeclaration(parent) && parent.name === node) return true;
|
|
303
|
+
if (ts.isImportSpecifier(parent) && parent.name === node) return true;
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
getScriptKind(filePath) {
|
|
308
|
+
if (filePath.endsWith('.ts')) return ts.ScriptKind.TS;
|
|
309
|
+
if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX;
|
|
310
|
+
if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX;
|
|
311
|
+
return ts.ScriptKind.JS;
|
|
312
|
+
}
|
|
313
|
+
}
|