pkg-scaffold 3.3.5 ā 3.3.6
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 +4 -1
- package/bin/cli.js +2 -2
- package/package.json +8 -3
- package/src/EngineContext.js +20 -9
- package/src/ast/ASTAnalyzer.js +6 -0
- package/src/ast/MagicDetector.js +2 -7
- package/src/ast/OxcAnalyzer.js +57 -15
- package/src/ast/SecretScanner.js +304 -0
- package/src/index.js +63 -2
- package/src/performance/WorkerPool.js +6 -3
- package/src/resolution/WorkSpaceGraph.js +9 -0
- package/src/performance/WorkerTaskRunner.js +0 -83
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* **š Massive Plugin Ecosystem:** Over 20+ built-in plugins (Next.js, Nuxt, SvelteKit, Tailwind, Jest, Vitest, Playwright, GitHub Actions, Webpack, Babel, Rollup, ESLint, Prettier, Husky, and many more).
|
|
15
15
|
* **š True Dead Code Detection:** Advanced graph-based reachability analysis to find truly dead files and unused exports, even deep within your codebase.
|
|
16
16
|
* **š Circular Dependency Detection:** High-performance Tarjan-based algorithm to detect and report circular dependencies.
|
|
17
|
+
* **š Secrets Scanning:** Automatically detects hardcoded API keys, tokens, and credentials (v3.3.6+).
|
|
17
18
|
* **š”ļø Supply Chain Guard:** Detects typosquatting and verifies integrity lockfile hashes.
|
|
18
19
|
* **š ļø Automated Structural Healing:** Not just reporting, but automatically fixing structural issues (removing dead files, pruning unused dependencies) with git-based rollback protection.
|
|
19
20
|
* **āļø Flexible Configuration:** Supports `pkg-scaffold.json`, `pkg-scaffold.ts`, `scaffold.config.js`, and more.
|
|
@@ -33,9 +34,11 @@ pnpm add -D pkg-scaffold
|
|
|
33
34
|
Run the CLI at the root of your project:
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
|
-
npx pkg-scaffold
|
|
37
|
+
npx pkg-scaffold -r
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
> **Note**: Always use the `-r` flag to start the analysis loop. v3.3.6+ also features **Auto-Detection for Monorepos**.
|
|
41
|
+
|
|
39
42
|
### CLI Options
|
|
40
43
|
|
|
41
44
|
* `-c, --cwd <path>`: Specify the execution context root directory.
|
package/bin/cli.js
CHANGED
|
@@ -64,7 +64,7 @@ async function bootstrap() {
|
|
|
64
64
|
const answer = await rl.question(ansis.bold.yellow('ā No "pkg-scaffold:run" script found in package.json. Install it? (y/n): '));
|
|
65
65
|
if (answer.toLowerCase() === 'y') {
|
|
66
66
|
pkgJson.scripts = pkgJson.scripts || {};
|
|
67
|
-
pkgJson.scripts['pkg-scaffold:run'] = 'pkg-scaffold --fix';
|
|
67
|
+
pkgJson.scripts['pkg-scaffold:run'] = 'npx pkg-scaffold --fix';
|
|
68
68
|
await fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
69
69
|
console.log(ansis.green('ā
"pkg-scaffold:run" script added to package.json.'));
|
|
70
70
|
}
|
|
@@ -95,7 +95,7 @@ async function bootstrap() {
|
|
|
95
95
|
|
|
96
96
|
if (pkgJson?.scripts?.['pkg-scaffold:run'] || configInstalled) {
|
|
97
97
|
console.log(ansis.bold.cyan('\nš Setup complete! To start the engine, run:'));
|
|
98
|
-
console.log(ansis.white(` - pkg-scaffold -r`));
|
|
98
|
+
console.log(ansis.white(` - npx pkg-scaffold -r`));
|
|
99
99
|
console.log(ansis.white(` - npm run pkg-scaffold:run\n`));
|
|
100
100
|
}
|
|
101
101
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkg-scaffold",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.6",
|
|
4
4
|
"description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node bin/cli.js",
|
|
12
|
-
"pkg-scaffold:run": "
|
|
12
|
+
"pkg-scaffold:run": "npx pkg-scaffold --fix",
|
|
13
13
|
"test": "echo \"Error: no test specified\" && exit 0",
|
|
14
14
|
"test:stability": "npm run test",
|
|
15
15
|
"docs:dev": "vitepress dev docs",
|
|
@@ -48,7 +48,12 @@
|
|
|
48
48
|
"package.json",
|
|
49
49
|
"packages",
|
|
50
50
|
"scan",
|
|
51
|
-
|
|
51
|
+
"security",
|
|
52
|
+
"secret-scanner",
|
|
53
|
+
"secrets",
|
|
54
|
+
"hardcoded-secrets",
|
|
55
|
+
"credentials",
|
|
56
|
+
"audit",
|
|
52
57
|
"structural-healing",
|
|
53
58
|
"setup",
|
|
54
59
|
"type",
|
package/src/EngineContext.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================================
|
|
3
|
-
* š¦ pkg-scaffold v3.3.
|
|
3
|
+
* š¦ pkg-scaffold v3.3.6: Enterprise In-Memory Codebase State Manifest
|
|
4
4
|
* ============================================================================
|
|
5
5
|
* Implements a high-density, centralized graph database context for tracking
|
|
6
6
|
* software engineering debt, dependencies, types, and vulnerabilities.
|
|
@@ -50,6 +50,8 @@ export class GraphNode {
|
|
|
50
50
|
|
|
51
51
|
// Detailed AST Location Diagnostics (Symbol -> Structural Location Mapping)
|
|
52
52
|
this.symbolSourceLocations = new Map(); // Symbol -> { line: number, column: number, length: number }
|
|
53
|
+
// Security threat findings for this file
|
|
54
|
+
this.securityThreats = [];
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
@@ -64,17 +66,25 @@ export class GraphNode {
|
|
|
64
66
|
if (!parentNode) continue;
|
|
65
67
|
|
|
66
68
|
// Check if the symbol is explicitly imported by the parent
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
+
// Strategy 1: Check absolute path based tokens (more reliable)
|
|
70
|
+
const absoluteImportKey = `${this.filePath}:${symbolName}`;
|
|
71
|
+
const absoluteStarKey = `${this.filePath}:*`;
|
|
69
72
|
|
|
70
|
-
if (parentNode.importedSymbols.has(
|
|
73
|
+
if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) {
|
|
71
74
|
return true;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
// Check
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
77
|
+
// Strategy 2: Check relative path based tokens (legacy/compatibility)
|
|
78
|
+
const relativePath = path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/');
|
|
79
|
+
const relativePathNoExt = relativePath.replace(/\.(js|ts|tsx|jsx)$/, '');
|
|
80
|
+
|
|
81
|
+
const importKey = `${relativePath}:${symbolName}`;
|
|
82
|
+
const importKeyAlt = `${relativePathNoExt}:${symbolName}`;
|
|
83
|
+
const starKey = `${relativePath}:*`;
|
|
84
|
+
const starKeyAlt = `${relativePathNoExt}:*`;
|
|
85
|
+
|
|
86
|
+
if (parentNode.importedSymbols.has(importKey) || parentNode.importedSymbols.has(importKeyAlt) ||
|
|
87
|
+
parentNode.importedSymbols.has(starKey) || parentNode.importedSymbols.has(starKeyAlt)) {
|
|
78
88
|
return true;
|
|
79
89
|
}
|
|
80
90
|
|
|
@@ -117,7 +127,7 @@ export class GraphNode {
|
|
|
117
127
|
incomingDependenciesCount: this.incomingEdges.size,
|
|
118
128
|
outgoingDependenciesCount: this.outgoingEdges.size,
|
|
119
129
|
isDanglingOrphan: this.incomingEdges.size === 0 && !this.isLibraryEntry,
|
|
120
|
-
trackedThreatsCount: this.securityThreats.length
|
|
130
|
+
trackedThreatsCount: (this.securityThreats || []).length
|
|
121
131
|
};
|
|
122
132
|
}
|
|
123
133
|
}
|
|
@@ -160,6 +170,7 @@ export class EngineContext {
|
|
|
160
170
|
};
|
|
161
171
|
this.usedExternalPackages = new Set(); // Global set of used npm packages
|
|
162
172
|
this.manifestDependencies = new Map(); // Package.json path -> { dependencies, devDependencies, peerDependencies, optionalDependencies }
|
|
173
|
+
this.allSecretFindings = []; // Aggregated hardcoded secret findings across all scanned files
|
|
163
174
|
|
|
164
175
|
// DependencyProfiler instance for implicit invocation tracing
|
|
165
176
|
this._depProfiler = new DependencyProfiler(this);
|
package/src/ast/ASTAnalyzer.js
CHANGED
|
@@ -274,6 +274,8 @@ export class ASTAnalyzer {
|
|
|
274
274
|
start: decl.getStart(sourceFile),
|
|
275
275
|
end: decl.getEnd()
|
|
276
276
|
});
|
|
277
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(decl.getStart(sourceFile));
|
|
278
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
277
279
|
this.addDeclaredSymbol(name, decl, sourceFile);
|
|
278
280
|
} else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
|
|
279
281
|
decl.name.elements.forEach(element => {
|
|
@@ -284,6 +286,8 @@ export class ASTAnalyzer {
|
|
|
284
286
|
start: element.getStart(sourceFile),
|
|
285
287
|
end: element.getEnd()
|
|
286
288
|
});
|
|
289
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
|
|
290
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
287
291
|
this.addDeclaredSymbol(name, element, sourceFile);
|
|
288
292
|
}
|
|
289
293
|
});
|
|
@@ -296,6 +300,8 @@ export class ASTAnalyzer {
|
|
|
296
300
|
start: element.getStart(sourceFile),
|
|
297
301
|
end: element.getEnd()
|
|
298
302
|
});
|
|
303
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
|
|
304
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
299
305
|
this.addDeclaredSymbol(name, element, sourceFile);
|
|
300
306
|
}
|
|
301
307
|
});
|
package/src/ast/MagicDetector.js
CHANGED
|
@@ -110,17 +110,12 @@ export class MagicDetector {
|
|
|
110
110
|
// CLI binaries
|
|
111
111
|
'/bin/cli.js', '/bin/cli.ts', '/bin/cli.mjs',
|
|
112
112
|
'/bin/index.js', '/bin/index.ts',
|
|
113
|
-
// Server / app entry points
|
|
114
|
-
'/src/server.ts', '/src/server.js',
|
|
113
|
+
// Server / app entry points (Reduced in v3.3.6 to avoid false positives in libraries)
|
|
115
114
|
'/src/main.ts', '/src/main.js',
|
|
116
115
|
'/src/app.ts', '/src/app.js',
|
|
117
|
-
'/src/
|
|
118
|
-
'/src/index.js', '/src/index.jsx',
|
|
119
|
-
'/server.ts', '/server.js',
|
|
116
|
+
'/src/api/HeadlessAPI.js', '/src/api/PluginSDK.js',
|
|
120
117
|
'/main.ts', '/main.js',
|
|
121
118
|
'/app.ts', '/app.js',
|
|
122
|
-
'/index.ts', '/index.tsx',
|
|
123
|
-
'/index.js', '/index.jsx',
|
|
124
119
|
];
|
|
125
120
|
if (entryPatterns.some(p => normalizedPath.endsWith(p))) return true;
|
|
126
121
|
|
package/src/ast/OxcAnalyzer.js
CHANGED
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
export class OxcAnalyzer {
|
|
2
2
|
constructor(context) {
|
|
3
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;
|
|
4
11
|
try {
|
|
5
|
-
|
|
12
|
+
// In ESM, we use dynamic import()
|
|
13
|
+
const oxc = await import("oxc-parser");
|
|
14
|
+
this.oxc = oxc;
|
|
6
15
|
this.isAvailable = true;
|
|
16
|
+
return true;
|
|
7
17
|
} catch (e) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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;
|
|
11
31
|
}
|
|
12
32
|
}
|
|
13
33
|
}
|
|
14
34
|
|
|
15
|
-
parseFile(filePath, content, fileNode) {
|
|
16
|
-
if (!this.isAvailable)
|
|
35
|
+
async parseFile(filePath, content, fileNode) {
|
|
36
|
+
if (!this.isAvailable) {
|
|
37
|
+
const initialized = await this.init();
|
|
38
|
+
if (!initialized) return false;
|
|
39
|
+
}
|
|
17
40
|
|
|
18
41
|
try {
|
|
19
42
|
if (this.context.verbose) {
|
|
@@ -32,6 +55,26 @@ export class OxcAnalyzer {
|
|
|
32
55
|
fileNode.decorators = new Set();
|
|
33
56
|
|
|
34
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
|
+
|
|
35
78
|
return true;
|
|
36
79
|
} catch (e) {
|
|
37
80
|
if (this.context.verbose) {
|
|
@@ -63,6 +106,10 @@ export class OxcAnalyzer {
|
|
|
63
106
|
case "Decorator":
|
|
64
107
|
this.handleDecorator(node, fileNode);
|
|
65
108
|
break;
|
|
109
|
+
case "StringLiteral":
|
|
110
|
+
// Track for secret scanning if it looks like a secret
|
|
111
|
+
fileNode.rawStringReferences.add(node.value);
|
|
112
|
+
break;
|
|
66
113
|
}
|
|
67
114
|
|
|
68
115
|
// Traverse children
|
|
@@ -109,7 +156,7 @@ export class OxcAnalyzer {
|
|
|
109
156
|
if (node.type === "ExportAllDeclaration") {
|
|
110
157
|
const sourceSpecifier = node.source ? node.source.value : null;
|
|
111
158
|
if (sourceSpecifier) {
|
|
112
|
-
//
|
|
159
|
+
// Register re-export source as an explicit import so the graph linker
|
|
113
160
|
// creates an incomingEdge on the re-exported file.
|
|
114
161
|
fileNode.explicitImports.add(sourceSpecifier);
|
|
115
162
|
|
|
@@ -128,7 +175,7 @@ export class OxcAnalyzer {
|
|
|
128
175
|
} else {
|
|
129
176
|
// export * from 'module'
|
|
130
177
|
fileNode.internalExports.set("*", { type: "re-export-all", source: sourceSpecifier });
|
|
131
|
-
//
|
|
178
|
+
// Register as wildcard importedSymbol so graph linker creates incomingEdge
|
|
132
179
|
fileNode.importedSymbols.add(`${sourceSpecifier}:*`);
|
|
133
180
|
}
|
|
134
181
|
}
|
|
@@ -138,7 +185,7 @@ export class OxcAnalyzer {
|
|
|
138
185
|
if (node.source) {
|
|
139
186
|
// Re-export with source: export { x } from 'module'
|
|
140
187
|
const specifier = node.source.value;
|
|
141
|
-
//
|
|
188
|
+
// Register re-export source as an explicit import
|
|
142
189
|
fileNode.explicitImports.add(specifier);
|
|
143
190
|
|
|
144
191
|
// Track external package usage from re-exports
|
|
@@ -157,7 +204,7 @@ export class OxcAnalyzer {
|
|
|
157
204
|
start: node.start,
|
|
158
205
|
end: node.end,
|
|
159
206
|
});
|
|
160
|
-
//
|
|
207
|
+
// Register as importedSymbol so barrel-tracer can resolve origin file
|
|
161
208
|
fileNode.importedSymbols.add(`${specifier}:${localName}`);
|
|
162
209
|
});
|
|
163
210
|
}
|
|
@@ -276,15 +323,10 @@ export class OxcAnalyzer {
|
|
|
276
323
|
if (node.expression.type === "CallExpression") {
|
|
277
324
|
node.expression.arguments.forEach(arg => {
|
|
278
325
|
// Further analysis of arguments can be done here if needed
|
|
279
|
-
// e.g., if (arg.type === "StringLiteral") fileNode.decoratorArgs.add(`${decoratorName}:${arg.value}`);
|
|
280
326
|
});
|
|
281
327
|
}
|
|
282
328
|
}
|
|
283
329
|
|
|
284
|
-
/**
|
|
285
|
-
* Extracts the root npm package name from an import specifier.
|
|
286
|
-
* Handles scoped packages (@scope/pkg) and subpath imports (pkg/utils, @scope/pkg/utils).
|
|
287
|
-
*/
|
|
288
330
|
_extractPackageName(specifier) {
|
|
289
331
|
if (specifier.startsWith('@')) {
|
|
290
332
|
const parts = specifier.split('/');
|
|
@@ -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.6: 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
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { DeadCodeDetector } from "./ast/DeadCodeDetector.js";
|
|
2
2
|
import { OxcAnalyzer } from "./ast/OxcAnalyzer.js";
|
|
3
|
+
import { SecretScanner } from './ast/SecretScanner.js';
|
|
3
4
|
/**
|
|
4
5
|
* ============================================================================
|
|
5
|
-
* š¦ pkg-scaffold v3.
|
|
6
|
+
* š¦ pkg-scaffold v3.3.6: Unified Architectural Refactoring Orchestrator
|
|
6
7
|
* ============================================================================
|
|
7
8
|
* Main execution bridge managing multi-pass compilation cycles, semantic cross-linking,
|
|
8
9
|
* supply-chain validation audits, and automated structural healing rollbacks.
|
|
@@ -64,6 +65,8 @@ export class RefactoringEngine {
|
|
|
64
65
|
this.workerPool = new WorkerPool(this.context);
|
|
65
66
|
this.gitSandbox = new GitSandbox(this.context);
|
|
66
67
|
this.selfHealer = new SelfHealer(this.context, this.txManager, this.gitSandbox);
|
|
68
|
+
// Stage 6: Secret / hardcoded credential scanner
|
|
69
|
+
this.secretScanner = new SecretScanner();
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
/**
|
|
@@ -130,6 +133,15 @@ export class RefactoringEngine {
|
|
|
130
133
|
if (isFileCached) {
|
|
131
134
|
this.context.metrics.cacheHits++;
|
|
132
135
|
this.hydrateNodeFromCache(node, cacheManifest[filePath]);
|
|
136
|
+
// Re-run secret scan even on cached files (secrets may change without AST change)
|
|
137
|
+
try {
|
|
138
|
+
const cachedContent = await fs.readFile(filePath, 'utf8');
|
|
139
|
+
const secretFindings = this.secretScanner.scanFileContent(filePath, cachedContent);
|
|
140
|
+
if (secretFindings.length > 0) {
|
|
141
|
+
node.securityThreats = (node.securityThreats || []).concat(secretFindings);
|
|
142
|
+
secretFindings.forEach(f => this.context.allSecretFindings.push(f));
|
|
143
|
+
}
|
|
144
|
+
} catch { /* unreadable file ā skip */ }
|
|
133
145
|
} else if (!parallelParseCompleted) {
|
|
134
146
|
this.context.metrics.cacheMisses++;
|
|
135
147
|
const fileContent = await fs.readFile(filePath, 'utf8'); // Read file content here
|
|
@@ -138,6 +150,12 @@ export class RefactoringEngine {
|
|
|
138
150
|
} else {
|
|
139
151
|
this.analyzer.parseFile(filePath, fileContent, node);
|
|
140
152
|
}
|
|
153
|
+
// Secret scan on freshly parsed content
|
|
154
|
+
const secretFindings = this.secretScanner.scanFileContent(filePath, fileContent);
|
|
155
|
+
if (secretFindings.length > 0) {
|
|
156
|
+
node.securityThreats = (node.securityThreats || []).concat(secretFindings);
|
|
157
|
+
secretFindings.forEach(f => this.context.allSecretFindings.push(f));
|
|
158
|
+
}
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
this.magicDetector.injectVirtualConsumerEdges(filePath, node, activeFrameworkEcosystems);
|
|
@@ -189,8 +207,33 @@ export class RefactoringEngine {
|
|
|
189
207
|
this.circularDetector.formatCycles().forEach(c => console.log(ansis.dim(` ⢠${c}`)));
|
|
190
208
|
}
|
|
191
209
|
|
|
210
|
+
// Pass 4b: Report hardcoded secrets
|
|
211
|
+
console.log(ansis.dim('š Scanning for hardcoded secrets...'));
|
|
212
|
+
const allSecrets = this.context.allSecretFindings || [];
|
|
213
|
+
if (allSecrets.length > 0) {
|
|
214
|
+
const criticalSecrets = allSecrets.filter(s => s.severity === 'CRITICAL');
|
|
215
|
+
const otherSecrets = allSecrets.filter(s => s.severity !== 'CRITICAL');
|
|
216
|
+
console.log(ansis.bold.red(`\nš Hardcoded Secrets Detected (${allSecrets.length}):`) );
|
|
217
|
+
if (criticalSecrets.length > 0) {
|
|
218
|
+
console.log(ansis.red(` CRITICAL (${criticalSecrets.length}):`));
|
|
219
|
+
criticalSecrets.forEach(s => {
|
|
220
|
+
const relPath = path.relative(this.context.cwd, s.file);
|
|
221
|
+
const varInfo = s.variableName ? ` [${s.label}]` : ` [${s.label}]`;
|
|
222
|
+
console.log(ansis.dim(` ⢠${s.variableName || '<literal>'} in ${relPath}:${s.line}${varInfo}`));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (otherSecrets.length > 0) {
|
|
226
|
+
console.log(ansis.yellow(` HIGH/MEDIUM (${otherSecrets.length}):`));
|
|
227
|
+
otherSecrets.forEach(s => {
|
|
228
|
+
const relPath = path.relative(this.context.cwd, s.file);
|
|
229
|
+
console.log(ansis.dim(` ⢠${s.variableName || '<literal>'} in ${relPath}:${s.line} [${s.label}]`));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
192
234
|
// Pass 5: Compile metrics summary and print diagnostics report
|
|
193
235
|
const analysisSummary = await this.context.generateSummaryReport();
|
|
236
|
+
analysisSummary.structuralIssuesDetected.hardcodedSecrets = allSecrets;
|
|
194
237
|
this.displayConsoleDiagnostics(analysisSummary);
|
|
195
238
|
|
|
196
239
|
// Pass 6: Display Optimization Plan and Run Automated Structural Healing
|
|
@@ -275,6 +318,7 @@ export class RefactoringEngine {
|
|
|
275
318
|
const res = path.resolve(dir, entry.name);
|
|
276
319
|
if (entry.isDirectory()) {
|
|
277
320
|
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.scaffold-cache') continue;
|
|
321
|
+
if (this.context.verbose) console.log(ansis.dim(`š Scanning deep folder: ${res}`));
|
|
278
322
|
await this.discoverSourceFiles(res, fileList);
|
|
279
323
|
} else {
|
|
280
324
|
const ext = path.extname(entry.name);
|
|
@@ -304,6 +348,15 @@ export class RefactoringEngine {
|
|
|
304
348
|
}
|
|
305
349
|
}
|
|
306
350
|
|
|
351
|
+
// Pass A.2: Mark package entry points as library entries
|
|
352
|
+
for (const pkg of this.workspaceGraph.packageManifests.values()) {
|
|
353
|
+
for (const entryPath of pkg.entryPoints) {
|
|
354
|
+
if (this.context.graph.has(entryPath)) {
|
|
355
|
+
this.context.graph.get(entryPath).isLibraryEntry = true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
307
360
|
// Pass B: Link named-symbol imports through barrel/re-export chains
|
|
308
361
|
for (const specToken of node.importedSymbols) {
|
|
309
362
|
const delimiterIndex = specToken.indexOf(':');
|
|
@@ -326,6 +379,9 @@ export class RefactoringEngine {
|
|
|
326
379
|
if (traceResolution && this.context.graph.has(traceResolution.originFile)) {
|
|
327
380
|
this.context.graph.get(traceResolution.originFile).incomingEdges.add(filePath);
|
|
328
381
|
node.outgoingEdges.add(traceResolution.originFile);
|
|
382
|
+
// Fix: Store the absolute resolution in importedSymbols so isSymbolReferencedExternally can find it
|
|
383
|
+
// Use the traced symbol name (in case of re-exports with renaming)
|
|
384
|
+
node.importedSymbols.add(`${traceResolution.originFile}:${traceResolution.symbolName}`);
|
|
329
385
|
}
|
|
330
386
|
}
|
|
331
387
|
}
|
|
@@ -356,9 +412,11 @@ export class RefactoringEngine {
|
|
|
356
412
|
console.log(`š¾ Cache Optimization: ${summary.graphCacheOptimization.ratio} hits`);
|
|
357
413
|
|
|
358
414
|
console.log(ansis.bold('\nš Structural Integrity:'));
|
|
415
|
+
const secretCount = (summary.structuralIssuesDetected.hardcodedSecrets || []).length;
|
|
359
416
|
if (summary.structuralIssuesDetected.deadFiles.length === 0 &&
|
|
360
417
|
summary.structuralIssuesDetected.deadExports.length === 0 &&
|
|
361
|
-
summary.structuralIssuesDetected.unusedDependencies.length === 0
|
|
418
|
+
summary.structuralIssuesDetected.unusedDependencies.length === 0 &&
|
|
419
|
+
secretCount === 0) {
|
|
362
420
|
console.log(ansis.green(' ā
No major structural debt detected.'));
|
|
363
421
|
} else {
|
|
364
422
|
if (summary.structuralIssuesDetected.deadFiles.length > 0) {
|
|
@@ -370,6 +428,9 @@ export class RefactoringEngine {
|
|
|
370
428
|
if (summary.structuralIssuesDetected.unusedDependencies.length > 0) {
|
|
371
429
|
console.log(ansis.yellow(` š¦ Found ${summary.structuralIssuesDetected.unusedDependencies.length} unused dependencies.`));
|
|
372
430
|
}
|
|
431
|
+
if (secretCount > 0) {
|
|
432
|
+
console.log(ansis.red(` š Found ${secretCount} hardcoded secret(s) / credential(s).`));
|
|
433
|
+
}
|
|
373
434
|
}
|
|
374
435
|
|
|
375
436
|
console.log(ansis.dim('\n------------------------------------------------------------\n'));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Worker
|
|
1
|
+
import { Worker } from 'worker_threads';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Host CPU Thread-Distribution Pipeline Supervisor
|
|
@@ -11,7 +12,9 @@ export class WorkerPool {
|
|
|
11
12
|
this.context = context;
|
|
12
13
|
// Dynamically query host specs; default down to 1 if threading channels are choked
|
|
13
14
|
this.hardwareConcurrencyCoreCount = maximumConcurrencyLimit || os.availableParallelism?.() || os.cpus().length || 2;
|
|
14
|
-
|
|
15
|
+
// Resolve worker script path relative to this module
|
|
16
|
+
const __dir = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
this.workerScriptPath = path.resolve(__dir, 'WorkerTaskRunner.js');
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -86,7 +89,7 @@ export class WorkerPool {
|
|
|
86
89
|
|
|
87
90
|
executeChunkInsideThread(fileChunkSubset) {
|
|
88
91
|
return new Promise((resolve, reject) => {
|
|
89
|
-
const workerInstance = new Worker(this.workerScriptPath, {
|
|
92
|
+
const workerInstance = new Worker(this.workerScriptPath, { type: 'module',
|
|
90
93
|
workerData: { files: fileChunkSubset, contextOptions: { verbose: this.context.verbose } }
|
|
91
94
|
});
|
|
92
95
|
|
|
@@ -83,6 +83,15 @@ export class WorkspaceGraph {
|
|
|
83
83
|
|
|
84
84
|
if (workspaceGlobs.length > 0) {
|
|
85
85
|
this.context.isWorkspaceEnabled = true;
|
|
86
|
+
if (this.context.verbose) {
|
|
87
|
+
console.log(`š Auto-detected monorepo layout with ${workspaceGlobs.length} glob patterns.`);
|
|
88
|
+
}
|
|
89
|
+
} else if (this.context.isWorkspaceEnabled) {
|
|
90
|
+
// Force enabled via flag but no patterns found; default to standard packages/*
|
|
91
|
+
workspaceGlobs = ['packages/*'];
|
|
92
|
+
if (this.context.verbose) {
|
|
93
|
+
console.log(`š Workspace mode forced via flag. Using default patterns: ${workspaceGlobs.join(', ')}`);
|
|
94
|
+
}
|
|
86
95
|
} else {
|
|
87
96
|
return; // Workspace mesh maps skipped for single-package targets
|
|
88
97
|
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { parentPort, workerData } from 'worker_threads';
|
|
2
|
-
import { ASTAnalyzer } from '../ast/ASTAnalyzer.js';
|
|
3
|
-
import ts from 'typescript';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Isolated Worker Thread Target Pipeline Task Loop Execution Instance
|
|
8
|
-
*/
|
|
9
|
-
async function processThreadChunks() {
|
|
10
|
-
const { files, contextOptions } = workerData;
|
|
11
|
-
const partialGraphPayloadResults = [];
|
|
12
|
-
|
|
13
|
-
// Construct a lightweight standalone instance of our analyzer core inside the worker
|
|
14
|
-
const standaloneAnalyzer = new ASTAnalyzer({ verbose: contextOptions.verbose });
|
|
15
|
-
|
|
16
|
-
for (const file of files) {
|
|
17
|
-
if (file.endsWith('package.json')) continue;
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
21
|
-
|
|
22
|
-
// Build a minimal virtual reference mapping node to capture features
|
|
23
|
-
const mockNode = {
|
|
24
|
-
explicitImports: new Set(),
|
|
25
|
-
dynamicImports: new Set(),
|
|
26
|
-
importedSymbols: new Set(),
|
|
27
|
-
rawStringReferences: new Set(),
|
|
28
|
-
instantiatedIdentifiers: new Set(),
|
|
29
|
-
propertyAccessChains: new Set(),
|
|
30
|
-
internalExports: new Map(),
|
|
31
|
-
securityThreats: [],
|
|
32
|
-
localSuppressedRules: new Set(),
|
|
33
|
-
externalPackageUsage: new Set(),
|
|
34
|
-
symbolSourceLocations: new Map(),
|
|
35
|
-
calculatedDynamicImports: [],
|
|
36
|
-
jsxComponents: new Set(),
|
|
37
|
-
jsxProps: new Set(),
|
|
38
|
-
decorators: new Set()
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// Use the public getScriptKind() method added to ASTAnalyzer
|
|
42
|
-
const scriptKind = standaloneAnalyzer.getScriptKind(file);
|
|
43
|
-
|
|
44
|
-
const sourceFile = ts.createSourceFile(
|
|
45
|
-
file,
|
|
46
|
-
text,
|
|
47
|
-
ts.ScriptTarget.Latest,
|
|
48
|
-
true,
|
|
49
|
-
scriptKind
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
standaloneAnalyzer.extractTopLevelJSDocSuppreessions(sourceFile, mockNode);
|
|
53
|
-
// Use the walkNode() alias that maps to walkAST() with the correct argument order
|
|
54
|
-
standaloneAnalyzer.walkNode(sourceFile, sourceFile, mockNode);
|
|
55
|
-
|
|
56
|
-
partialGraphPayloadResults.push({
|
|
57
|
-
filePath: file,
|
|
58
|
-
explicitImports: Array.from(mockNode.explicitImports),
|
|
59
|
-
dynamicImports: Array.from(mockNode.dynamicImports),
|
|
60
|
-
importedSymbols: Array.from(mockNode.importedSymbols),
|
|
61
|
-
rawStringReferences: Array.from(mockNode.rawStringReferences),
|
|
62
|
-
instantiatedIdentifiers: Array.from(mockNode.instantiatedIdentifiers),
|
|
63
|
-
propertyAccessChains: Array.from(mockNode.propertyAccessChains),
|
|
64
|
-
internalExports: Object.fromEntries(mockNode.internalExports),
|
|
65
|
-
securityThreats: mockNode.securityThreats,
|
|
66
|
-
localSuppressedRules: Array.from(mockNode.localSuppressedRules),
|
|
67
|
-
externalPackageUsage: Array.from(mockNode.externalPackageUsage),
|
|
68
|
-
symbolSourceLocations: Object.fromEntries(mockNode.symbolSourceLocations),
|
|
69
|
-
calculatedDynamicImports: mockNode.calculatedDynamicImports
|
|
70
|
-
});
|
|
71
|
-
} catch (err) {
|
|
72
|
-
// Log parse errors in verbose mode so they are not silently swallowed
|
|
73
|
-
if (contextOptions.verbose) {
|
|
74
|
-
console.warn(`[Worker] Failed to parse ${file}: ${err.message}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Stream compiled metadata structures directly back to the primary supervisor pool thread channel
|
|
80
|
-
parentPort.postMessage(partialGraphPayloadResults);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
processThreadChunks();
|