@equilateral_ai/mindmeld 3.1.2 ā 3.2.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/hooks/pre-compact.js +28 -11
- package/hooks/session-start.js +10 -2
- package/package.json +2 -1
- package/scripts/harvest.js +59 -19
- package/scripts/init-project.js +80 -14
- package/scripts/inject.js +30 -9
- package/scripts/repo-analyzer.js +870 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MindMeld Repository Analyzer
|
|
5
|
+
*
|
|
6
|
+
* Rich repository analysis for CLI onboarding. Detects:
|
|
7
|
+
* - Technology stack (languages, frameworks, build systems)
|
|
8
|
+
* - Project structure and architecture patterns
|
|
9
|
+
* - Security anti-patterns (linked to remediation standards)
|
|
10
|
+
* - Existing documentation and standards
|
|
11
|
+
*
|
|
12
|
+
* Based on EquilateralAgents analysis patterns.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs').promises;
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// SECURITY ANTI-PATTERNS (from securityScanner.js)
|
|
20
|
+
// Each links to relevant standards for remediation
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
const SECURITY_PATTERNS = {
|
|
24
|
+
critical: [
|
|
25
|
+
{
|
|
26
|
+
name: 'Hardcoded AWS Credentials',
|
|
27
|
+
pattern: /(?:AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY)\s*=\s*["'][A-Z0-9]{20,}["']/gi,
|
|
28
|
+
description: 'AWS credentials hardcoded in source code',
|
|
29
|
+
recommendation: 'Use environment variables or AWS Secrets Manager',
|
|
30
|
+
cwe: 'CWE-798',
|
|
31
|
+
standardsLink: 'serverless-saas-aws/security/secrets_management.md'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Hardcoded API Keys',
|
|
35
|
+
pattern: /(?:api[_-]?key|apikey|api[_-]?secret)\s*=\s*["'][a-zA-Z0-9_\-]{20,}["']/gi,
|
|
36
|
+
description: 'API keys hardcoded in source code',
|
|
37
|
+
recommendation: 'Store API keys in environment variables or secure secret management',
|
|
38
|
+
cwe: 'CWE-798',
|
|
39
|
+
standardsLink: 'serverless-saas-aws/security/secrets_management.md'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Hardcoded Private Keys',
|
|
43
|
+
pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/gi,
|
|
44
|
+
description: 'Private cryptographic key found in source code',
|
|
45
|
+
recommendation: 'Never commit private keys. Use secure key management',
|
|
46
|
+
cwe: 'CWE-798',
|
|
47
|
+
standardsLink: 'serverless-saas-aws/security/secrets_management.md'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'SQL Injection Risk',
|
|
51
|
+
pattern: /(?:execute|query)\s*\(\s*["'](?:SELECT|INSERT|UPDATE|DELETE)[^"']*["']\s*\+/gi,
|
|
52
|
+
description: 'SQL query using string concatenation',
|
|
53
|
+
recommendation: 'Use parameterized queries or prepared statements',
|
|
54
|
+
cwe: 'CWE-89',
|
|
55
|
+
standardsLink: 'serverless-saas-aws/backend_handler_standards.md'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Command Injection Risk',
|
|
59
|
+
pattern: /(?:exec|spawn|system)\s*\([^)]*\$\{[^}]+\}[^)]*\)/gi,
|
|
60
|
+
description: 'Command execution with template literal interpolation',
|
|
61
|
+
recommendation: 'Validate and sanitize all input before command execution',
|
|
62
|
+
cwe: 'CWE-78',
|
|
63
|
+
standardsLink: 'serverless-saas-aws/security/input_validation.md'
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
high: [
|
|
67
|
+
{
|
|
68
|
+
name: 'Hardcoded Passwords',
|
|
69
|
+
pattern: /(?:password|passwd|pwd)\s*=\s*["'][^"']{8,}["']/gi,
|
|
70
|
+
description: 'Password hardcoded in source code',
|
|
71
|
+
recommendation: 'Use environment variables or secure credential management',
|
|
72
|
+
cwe: 'CWE-798',
|
|
73
|
+
standardsLink: 'serverless-saas-aws/security/secrets_management.md'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'Weak Hash - MD5',
|
|
77
|
+
pattern: /createHash\s*\(\s*["']md5["']\s*\)/gi,
|
|
78
|
+
description: 'MD5 hash algorithm used (cryptographically broken)',
|
|
79
|
+
recommendation: 'Use SHA-256 or stronger for security-critical operations',
|
|
80
|
+
cwe: 'CWE-327',
|
|
81
|
+
standardsLink: 'serverless-saas-aws/security/cryptography.md'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'Weak Hash - SHA1',
|
|
85
|
+
pattern: /createHash\s*\(\s*["']sha1["']\s*\)/gi,
|
|
86
|
+
description: 'SHA1 hash algorithm used (cryptographically weak)',
|
|
87
|
+
recommendation: 'Use SHA-256 or stronger for security-critical operations',
|
|
88
|
+
cwe: 'CWE-327',
|
|
89
|
+
standardsLink: 'serverless-saas-aws/security/cryptography.md'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'Insecure Random',
|
|
93
|
+
pattern: /Math\.random\(\)/gi,
|
|
94
|
+
description: 'Math.random() used (not cryptographically secure)',
|
|
95
|
+
recommendation: 'Use crypto.randomBytes() for security-sensitive operations',
|
|
96
|
+
cwe: 'CWE-338',
|
|
97
|
+
standardsLink: 'serverless-saas-aws/security/cryptography.md'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'Path Traversal Risk',
|
|
101
|
+
pattern: /(?:readFile|writeFile|unlink|rmdir)\s*\([^)]*\.\.[\/\\]/gi,
|
|
102
|
+
description: 'Potential path traversal vulnerability',
|
|
103
|
+
recommendation: 'Validate paths with path.normalize() and check bounds',
|
|
104
|
+
cwe: 'CWE-22',
|
|
105
|
+
standardsLink: 'serverless-saas-aws/security/input_validation.md'
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
medium: [
|
|
109
|
+
{
|
|
110
|
+
name: 'eval() Usage',
|
|
111
|
+
pattern: /\beval\s*\(/gi,
|
|
112
|
+
description: 'eval() can execute arbitrary code',
|
|
113
|
+
recommendation: 'Avoid eval(). Use JSON.parse() for JSON or safer alternatives',
|
|
114
|
+
cwe: 'CWE-95',
|
|
115
|
+
standardsLink: 'serverless-saas-aws/security/input_validation.md'
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'Insecure Deserialization',
|
|
119
|
+
pattern: /(?:unserialize|pickle\.loads|yaml\.load\()/gi,
|
|
120
|
+
description: 'Insecure deserialization pattern',
|
|
121
|
+
recommendation: 'Use safe deserialization methods and validate input',
|
|
122
|
+
cwe: 'CWE-502',
|
|
123
|
+
standardsLink: 'serverless-saas-aws/security/input_validation.md'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'HTTP instead of HTTPS',
|
|
127
|
+
pattern: /["']http:\/\/(?!localhost|127\.0\.0\.1)/gi,
|
|
128
|
+
description: 'HTTP URL found (unencrypted communication)',
|
|
129
|
+
recommendation: 'Use HTTPS for all external communications',
|
|
130
|
+
cwe: 'CWE-319',
|
|
131
|
+
standardsLink: 'serverless-saas-aws/security/transport.md'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'Disabled Certificate Validation',
|
|
135
|
+
pattern: /rejectUnauthorized\s*:\s*false/gi,
|
|
136
|
+
description: 'SSL/TLS certificate validation disabled',
|
|
137
|
+
recommendation: 'Enable certificate validation for HTTPS connections',
|
|
138
|
+
cwe: 'CWE-295',
|
|
139
|
+
standardsLink: 'serverless-saas-aws/security/transport.md'
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
low: [
|
|
143
|
+
{
|
|
144
|
+
name: 'Console.log in Production',
|
|
145
|
+
pattern: /console\.(log|debug|info)\(/gi,
|
|
146
|
+
description: 'Console logging may expose sensitive information',
|
|
147
|
+
recommendation: 'Use proper logging frameworks, avoid logging sensitive data',
|
|
148
|
+
cwe: 'CWE-532',
|
|
149
|
+
standardsLink: 'serverless-saas-aws/logging_standards.md'
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'Security TODO Comments',
|
|
153
|
+
pattern: /\/\/\s*(?:TODO|FIXME).*(?:security|password|key|token|auth)/gi,
|
|
154
|
+
description: 'Security-related TODO/FIXME comment found',
|
|
155
|
+
recommendation: 'Address security TODOs before production deployment',
|
|
156
|
+
cwe: 'CWE-1188',
|
|
157
|
+
standardsLink: null
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// TECHNOLOGY DETECTION (from PatternHarvestingAgent.js)
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
const TECH_SIGNATURES = {
|
|
167
|
+
// Languages
|
|
168
|
+
javascript: {
|
|
169
|
+
extensions: ['.js', '.mjs', '.cjs'],
|
|
170
|
+
configFiles: ['package.json', 'jsconfig.json'],
|
|
171
|
+
frameworks: {
|
|
172
|
+
react: { indicators: ['react', 'react-dom'], configFiles: [] },
|
|
173
|
+
vue: { indicators: ['vue'], configFiles: ['vue.config.js'] },
|
|
174
|
+
angular: { indicators: ['@angular/core'], configFiles: ['angular.json'] },
|
|
175
|
+
express: { indicators: ['express'], configFiles: [] },
|
|
176
|
+
nest: { indicators: ['@nestjs/core'], configFiles: ['nest-cli.json'] },
|
|
177
|
+
next: { indicators: ['next'], configFiles: ['next.config.js', 'next.config.mjs'] },
|
|
178
|
+
gatsby: { indicators: ['gatsby'], configFiles: ['gatsby-config.js'] }
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
typescript: {
|
|
182
|
+
extensions: ['.ts', '.tsx'],
|
|
183
|
+
configFiles: ['tsconfig.json'],
|
|
184
|
+
frameworks: {} // Same as JS
|
|
185
|
+
},
|
|
186
|
+
python: {
|
|
187
|
+
extensions: ['.py'],
|
|
188
|
+
configFiles: ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'],
|
|
189
|
+
frameworks: {
|
|
190
|
+
django: { indicators: ['django'], configFiles: ['manage.py'] },
|
|
191
|
+
flask: { indicators: ['flask'], configFiles: [] },
|
|
192
|
+
fastapi: { indicators: ['fastapi'], configFiles: [] }
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
java: {
|
|
196
|
+
extensions: ['.java'],
|
|
197
|
+
configFiles: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
|
198
|
+
frameworks: {
|
|
199
|
+
spring: { indicators: ['springframework', 'spring-boot'], configFiles: [] }
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
csharp: {
|
|
203
|
+
extensions: ['.cs'],
|
|
204
|
+
configFiles: ['*.csproj', '*.sln'],
|
|
205
|
+
frameworks: {
|
|
206
|
+
aspnet: { indicators: ['Microsoft.AspNetCore'], configFiles: [] }
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
go: {
|
|
210
|
+
extensions: ['.go'],
|
|
211
|
+
configFiles: ['go.mod', 'go.sum'],
|
|
212
|
+
frameworks: {}
|
|
213
|
+
},
|
|
214
|
+
rust: {
|
|
215
|
+
extensions: ['.rs'],
|
|
216
|
+
configFiles: ['Cargo.toml'],
|
|
217
|
+
frameworks: {}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Build systems and infrastructure
|
|
222
|
+
const INFRASTRUCTURE_SIGNATURES = {
|
|
223
|
+
docker: {
|
|
224
|
+
configFiles: ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', '.dockerignore']
|
|
225
|
+
},
|
|
226
|
+
kubernetes: {
|
|
227
|
+
configFiles: ['*.yaml', '*.yml'],
|
|
228
|
+
patterns: [/kind:\s*(?:Deployment|Service|Pod|ConfigMap)/]
|
|
229
|
+
},
|
|
230
|
+
terraform: {
|
|
231
|
+
configFiles: ['*.tf', 'terraform.tfvars']
|
|
232
|
+
},
|
|
233
|
+
sam: {
|
|
234
|
+
configFiles: ['template.yaml', 'template.yml', 'samconfig.toml']
|
|
235
|
+
},
|
|
236
|
+
serverless: {
|
|
237
|
+
configFiles: ['serverless.yml', 'serverless.yaml']
|
|
238
|
+
},
|
|
239
|
+
cloudformation: {
|
|
240
|
+
configFiles: ['cloudformation.yaml', 'cloudformation.yml', 'cfn-*.yaml']
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// PROJECT STRUCTURE PATTERNS (from ProjectStructureDiscoveryAgent.js)
|
|
246
|
+
// ============================================================================
|
|
247
|
+
|
|
248
|
+
const DIRECTORY_PATTERNS = {
|
|
249
|
+
backend: ['src/backend', 'backend', 'server', 'api', 'src/server', 'lambda', 'functions'],
|
|
250
|
+
handlers: ['src/handlers', 'handlers', 'api/handlers', 'lambda/handlers', 'functions'],
|
|
251
|
+
helpers: ['src/helpers', 'helpers', 'utils', 'lib', 'common', 'shared'],
|
|
252
|
+
frontend: ['src/frontend', 'frontend', 'client', 'web', 'ui', 'app', 'src/pages', 'src/components'],
|
|
253
|
+
database: ['database', 'db', 'migrations', 'prisma', 'sql', 'schemas'],
|
|
254
|
+
tests: ['tests', 'test', '__tests__', 'spec', 'e2e', 'integration'],
|
|
255
|
+
deployment: ['deployment', 'deploy', 'infrastructure', '.aws-sam', 'cloudformation', 'terraform', 'k8s'],
|
|
256
|
+
standards: ['.equilateral-standards', '.clinerules', 'standards', '.standards'],
|
|
257
|
+
docs: ['docs', 'documentation', 'doc']
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// DOCUMENTATION PATTERNS (from LibrarianAgent.js)
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
const DOC_PATTERNS = {
|
|
265
|
+
readme: {
|
|
266
|
+
patterns: [/^readme/i, /^README/],
|
|
267
|
+
description: 'Project README'
|
|
268
|
+
},
|
|
269
|
+
adr: {
|
|
270
|
+
patterns: [/adr[-_]?\d+/i, /decision[-_]record/i, /architecture[-_]decision/i],
|
|
271
|
+
description: 'Architecture Decision Records'
|
|
272
|
+
},
|
|
273
|
+
standards: {
|
|
274
|
+
patterns: [/standards/i, /guidelines/i, /conventions/i, /coding[-_]style/i],
|
|
275
|
+
description: 'Coding standards/guidelines'
|
|
276
|
+
},
|
|
277
|
+
api: {
|
|
278
|
+
patterns: [/^api/i, /swagger/i, /openapi/i],
|
|
279
|
+
description: 'API documentation'
|
|
280
|
+
},
|
|
281
|
+
contributing: {
|
|
282
|
+
patterns: [/contributing/i, /contribution/i],
|
|
283
|
+
description: 'Contribution guidelines'
|
|
284
|
+
},
|
|
285
|
+
changelog: {
|
|
286
|
+
patterns: [/changelog/i, /history/i, /releases/i],
|
|
287
|
+
description: 'Change log'
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// SCANNABLE FILES CONFIG
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
const SCANNABLE_EXTENSIONS = [
|
|
296
|
+
'.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.go', '.rb', '.php',
|
|
297
|
+
'.cpp', '.c', '.h', '.cs', '.swift', '.kt', '.rs', '.scala'
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const SKIP_DIRECTORIES = [
|
|
301
|
+
'node_modules', '.git', 'dist', 'build', 'coverage', '.next',
|
|
302
|
+
'target', 'vendor', '__pycache__', '.gradle', 'bin', 'obj',
|
|
303
|
+
'.aws-sam', '.serverless', '.terraform'
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// ANALYZER CLASS
|
|
308
|
+
// ============================================================================
|
|
309
|
+
|
|
310
|
+
class RepoAnalyzer {
|
|
311
|
+
constructor(projectPath) {
|
|
312
|
+
this.projectPath = projectPath;
|
|
313
|
+
this.results = {
|
|
314
|
+
timestamp: new Date().toISOString(),
|
|
315
|
+
projectPath,
|
|
316
|
+
projectName: path.basename(projectPath),
|
|
317
|
+
techStack: {
|
|
318
|
+
languages: [],
|
|
319
|
+
frameworks: [],
|
|
320
|
+
buildSystems: [],
|
|
321
|
+
infrastructure: []
|
|
322
|
+
},
|
|
323
|
+
structure: {
|
|
324
|
+
type: 'unknown',
|
|
325
|
+
directories: {},
|
|
326
|
+
hasTests: false,
|
|
327
|
+
hasDocs: false,
|
|
328
|
+
hasStandards: false
|
|
329
|
+
},
|
|
330
|
+
documentation: [],
|
|
331
|
+
securityFindings: {
|
|
332
|
+
critical: [],
|
|
333
|
+
high: [],
|
|
334
|
+
medium: [],
|
|
335
|
+
low: [],
|
|
336
|
+
summary: {}
|
|
337
|
+
},
|
|
338
|
+
recommendations: []
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Run full repository analysis
|
|
344
|
+
*/
|
|
345
|
+
async analyze(options = {}) {
|
|
346
|
+
const {
|
|
347
|
+
scanSecurity = true,
|
|
348
|
+
maxFiles = 500,
|
|
349
|
+
verbose = false
|
|
350
|
+
} = options;
|
|
351
|
+
|
|
352
|
+
console.log(`\nš Analyzing repository: ${this.results.projectName}`);
|
|
353
|
+
console.log('ā'.repeat(50));
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
// 1. Detect technology stack
|
|
357
|
+
if (verbose) console.log('\nš Detecting technology stack...');
|
|
358
|
+
await this.detectTechStack();
|
|
359
|
+
|
|
360
|
+
// 2. Analyze project structure
|
|
361
|
+
if (verbose) console.log('š Analyzing project structure...');
|
|
362
|
+
await this.analyzeStructure();
|
|
363
|
+
|
|
364
|
+
// 3. Discover documentation
|
|
365
|
+
if (verbose) console.log('š Discovering documentation...');
|
|
366
|
+
await this.discoverDocumentation();
|
|
367
|
+
|
|
368
|
+
// 4. Scan for security anti-patterns
|
|
369
|
+
if (scanSecurity) {
|
|
370
|
+
if (verbose) console.log('š Scanning for security anti-patterns...');
|
|
371
|
+
await this.scanSecurity(maxFiles);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 5. Generate recommendations
|
|
375
|
+
if (verbose) console.log('š” Generating recommendations...');
|
|
376
|
+
this.generateRecommendations();
|
|
377
|
+
|
|
378
|
+
return this.results;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
// Preserve full error context for debugging
|
|
381
|
+
this.results.error = {
|
|
382
|
+
message: error.message,
|
|
383
|
+
code: error.code || 'UNKNOWN',
|
|
384
|
+
phase: 'analysis'
|
|
385
|
+
};
|
|
386
|
+
console.error('Analysis error:', error.message);
|
|
387
|
+
return this.results;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Detect languages, frameworks, and build systems
|
|
393
|
+
*/
|
|
394
|
+
async detectTechStack() {
|
|
395
|
+
const files = await this.getAllFiles(this.projectPath, 2); // Shallow scan first
|
|
396
|
+
const fileCounts = {};
|
|
397
|
+
const detectedFrameworks = new Set();
|
|
398
|
+
const detectedInfra = new Set();
|
|
399
|
+
|
|
400
|
+
// Count files by extension
|
|
401
|
+
for (const file of files) {
|
|
402
|
+
const ext = path.extname(file).toLowerCase();
|
|
403
|
+
fileCounts[ext] = (fileCounts[ext] || 0) + 1;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Detect languages
|
|
407
|
+
for (const [language, config] of Object.entries(TECH_SIGNATURES)) {
|
|
408
|
+
const hasExtensions = config.extensions.some(ext => fileCounts[ext] > 0);
|
|
409
|
+
const hasConfig = await this.hasAnyFile(config.configFiles);
|
|
410
|
+
|
|
411
|
+
if (hasExtensions || hasConfig) {
|
|
412
|
+
const count = config.extensions.reduce((sum, ext) => sum + (fileCounts[ext] || 0), 0);
|
|
413
|
+
this.results.techStack.languages.push({
|
|
414
|
+
name: language,
|
|
415
|
+
fileCount: count,
|
|
416
|
+
hasConfig
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Check for frameworks
|
|
420
|
+
for (const [framework, fwConfig] of Object.entries(config.frameworks || {})) {
|
|
421
|
+
const hasFramework = await this.detectFramework(framework, fwConfig);
|
|
422
|
+
if (hasFramework) {
|
|
423
|
+
detectedFrameworks.add(framework);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Detect infrastructure
|
|
430
|
+
for (const [infra, config] of Object.entries(INFRASTRUCTURE_SIGNATURES)) {
|
|
431
|
+
const hasInfra = await this.hasAnyFile(config.configFiles);
|
|
432
|
+
if (hasInfra) {
|
|
433
|
+
detectedInfra.add(infra);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
this.results.techStack.frameworks = Array.from(detectedFrameworks);
|
|
438
|
+
this.results.techStack.infrastructure = Array.from(detectedInfra);
|
|
439
|
+
|
|
440
|
+
// Sort languages by file count
|
|
441
|
+
this.results.techStack.languages.sort((a, b) => b.fileCount - a.fileCount);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Detect a specific framework
|
|
446
|
+
*/
|
|
447
|
+
async detectFramework(name, config) {
|
|
448
|
+
// Check config files
|
|
449
|
+
if (config.configFiles?.length) {
|
|
450
|
+
const hasConfig = await this.hasAnyFile(config.configFiles);
|
|
451
|
+
if (hasConfig) return true;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Check package.json dependencies
|
|
455
|
+
if (config.indicators?.length) {
|
|
456
|
+
const packageJson = await this.readPackageJson();
|
|
457
|
+
if (packageJson) {
|
|
458
|
+
const allDeps = {
|
|
459
|
+
...(packageJson.dependencies || {}),
|
|
460
|
+
...(packageJson.devDependencies || {})
|
|
461
|
+
};
|
|
462
|
+
return config.indicators.some(ind => allDeps[ind]);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Analyze project structure
|
|
471
|
+
*/
|
|
472
|
+
async analyzeStructure() {
|
|
473
|
+
const discovered = {};
|
|
474
|
+
|
|
475
|
+
for (const [category, patterns] of Object.entries(DIRECTORY_PATTERNS)) {
|
|
476
|
+
for (const pattern of patterns) {
|
|
477
|
+
const fullPath = path.join(this.projectPath, pattern);
|
|
478
|
+
try {
|
|
479
|
+
const stats = await fs.stat(fullPath);
|
|
480
|
+
if (stats.isDirectory()) {
|
|
481
|
+
if (!discovered[category]) {
|
|
482
|
+
discovered[category] = [];
|
|
483
|
+
}
|
|
484
|
+
discovered[category].push(pattern);
|
|
485
|
+
}
|
|
486
|
+
} catch (error) {
|
|
487
|
+
// Expected: directory doesn't exist (ENOENT) or not accessible (EACCES)
|
|
488
|
+
if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
|
|
489
|
+
console.error(`Unexpected error checking ${pattern}:`, error.message);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this.results.structure.directories = discovered;
|
|
496
|
+
this.results.structure.hasTests = !!discovered.tests?.length;
|
|
497
|
+
this.results.structure.hasDocs = !!discovered.docs?.length;
|
|
498
|
+
this.results.structure.hasStandards = !!discovered.standards?.length;
|
|
499
|
+
|
|
500
|
+
// Detect project type
|
|
501
|
+
this.results.structure.type = this.detectProjectType(discovered);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Detect project type based on structure
|
|
506
|
+
*/
|
|
507
|
+
detectProjectType(directories) {
|
|
508
|
+
const hasBackend = !!directories.backend?.length || !!directories.handlers?.length;
|
|
509
|
+
const hasFrontend = !!directories.frontend?.length;
|
|
510
|
+
const hasInfra = this.results.techStack.infrastructure.length > 0;
|
|
511
|
+
|
|
512
|
+
if (hasBackend && hasFrontend) return 'fullstack';
|
|
513
|
+
if (hasBackend && hasInfra) return 'serverless-backend';
|
|
514
|
+
if (hasBackend) return 'backend';
|
|
515
|
+
if (hasFrontend) return 'frontend';
|
|
516
|
+
return 'library';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Discover existing documentation
|
|
521
|
+
*/
|
|
522
|
+
async discoverDocumentation() {
|
|
523
|
+
const docs = [];
|
|
524
|
+
|
|
525
|
+
// Scan root and docs directories
|
|
526
|
+
const dirsToScan = [this.projectPath];
|
|
527
|
+
if (this.results.structure.directories.docs) {
|
|
528
|
+
for (const docDir of this.results.structure.directories.docs) {
|
|
529
|
+
dirsToScan.push(path.join(this.projectPath, docDir));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for (const dir of dirsToScan) {
|
|
534
|
+
try {
|
|
535
|
+
const entries = await fs.readdir(dir);
|
|
536
|
+
for (const entry of entries) {
|
|
537
|
+
if (entry.endsWith('.md') || entry.endsWith('.txt')) {
|
|
538
|
+
const docType = this.categorizeDoc(entry);
|
|
539
|
+
if (docType) {
|
|
540
|
+
docs.push({
|
|
541
|
+
path: path.join(dir, entry),
|
|
542
|
+
relativePath: path.relative(this.projectPath, path.join(dir, entry)),
|
|
543
|
+
name: entry,
|
|
544
|
+
type: docType.type,
|
|
545
|
+
description: docType.description
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
// Expected: directory doesn't exist or not readable
|
|
552
|
+
if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
|
|
553
|
+
console.error(`Unexpected error scanning ${dir}:`, error.message);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
this.results.documentation = docs;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Categorize a documentation file
|
|
563
|
+
*/
|
|
564
|
+
categorizeDoc(filename) {
|
|
565
|
+
for (const [type, config] of Object.entries(DOC_PATTERNS)) {
|
|
566
|
+
if (config.patterns.some(p => p.test(filename))) {
|
|
567
|
+
return { type, description: config.description };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Generic markdown
|
|
571
|
+
if (filename.endsWith('.md')) {
|
|
572
|
+
return { type: 'other', description: 'Documentation' };
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Scan for security anti-patterns
|
|
579
|
+
*/
|
|
580
|
+
async scanSecurity(maxFiles = 500) {
|
|
581
|
+
const files = await this.getAllFiles(this.projectPath, 10);
|
|
582
|
+
const scannableFiles = files
|
|
583
|
+
.filter(f => SCANNABLE_EXTENSIONS.includes(path.extname(f).toLowerCase()))
|
|
584
|
+
.slice(0, maxFiles);
|
|
585
|
+
|
|
586
|
+
let scanned = 0;
|
|
587
|
+
for (const file of scannableFiles) {
|
|
588
|
+
try {
|
|
589
|
+
const content = await fs.readFile(file, 'utf8');
|
|
590
|
+
const lines = content.split('\n');
|
|
591
|
+
const relativePath = path.relative(this.projectPath, file);
|
|
592
|
+
|
|
593
|
+
for (const [severity, patterns] of Object.entries(SECURITY_PATTERNS)) {
|
|
594
|
+
for (const pattern of patterns) {
|
|
595
|
+
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
|
|
596
|
+
|
|
597
|
+
lines.forEach((line, lineIndex) => {
|
|
598
|
+
if (regex.test(line)) {
|
|
599
|
+
this.results.securityFindings[severity].push({
|
|
600
|
+
file: relativePath,
|
|
601
|
+
line: lineIndex + 1,
|
|
602
|
+
finding: pattern.name,
|
|
603
|
+
description: pattern.description,
|
|
604
|
+
recommendation: pattern.recommendation,
|
|
605
|
+
cwe: pattern.cwe,
|
|
606
|
+
standardsLink: pattern.standardsLink,
|
|
607
|
+
snippet: line.trim().substring(0, 100)
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
scanned++;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
// Expected: file not readable or binary file
|
|
616
|
+
if (error.code !== 'ENOENT' && error.code !== 'EACCES' && !error.message.includes('encoding')) {
|
|
617
|
+
console.error(`Unexpected error scanning ${file}:`, error.message);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Summary
|
|
623
|
+
this.results.securityFindings.summary = {
|
|
624
|
+
filesScanned: scanned,
|
|
625
|
+
critical: this.results.securityFindings.critical.length,
|
|
626
|
+
high: this.results.securityFindings.high.length,
|
|
627
|
+
medium: this.results.securityFindings.medium.length,
|
|
628
|
+
low: this.results.securityFindings.low.length,
|
|
629
|
+
total: this.results.securityFindings.critical.length +
|
|
630
|
+
this.results.securityFindings.high.length +
|
|
631
|
+
this.results.securityFindings.medium.length +
|
|
632
|
+
this.results.securityFindings.low.length
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Generate recommendations based on analysis
|
|
638
|
+
*/
|
|
639
|
+
generateRecommendations() {
|
|
640
|
+
const recs = [];
|
|
641
|
+
|
|
642
|
+
// Tech stack recommendations
|
|
643
|
+
const primaryLang = this.results.techStack.languages[0]?.name;
|
|
644
|
+
if (primaryLang) {
|
|
645
|
+
recs.push({
|
|
646
|
+
category: 'standards',
|
|
647
|
+
priority: 'high',
|
|
648
|
+
title: `${primaryLang} Standards Available`,
|
|
649
|
+
description: `Apply ${primaryLang} coding standards and best practices`,
|
|
650
|
+
standardsPack: `${primaryLang}-standards`
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Infrastructure recommendations
|
|
655
|
+
if (this.results.techStack.infrastructure.includes('sam')) {
|
|
656
|
+
recs.push({
|
|
657
|
+
category: 'standards',
|
|
658
|
+
priority: 'high',
|
|
659
|
+
title: 'AWS SAM Standards Available',
|
|
660
|
+
description: 'Apply serverless Lambda handler patterns and SAM template standards',
|
|
661
|
+
standardsPack: 'serverless-saas-aws'
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Security recommendations
|
|
666
|
+
const secSummary = this.results.securityFindings.summary;
|
|
667
|
+
if (secSummary.critical > 0 || secSummary.high > 0) {
|
|
668
|
+
recs.push({
|
|
669
|
+
category: 'security',
|
|
670
|
+
priority: 'critical',
|
|
671
|
+
title: 'Security Issues Detected',
|
|
672
|
+
description: `Found ${secSummary.critical} critical and ${secSummary.high} high severity security issues`,
|
|
673
|
+
action: 'Review security findings and apply recommended fixes'
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Structure recommendations
|
|
678
|
+
if (!this.results.structure.hasTests) {
|
|
679
|
+
recs.push({
|
|
680
|
+
category: 'quality',
|
|
681
|
+
priority: 'medium',
|
|
682
|
+
title: 'No Test Directory Found',
|
|
683
|
+
description: 'Consider adding automated tests',
|
|
684
|
+
standardsPack: 'testing-standards'
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (!this.results.structure.hasStandards && !this.results.structure.hasDocs) {
|
|
689
|
+
recs.push({
|
|
690
|
+
category: 'documentation',
|
|
691
|
+
priority: 'low',
|
|
692
|
+
title: 'No Standards or Documentation Found',
|
|
693
|
+
description: 'Consider documenting coding standards and architecture decisions',
|
|
694
|
+
action: 'MindMeld can inject standards based on your tech stack'
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
this.results.recommendations = recs;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ============================================================================
|
|
702
|
+
// HELPER METHODS
|
|
703
|
+
// ============================================================================
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Get all files in a directory recursively
|
|
707
|
+
*/
|
|
708
|
+
async getAllFiles(dirPath, maxDepth = 10, currentDepth = 0) {
|
|
709
|
+
const files = [];
|
|
710
|
+
if (currentDepth >= maxDepth) return files;
|
|
711
|
+
|
|
712
|
+
try {
|
|
713
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
714
|
+
|
|
715
|
+
for (const entry of entries) {
|
|
716
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
717
|
+
|
|
718
|
+
if (entry.isDirectory()) {
|
|
719
|
+
if (!SKIP_DIRECTORIES.includes(entry.name)) {
|
|
720
|
+
files.push(...await this.getAllFiles(fullPath, maxDepth, currentDepth + 1));
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
files.push(fullPath);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
} catch (error) {
|
|
727
|
+
// Expected: directory not readable or doesn't exist
|
|
728
|
+
if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
|
|
729
|
+
console.error(`Unexpected error reading ${dirPath}:`, error.message);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return files;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Check if any of the given files exist
|
|
738
|
+
*/
|
|
739
|
+
async hasAnyFile(patterns) {
|
|
740
|
+
for (const pattern of patterns) {
|
|
741
|
+
if (pattern.includes('*')) {
|
|
742
|
+
// Glob pattern - simplified check
|
|
743
|
+
const dir = path.dirname(pattern) || '.';
|
|
744
|
+
const ext = path.extname(pattern);
|
|
745
|
+
try {
|
|
746
|
+
const entries = await fs.readdir(path.join(this.projectPath, dir));
|
|
747
|
+
if (entries.some(e => e.endsWith(ext))) return true;
|
|
748
|
+
} catch (error) {
|
|
749
|
+
// Expected: directory doesn't exist
|
|
750
|
+
if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
|
|
751
|
+
console.error(`Unexpected error checking ${dir}:`, error.message);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
try {
|
|
756
|
+
await fs.access(path.join(this.projectPath, pattern));
|
|
757
|
+
return true;
|
|
758
|
+
} catch (error) {
|
|
759
|
+
// Expected: file doesn't exist
|
|
760
|
+
if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
|
|
761
|
+
console.error(`Unexpected error checking ${pattern}:`, error.message);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Read package.json if it exists
|
|
771
|
+
*/
|
|
772
|
+
async readPackageJson() {
|
|
773
|
+
try {
|
|
774
|
+
const content = await fs.readFile(path.join(this.projectPath, 'package.json'), 'utf8');
|
|
775
|
+
return JSON.parse(content);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
// Expected: package.json doesn't exist or is invalid JSON
|
|
778
|
+
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) {
|
|
779
|
+
console.error('Unexpected error reading package.json:', error.message);
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Generate a human-readable summary
|
|
787
|
+
*/
|
|
788
|
+
getSummary() {
|
|
789
|
+
const r = this.results;
|
|
790
|
+
const lines = [];
|
|
791
|
+
|
|
792
|
+
lines.push(`\nš¦ Project: ${r.projectName}`);
|
|
793
|
+
lines.push(` Type: ${r.structure.type}`);
|
|
794
|
+
|
|
795
|
+
if (r.techStack.languages.length) {
|
|
796
|
+
const langs = r.techStack.languages.map(l => `${l.name}(${l.fileCount})`).join(', ');
|
|
797
|
+
lines.push(`\nš§ Languages: ${langs}`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (r.techStack.frameworks.length) {
|
|
801
|
+
lines.push(` Frameworks: ${r.techStack.frameworks.join(', ')}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (r.techStack.infrastructure.length) {
|
|
805
|
+
lines.push(` Infrastructure: ${r.techStack.infrastructure.join(', ')}`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (r.documentation.length) {
|
|
809
|
+
lines.push(`\nš Documentation found: ${r.documentation.length} files`);
|
|
810
|
+
for (const doc of r.documentation.slice(0, 5)) {
|
|
811
|
+
lines.push(` - ${doc.relativePath} (${doc.description})`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const sec = r.securityFindings.summary;
|
|
816
|
+
if (sec.total > 0) {
|
|
817
|
+
lines.push(`\nš Security findings:`);
|
|
818
|
+
if (sec.critical) lines.push(` ā Critical: ${sec.critical}`);
|
|
819
|
+
if (sec.high) lines.push(` š“ High: ${sec.high}`);
|
|
820
|
+
if (sec.medium) lines.push(` š” Medium: ${sec.medium}`);
|
|
821
|
+
if (sec.low) lines.push(` š¢ Low: ${sec.low}`);
|
|
822
|
+
} else if (sec.filesScanned > 0) {
|
|
823
|
+
lines.push(`\nā
No security issues found in ${sec.filesScanned} files scanned`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (r.recommendations.length) {
|
|
827
|
+
lines.push(`\nš” Recommendations:`);
|
|
828
|
+
for (const rec of r.recommendations) {
|
|
829
|
+
const icon = rec.priority === 'critical' ? 'ā' : rec.priority === 'high' ? 'š¶' : 'š”';
|
|
830
|
+
lines.push(` ${icon} ${rec.title}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return lines.join('\n');
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ============================================================================
|
|
839
|
+
// CLI ENTRY POINT
|
|
840
|
+
// ============================================================================
|
|
841
|
+
|
|
842
|
+
async function main() {
|
|
843
|
+
const args = process.argv.slice(2);
|
|
844
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
845
|
+
const json = args.includes('--json');
|
|
846
|
+
|
|
847
|
+
// Filter out flags to get the path
|
|
848
|
+
const pathArg = args.find(a => !a.startsWith('-'));
|
|
849
|
+
const projectPath = pathArg || process.cwd();
|
|
850
|
+
|
|
851
|
+
const analyzer = new RepoAnalyzer(projectPath);
|
|
852
|
+
const results = await analyzer.analyze({ verbose });
|
|
853
|
+
|
|
854
|
+
if (json) {
|
|
855
|
+
console.log(JSON.stringify(results, null, 2));
|
|
856
|
+
} else {
|
|
857
|
+
console.log(analyzer.getSummary());
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Export for use as module
|
|
862
|
+
module.exports = { RepoAnalyzer, SECURITY_PATTERNS, TECH_SIGNATURES, DIRECTORY_PATTERNS };
|
|
863
|
+
|
|
864
|
+
// Run if called directly
|
|
865
|
+
if (require.main === module) {
|
|
866
|
+
main().catch(error => {
|
|
867
|
+
console.error('Fatal error:', error.message);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
});
|
|
870
|
+
}
|