thuban 0.3.2 → 0.3.3
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 +137 -102
- package/cli.js +310 -95
- package/package.json +5 -2
- package/packages/scanner/alert-manager.js +398 -398
- package/packages/scanner/code-scanner.js +758 -713
- package/packages/scanner/copy-paste-detector.js +33 -7
- package/packages/scanner/dependency-graph.js +541 -536
- package/packages/scanner/drift-detector.js +423 -423
- package/packages/scanner/file-collector.js +64 -0
- package/packages/scanner/file-watcher.js +323 -323
- package/packages/scanner/ghost-code-detector.js +80 -5
- package/packages/scanner/hallucination-detector.js +189 -19
- package/packages/scanner/health-checker.js +586 -586
- package/packages/scanner/index.js +100 -92
- package/packages/scanner/license-manager.js +15 -2
- package/packages/scanner/master-health-checker.js +513 -513
- package/packages/scanner/monitor-notifier.js +64 -0
- package/packages/scanner/monitor-service.js +103 -0
- package/packages/scanner/monitor-store.js +117 -0
- package/packages/scanner/pre-commit-gate.js +1 -1
- package/packages/scanner/scan-diff.js +50 -0
- package/packages/scanner/scan-runner.js +172 -0
- package/packages/scanner/secret-scanner.js +612 -0
- package/packages/scanner/secret-scanner.test.js +103 -0
- package/packages/scanner/sentinel-core.js +616 -608
- package/packages/scanner/sentinel-knowledge.js +322 -322
- package/packages/scanner/tech-debt-analyzer.js +46 -28
- package/packages/scanner/widget-generator.js +415 -415
|
@@ -1,713 +1,758 @@
|
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
{
|
|
149
|
-
id: '
|
|
150
|
-
name: '
|
|
151
|
-
pattern:
|
|
152
|
-
severity: '
|
|
153
|
-
message: '
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
id: '
|
|
208
|
-
name: '
|
|
209
|
-
pattern:
|
|
210
|
-
severity: 'warning',
|
|
211
|
-
message: '
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
{
|
|
232
|
-
id: '
|
|
233
|
-
name: '
|
|
234
|
-
pattern: /
|
|
235
|
-
severity: '
|
|
236
|
-
message: '
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
summary
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
result.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
.
|
|
452
|
-
|
|
453
|
-
.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
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;
|