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 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 --run
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.5",
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": "node bin/cli.js",
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",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ============================================================================
3
- * šŸ“¦ pkg-scaffold v3.3.5: Enterprise In-Memory Codebase State Manifest
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
- const importKey = `${path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/')}:${symbolName}`;
68
- const importKeyAlt = `${path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/').replace(/\.(js|ts|tsx|jsx)$/, '')}:${symbolName}`;
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(importKey) || parentNode.importedSymbols.has(importKeyAlt)) {
73
+ if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) {
71
74
  return true;
72
75
  }
73
76
 
74
- // Check for star imports or namespace imports
75
- const starKey = `${path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/')}:*`;
76
- const starKeyAlt = `${path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/').replace(/\.(js|ts|tsx|jsx)$/, '')}:*`;
77
- if (parentNode.importedSymbols.has(starKey) || parentNode.importedSymbols.has(starKeyAlt)) {
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);
@@ -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
  });
@@ -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/index.ts', '/src/index.tsx',
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
 
@@ -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
- this.oxc = require("oxc-parser");
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
- this.isAvailable = false;
9
- if (this.context.verbose) {
10
- console.warn("[OxcAnalyzer] oxc-parser not found, falling back to TypeScript compiler API.");
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) return false;
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
- // FIX: Register re-export source as an explicit import so the graph linker
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
- // FIX: Register as wildcard importedSymbol so graph linker creates incomingEdge
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
- // FIX: Register re-export source as an explicit import
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
- // FIX: Register as importedSymbol so barrel-tracer can resolve origin file
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.4.0: Unified Architectural Refactoring Orchestrator
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, isMainThread, parentPort, workerData } from 'worker_threads';
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
- this.workerScriptPath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'WorkerTaskRunner.js');
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();