thuban 0.3.3 → 0.3.4

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.
Files changed (66) hide show
  1. package/dist/LICENSE +21 -0
  2. package/dist/README.md +185 -0
  3. package/dist/cli.js +2 -0
  4. package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
  5. package/dist/packages/scanner/alert-manager.js +1 -0
  6. package/dist/packages/scanner/baseline-manager.js +1 -0
  7. package/dist/packages/scanner/code-scanner.js +1 -0
  8. package/dist/packages/scanner/codebase-passport.js +1 -0
  9. package/dist/packages/scanner/copy-paste-detector.js +1 -0
  10. package/dist/packages/scanner/dependency-graph.js +1 -0
  11. package/dist/packages/scanner/drift-detector.js +1 -0
  12. package/dist/packages/scanner/executive-report.js +1 -0
  13. package/dist/packages/scanner/file-collector.js +1 -0
  14. package/dist/packages/scanner/file-watcher.js +1 -0
  15. package/dist/packages/scanner/ghost-code-detector.js +1 -0
  16. package/dist/packages/scanner/hallucination-detector.js +1 -0
  17. package/dist/packages/scanner/health-checker.js +1 -0
  18. package/dist/packages/scanner/html-report.js +1 -0
  19. package/dist/packages/scanner/index.js +1 -0
  20. package/dist/packages/scanner/license-manager.js +1 -0
  21. package/dist/packages/scanner/master-health-checker.js +1 -0
  22. package/dist/packages/scanner/monitor-notifier.js +1 -0
  23. package/dist/packages/scanner/monitor-service.js +1 -0
  24. package/dist/packages/scanner/monitor-store.js +1 -0
  25. package/dist/packages/scanner/pre-commit-gate.js +1 -0
  26. package/dist/packages/scanner/scan-diff.js +1 -0
  27. package/dist/packages/scanner/scan-runner.js +1 -0
  28. package/dist/packages/scanner/secret-scanner.js +1 -0
  29. package/dist/packages/scanner/sentinel-core.js +1 -0
  30. package/dist/packages/scanner/sentinel-knowledge.js +1 -0
  31. package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
  32. package/dist/packages/scanner/tech-debt-cost.js +1 -0
  33. package/dist/packages/scanner/widget-generator.js +1 -0
  34. package/package.json +12 -7
  35. package/cli.js +0 -2627
  36. package/packages/scanner/ai-confidence-scorer.js +0 -260
  37. package/packages/scanner/alert-manager.js +0 -398
  38. package/packages/scanner/baseline-manager.js +0 -109
  39. package/packages/scanner/code-scanner.js +0 -758
  40. package/packages/scanner/codebase-passport.js +0 -299
  41. package/packages/scanner/copy-paste-detector.js +0 -276
  42. package/packages/scanner/dependency-graph.js +0 -541
  43. package/packages/scanner/drift-detector.js +0 -423
  44. package/packages/scanner/executive-report.js +0 -774
  45. package/packages/scanner/file-collector.js +0 -64
  46. package/packages/scanner/file-watcher.js +0 -323
  47. package/packages/scanner/ghost-code-detector.js +0 -301
  48. package/packages/scanner/hallucination-detector.js +0 -822
  49. package/packages/scanner/health-checker.js +0 -586
  50. package/packages/scanner/html-report.js +0 -634
  51. package/packages/scanner/index.js +0 -100
  52. package/packages/scanner/license-manager.js +0 -331
  53. package/packages/scanner/master-health-checker.js +0 -513
  54. package/packages/scanner/monitor-notifier.js +0 -64
  55. package/packages/scanner/monitor-service.js +0 -103
  56. package/packages/scanner/monitor-store.js +0 -117
  57. package/packages/scanner/pre-commit-gate.js +0 -216
  58. package/packages/scanner/scan-diff.js +0 -50
  59. package/packages/scanner/scan-runner.js +0 -172
  60. package/packages/scanner/secret-scanner.js +0 -612
  61. package/packages/scanner/secret-scanner.test.js +0 -103
  62. package/packages/scanner/sentinel-core.js +0 -616
  63. package/packages/scanner/sentinel-knowledge.js +0 -322
  64. package/packages/scanner/tech-debt-analyzer.js +0 -583
  65. package/packages/scanner/tech-debt-cost.js +0 -194
  66. package/packages/scanner/widget-generator.js +0 -415
@@ -1,758 +0,0 @@
1
- /**
2
- * Code Scanner
3
- *
4
- * @purpose Event-driven CodeScanner component
5
- * @module sentinel
6
- * @layer application
7
- * @imports-from fs, path, events
8
- * @exports-to sentinel
9
- * @side-effects File system reads, Spawns child processes, Console output, Event emission
10
- * @critical-for sentinel
11
- */
12
-
13
- /**
14
- * Sentinel Code Scanner
15
- *
16
- * Scans codebase for security, quality, performance, and AI-specific issues.
17
- * Based on CODEX/Thuban FORGE layer specifications.
18
- *
19
- * Categories:
20
- * - Security: Secrets, injection, auth gaps
21
- * - Quality: Complexity, duplication, dead code
22
- * - Performance: N+1, memory leaks, blocking
23
- * - Maintainability: Docs, nesting, coupling
24
- * - AI-Specific: Hallucinated imports, inconsistent patterns
25
- */
26
-
27
- const fs = require('fs');
28
- const path = require('path');
29
- const EventEmitter = require('events');
30
-
31
- class CodeScanner extends EventEmitter {
32
- constructor(config = {}) {
33
- super();
34
-
35
- this.config = {
36
- rootPath: config.rootPath || process.cwd(),
37
- ignorePatterns: config.ignorePatterns || [
38
- 'node_modules/**',
39
- '.git/**',
40
- 'dist/**',
41
- '.orion/snapshots/**'
42
- ],
43
- maxFileSize: config.maxFileSize || 1024 * 1024, // 1MB
44
- complexityThreshold: config.complexityThreshold || 15,
45
- nestingThreshold: config.nestingThreshold || 4,
46
- ...config
47
- };
48
-
49
- // Safe patterns that should NOT trigger security warnings
50
- this.safePatterns = [
51
- // Environment variable names (not actual secrets)
52
- /keyEnv\s*[:=]/i,
53
- /envVar\s*[:=]/i,
54
- /process\.env\[/,
55
- /process\.env\./,
56
- // Model version strings and hashes
57
- /model\s*[:=]\s*['"][^'"]+['"]/i,
58
- /version\s*[:=]\s*['"][^'"]+['"]/i, // version: "hash"
59
- /claude-|gpt-|gemini-|llama-|mistral-|deepseek-|grok-|sonar-/i,
60
- // API endpoints (URLs, not keys)
61
- /endpoint\s*[:=]/i,
62
- /https?:\/\//,
63
- // Common safe strings
64
- /_API_KEY['"]/, // env var names end with _API_KEY
65
- /API_KEY['"]\s*\)/, // process.env["API_KEY"]
66
- // Error messages and validation
67
- /errors?\.\w+\s*=/i, // errors.password = "message"
68
- /Error\s*[:=]/i,
69
- /required|invalid|must be|cannot be/i, // validation messages
70
- ];
71
-
72
- // Test file patterns (allow mock data)
73
- this.testFilePatterns = [
74
- /\.test\.(js|ts|jsx|tsx)$/,
75
- /\.spec\.(js|ts|jsx|tsx)$/,
76
- /__tests__\//,
77
- /\.mock\.(js|ts)$/,
78
- /test-.*\.js$/,
79
- ];
80
-
81
- // Issue patterns - security focused
82
- this.securityPatterns = [
83
- {
84
- id: 'SEC001',
85
- name: 'Hardcoded API Key',
86
- // Match strings that look like actual API keys:
87
- // - sk-xxx, pk-xxx (Stripe-style)
88
- // - xox-xxx (Slack-style)
89
- // - AKIA... (AWS-style)
90
- // - Mix of numbers/letters that looks random (not readable words)
91
- pattern: /['"](?:sk-|pk-|xox[pboa]-|AKIA|ghp_|gho_|github_pat_)[a-zA-Z0-9_-]{10,}['"]|['"][a-zA-Z0-9]{32,}['"]/,
92
- context: /(api[_-]?key|apikey|secret[_\s]*key|token|credential|bearer)/i,
93
- severity: 'critical',
94
- message: 'Possible hardcoded API key or secret detected',
95
- skipIfSafe: true // Skip if line matches safe patterns
96
- },
97
- {
98
- id: 'SEC002',
99
- name: 'Hardcoded Password',
100
- pattern: /password\s*[:=]\s*['"][^'"]+['"]/i,
101
- severity: 'critical',
102
- message: 'Hardcoded password detected',
103
- skipInTests: true, // Skip in test files
104
- skipIfSafe: true // Skip error messages and validation
105
- },
106
- {
107
- id: 'SEC003',
108
- name: 'SQL Injection Risk',
109
- // Only match actual SQL queries with string interpolation
110
- // Requires SQL keyword as whole word (not path.join etc)
111
- pattern: /\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|WHERE)\b.*(\$\{|\+\s*['"]|\+\s*\w+|f['"].*\{|%s|%d)/i,
112
- severity: 'error',
113
- message: 'Potential SQL injection - use parameterized queries'
114
- },
115
- {
116
- id: 'SEC004',
117
- name: 'eval() Usage',
118
- pattern: /\beval\s*\(/,
119
- severity: 'error',
120
- message: 'eval() is dangerous and should be avoided'
121
- },
122
- {
123
- id: 'SEC005',
124
- name: 'innerHTML Assignment',
125
- pattern: /\.innerHTML\s*=/,
126
- severity: 'warning',
127
- message: 'innerHTML can cause XSS - consider using textContent or sanitization'
128
- },
129
- {
130
- id: 'SEC006',
131
- name: 'Disabled Security',
132
- // JS/Node: rejectUnauthorized: false
133
- // Python: verify=False (requests)
134
- // Go: InsecureSkipVerify: true
135
- pattern: /rejectUnauthorized\s*:\s*false|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true/,
136
- severity: 'error',
137
- message: 'SSL certificate validation disabled'
138
- },
139
- {
140
- id: 'SEC007',
141
- name: 'Exposed .env Reference',
142
- pattern: /\.env['"]/,
143
- // JS: fs.read/write Python: open( Go: os.Open(
144
- context: /fs\.(read|write)|path\.join|open\s*\(|os\.Open/i,
145
- severity: 'warning',
146
- message: 'Direct .env file manipulation - ensure not exposed'
147
- },
148
- {
149
- id: 'SEC008',
150
- name: 'Unsafe Block (Rust)',
151
- pattern: /\bunsafe\s*\{/,
152
- severity: 'warning',
153
- message: 'Rust unsafe block - verify memory safety guarantees'
154
- },
155
- {
156
- id: 'SEC009',
157
- name: 'Shell Execution (PHP/Ruby)',
158
- pattern: /\bshell_exec\s*\(|\bpassthru\s*\(|`[^`]+`/,
159
- severity: 'error',
160
- message: 'Shell execution function - command injection risk if input unsanitized'
161
- }
162
- ];
163
-
164
- // Quality patterns
165
- this.qualityPatterns = [
166
- {
167
- id: 'QUAL001',
168
- name: 'Console.log in Production',
169
- pattern: /console\.(log|debug|info)\(/,
170
- severity: 'info',
171
- message: 'console.log found - consider using proper logging',
172
- // Only report in non-dev files - skip test files and dev utilities
173
- skipInTests: true,
174
- skipInDirs: ['test', 'scripts', 'tools', 'dev']
175
- },
176
- {
177
- id: 'QUAL002',
178
- name: 'TODO Comment',
179
- pattern: /\/\/\s*TODO|\/\*\s*TODO|\*\s*TODO|#\s*TODO/i,
180
- severity: 'info',
181
- message: 'TODO comment found - track in issue system'
182
- },
183
- {
184
- id: 'QUAL003',
185
- name: 'FIXME Comment',
186
- pattern: /\/\/\s*FIXME|\/\*\s*FIXME|\*\s*FIXME|#\s*FIXME/i,
187
- severity: 'warning',
188
- message: 'FIXME comment found - needs attention'
189
- },
190
- {
191
- id: 'QUAL004',
192
- name: 'Empty Catch Block',
193
- pattern: /catch\s*\([^)]*\)\s*\{\s*\}/,
194
- severity: 'warning',
195
- message: 'Empty catch block swallows errors'
196
- },
197
- {
198
- id: 'QUAL005',
199
- name: 'Magic Number',
200
- // Only flag suspicious numbers, not common ones like ports, timeouts, dates
201
- pattern: /(?<![0-9a-zA-Z_])(?!(?:1000|2000|3000|3001|4000|5000|8000|8080|9000|80|443|24|60|1024|2048|4096)\b)\d{5,}(?![0-9])/,
202
- severity: 'info',
203
- message: 'Magic number detected - consider using named constant',
204
- skipInTests: true
205
- },
206
- {
207
- id: 'QUAL006',
208
- name: 'Callback Hell',
209
- pattern: /\)\s*=>\s*\{[^}]*\)\s*=>\s*\{[^}]*\)\s*=>\s*\{/,
210
- severity: 'warning',
211
- message: 'Deeply nested callbacks - consider async/await'
212
- }
213
- ];
214
-
215
- // Performance patterns
216
- this.performancePatterns = [
217
- {
218
- id: 'PERF001',
219
- name: 'Sync FS in Async',
220
- pattern: /fs\.(readFileSync|writeFileSync|readdirSync|statSync)/,
221
- severity: 'warning',
222
- message: 'Synchronous file operation - may block event loop'
223
- },
224
- {
225
- id: 'PERF002',
226
- name: 'Missing Await',
227
- pattern: /async\s+function[^{]*\{[^}]*(?<!await\s)new\s+Promise/,
228
- severity: 'warning',
229
- message: 'Promise created in async function without await'
230
- },
231
- {
232
- id: 'PERF003',
233
- name: 'Loop Database Query',
234
- pattern: /for\s*\([^)]*\)\s*\{[^}]*(query|find|select|update)/i,
235
- severity: 'error',
236
- message: 'Possible N+1 query pattern - consider batch operation'
237
- },
238
- {
239
- id: 'PERF004',
240
- name: 'Large Array in Memory',
241
- pattern: /new\s+Array\s*\(\s*\d{6,}\s*\)/,
242
- severity: 'warning',
243
- message: 'Large array allocation - consider streaming'
244
- }
245
- ];
246
-
247
- // AI-specific patterns
248
- this.aiPatterns = [
249
- {
250
- id: 'AI001',
251
- name: 'Potentially Hallucinated Import',
252
- pattern: /require\(['"](?!\.)[^'"]+['"]\)/,
253
- severity: 'info',
254
- message: 'External dependency - verify it exists in package.json',
255
- check: (match, content, filePath) => this._checkDependencyExists(match, filePath)
256
- },
257
- {
258
- id: 'AI002',
259
- name: 'Unused Import',
260
- pattern: /const\s+(\w+)\s*=\s*require\(['"][^'"]+['"]\)/,
261
- severity: 'info',
262
- message: 'Import may be unused - verify usage',
263
- check: (match, content) => this._checkImportUsage(match, content)
264
- },
265
- {
266
- id: 'AI003',
267
- name: 'Over-Engineered Pattern',
268
- pattern: /class\s+\w+Factory\s*\{|AbstractFactory|Singleton\.getInstance/,
269
- severity: 'info',
270
- message: 'Complex pattern detected - ensure justified by requirements'
271
- }
272
- ];
273
- }
274
-
275
- /**
276
- * Scan all files in the codebase
277
- */
278
- async scanAll() {
279
- console.log('[CODE-SCANNER] Starting full codebase scan...');
280
- const startTime = Date.now();
281
-
282
- const result = {
283
- filesScanned: 0,
284
- issues: [],
285
- summary: {},
286
- duration: 0
287
- };
288
-
289
- try {
290
- const files = await this._discoverFiles(this.config.rootPath);
291
- console.log(`[CODE-SCANNER] Found ${files.length} files to scan`);
292
-
293
- for (const file of files) {
294
- const fileIssues = await this.scanFile(file);
295
- result.filesScanned++;
296
- result.issues.push(...fileIssues);
297
- }
298
-
299
- result.duration = Date.now() - startTime;
300
- result.summary = this._summarizeIssues(result.issues);
301
-
302
- console.log(`[CODE-SCANNER] Scan complete: ${result.filesScanned} files, ${result.issues.length} issues`);
303
- return result;
304
- } catch (error) {
305
- console.error('[CODE-SCANNER] Scan failed:', error.message);
306
- result.error = error.message;
307
- return result;
308
- }
309
- }
310
-
311
- /**
312
- * Scan specific files
313
- */
314
- async scanFiles(filePaths) {
315
- const result = {
316
- filesScanned: 0,
317
- issues: [],
318
- summary: {}
319
- };
320
-
321
- for (const filePath of filePaths) {
322
- const fileIssues = await this.scanFile(filePath);
323
- result.filesScanned++;
324
- result.issues.push(...fileIssues);
325
- }
326
-
327
- result.summary = this._summarizeIssues(result.issues);
328
- return result;
329
- }
330
-
331
- /**
332
- * Scan a single file
333
- */
334
- async scanFile(filePath) {
335
- const issues = [];
336
-
337
- try {
338
- // Check file size
339
- const stats = fs.statSync(filePath);
340
-
341
- // Skip symlinks that point outside the project (prevent loops)
342
- if (stats.isSymbolicLink && stats.isSymbolicLink()) {
343
- return [];
344
- }
345
-
346
- if (stats.size === 0) {
347
- return []; // Empty file — nothing to scan
348
- }
349
-
350
- if (stats.size > this.config.maxFileSize) {
351
- return [{
352
- file: filePath,
353
- category: 'quality',
354
- severity: 'info',
355
- id: 'FILE001',
356
- message: `File too large (${Math.round(stats.size / 1024)}KB) - skipped detailed scan`
357
- }];
358
- }
359
-
360
- // Read file content
361
- let rawContent;
362
- try {
363
- rawContent = fs.readFileSync(filePath);
364
- } catch (readErr) {
365
- return []; // Unreadable file — skip silently
366
- }
367
-
368
- // Detect binary files by checking for null bytes in first 512 bytes
369
- const sampleSize = Math.min(rawContent.length, 512);
370
- for (let i = 0; i < sampleSize; i++) {
371
- if (rawContent[i] === 0) {
372
- return []; // Binary file — skip
373
- }
374
- }
375
-
376
- const content = rawContent.toString('utf8');
377
- const lines = content.split('\n');
378
- const ext = path.extname(filePath).toLowerCase();
379
- const supportedExts = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.cs', '.php', '.rb'];
380
-
381
- // Scan supported source files with pattern matching
382
- if (supportedExts.includes(ext)) {
383
- // Security scan
384
- issues.push(...this._runPatternScan(filePath, content, lines, this.securityPatterns, 'security'));
385
-
386
- // Quality scan
387
- issues.push(...this._runPatternScan(filePath, content, lines, this.qualityPatterns, 'quality'));
388
-
389
- // Performance scan
390
- issues.push(...this._runPatternScan(filePath, content, lines, this.performancePatterns, 'performance'));
391
-
392
- // AI-specific scan
393
- issues.push(...this._runPatternScan(filePath, content, lines, this.aiPatterns, 'ai'));
394
-
395
- // Complexity analysis
396
- issues.push(...this._analyzeComplexity(filePath, content, lines));
397
-
398
- // Nesting analysis
399
- issues.push(...this._analyzeNesting(filePath, content, lines));
400
- }
401
-
402
- // JSON-specific checks
403
- if (ext === '.json') {
404
- issues.push(...this._scanJson(filePath, content));
405
- }
406
-
407
- // Emit event for each issue found
408
- for (const issue of issues) {
409
- this.emit('issueFound', issue);
410
- }
411
-
412
- return issues;
413
- } catch (error) {
414
- return [{
415
- file: filePath,
416
- category: 'error',
417
- severity: 'error',
418
- id: 'SCAN001',
419
- message: `Failed to scan file: ${error.message}`
420
- }];
421
- }
422
- }
423
-
424
- /**
425
- * Get current scan rules
426
- */
427
- getRules() {
428
- return {
429
- security: this.securityPatterns.map(p => ({ id: p.id, name: p.name, severity: p.severity })),
430
- quality: this.qualityPatterns.map(p => ({ id: p.id, name: p.name, severity: p.severity })),
431
- performance: this.performancePatterns.map(p => ({ id: p.id, name: p.name, severity: p.severity })),
432
- ai: this.aiPatterns.map(p => ({ id: p.id, name: p.name, severity: p.severity }))
433
- };
434
- }
435
-
436
- // Private methods
437
-
438
- async _discoverFiles(dir, files = []) {
439
- try {
440
- const entries = fs.readdirSync(dir, { withFileTypes: true });
441
-
442
- for (const entry of entries) {
443
- const fullPath = path.join(dir, entry.name);
444
- const relativePath = path.relative(this.config.rootPath, fullPath);
445
-
446
- // Check ignore patterns
447
- if (this._shouldIgnore(relativePath)) {
448
- continue;
449
- }
450
-
451
- if (entry.isDirectory()) {
452
- await this._discoverFiles(fullPath, files);
453
- } else if (entry.isFile()) {
454
- const ext = path.extname(entry.name).toLowerCase();
455
- if (['.js', '.ts', '.jsx', '.tsx', '.json', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.cs', '.php', '.rb'].includes(ext)) {
456
- files.push(fullPath);
457
- }
458
- }
459
- }
460
- } catch (error) {
461
- // Skip inaccessible directories
462
- }
463
-
464
- return files;
465
- }
466
-
467
- _shouldIgnore(relativePath) {
468
- const normalized = relativePath.replace(/\\/g, '/');
469
-
470
- // Always ignore any path containing these directories (anywhere in path)
471
- const alwaysIgnore = ['node_modules', '.git', 'dist', '.vite'];
472
- for (const dir of alwaysIgnore) {
473
- if (normalized.includes('/' + dir + '/') ||
474
- normalized.startsWith(dir + '/') ||
475
- normalized === dir ||
476
- normalized.endsWith('/' + dir)) {
477
- return true;
478
- }
479
- }
480
-
481
- for (const pattern of this.config.ignorePatterns) {
482
- const normalizedPattern = pattern.replace(/\\/g, '/');
483
-
484
- // Extract base directory from pattern (e.g., 'node_modules' from 'node_modules/**')
485
- const baseDir = normalizedPattern.split('/')[0].replace(/\*/g, '');
486
-
487
- // Check if path IS the ignored directory or starts with it
488
- if (baseDir && (normalized === baseDir || normalized.startsWith(baseDir + '/'))) {
489
- return true;
490
- }
491
-
492
- // Simple glob matching for more complex patterns
493
- let regexPattern = normalizedPattern
494
- .replace(/\./g, '\\.')
495
- .replace(/\*\*/g, '.*')
496
- .replace(/\*/g, '[^/]*');
497
-
498
- if (new RegExp('^' + regexPattern).test(normalized)) {
499
- return true;
500
- }
501
- }
502
-
503
- return false;
504
- }
505
-
506
- _runPatternScan(filePath, content, lines, patterns, category) {
507
- const issues = [];
508
-
509
- // Check if this is a test file (test against basename only to avoid matching parent folder names)
510
- const basename = path.basename(filePath);
511
- const isTestFile = this.testFilePatterns.some(p => p.test(basename)) ||
512
- this.testFilePatterns.some(p => p.test(filePath) && !p.source.includes('\\.js$'));
513
- const relativePath = path.relative(this.config.rootPath, filePath).replace(/\\/g, '/');
514
-
515
- for (const pattern of patterns) {
516
- // Skip this pattern entirely in test files if flagged
517
- if (pattern.skipInTests && isTestFile) {
518
- continue;
519
- }
520
-
521
- // Skip if file is in excluded directories
522
- if (pattern.skipInDirs) {
523
- const inSkippedDir = pattern.skipInDirs.some(dir =>
524
- relativePath.startsWith(dir + '/') || relativePath.includes('/' + dir + '/')
525
- );
526
- if (inSkippedDir) continue;
527
- }
528
-
529
- // Check context pattern first if it exists (file-level)
530
- if (pattern.context && !pattern.lineContext && !pattern.context.test(content)) {
531
- continue;
532
- }
533
-
534
- // Find all matches
535
- let lineNum = 0;
536
- for (const line of lines) {
537
- lineNum++;
538
- const match = line.match(pattern.pattern);
539
-
540
- if (match) {
541
- // Skip if line matches any safe pattern (for SEC001 type checks)
542
- if (pattern.skipIfSafe) {
543
- const isSafe = this.safePatterns.some(sp => sp.test(line));
544
- if (isSafe) continue;
545
- }
546
-
547
- // Run additional check if defined
548
- if (pattern.check) {
549
- const shouldReport = pattern.check(match, content, filePath);
550
- if (!shouldReport) continue;
551
- }
552
-
553
- issues.push({
554
- file: filePath,
555
- line: lineNum,
556
- column: match.index,
557
- category,
558
- severity: pattern.severity,
559
- id: pattern.id,
560
- name: pattern.name,
561
- message: pattern.message,
562
- code: line.trim().substring(0, 100)
563
- });
564
- }
565
- }
566
- }
567
-
568
- return issues;
569
- }
570
-
571
- _analyzeComplexity(filePath, content, lines) {
572
- const issues = [];
573
-
574
- // Count decision points (simplified cyclomatic complexity)
575
- const decisionKeywords = /\b(if|else|for|while|switch|case|catch|&&|\|\||\?)\b/g;
576
- const functionMatches = content.match(/function\s+\w+|=>\s*\{|async\s+\w+/g) || [];
577
- const decisionMatches = content.match(decisionKeywords) || [];
578
-
579
- const avgComplexity = functionMatches.length > 0
580
- ? decisionMatches.length / functionMatches.length
581
- : 0;
582
-
583
- if (avgComplexity > this.config.complexityThreshold) {
584
- issues.push({
585
- file: filePath,
586
- category: 'quality',
587
- severity: 'warning',
588
- id: 'CMPLX001',
589
- name: 'High Complexity',
590
- message: `Average cyclomatic complexity is ${avgComplexity.toFixed(1)} (threshold: ${this.config.complexityThreshold})`
591
- });
592
- }
593
-
594
- // Check file length
595
- if (lines.length > 500) {
596
- issues.push({
597
- file: filePath,
598
- category: 'quality',
599
- severity: 'info',
600
- id: 'CMPLX002',
601
- name: 'Long File',
602
- message: `File has ${lines.length} lines - consider splitting`
603
- });
604
- }
605
-
606
- return issues;
607
- }
608
-
609
- _analyzeNesting(filePath, content, lines) {
610
- const issues = [];
611
- let maxNesting = 0;
612
- let currentNesting = 0;
613
- let maxNestingLine = 0;
614
-
615
- let lineNum = 0;
616
- for (const line of lines) {
617
- lineNum++;
618
-
619
- // Count braces (simplified)
620
- const opens = (line.match(/\{/g) || []).length;
621
- const closes = (line.match(/\}/g) || []).length;
622
-
623
- currentNesting += opens - closes;
624
-
625
- if (currentNesting > maxNesting) {
626
- maxNesting = currentNesting;
627
- maxNestingLine = lineNum;
628
- }
629
- }
630
-
631
- if (maxNesting > this.config.nestingThreshold) {
632
- issues.push({
633
- file: filePath,
634
- line: maxNestingLine,
635
- category: 'quality',
636
- severity: 'warning',
637
- id: 'NEST001',
638
- name: 'Deep Nesting',
639
- message: `Max nesting depth is ${maxNesting} at line ${maxNestingLine} (threshold: ${this.config.nestingThreshold})`
640
- });
641
- }
642
-
643
- return issues;
644
- }
645
-
646
- _scanJson(filePath, content) {
647
- const issues = [];
648
-
649
- try {
650
- JSON.parse(content);
651
- } catch (error) {
652
- issues.push({
653
- file: filePath,
654
- category: 'quality',
655
- severity: 'error',
656
- id: 'JSON001',
657
- name: 'Invalid JSON',
658
- message: `JSON parse error: ${error.message}`
659
- });
660
- }
661
-
662
- // Check for sensitive data in JSON
663
- if (/(password|secret|api[_-]?key|token)/i.test(content)) {
664
- issues.push({
665
- file: filePath,
666
- category: 'security',
667
- severity: 'warning',
668
- id: 'JSON002',
669
- name: 'Sensitive Data in JSON',
670
- message: 'Possible sensitive data in JSON file - verify not committed'
671
- });
672
- }
673
-
674
- return issues;
675
- }
676
-
677
- _checkDependencyExists(match, filePath) {
678
- // Extract module name
679
- const moduleMatch = match[0].match(/require\(['"]([^'"]+)['"]\)/);
680
- if (!moduleMatch) return true;
681
-
682
- const moduleName = moduleMatch[1];
683
-
684
- // Skip relative imports
685
- if (moduleName.startsWith('.')) return false;
686
-
687
- // Skip Node built-ins
688
- const builtins = ['fs', 'path', 'os', 'crypto', 'http', 'https', 'events', 'util', 'stream', 'child_process', 'url', 'querystring', 'buffer'];
689
- if (builtins.includes(moduleName.split('/')[0])) return false;
690
-
691
- // Check package.json
692
- try {
693
- let pkgPath = path.join(this.config.rootPath, 'package.json');
694
-
695
- // Also check server/package.json for server files
696
- if (filePath.includes('server')) {
697
- const serverPkgPath = path.join(this.config.rootPath, 'server', 'package.json');
698
- if (fs.existsSync(serverPkgPath)) {
699
- pkgPath = serverPkgPath;
700
- }
701
- }
702
-
703
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
704
- const allDeps = {
705
- ...(pkg.dependencies || {}),
706
- ...(pkg.devDependencies || {})
707
- };
708
-
709
- const baseModule = moduleName.split('/')[0];
710
- return !allDeps[baseModule] && !allDeps[moduleName];
711
- } catch (e) {
712
- return false;
713
- }
714
- }
715
-
716
- _checkImportUsage(match, content) {
717
- const varName = match[1];
718
- if (!varName) return false;
719
-
720
- // Count occurrences (beyond the require line)
721
- const regex = new RegExp('\\b' + varName + '\\b', 'g');
722
- const occurrences = (content.match(regex) || []).length;
723
-
724
- // If only appears once (the require), it's unused
725
- return occurrences <= 1;
726
- }
727
-
728
- _summarizeIssues(issues) {
729
- return {
730
- total: issues.length,
731
- bySeverity: {
732
- critical: issues.filter(i => i.severity === 'critical').length,
733
- error: issues.filter(i => i.severity === 'error').length,
734
- warning: issues.filter(i => i.severity === 'warning').length,
735
- info: issues.filter(i => i.severity === 'info').length
736
- },
737
- byCategory: issues.reduce((acc, i) => {
738
- acc[i.category] = (acc[i.category] || 0) + 1;
739
- return acc;
740
- }, {}),
741
- topFiles: this._getTopFiles(issues, 10)
742
- };
743
- }
744
-
745
- _getTopFiles(issues, limit) {
746
- const fileCounts = issues.reduce((acc, i) => {
747
- acc[i.file] = (acc[i.file] || 0) + 1;
748
- return acc;
749
- }, {});
750
-
751
- return Object.entries(fileCounts)
752
- .sort((a, b) => b[1] - a[1])
753
- .slice(0, limit)
754
- .map(([file, count]) => ({ file: path.relative(this.config.rootPath, file), count }));
755
- }
756
- }
757
-
758
- module.exports = CodeScanner;