aura-security 0.4.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +446 -0
  3. package/deploy/AWS-DEPLOYMENT.md +358 -0
  4. package/deploy/terraform/main.tf +362 -0
  5. package/deploy/terraform/terraform.tfvars.example +6 -0
  6. package/dist/agents/base.d.ts +44 -0
  7. package/dist/agents/base.js +96 -0
  8. package/dist/agents/index.d.ts +14 -0
  9. package/dist/agents/index.js +17 -0
  10. package/dist/agents/policy/evaluator.d.ts +15 -0
  11. package/dist/agents/policy/evaluator.js +183 -0
  12. package/dist/agents/policy/index.d.ts +12 -0
  13. package/dist/agents/policy/index.js +15 -0
  14. package/dist/agents/policy/validator.d.ts +15 -0
  15. package/dist/agents/policy/validator.js +182 -0
  16. package/dist/agents/scanners/gitleaks.d.ts +14 -0
  17. package/dist/agents/scanners/gitleaks.js +155 -0
  18. package/dist/agents/scanners/grype.d.ts +14 -0
  19. package/dist/agents/scanners/grype.js +109 -0
  20. package/dist/agents/scanners/index.d.ts +15 -0
  21. package/dist/agents/scanners/index.js +27 -0
  22. package/dist/agents/scanners/npm-audit.d.ts +13 -0
  23. package/dist/agents/scanners/npm-audit.js +129 -0
  24. package/dist/agents/scanners/semgrep.d.ts +14 -0
  25. package/dist/agents/scanners/semgrep.js +131 -0
  26. package/dist/agents/scanners/trivy.d.ts +14 -0
  27. package/dist/agents/scanners/trivy.js +122 -0
  28. package/dist/agents/types.d.ts +137 -0
  29. package/dist/agents/types.js +91 -0
  30. package/dist/auditor/index.d.ts +3 -0
  31. package/dist/auditor/index.js +2 -0
  32. package/dist/auditor/pipeline.d.ts +19 -0
  33. package/dist/auditor/pipeline.js +240 -0
  34. package/dist/auditor/validator.d.ts +17 -0
  35. package/dist/auditor/validator.js +58 -0
  36. package/dist/aura/client.d.ts +29 -0
  37. package/dist/aura/client.js +125 -0
  38. package/dist/aura/index.d.ts +4 -0
  39. package/dist/aura/index.js +2 -0
  40. package/dist/aura/server.d.ts +45 -0
  41. package/dist/aura/server.js +343 -0
  42. package/dist/cli.d.ts +17 -0
  43. package/dist/cli.js +1433 -0
  44. package/dist/client/index.d.ts +41 -0
  45. package/dist/client/index.js +170 -0
  46. package/dist/compliance/index.d.ts +40 -0
  47. package/dist/compliance/index.js +292 -0
  48. package/dist/database/index.d.ts +77 -0
  49. package/dist/database/index.js +395 -0
  50. package/dist/index.d.ts +25 -0
  51. package/dist/index.js +762 -0
  52. package/dist/integrations/aura-scanner.d.ts +69 -0
  53. package/dist/integrations/aura-scanner.js +155 -0
  54. package/dist/integrations/aws-scanner.d.ts +63 -0
  55. package/dist/integrations/aws-scanner.js +624 -0
  56. package/dist/integrations/config.d.ts +69 -0
  57. package/dist/integrations/config.js +212 -0
  58. package/dist/integrations/github.d.ts +45 -0
  59. package/dist/integrations/github.js +201 -0
  60. package/dist/integrations/gitlab.d.ts +36 -0
  61. package/dist/integrations/gitlab.js +110 -0
  62. package/dist/integrations/index.d.ts +11 -0
  63. package/dist/integrations/index.js +11 -0
  64. package/dist/integrations/local-scanner.d.ts +146 -0
  65. package/dist/integrations/local-scanner.js +1654 -0
  66. package/dist/integrations/notifications.d.ts +99 -0
  67. package/dist/integrations/notifications.js +305 -0
  68. package/dist/integrations/scanners.d.ts +57 -0
  69. package/dist/integrations/scanners.js +217 -0
  70. package/dist/integrations/slop-scanner.d.ts +69 -0
  71. package/dist/integrations/slop-scanner.js +155 -0
  72. package/dist/integrations/webhook.d.ts +37 -0
  73. package/dist/integrations/webhook.js +256 -0
  74. package/dist/orchestrator/index.d.ts +72 -0
  75. package/dist/orchestrator/index.js +187 -0
  76. package/dist/output/index.d.ts +152 -0
  77. package/dist/output/index.js +399 -0
  78. package/dist/pipeline/index.d.ts +72 -0
  79. package/dist/pipeline/index.js +313 -0
  80. package/dist/sbom/index.d.ts +94 -0
  81. package/dist/sbom/index.js +298 -0
  82. package/dist/schemas/index.d.ts +2 -0
  83. package/dist/schemas/index.js +2 -0
  84. package/dist/schemas/input.schema.d.ts +87 -0
  85. package/dist/schemas/input.schema.js +44 -0
  86. package/dist/schemas/output.schema.d.ts +115 -0
  87. package/dist/schemas/output.schema.js +64 -0
  88. package/dist/serve-visualizer.d.ts +2 -0
  89. package/dist/serve-visualizer.js +78 -0
  90. package/dist/slop/client.d.ts +29 -0
  91. package/dist/slop/client.js +125 -0
  92. package/dist/slop/index.d.ts +4 -0
  93. package/dist/slop/index.js +2 -0
  94. package/dist/slop/server.d.ts +45 -0
  95. package/dist/slop/server.js +343 -0
  96. package/dist/types/events.d.ts +62 -0
  97. package/dist/types/events.js +2 -0
  98. package/dist/types/index.d.ts +1 -0
  99. package/dist/types/index.js +1 -0
  100. package/dist/visualizer/index.d.ts +4 -0
  101. package/dist/visualizer/index.js +181 -0
  102. package/dist/websocket/index.d.ts +88 -0
  103. package/dist/websocket/index.js +195 -0
  104. package/dist/zones/index.d.ts +7 -0
  105. package/dist/zones/index.js +7 -0
  106. package/dist/zones/manager.d.ts +101 -0
  107. package/dist/zones/manager.js +304 -0
  108. package/dist/zones/types.d.ts +78 -0
  109. package/dist/zones/types.js +33 -0
  110. package/package.json +84 -0
  111. package/visualizer/app.js +0 -0
  112. package/visualizer/index-minimal.html +1771 -0
  113. package/visualizer/index.html +2933 -0
  114. package/visualizer/landing.html +1328 -0
  115. package/visualizer/styles.css +0 -0
@@ -0,0 +1,2933 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Aura Control Plane - Security Auditor</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ background: #0a0a0f;
11
+ font-family: 'Courier New', monospace;
12
+ color: #00ff88;
13
+ overflow: hidden;
14
+ }
15
+
16
+ #canvas-container {
17
+ position: fixed;
18
+ top: 0;
19
+ left: 0;
20
+ width: 100%;
21
+ height: 100%;
22
+ z-index: 1;
23
+ }
24
+
25
+ /* UI Overlay */
26
+ .ui-layer {
27
+ position: fixed;
28
+ z-index: 100;
29
+ pointer-events: none;
30
+ }
31
+
32
+ .ui-layer > * {
33
+ pointer-events: auto;
34
+ }
35
+
36
+ /* Header */
37
+ #header {
38
+ top: 0;
39
+ left: 0;
40
+ right: 0;
41
+ height: 50px;
42
+ background: linear-gradient(180deg, rgba(0,30,15,0.95) 0%, rgba(0,15,8,0.9) 100%);
43
+ border-bottom: 2px solid #00ff88;
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ padding: 0 20px;
48
+ }
49
+
50
+ #header h1 {
51
+ font-size: 16px;
52
+ letter-spacing: 3px;
53
+ }
54
+
55
+ .status-badge {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 8px;
59
+ font-size: 12px;
60
+ }
61
+
62
+ .status-dot {
63
+ width: 10px;
64
+ height: 10px;
65
+ border-radius: 50%;
66
+ background: #00ff88;
67
+ animation: pulse 2s infinite;
68
+ }
69
+
70
+ .status-dot.error { background: #ff4444; }
71
+
72
+ @keyframes pulse {
73
+ 0%, 100% { opacity: 1; box-shadow: 0 0 10px #00ff88; }
74
+ 50% { opacity: 0.5; box-shadow: 0 0 5px #00ff88; }
75
+ }
76
+
77
+ /* Left Panel - Controls */
78
+ #left-panel {
79
+ top: 60px;
80
+ left: 20px;
81
+ width: 320px;
82
+ max-height: calc(100vh - 80px);
83
+ overflow-y: auto;
84
+ }
85
+
86
+ .panel {
87
+ background: rgba(0, 20, 10, 0.92);
88
+ border: 1px solid #00ff88;
89
+ padding: 15px;
90
+ margin-bottom: 10px;
91
+ }
92
+
93
+ .panel-title {
94
+ font-size: 12px;
95
+ font-weight: bold;
96
+ text-transform: uppercase;
97
+ letter-spacing: 2px;
98
+ margin-bottom: 12px;
99
+ padding-bottom: 8px;
100
+ border-bottom: 1px solid #00ff8844;
101
+ }
102
+
103
+ /* Legend */
104
+ .legend-item {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 10px;
108
+ margin: 8px 0;
109
+ font-size: 11px;
110
+ }
111
+
112
+ .legend-icon {
113
+ width: 20px;
114
+ height: 20px;
115
+ border-radius: 3px;
116
+ }
117
+
118
+ .legend-icon.module { background: #00ff88; }
119
+ .legend-icon.auditor { background: #00aaff; border-radius: 50%; }
120
+ .legend-icon.connection { height: 3px; background: linear-gradient(90deg, #00ff88, #00aaff); }
121
+ .legend-icon.finding-crit { background: #ff0044; }
122
+ .legend-icon.finding-high { background: #ff4400; }
123
+ .legend-icon.finding-med { background: #ffaa00; }
124
+ .legend-icon.finding-low { background: #00ff88; }
125
+
126
+ /* Action Buttons */
127
+ .btn {
128
+ width: 100%;
129
+ padding: 12px;
130
+ margin: 5px 0;
131
+ background: linear-gradient(180deg, #00ff88 0%, #00aa55 100%);
132
+ border: none;
133
+ color: #000;
134
+ font-family: inherit;
135
+ font-size: 12px;
136
+ font-weight: bold;
137
+ text-transform: uppercase;
138
+ letter-spacing: 1px;
139
+ cursor: pointer;
140
+ transition: all 0.2s;
141
+ }
142
+
143
+ .btn:hover {
144
+ transform: translateY(-2px);
145
+ box-shadow: 0 5px 20px rgba(0, 255, 136, 0.3);
146
+ }
147
+
148
+ .btn.danger {
149
+ background: linear-gradient(180deg, #ff4444 0%, #aa2222 100%);
150
+ }
151
+
152
+ .btn.secondary {
153
+ background: transparent;
154
+ border: 1px solid #00ff88;
155
+ color: #00ff88;
156
+ }
157
+
158
+ .btn-group {
159
+ display: grid;
160
+ grid-template-columns: 1fr 1fr;
161
+ gap: 8px;
162
+ }
163
+
164
+ /* Right Panel - Findings */
165
+ #right-panel {
166
+ top: 60px;
167
+ right: 20px;
168
+ width: 350px;
169
+ max-height: calc(100vh - 80px);
170
+ overflow-y: auto;
171
+ }
172
+
173
+ .finding {
174
+ margin: 10px 0;
175
+ padding: 12px;
176
+ border-left: 4px solid;
177
+ background: rgba(0, 0, 0, 0.4);
178
+ }
179
+
180
+ .finding.critical { border-color: #ff0044; }
181
+ .finding.high { border-color: #ff4400; }
182
+ .finding.medium { border-color: #ffaa00; }
183
+ .finding.low { border-color: #00ff88; }
184
+
185
+ .finding-header {
186
+ display: flex;
187
+ justify-content: space-between;
188
+ align-items: center;
189
+ margin-bottom: 8px;
190
+ }
191
+
192
+ .severity-badge {
193
+ font-size: 10px;
194
+ font-weight: bold;
195
+ padding: 3px 8px;
196
+ color: #000;
197
+ }
198
+
199
+ .severity-badge.critical { background: #ff0044; color: #fff; }
200
+ .severity-badge.high { background: #ff4400; }
201
+ .severity-badge.medium { background: #ffaa00; }
202
+ .severity-badge.low { background: #00ff88; }
203
+
204
+ .finding-module {
205
+ font-size: 10px;
206
+ color: #888;
207
+ }
208
+
209
+ .finding-claim {
210
+ font-size: 12px;
211
+ line-height: 1.4;
212
+ }
213
+
214
+ .finding-details {
215
+ font-size: 10px;
216
+ color: #666;
217
+ margin-top: 8px;
218
+ display: none;
219
+ }
220
+
221
+ .finding:hover .finding-details {
222
+ display: block;
223
+ }
224
+
225
+ /* Bottom Stats */
226
+ #stats-bar {
227
+ bottom: 20px;
228
+ left: 50%;
229
+ transform: translateX(-50%);
230
+ display: flex;
231
+ gap: 20px;
232
+ }
233
+
234
+ .stat-box {
235
+ text-align: center;
236
+ padding: 15px 25px;
237
+ background: rgba(0, 20, 10, 0.9);
238
+ border: 1px solid #00ff8844;
239
+ }
240
+
241
+ .stat-value {
242
+ font-size: 32px;
243
+ font-weight: bold;
244
+ }
245
+
246
+ .stat-label {
247
+ font-size: 10px;
248
+ text-transform: uppercase;
249
+ letter-spacing: 1px;
250
+ color: #888;
251
+ }
252
+
253
+ .stat-box.critical .stat-value { color: #ff0044; }
254
+ .stat-box.high .stat-value { color: #ff4400; }
255
+ .stat-box.medium .stat-value { color: #ffaa00; }
256
+ .stat-box.low .stat-value { color: #00ff88; }
257
+
258
+ /* Module Info Tooltip */
259
+ #tooltip {
260
+ position: fixed;
261
+ background: rgba(0, 30, 15, 0.95);
262
+ border: 1px solid #00ff88;
263
+ padding: 12px;
264
+ font-size: 11px;
265
+ max-width: 250px;
266
+ display: none;
267
+ z-index: 200;
268
+ }
269
+
270
+ #tooltip.visible { display: block; }
271
+ #tooltip h4 { margin-bottom: 5px; font-size: 13px; }
272
+ #tooltip .module-status { margin: 5px 0; }
273
+
274
+ /* Module Management Modal */
275
+ #module-modal {
276
+ position: fixed;
277
+ top: 0;
278
+ left: 0;
279
+ right: 0;
280
+ bottom: 0;
281
+ background: rgba(0, 10, 5, 0.95);
282
+ display: none;
283
+ align-items: center;
284
+ justify-content: center;
285
+ z-index: 400;
286
+ }
287
+
288
+ #module-modal.visible { display: flex; }
289
+
290
+ .modal-content {
291
+ background: rgba(0, 30, 15, 0.98);
292
+ border: 2px solid #00ff88;
293
+ padding: 25px;
294
+ min-width: 400px;
295
+ max-width: 500px;
296
+ }
297
+
298
+ .modal-header {
299
+ display: flex;
300
+ justify-content: space-between;
301
+ align-items: center;
302
+ margin-bottom: 20px;
303
+ padding-bottom: 15px;
304
+ border-bottom: 1px solid #00ff8844;
305
+ }
306
+
307
+ .modal-header h3 {
308
+ font-size: 14px;
309
+ letter-spacing: 2px;
310
+ }
311
+
312
+ .modal-close {
313
+ background: none;
314
+ border: none;
315
+ color: #00ff88;
316
+ font-size: 24px;
317
+ cursor: pointer;
318
+ }
319
+
320
+ .modal-body { margin-bottom: 20px; }
321
+
322
+ .form-group {
323
+ margin-bottom: 15px;
324
+ }
325
+
326
+ .form-group label {
327
+ display: block;
328
+ font-size: 11px;
329
+ text-transform: uppercase;
330
+ letter-spacing: 1px;
331
+ margin-bottom: 8px;
332
+ color: #888;
333
+ }
334
+
335
+ .form-group input, .form-group select {
336
+ width: 100%;
337
+ padding: 10px;
338
+ background: rgba(0, 20, 10, 0.8);
339
+ border: 1px solid #00ff88;
340
+ color: #00ff88;
341
+ font-family: inherit;
342
+ font-size: 12px;
343
+ }
344
+
345
+ .form-group input::placeholder { color: #00ff8866; }
346
+
347
+ /* Settings Panel Styles */
348
+ .settings-row {
349
+ margin-bottom: 15px;
350
+ }
351
+
352
+ .settings-row label {
353
+ display: block;
354
+ font-size: 12px;
355
+ margin-bottom: 6px;
356
+ color: #00ff88;
357
+ }
358
+
359
+ .settings-row input[type="checkbox"] {
360
+ margin-right: 8px;
361
+ accent-color: #00ff88;
362
+ }
363
+
364
+ .settings-tab {
365
+ padding: 8px 15px !important;
366
+ font-size: 11px !important;
367
+ }
368
+
369
+ .settings-tab.active {
370
+ background: #00ff88 !important;
371
+ color: #000 !important;
372
+ }
373
+
374
+ .settings-panel {
375
+ padding: 10px 0;
376
+ }
377
+
378
+ .module-toggle {
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: space-between;
382
+ padding: 10px;
383
+ background: rgba(0, 20, 10, 0.5);
384
+ margin: 5px 0;
385
+ cursor: pointer;
386
+ }
387
+
388
+ .module-toggle:hover { background: rgba(0, 40, 20, 0.5); }
389
+
390
+ .module-toggle.disabled { opacity: 0.5; }
391
+
392
+ .toggle-switch {
393
+ width: 40px;
394
+ height: 20px;
395
+ background: #333;
396
+ border-radius: 10px;
397
+ position: relative;
398
+ cursor: pointer;
399
+ }
400
+
401
+ .toggle-switch::after {
402
+ content: '';
403
+ position: absolute;
404
+ width: 16px;
405
+ height: 16px;
406
+ background: #888;
407
+ border-radius: 50%;
408
+ top: 2px;
409
+ left: 2px;
410
+ transition: all 0.2s;
411
+ }
412
+
413
+ .toggle-switch.active { background: #00ff8844; }
414
+ .toggle-switch.active::after {
415
+ background: #00ff88;
416
+ left: 22px;
417
+ }
418
+
419
+ /* Selected module highlight */
420
+ .module-selected {
421
+ box-shadow: 0 0 30px #00ff88;
422
+ }
423
+
424
+ /* Scrollbar */
425
+ ::-webkit-scrollbar { width: 6px; }
426
+ ::-webkit-scrollbar-track { background: #0a0a0f; }
427
+ ::-webkit-scrollbar-thumb { background: #00ff88; }
428
+
429
+ /* Toast */
430
+ #toast-container {
431
+ position: fixed;
432
+ top: 60px;
433
+ right: 20px;
434
+ z-index: 300;
435
+ }
436
+
437
+ .toast {
438
+ background: rgba(0, 40, 20, 0.95);
439
+ border: 1px solid #00ff88;
440
+ padding: 12px 20px;
441
+ margin-bottom: 10px;
442
+ animation: slideIn 0.3s ease;
443
+ }
444
+
445
+ .toast.error { border-color: #ff4444; color: #ff4444; }
446
+
447
+ @keyframes slideIn {
448
+ from { transform: translateX(100%); opacity: 0; }
449
+ to { transform: translateX(0); opacity: 1; }
450
+ }
451
+
452
+ /* Loading */
453
+ #loading {
454
+ position: fixed;
455
+ top: 0;
456
+ left: 0;
457
+ right: 0;
458
+ bottom: 0;
459
+ background: rgba(0, 10, 5, 0.9);
460
+ display: flex;
461
+ align-items: center;
462
+ justify-content: center;
463
+ z-index: 500;
464
+ opacity: 0;
465
+ pointer-events: none;
466
+ transition: opacity 0.3s;
467
+ }
468
+
469
+ #loading.active {
470
+ opacity: 1;
471
+ pointer-events: auto;
472
+ }
473
+
474
+ .spinner {
475
+ width: 50px;
476
+ height: 50px;
477
+ border: 3px solid #00ff8833;
478
+ border-top-color: #00ff88;
479
+ border-radius: 50%;
480
+ animation: spin 1s linear infinite;
481
+ }
482
+
483
+ @keyframes spin { to { transform: rotate(360deg); } }
484
+ </style>
485
+ </head>
486
+ <body>
487
+ <div id="canvas-container"></div>
488
+
489
+ <!-- Header -->
490
+ <header id="header" class="ui-layer">
491
+ <h1>AURA CONTROL PLANE</h1>
492
+ <div class="status-badge">
493
+ <div class="status-dot" id="statusDot"></div>
494
+ <span id="statusText">Connecting...</span>
495
+ <span style="margin-left: 15px;" id="auditCount">0 Audits</span>
496
+ </div>
497
+ </header>
498
+
499
+ <!-- Left Panel -->
500
+ <div id="left-panel" class="ui-layer">
501
+ <!-- Legend -->
502
+ <div class="panel">
503
+ <div class="panel-title">Control Plane Legend</div>
504
+ <div class="legend-item">
505
+ <div class="legend-icon auditor"></div>
506
+ <span>Security Auditor (Central Agent)</span>
507
+ </div>
508
+ <div class="legend-item">
509
+ <div class="legend-icon module"></div>
510
+ <span>System Module (Auth, DB, API, etc.)</span>
511
+ </div>
512
+ <div class="legend-item">
513
+ <div class="legend-icon connection"></div>
514
+ <span>Data Flow / Audit Connection</span>
515
+ </div>
516
+ <div class="legend-item">
517
+ <div class="legend-icon finding-crit"></div>
518
+ <span>Critical Finding</span>
519
+ </div>
520
+ <div class="legend-item">
521
+ <div class="legend-icon finding-high"></div>
522
+ <span>High Severity Finding</span>
523
+ </div>
524
+ <div class="legend-item">
525
+ <div class="legend-icon finding-med"></div>
526
+ <span>Medium Severity Finding</span>
527
+ </div>
528
+ </div>
529
+
530
+ <!-- Project Scanner -->
531
+ <div class="panel">
532
+ <div class="panel-title">Project Scanner</div>
533
+
534
+ <!-- Local Path Section -->
535
+ <div style="margin-bottom: 12px;">
536
+ <div style="font-size: 10px; color: #888; margin-bottom: 5px;">SCAN LOCAL DIRECTORY:</div>
537
+ <input type="text" id="scanPath" placeholder="C:\path\to\project" style="width: 100%; padding: 8px; background: rgba(0,20,10,0.8); border: 1px solid #00ff88; color: #00ff88; font-family: inherit; margin-bottom: 6px;">
538
+ <button class="btn" onclick="doScan()" style="width: 100%;">Scan Local</button>
539
+ </div>
540
+
541
+ <!-- Divider -->
542
+ <div style="border-top: 1px solid #00ff8833; margin: 12px 0;"></div>
543
+
544
+ <!-- Git URL Section -->
545
+ <div style="margin-bottom: 10px;">
546
+ <div style="font-size: 10px; color: #888; margin-bottom: 5px;">SCAN REMOTE GIT REPO (no clone):</div>
547
+ <input type="text" id="gitUrl" placeholder="https://github.com/user/repo" style="width: 100%; padding: 8px; background: rgba(0,20,10,0.8); border: 1px solid #00ff88; color: #00ff88; font-family: inherit; margin-bottom: 6px;">
548
+ <button class="btn secondary" onclick="scanGitRepo()" style="width: 100%;">Scan Remote</button>
549
+ <div style="font-size: 9px; color: #666; margin-top: 4px;">Fetches files via API - safe to scan untrusted repos</div>
550
+ </div>
551
+
552
+ <!-- Project Tabs -->
553
+ <div id="projectTabs" style="margin-top: 12px; display: none;">
554
+ <div style="font-size: 10px; color: #888; margin-bottom: 5px;">SCANNED PROJECTS:</div>
555
+ <div id="projectTabList" style="display: flex; flex-wrap: wrap; gap: 5px;"></div>
556
+ </div>
557
+
558
+ <!-- Scan Status -->
559
+ <div id="scanStatus" style="margin-top: 10px; font-size: 11px; color: #888;"></div>
560
+ </div>
561
+
562
+ <!-- Demo Mode -->
563
+ <div class="panel" id="demoPresets">
564
+ <div class="panel-title">Demo Presets</div>
565
+ <p style="font-size: 10px; color: #666; margin-bottom: 8px;">Test with sample security scenarios</p>
566
+ <button class="btn secondary" onclick="runPreset('secrets')">Secrets Leak</button>
567
+ <button class="btn secondary" onclick="runPreset('vulns')">Vulnerabilities</button>
568
+ <button class="btn secondary" onclick="runPreset('infra')">Infra Change</button>
569
+ <button class="btn secondary" onclick="runPreset('prod-deploy')">Prod Deploy</button>
570
+ </div>
571
+
572
+ <!-- Module Management -->
573
+ <div class="panel">
574
+ <div class="panel-title">Module Management</div>
575
+ <p style="font-size: 10px; color: #666; margin-bottom: 10px;">Click modules in 3D view to select. Use buttons below to manage.</p>
576
+ <button class="btn secondary" onclick="openModuleManager()">Manage Modules</button>
577
+ <button class="btn secondary" onclick="addNewModule()">Add New Module</button>
578
+ <div id="selectedModuleInfo" style="margin-top: 10px; font-size: 11px; display: none;">
579
+ <strong>Selected:</strong> <span id="selectedModuleName"></span>
580
+ <button class="btn danger" style="margin-top: 8px; padding: 8px;" onclick="removeSelectedModule()">Remove Module</button>
581
+ </div>
582
+ </div>
583
+
584
+ <!-- Module Status -->
585
+ <div class="panel">
586
+ <div class="panel-title">Module Status</div>
587
+ <div id="moduleStatus">
588
+ <div class="legend-item"><div class="legend-icon module"></div><span>AUTH - Secure</span></div>
589
+ <div class="legend-item"><div class="legend-icon module"></div><span>DATABASE - Secure</span></div>
590
+ <div class="legend-item"><div class="legend-icon module"></div><span>API - Secure</span></div>
591
+ <div class="legend-item"><div class="legend-icon module"></div><span>INFRA - Secure</span></div>
592
+ <div class="legend-item"><div class="legend-icon module"></div><span>BILLING - Secure</span></div>
593
+ <div class="legend-item"><div class="legend-icon module"></div><span>SECRETS - Secure</span></div>
594
+ </div>
595
+ </div>
596
+ </div>
597
+
598
+ <!-- Right Panel -->
599
+ <div id="right-panel" class="ui-layer">
600
+ <!-- Audit History Panel -->
601
+ <div class="panel">
602
+ <div class="panel-title">Audit History <span id="auditHistoryCount" style="color: #888; font-size: 10px;">(0)</span></div>
603
+ <div id="auditHistoryList" style="max-height: 200px; overflow-y: auto;">
604
+ <p style="color: #666; font-size: 11px;">Loading audits...</p>
605
+ </div>
606
+ <button class="btn secondary" style="margin-top: 10px; font-size: 10px;" onclick="refreshAuditHistory()">Refresh History</button>
607
+ </div>
608
+
609
+ <!-- Selected Audit Details -->
610
+ <div class="panel" id="selectedAuditPanel" style="display: none;">
611
+ <div class="panel-title">Selected Audit</div>
612
+ <div id="selectedAuditInfo" style="font-size: 11px; margin-bottom: 10px;"></div>
613
+ <button class="btn secondary" style="font-size: 10px;" onclick="clearSelectedAudit()">Clear Selection</button>
614
+ </div>
615
+
616
+ <div class="panel">
617
+ <div class="panel-title">Security Findings</div>
618
+ <div id="findingsList">
619
+ <p style="color: #666; font-size: 11px;">No findings yet. Run an audit to detect issues.</p>
620
+ </div>
621
+ </div>
622
+
623
+ <!-- Settings Panel -->
624
+ <div class="panel">
625
+ <div class="panel-title">Settings <span id="settingsStatus" style="color: #666; font-size: 10px;"></span></div>
626
+ <button class="btn secondary" style="width: 100%;" onclick="openSettingsModal()">Configure Integrations</button>
627
+ </div>
628
+ </div>
629
+
630
+ <!-- Settings Modal -->
631
+ <div id="settingsModal" class="modal" style="display: none;">
632
+ <div class="modal-content" style="max-width: 600px;">
633
+ <div class="modal-header">
634
+ <h3>Integration Settings</h3>
635
+ <button class="modal-close" onclick="closeSettingsModal()">&times;</button>
636
+ </div>
637
+ <div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
638
+ <!-- Tabs -->
639
+ <div style="display: flex; gap: 5px; margin-bottom: 15px; border-bottom: 1px solid #00ff8844; padding-bottom: 10px;">
640
+ <button class="btn secondary settings-tab active" data-tab="aws" onclick="switchSettingsTab('aws')">AWS</button>
641
+ <button class="btn secondary settings-tab" data-tab="slack" onclick="switchSettingsTab('slack')">Slack</button>
642
+ <button class="btn secondary settings-tab" data-tab="discord" onclick="switchSettingsTab('discord')">Discord</button>
643
+ <button class="btn secondary settings-tab" data-tab="webhook" onclick="switchSettingsTab('webhook')">Webhook</button>
644
+ </div>
645
+
646
+ <!-- AWS Settings -->
647
+ <div id="settings-aws" class="settings-panel">
648
+ <div class="settings-row">
649
+ <label>
650
+ <input type="checkbox" id="aws-enabled" onchange="markSettingsDirty()">
651
+ Enable AWS Scanning
652
+ </label>
653
+ </div>
654
+ <div class="settings-row">
655
+ <label>AWS Region:</label>
656
+ <select id="aws-region" style="width: 100%; padding: 8px; background: rgba(0,20,10,0.8); border: 1px solid #00ff88; color: #00ff88;" onchange="markSettingsDirty()">
657
+ <option value="us-east-1">us-east-1 (N. Virginia)</option>
658
+ <option value="us-east-2">us-east-2 (Ohio)</option>
659
+ <option value="us-west-1">us-west-1 (N. California)</option>
660
+ <option value="us-west-2">us-west-2 (Oregon)</option>
661
+ <option value="eu-west-1">eu-west-1 (Ireland)</option>
662
+ <option value="eu-central-1">eu-central-1 (Frankfurt)</option>
663
+ <option value="ap-southeast-1">ap-southeast-1 (Singapore)</option>
664
+ <option value="ap-northeast-1">ap-northeast-1 (Tokyo)</option>
665
+ </select>
666
+ </div>
667
+ <div class="settings-row">
668
+ <label>AWS Profile (optional):</label>
669
+ <input type="text" id="aws-profile" placeholder="default" style="width: 100%; padding: 8px; background: rgba(0,20,10,0.8); border: 1px solid #00ff88; color: #00ff88;" onchange="markSettingsDirty()">
670
+ </div>
671
+ <div class="settings-row">
672
+ <label>Services to Scan:</label>
673
+ <div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 5px;">
674
+ <label><input type="checkbox" id="aws-iam" checked onchange="markSettingsDirty()"> IAM</label>
675
+ <label><input type="checkbox" id="aws-s3" checked onchange="markSettingsDirty()"> S3</label>
676
+ <label><input type="checkbox" id="aws-ec2" checked onchange="markSettingsDirty()"> EC2</label>
677
+ <label><input type="checkbox" id="aws-lambda" checked onchange="markSettingsDirty()"> Lambda</label>
678
+ <label><input type="checkbox" id="aws-rds" checked onchange="markSettingsDirty()"> RDS</label>
679
+ </div>
680
+ </div>
681
+ <button class="btn" style="margin-top: 15px;" onclick="runAWSScan()">Run AWS Scan Now</button>
682
+ </div>
683
+
684
+ <!-- Slack Settings -->
685
+ <div id="settings-slack" class="settings-panel" style="display: none;">
686
+ <div class="settings-row">
687
+ <label>
688
+ <input type="checkbox" id="slack-enabled" onchange="markSettingsDirty()">
689
+ Enable Slack Notifications
690
+ </label>
691
+ </div>
692
+ <div class="settings-row">
693
+ <label>Webhook URL:</label>
694
+ <input type="text" id="slack-webhook" placeholder="https://hooks.slack.com/services/..." style="width: 100%; padding: 8px; background: rgba(0,20,10,0.8); border: 1px solid #00ff88; color: #00ff88;" onchange="markSettingsDirty()">
695
+ </div>
696
+ <div class="settings-row">
697
+ <label>Channel (optional):</label>
698
+ <input type="text" id="slack-channel" placeholder="#security-alerts" style="width: 100%; padding: 8px; background: rgba(0,20,10,0.8); border: 1px solid #00ff88; color: #00ff88;" onchange="markSettingsDirty()">
699
+ </div>
700
+ <button class="btn secondary" style="margin-top: 15px;" onclick="testNotification('slack')">Test Slack</button>
701
+ </div>
702
+
703
+ <!-- Discord Settings -->
704
+ <div id="settings-discord" class="settings-panel" style="display: none;">
705
+ <div class="settings-row">
706
+ <label>
707
+ <input type="checkbox" id="discord-enabled" onchange="markSettingsDirty()">
708
+ Enable Discord Notifications
709
+ </label>
710
+ </div>
711
+ <div class="settings-row">
712
+ <label>Webhook URL:</label>
713
+ <input type="text" id="discord-webhook" placeholder="https://discord.com/api/webhooks/..." style="width: 100%; padding: 8px; background: rgba(0,20,10,0.8); border: 1px solid #00ff88; color: #00ff88;" onchange="markSettingsDirty()">
714
+ </div>
715
+ <button class="btn secondary" style="margin-top: 15px;" onclick="testNotification('discord')">Test Discord</button>
716
+ </div>
717
+
718
+ <!-- Custom Webhook Settings -->
719
+ <div id="settings-webhook" class="settings-panel" style="display: none;">
720
+ <div class="settings-row">
721
+ <label>
722
+ <input type="checkbox" id="webhook-enabled" onchange="markSettingsDirty()">
723
+ Enable Custom Webhook
724
+ </label>
725
+ </div>
726
+ <div class="settings-row">
727
+ <label>Webhook URL:</label>
728
+ <input type="text" id="webhook-url" placeholder="https://your-server.com/webhook" style="width: 100%; padding: 8px; background: rgba(0,20,10,0.8); border: 1px solid #00ff88; color: #00ff88;" onchange="markSettingsDirty()">
729
+ </div>
730
+ <div class="settings-row">
731
+ <label>Custom Headers (JSON, optional):</label>
732
+ <textarea id="webhook-headers" placeholder='{"Authorization": "Bearer token"}' style="width: 100%; height: 60px; padding: 8px; background: rgba(0,20,10,0.8); border: 1px solid #00ff88; color: #00ff88; font-family: inherit; resize: vertical;" onchange="markSettingsDirty()"></textarea>
733
+ </div>
734
+ <button class="btn secondary" style="margin-top: 15px;" onclick="testNotification('webhook')">Test Webhook</button>
735
+ </div>
736
+ </div>
737
+ <div class="modal-footer" style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 15px; padding-top: 15px; border-top: 1px solid #00ff8844;">
738
+ <button class="btn secondary" onclick="closeSettingsModal()">Cancel</button>
739
+ <button class="btn" id="saveSettingsBtn" onclick="saveSettings()" disabled>Save Settings</button>
740
+ </div>
741
+ </div>
742
+ </div>
743
+
744
+ <!-- Stats Bar -->
745
+ <div id="stats-bar" class="ui-layer">
746
+ <div class="stat-box critical">
747
+ <div class="stat-value" id="statCritical">0</div>
748
+ <div class="stat-label">Critical</div>
749
+ </div>
750
+ <div class="stat-box high">
751
+ <div class="stat-value" id="statHigh">0</div>
752
+ <div class="stat-label">High</div>
753
+ </div>
754
+ <div class="stat-box medium">
755
+ <div class="stat-value" id="statMedium">0</div>
756
+ <div class="stat-label">Medium</div>
757
+ </div>
758
+ <div class="stat-box low">
759
+ <div class="stat-value" id="statLow">0</div>
760
+ <div class="stat-label">Low</div>
761
+ </div>
762
+ </div>
763
+
764
+ <!-- Tooltip -->
765
+ <div id="tooltip">
766
+ <h4 id="tooltipTitle">Module</h4>
767
+ <div id="tooltipContent"></div>
768
+ </div>
769
+
770
+ <!-- Toast Container -->
771
+ <div id="toast-container"></div>
772
+
773
+ <!-- Loading -->
774
+ <div id="loading"><div class="spinner"></div></div>
775
+
776
+ <!-- Module Manager Modal -->
777
+ <div id="module-modal">
778
+ <div class="modal-content">
779
+ <div class="modal-header">
780
+ <h3 id="modalTitle">MANAGE MODULES</h3>
781
+ <button class="modal-close" onclick="closeModal()">&times;</button>
782
+ </div>
783
+ <div class="modal-body" id="modalBody">
784
+ <!-- Content injected by JS -->
785
+ </div>
786
+ <div class="modal-footer">
787
+ <button class="btn" onclick="saveModuleChanges()">Save Changes</button>
788
+ <button class="btn secondary" onclick="closeModal()">Cancel</button>
789
+ </div>
790
+ </div>
791
+ </div>
792
+
793
+ <script type="importmap">
794
+ {
795
+ "imports": {
796
+ "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
797
+ "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
798
+ }
799
+ }
800
+ </script>
801
+
802
+ <script type="module">
803
+ import * as THREE from 'three';
804
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
805
+
806
+ // ========== CONFIGURATION ==========
807
+ const AURA_URL = 'http://127.0.0.1:3000';
808
+ const WS_URL = 'ws://127.0.0.1:3001';
809
+ const POLL_INTERVAL = 5000; // Slower polling as WebSocket handles real-time
810
+ let wsConnected = false;
811
+ let ws = null;
812
+
813
+ // Module definitions - what systems exist in the control plane
814
+ // Base modules - always restored after clearing the map
815
+ const BASE_MODULES = [
816
+ { id: 'auth', name: 'AUTH', desc: 'Authentication & Identity', pos: [-15, 0, -10], color: 0x00ff88 },
817
+ { id: 'database', name: 'DATABASE', desc: 'Data Storage & Queries', pos: [15, 0, -10], color: 0x00aaff },
818
+ { id: 'api', name: 'API', desc: 'External Endpoints', pos: [-15, 0, 10], color: 0xffaa00 },
819
+ { id: 'infra', name: 'INFRA', desc: 'Infrastructure & Network', pos: [15, 0, 10], color: 0xff00ff },
820
+ { id: 'billing', name: 'BILLING', desc: 'Payment Processing', pos: [-20, 0, 0], color: 0xff8800 },
821
+ { id: 'secrets', name: 'SECRETS', desc: 'Credentials & Keys', pos: [20, 0, 0], color: 0xff0088 }
822
+ ];
823
+
824
+ // Active modules - starts with base modules, can have more added
825
+ const MODULES = [...BASE_MODULES];
826
+
827
+ // Connections between modules (data flows)
828
+ const CONNECTIONS = [
829
+ { from: 'auth', to: 'database', label: 'User Data' },
830
+ { from: 'auth', to: 'secrets', label: 'Credentials' },
831
+ { from: 'api', to: 'auth', label: 'Auth Requests' },
832
+ { from: 'api', to: 'database', label: 'Queries' },
833
+ { from: 'billing', to: 'database', label: 'Transactions' },
834
+ { from: 'billing', to: 'secrets', label: 'Payment Keys' },
835
+ { from: 'infra', to: 'database', label: 'Backups' },
836
+ { from: 'infra', to: 'secrets', label: 'Service Accounts' }
837
+ ];
838
+
839
+ // Presets for quick testing
840
+ const PRESETS = {
841
+ 'secrets': {
842
+ change_event: {
843
+ id: 'preset-secrets-' + Date.now(),
844
+ type: 'pull_request',
845
+ environment: 'staging',
846
+ repo: 'acme/webapp',
847
+ commit: 'a'.repeat(40),
848
+ files_changed: ['src/config/secrets.ts', 'src/auth/oauth.ts'],
849
+ diff: '+const AWS_SECRET = "AKIAIOSFODNN7EXAMPLE";\n+const DB_PASSWORD = "super_secret_123";'
850
+ },
851
+ evidence_bundle: { vuln_scan: 'critical: 0\nhigh: 0' },
852
+ policy_context: { critical_assets: ['auth', 'secrets'], risk_tolerance: 'low' }
853
+ },
854
+ 'vulns': {
855
+ change_event: {
856
+ id: 'preset-vulns-' + Date.now(),
857
+ type: 'pull_request',
858
+ environment: 'staging',
859
+ repo: 'acme/webapp',
860
+ commit: 'b'.repeat(40),
861
+ files_changed: ['package.json', 'package-lock.json'],
862
+ diff: '+ "lodash": "4.17.0"'
863
+ },
864
+ evidence_bundle: { vuln_scan: 'critical: 5\nhigh: 12\nmedium: 23' },
865
+ policy_context: { critical_assets: ['dependencies'], risk_tolerance: 'low' }
866
+ },
867
+ 'prod-deploy': {
868
+ change_event: {
869
+ id: 'preset-deploy-' + Date.now(),
870
+ type: 'deploy',
871
+ environment: 'prod',
872
+ repo: 'acme/webapp',
873
+ commit: 'c'.repeat(40),
874
+ files_changed: ['dist/app.js'],
875
+ diff: ''
876
+ },
877
+ evidence_bundle: { vuln_scan: 'critical: 0\nhigh: 1' },
878
+ policy_context: { critical_assets: ['production'], risk_tolerance: 'medium' }
879
+ },
880
+ 'infra': {
881
+ change_event: {
882
+ id: 'preset-infra-' + Date.now(),
883
+ type: 'infra_change',
884
+ environment: 'staging',
885
+ repo: 'acme/terraform',
886
+ commit: 'd'.repeat(40),
887
+ files_changed: ['modules/vpc/main.tf', 'modules/security-groups/main.tf'],
888
+ diff: '+resource "aws_security_group_rule" "allow_all" {\n+ cidr_blocks = ["0.0.0.0/0"]\n+}'
889
+ },
890
+ evidence_bundle: { iac_scan: 'warnings: 3' },
891
+ policy_context: { critical_assets: ['infra', 'network'], risk_tolerance: 'low' }
892
+ },
893
+ 'clean': {
894
+ change_event: {
895
+ id: 'preset-clean-' + Date.now(),
896
+ type: 'pull_request',
897
+ environment: 'dev',
898
+ repo: 'acme/webapp',
899
+ commit: 'e'.repeat(40),
900
+ files_changed: ['src/utils/helpers.ts'],
901
+ diff: '+export function formatDate(d: Date) { return d.toISOString(); }'
902
+ },
903
+ evidence_bundle: { vuln_scan: 'critical: 0\nhigh: 0' },
904
+ policy_context: { critical_assets: ['auth'], risk_tolerance: 'high' }
905
+ },
906
+ 'full-breach': {
907
+ change_event: {
908
+ id: 'preset-breach-' + Date.now(),
909
+ type: 'pull_request',
910
+ environment: 'prod',
911
+ repo: 'acme/webapp',
912
+ commit: 'f'.repeat(40),
913
+ files_changed: ['src/auth/admin.ts', 'src/billing/stripe.ts', 'config/database.yml'],
914
+ diff: '+const ADMIN_PASSWORD = "admin123";\n+const STRIPE_SECRET = "sk_live_xxx";\n+const DB_CONNECTION = "postgres://root:password@prod-db:5432";'
915
+ },
916
+ evidence_bundle: { vuln_scan: 'critical: 8\nhigh: 15', sast_results: 'sql_injection: 3\nxss: 5' },
917
+ policy_context: { critical_assets: ['auth', 'billing', 'database', 'secrets'], risk_tolerance: 'low' }
918
+ }
919
+ };
920
+
921
+ // ========== THREE.JS SETUP ==========
922
+ let scene, camera, renderer, controls;
923
+ let auditorNode, moduleNodes = {}, connectionLines = [], findingMarkers = [];
924
+ let raycaster, mouse;
925
+ let connected = false;
926
+ let currentFindings = [];
927
+ let mapCleared = false; // Track if map has been cleared for scan mode
928
+
929
+ // Multi-project support
930
+ const scannedProjects = {}; // { projectName: { path, scanResult, modules, services } }
931
+ let activeProject = null;
932
+
933
+ const COLORS = {
934
+ idle: 0x888888,
935
+ analyzing: 0x00aaff,
936
+ conflict: 0xffaa00,
937
+ escalated: 0xff00ff,
938
+ blocked: 0xff0044
939
+ };
940
+
941
+ // Clear the map - remove all modules and connections to start fresh
942
+ function clearMap() {
943
+ console.log('Clearing map for fresh scan...');
944
+
945
+ if (!scene) {
946
+ console.warn('Scene not initialized yet');
947
+ return;
948
+ }
949
+
950
+ // Remove all module nodes from scene
951
+ for (const id in moduleNodes) {
952
+ if (moduleNodes[id]) {
953
+ scene.remove(moduleNodes[id]);
954
+ }
955
+ }
956
+ moduleNodes = {};
957
+
958
+ // Remove all connection lines from scene
959
+ connectionLines.forEach(line => {
960
+ if (line) scene.remove(line);
961
+ });
962
+ connectionLines = [];
963
+
964
+ // Remove all finding markers
965
+ findingMarkers.forEach(marker => {
966
+ if (marker) scene.remove(marker);
967
+ });
968
+ findingMarkers = [];
969
+
970
+ // Clear the MODULES array and restore base modules
971
+ MODULES.length = 0;
972
+ BASE_MODULES.forEach(mod => MODULES.push({...mod}));
973
+
974
+ // Clear module enabled states and restore base modules
975
+ for (const key in moduleEnabled) {
976
+ delete moduleEnabled[key];
977
+ }
978
+ BASE_MODULES.forEach(mod => moduleEnabled[mod.id] = true);
979
+
980
+ // Clear module statuses
981
+ for (const key in moduleStatuses) {
982
+ delete moduleStatuses[key];
983
+ }
984
+
985
+ // Update UI
986
+ updateModuleStatusPanel([]);
987
+ currentFindings = [];
988
+ selectedModule = null;
989
+
990
+ // Hide selected module info
991
+ const selectedInfo = document.getElementById('selectedModuleInfo');
992
+ if (selectedInfo) selectedInfo.style.display = 'none';
993
+
994
+ // Recreate base module nodes and connections in 3D
995
+ createModuleNodes();
996
+ createConnections();
997
+
998
+ mapCleared = true;
999
+ }
1000
+
1001
+ function initScene() {
1002
+ // Scene
1003
+ scene = new THREE.Scene();
1004
+ scene.background = new THREE.Color(0x0a0a0f);
1005
+ scene.fog = new THREE.Fog(0x0a0a0f, 60, 150);
1006
+
1007
+ // Camera
1008
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
1009
+ camera.position.set(0, 50, 50);
1010
+ camera.lookAt(0, 0, 0);
1011
+
1012
+ // Renderer
1013
+ renderer = new THREE.WebGLRenderer({ antialias: true });
1014
+ renderer.setSize(window.innerWidth, window.innerHeight);
1015
+ renderer.setPixelRatio(window.devicePixelRatio);
1016
+ document.getElementById('canvas-container').appendChild(renderer.domElement);
1017
+
1018
+ // Controls
1019
+ controls = new OrbitControls(camera, renderer.domElement);
1020
+ controls.enableDamping = true;
1021
+ controls.dampingFactor = 0.05;
1022
+ controls.maxPolarAngle = Math.PI / 2.2;
1023
+ controls.minDistance = 20;
1024
+ controls.maxDistance = 100;
1025
+
1026
+ // Raycaster for mouse interaction
1027
+ raycaster = new THREE.Raycaster();
1028
+ mouse = new THREE.Vector2();
1029
+
1030
+ // Lighting
1031
+ const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
1032
+ scene.add(ambientLight);
1033
+
1034
+ const pointLight = new THREE.PointLight(0x00ff88, 1, 100);
1035
+ pointLight.position.set(0, 30, 0);
1036
+ scene.add(pointLight);
1037
+
1038
+ // Grid
1039
+ const gridHelper = new THREE.GridHelper(80, 40, 0x00ff88, 0x002211);
1040
+ scene.add(gridHelper);
1041
+
1042
+ // Ground plane
1043
+ const groundGeom = new THREE.PlaneGeometry(80, 80);
1044
+ const groundMat = new THREE.MeshBasicMaterial({ color: 0x001108, transparent: true, opacity: 0.8 });
1045
+ const ground = new THREE.Mesh(groundGeom, groundMat);
1046
+ ground.rotation.x = -Math.PI / 2;
1047
+ ground.position.y = -0.1;
1048
+ scene.add(ground);
1049
+
1050
+ // Create scene elements
1051
+ createAuditorNode();
1052
+ createModuleNodes();
1053
+ createConnections();
1054
+ createParticles();
1055
+
1056
+ // Event listeners
1057
+ window.addEventListener('resize', onWindowResize);
1058
+ window.addEventListener('mousemove', onMouseMove);
1059
+ }
1060
+
1061
+ function createAuditorNode() {
1062
+ const group = new THREE.Group();
1063
+
1064
+ // Base platform
1065
+ const baseGeom = new THREE.CylinderGeometry(4, 5, 1.5, 8);
1066
+ const baseMat = new THREE.MeshPhongMaterial({
1067
+ color: 0x00aaff,
1068
+ emissive: 0x00aaff,
1069
+ emissiveIntensity: 0.3
1070
+ });
1071
+ const base = new THREE.Mesh(baseGeom, baseMat);
1072
+ base.name = 'auditor-base';
1073
+ group.add(base);
1074
+
1075
+ // Core sphere
1076
+ const coreGeom = new THREE.IcosahedronGeometry(2.5, 1);
1077
+ const coreMat = new THREE.MeshPhongMaterial({
1078
+ color: 0x00ff88,
1079
+ emissive: 0x00ff88,
1080
+ emissiveIntensity: 0.5,
1081
+ wireframe: false
1082
+ });
1083
+ const core = new THREE.Mesh(coreGeom, coreMat);
1084
+ core.position.y = 5;
1085
+ core.name = 'auditor-core';
1086
+ group.add(core);
1087
+
1088
+ // Rotating rings
1089
+ for (let i = 0; i < 3; i++) {
1090
+ const ringGeom = new THREE.TorusGeometry(3 + i * 0.5, 0.1, 8, 32);
1091
+ const ringMat = new THREE.MeshBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.6 });
1092
+ const ring = new THREE.Mesh(ringGeom, ringMat);
1093
+ ring.position.y = 5;
1094
+ ring.rotation.x = Math.PI / 2 + (i * 0.3);
1095
+ ring.name = `auditor-ring-${i}`;
1096
+ group.add(ring);
1097
+ }
1098
+
1099
+ // Label
1100
+ const labelSprite = createTextSprite('SECURITY\nAUDITOR', 0x00aaff);
1101
+ labelSprite.position.y = 10;
1102
+ labelSprite.scale.set(8, 4, 1);
1103
+ group.add(labelSprite);
1104
+
1105
+ group.position.y = 1;
1106
+ group.userData = { type: 'auditor', name: 'Security Auditor', desc: 'Central security analysis agent' };
1107
+ auditorNode = group;
1108
+ scene.add(auditorNode);
1109
+ }
1110
+
1111
+ function createModuleNodes() {
1112
+ MODULES.forEach(mod => {
1113
+ const group = new THREE.Group();
1114
+
1115
+ // Module box
1116
+ const boxGeom = new THREE.BoxGeometry(5, 3, 5);
1117
+ const boxMat = new THREE.MeshPhongMaterial({
1118
+ color: mod.color,
1119
+ emissive: mod.color,
1120
+ emissiveIntensity: 0.2
1121
+ });
1122
+ const box = new THREE.Mesh(boxGeom, boxMat);
1123
+ box.name = `module-${mod.id}`;
1124
+ group.add(box);
1125
+
1126
+ // Top indicator
1127
+ const indicatorGeom = new THREE.CylinderGeometry(0.5, 0.5, 0.3, 16);
1128
+ const indicatorMat = new THREE.MeshBasicMaterial({ color: 0x00ff88 });
1129
+ const indicator = new THREE.Mesh(indicatorGeom, indicatorMat);
1130
+ indicator.position.y = 1.8;
1131
+ indicator.name = `indicator-${mod.id}`;
1132
+ group.add(indicator);
1133
+
1134
+ // Label
1135
+ const labelSprite = createTextSprite(mod.name, mod.color);
1136
+ labelSprite.position.y = 4;
1137
+ labelSprite.scale.set(6, 3, 1);
1138
+ group.add(labelSprite);
1139
+
1140
+ group.position.set(mod.pos[0], mod.pos[1] + 2, mod.pos[2]);
1141
+ group.userData = { type: 'module', id: mod.id, name: mod.name, desc: mod.desc, status: 'secure' };
1142
+ moduleNodes[mod.id] = group;
1143
+ scene.add(group);
1144
+ });
1145
+ }
1146
+
1147
+ function createConnections() {
1148
+ // Connections from auditor to all modules
1149
+ MODULES.forEach(mod => {
1150
+ const line = createConnectionLine(
1151
+ new THREE.Vector3(0, 5, 0),
1152
+ new THREE.Vector3(mod.pos[0], mod.pos[1] + 2, mod.pos[2]),
1153
+ 0x00aaff,
1154
+ true
1155
+ );
1156
+ line.userData = { type: 'audit-connection', to: mod.id };
1157
+ connectionLines.push(line);
1158
+ scene.add(line);
1159
+ });
1160
+
1161
+ // Connections between modules
1162
+ CONNECTIONS.forEach(conn => {
1163
+ const fromMod = MODULES.find(m => m.id === conn.from);
1164
+ const toMod = MODULES.find(m => m.id === conn.to);
1165
+ if (fromMod && toMod) {
1166
+ const line = createConnectionLine(
1167
+ new THREE.Vector3(fromMod.pos[0], fromMod.pos[1] + 2, fromMod.pos[2]),
1168
+ new THREE.Vector3(toMod.pos[0], toMod.pos[1] + 2, toMod.pos[2]),
1169
+ 0x00ff8844,
1170
+ false
1171
+ );
1172
+ line.userData = { type: 'data-flow', from: conn.from, to: conn.to, label: conn.label };
1173
+ connectionLines.push(line);
1174
+ scene.add(line);
1175
+ }
1176
+ });
1177
+ }
1178
+
1179
+ function createConnectionLine(start, end, color, dashed) {
1180
+ const points = [start, end];
1181
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
1182
+
1183
+ let material;
1184
+ if (dashed) {
1185
+ material = new THREE.LineDashedMaterial({
1186
+ color: color,
1187
+ dashSize: 1,
1188
+ gapSize: 0.5,
1189
+ transparent: true,
1190
+ opacity: 0.6
1191
+ });
1192
+ } else {
1193
+ material = new THREE.LineBasicMaterial({
1194
+ color: color,
1195
+ transparent: true,
1196
+ opacity: 0.3
1197
+ });
1198
+ }
1199
+
1200
+ const line = new THREE.Line(geometry, material);
1201
+ if (dashed) line.computeLineDistances();
1202
+ return line;
1203
+ }
1204
+
1205
+ function createTextSprite(text, color) {
1206
+ const canvas = document.createElement('canvas');
1207
+ const ctx = canvas.getContext('2d');
1208
+ canvas.width = 256;
1209
+ canvas.height = 128;
1210
+
1211
+ ctx.fillStyle = 'transparent';
1212
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1213
+
1214
+ ctx.font = 'bold 28px Courier New';
1215
+ ctx.fillStyle = '#' + color.toString(16).padStart(6, '0');
1216
+ ctx.textAlign = 'center';
1217
+ ctx.textBaseline = 'middle';
1218
+
1219
+ const lines = text.split('\n');
1220
+ lines.forEach((line, i) => {
1221
+ ctx.fillText(line, canvas.width / 2, canvas.height / 2 + (i - (lines.length - 1) / 2) * 30);
1222
+ });
1223
+
1224
+ const texture = new THREE.CanvasTexture(canvas);
1225
+ const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
1226
+ return new THREE.Sprite(material);
1227
+ }
1228
+
1229
+ function createParticles() {
1230
+ const particleCount = 300;
1231
+ const geometry = new THREE.BufferGeometry();
1232
+ const positions = new Float32Array(particleCount * 3);
1233
+
1234
+ for (let i = 0; i < particleCount; i++) {
1235
+ positions[i * 3] = (Math.random() - 0.5) * 80;
1236
+ positions[i * 3 + 1] = Math.random() * 40;
1237
+ positions[i * 3 + 2] = (Math.random() - 0.5) * 80;
1238
+ }
1239
+
1240
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
1241
+
1242
+ const material = new THREE.PointsMaterial({
1243
+ color: 0x00ff88,
1244
+ size: 0.2,
1245
+ transparent: true,
1246
+ opacity: 0.4
1247
+ });
1248
+
1249
+ const particles = new THREE.Points(geometry, material);
1250
+ particles.name = 'particles';
1251
+ scene.add(particles);
1252
+ }
1253
+
1254
+ // ========== STATE UPDATES ==========
1255
+ function updateAuditorState(state) {
1256
+ const color = COLORS[state] || COLORS.idle;
1257
+
1258
+ auditorNode.traverse(child => {
1259
+ if (child.isMesh && child.name === 'auditor-base') {
1260
+ child.material.color.setHex(color);
1261
+ child.material.emissive.setHex(color);
1262
+ }
1263
+ });
1264
+ }
1265
+
1266
+ function updateModuleStatus(moduleId, status, severity) {
1267
+ const node = moduleNodes[moduleId];
1268
+ if (!node) {
1269
+ console.warn('[VISUALIZER] Module node not found:', moduleId);
1270
+ return;
1271
+ }
1272
+
1273
+ let color = 0x00ff88; // secure (green)
1274
+ if (status === 'warning') color = 0xffaa00; // warning (orange)
1275
+ if (status === 'critical') color = 0xff0044; // critical (red)
1276
+
1277
+ console.log('[VISUALIZER] Updating module:', moduleId, 'to', status, 'color:', color.toString(16));
1278
+
1279
+ // Update the status indicator sphere
1280
+ node.traverse(child => {
1281
+ if (child.name === `indicator-${moduleId}`) {
1282
+ child.material.color.setHex(color);
1283
+ // Make critical indicators glow more
1284
+ if (status === 'critical') {
1285
+ child.material.emissive = new THREE.Color(color);
1286
+ child.material.emissiveIntensity = 0.5;
1287
+ } else {
1288
+ child.material.emissiveIntensity = 0;
1289
+ }
1290
+ }
1291
+ // Also change the main module box color for critical findings
1292
+ if (child.name === `module-${moduleId}` && status === 'critical') {
1293
+ child.material.color.setHex(0xff2222);
1294
+ child.material.emissive = new THREE.Color(0xff0000);
1295
+ child.material.emissiveIntensity = 0.3;
1296
+ } else if (child.name === `module-${moduleId}` && status !== 'critical') {
1297
+ // Restore original color for the module
1298
+ const mod = MODULES.find(m => m.id === moduleId);
1299
+ if (mod) {
1300
+ child.material.color.setHex(mod.color);
1301
+ child.material.emissiveIntensity = 0.2;
1302
+ }
1303
+ }
1304
+ });
1305
+
1306
+ node.userData.status = status;
1307
+ }
1308
+
1309
+ function clearFindingMarkers() {
1310
+ findingMarkers.forEach(marker => scene.remove(marker));
1311
+ findingMarkers = [];
1312
+ }
1313
+
1314
+ function addFindingMarker(finding, index) {
1315
+ const severity = finding.payload.severity;
1316
+ const colors = { critical: 0xff0044, high: 0xff4400, medium: 0xffaa00, low: 0x00ff88 };
1317
+ const color = colors[severity] || 0xffffff;
1318
+
1319
+ // Position around auditor
1320
+ const angle = (index / 8) * Math.PI * 2;
1321
+ const radius = 8;
1322
+
1323
+ const geometry = new THREE.OctahedronGeometry(0.8, 0);
1324
+ const material = new THREE.MeshBasicMaterial({ color });
1325
+ const marker = new THREE.Mesh(geometry, material);
1326
+
1327
+ marker.position.set(
1328
+ Math.cos(angle) * radius,
1329
+ 6 + Math.sin(index) * 2,
1330
+ Math.sin(angle) * radius
1331
+ );
1332
+
1333
+ marker.userData = { finding, severity };
1334
+ findingMarkers.push(marker);
1335
+ scene.add(marker);
1336
+
1337
+ // Update affected modules
1338
+ finding.payload.affected_assets.forEach(asset => {
1339
+ const modId = asset.toLowerCase();
1340
+ if (moduleNodes[modId]) {
1341
+ updateModuleStatus(modId, severity === 'critical' || severity === 'high' ? 'critical' : 'warning', severity);
1342
+ }
1343
+ });
1344
+ }
1345
+
1346
+ function updateFindings(events) {
1347
+ clearFindingMarkers();
1348
+ currentFindings = events.filter(e =>
1349
+ e.event_type === 'finding_raised' || e.event_type === 'escalation_triggered'
1350
+ );
1351
+
1352
+ currentFindings.forEach((event, i) => addFindingMarker(event, i));
1353
+ renderFindingsList(currentFindings);
1354
+ updateStats(currentFindings);
1355
+ updateModuleStatusPanel(currentFindings);
1356
+ }
1357
+
1358
+ function renderFindingsList(findings) {
1359
+ const container = document.getElementById('findingsList');
1360
+ if (findings.length === 0) {
1361
+ container.innerHTML = '<p style="color: #666; font-size: 11px;">No findings yet. Run an audit to detect issues.</p>';
1362
+ return;
1363
+ }
1364
+
1365
+ container.innerHTML = findings.map(f => `
1366
+ <div class="finding ${f.payload.severity}">
1367
+ <div class="finding-header">
1368
+ <span class="severity-badge ${f.payload.severity}">${f.payload.severity.toUpperCase()}</span>
1369
+ <span class="finding-module">${f.payload.affected_assets[0] || 'system'}</span>
1370
+ </div>
1371
+ <div class="finding-claim">${f.payload.claim}</div>
1372
+ <div class="finding-details">
1373
+ <strong>Attack Path:</strong><br>
1374
+ ${f.payload.attack_path.map(p => '&bull; ' + p).join('<br>')}
1375
+ <br><br>
1376
+ <strong>Confidence:</strong> ${(f.payload.confidence * 100).toFixed(0)}%
1377
+ </div>
1378
+ </div>
1379
+ `).join('');
1380
+ }
1381
+
1382
+ function updateStats(findings) {
1383
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
1384
+ findings.forEach(f => {
1385
+ if (counts[f.payload.severity] !== undefined) {
1386
+ counts[f.payload.severity]++;
1387
+ }
1388
+ });
1389
+
1390
+ document.getElementById('statCritical').textContent = counts.critical;
1391
+ document.getElementById('statHigh').textContent = counts.high;
1392
+ document.getElementById('statMedium').textContent = counts.medium;
1393
+ document.getElementById('statLow').textContent = counts.low;
1394
+ }
1395
+
1396
+ function updateModuleStatusPanel(findings) {
1397
+ // Reset all modules
1398
+ MODULES.forEach(mod => updateModuleStatus(mod.id, 'secure', 'low'));
1399
+
1400
+ // Update based on findings
1401
+ findings.forEach(f => {
1402
+ f.payload.affected_assets.forEach(asset => {
1403
+ const modId = MODULES.find(m =>
1404
+ asset.toLowerCase().includes(m.id) ||
1405
+ m.id.includes(asset.toLowerCase())
1406
+ )?.id;
1407
+
1408
+ if (modId) {
1409
+ const status = f.payload.severity === 'critical' || f.payload.severity === 'high' ? 'critical' : 'warning';
1410
+ updateModuleStatus(modId, status, f.payload.severity);
1411
+ }
1412
+ });
1413
+ });
1414
+
1415
+ // Refresh the sidebar display
1416
+ refreshModuleStatusPanel();
1417
+ }
1418
+
1419
+ // Refresh the module status sidebar without resetting statuses
1420
+ function refreshModuleStatusPanel() {
1421
+ const container = document.getElementById('moduleStatus');
1422
+ container.innerHTML = MODULES.map(mod => {
1423
+ const node = moduleNodes[mod.id];
1424
+ const status = node?.userData?.status || 'secure';
1425
+ const color = status === 'critical' ? '#ff0044' : status === 'warning' ? '#ffaa00' : '#00ff88';
1426
+ return `<div class="legend-item">
1427
+ <div class="legend-icon module" style="background: ${color}"></div>
1428
+ <span>${mod.name} - ${status.toUpperCase()}</span>
1429
+ </div>`;
1430
+ }).join('');
1431
+ }
1432
+
1433
+ // ========== API INTEGRATION ==========
1434
+ async function pollAura() {
1435
+ try {
1436
+ const infoRes = await fetch(`${AURA_URL}/info`);
1437
+ if (!infoRes.ok) throw new Error('Not connected');
1438
+
1439
+ if (!connected) {
1440
+ connected = true;
1441
+ document.getElementById('statusDot').classList.remove('error');
1442
+ document.getElementById('statusText').textContent = wsConnected ? 'Connected (WS)' : 'Connected';
1443
+ }
1444
+
1445
+ // Use the real /stats endpoint for database-backed statistics
1446
+ const statsRes = await fetch(`${AURA_URL}/stats`);
1447
+ const stats = await statsRes.json();
1448
+
1449
+ document.getElementById('auditCount').textContent = `${stats.totalAudits || 0} Audits`;
1450
+
1451
+ // Update severity counts from real data (only if no audit is selected)
1452
+ if (!selectedAuditId) {
1453
+ const sev = stats.severityCounts || { critical: 0, high: 0, medium: 0, low: 0 };
1454
+ document.getElementById('statCritical').textContent = sev.critical;
1455
+ document.getElementById('statHigh').textContent = sev.high;
1456
+ document.getElementById('statMedium').textContent = sev.medium;
1457
+ document.getElementById('statLow').textContent = sev.low;
1458
+ }
1459
+
1460
+ } catch (err) {
1461
+ if (connected) {
1462
+ connected = false;
1463
+ document.getElementById('statusDot').classList.add('error');
1464
+ document.getElementById('statusText').textContent = 'Disconnected';
1465
+ }
1466
+ }
1467
+ }
1468
+
1469
+ // ========== WEBSOCKET CONNECTION ==========
1470
+ function connectWebSocket() {
1471
+ if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
1472
+ return; // Already connected or connecting
1473
+ }
1474
+
1475
+ try {
1476
+ ws = new WebSocket(WS_URL);
1477
+
1478
+ ws.onopen = () => {
1479
+ console.log('[WS] Connected to WebSocket server');
1480
+ wsConnected = true;
1481
+ document.getElementById('statusDot').classList.remove('error');
1482
+ document.getElementById('statusText').textContent = 'Connected (WS)';
1483
+ };
1484
+
1485
+ ws.onmessage = (event) => {
1486
+ try {
1487
+ const msg = JSON.parse(event.data);
1488
+ handleWebSocketMessage(msg);
1489
+ } catch (err) {
1490
+ console.error('[WS] Invalid message:', err);
1491
+ }
1492
+ };
1493
+
1494
+ ws.onclose = () => {
1495
+ console.log('[WS] Disconnected');
1496
+ wsConnected = false;
1497
+ // Retry connection after 3 seconds
1498
+ setTimeout(connectWebSocket, 3000);
1499
+ };
1500
+
1501
+ ws.onerror = (err) => {
1502
+ console.error('[WS] Error:', err);
1503
+ wsConnected = false;
1504
+ };
1505
+ } catch (err) {
1506
+ console.error('[WS] Failed to connect:', err);
1507
+ wsConnected = false;
1508
+ }
1509
+ }
1510
+
1511
+ function handleWebSocketMessage(msg) {
1512
+ switch (msg.type) {
1513
+ case 'audit_started':
1514
+ console.log('[WS] Audit started:', msg.payload);
1515
+ showToast(`Scanning: ${msg.payload.target}`, false);
1516
+ showLoading(true);
1517
+ break;
1518
+
1519
+ case 'audit_completed':
1520
+ console.log('[WS] Audit completed:', msg.payload);
1521
+ showLoading(false);
1522
+ // Refresh the audit history to show the new audit
1523
+ refreshAuditHistory();
1524
+ // Update stats display
1525
+ if (msg.payload.summary) {
1526
+ const s = msg.payload.summary;
1527
+ const total = s.critical + s.high + s.medium + s.low;
1528
+ showToast(`Scan complete: ${total} findings`, s.critical > 0 || s.high > 0);
1529
+ }
1530
+ break;
1531
+
1532
+ case 'finding':
1533
+ console.log('[WS] Finding:', msg.payload);
1534
+ // Could add real-time finding display here
1535
+ break;
1536
+
1537
+ case 'settings_changed':
1538
+ console.log('[WS] Settings changed:', msg.payload);
1539
+ showToast('Settings updated', false);
1540
+ break;
1541
+
1542
+ case 'ping':
1543
+ // Respond to ping
1544
+ if (ws && ws.readyState === WebSocket.OPEN) {
1545
+ ws.send(JSON.stringify({ type: 'pong', payload: { time: Date.now() }, timestamp: new Date().toISOString() }));
1546
+ }
1547
+ break;
1548
+
1549
+ case 'status':
1550
+ console.log('[WS] Status:', msg.payload);
1551
+ break;
1552
+ }
1553
+ }
1554
+
1555
+ async function runPreset(presetName) {
1556
+ showLoading(true);
1557
+
1558
+ const preset = PRESETS[presetName];
1559
+ if (!preset) {
1560
+ showToast('Unknown preset', true);
1561
+ showLoading(false);
1562
+ return;
1563
+ }
1564
+
1565
+ preset.change_event.id = `preset-${presetName}-${Date.now()}`;
1566
+
1567
+ try {
1568
+ const res = await fetch(`${AURA_URL}/tools`, {
1569
+ method: 'POST',
1570
+ headers: { 'Content-Type': 'application/json' },
1571
+ body: JSON.stringify({ tool: 'audit', arguments: preset })
1572
+ });
1573
+
1574
+ const data = await res.json();
1575
+
1576
+ if (data.result) {
1577
+ updateAuditorState(data.result.agent_state);
1578
+ updateFindings(data.result.events);
1579
+ showToast(`Audit: ${data.result.agent_state.toUpperCase()}`);
1580
+ } else {
1581
+ showToast('Audit failed', true);
1582
+ }
1583
+ } catch (err) {
1584
+ showToast('Request failed: ' + err.message, true);
1585
+ }
1586
+
1587
+ showLoading(false);
1588
+ }
1589
+
1590
+ // ========== AUDIT HISTORY ==========
1591
+ let auditHistoryCache = [];
1592
+ let selectedAuditId = null;
1593
+
1594
+ async function refreshAuditHistory() {
1595
+ try {
1596
+ // Use the new /audits database endpoint for real data
1597
+ const res = await fetch(`${AURA_URL}/audits?limit=20`);
1598
+ const data = await res.json();
1599
+ auditHistoryCache = data.audits || [];
1600
+
1601
+ document.getElementById('auditHistoryCount').textContent = `(${data.total || 0})`;
1602
+
1603
+ const listEl = document.getElementById('auditHistoryList');
1604
+ if (auditHistoryCache.length === 0) {
1605
+ listEl.innerHTML = '<p style="color: #666; font-size: 11px;">No audits yet. Run a scan to see results.</p>';
1606
+ return;
1607
+ }
1608
+
1609
+ listEl.innerHTML = auditHistoryCache.map((audit) => {
1610
+ const shortId = audit.id.length > 25 ? audit.id.substring(0, 25) + '...' : audit.id;
1611
+ const shortTarget = audit.target ? (audit.target.length > 30 ? '...' + audit.target.slice(-30) : audit.target) : 'Unknown';
1612
+ const summary = audit.summary || { critical: 0, high: 0, medium: 0, low: 0 };
1613
+ const totalFindings = summary.critical + summary.high + summary.medium + summary.low;
1614
+
1615
+ // Color based on severity
1616
+ let stateColor = '#00ff88'; // green = safe
1617
+ if (summary.critical > 0) stateColor = '#ff0044';
1618
+ else if (summary.high > 0) stateColor = '#ff4400';
1619
+ else if (summary.medium > 0) stateColor = '#ffaa00';
1620
+
1621
+ const timestamp = new Date(audit.timestamp).toLocaleString();
1622
+ const isSelected = audit.id === selectedAuditId ? 'background: rgba(0,255,136,0.2);' : '';
1623
+
1624
+ return `
1625
+ <div class="audit-history-item" onclick="selectAudit('${audit.id}')" style="
1626
+ padding: 8px;
1627
+ margin: 4px 0;
1628
+ border-left: 3px solid ${stateColor};
1629
+ background: rgba(0,0,0,0.3);
1630
+ cursor: pointer;
1631
+ ${isSelected}
1632
+ ">
1633
+ <div style="font-size: 11px; font-weight: bold;">${audit.type.toUpperCase()} Scan</div>
1634
+ <div style="font-size: 10px; color: #888; margin: 2px 0;">${shortTarget}</div>
1635
+ <div style="font-size: 9px; color: ${stateColor};">
1636
+ ${totalFindings > 0 ? `${totalFindings} findings (${summary.critical}C/${summary.high}H/${summary.medium}M/${summary.low}L)` : 'No issues found'}
1637
+ </div>
1638
+ <div style="font-size: 8px; color: #555; margin-top: 3px;">${timestamp}</div>
1639
+ </div>
1640
+ `;
1641
+ }).join('');
1642
+
1643
+ } catch (err) {
1644
+ console.error('Failed to load audit history:', err);
1645
+ document.getElementById('auditHistoryList').innerHTML =
1646
+ `<p style="color: #ff4444; font-size: 11px;">Error: ${err.message}</p>`;
1647
+ }
1648
+ }
1649
+
1650
+ async function selectAudit(auditId) {
1651
+ selectedAuditId = auditId;
1652
+ showLoading(true);
1653
+
1654
+ try {
1655
+ // Use the new /audits/:id endpoint for real database data
1656
+ const res = await fetch(`${AURA_URL}/audits/${encodeURIComponent(auditId)}`);
1657
+ const audit = await res.json();
1658
+
1659
+ if (audit && !audit.error) {
1660
+ // Show selected audit panel
1661
+ const panel = document.getElementById('selectedAuditPanel');
1662
+ panel.style.display = 'block';
1663
+
1664
+ const summary = audit.summary || { critical: 0, high: 0, medium: 0, low: 0 };
1665
+ const scanData = audit.data || {};
1666
+ const totalFindings = summary.critical + summary.high + summary.medium + summary.low;
1667
+
1668
+ let info = `<div><strong>ID:</strong> ${audit.id}</div>`;
1669
+ info += `<div><strong>Type:</strong> ${audit.type.toUpperCase()}</div>`;
1670
+ info += `<div><strong>Target:</strong> ${audit.target}</div>`;
1671
+ info += `<div><strong>Time:</strong> ${new Date(audit.timestamp).toLocaleString()}</div>`;
1672
+
1673
+ info += `<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #00ff8844;">`;
1674
+ info += `<div><strong>Findings:</strong> ${totalFindings}</div>`;
1675
+ info += `<div style="font-size: 10px; color: #888;">
1676
+ Critical: ${summary.critical} | High: ${summary.high} | Medium: ${summary.medium} | Low: ${summary.low}
1677
+ </div>`;
1678
+
1679
+ if (scanData.secrets && scanData.secrets.length > 0) {
1680
+ info += `<div style="margin-top: 5px;"><strong>Secrets Found:</strong> ${scanData.secrets.length}</div>`;
1681
+ }
1682
+ if (scanData.packages && scanData.packages.length > 0) {
1683
+ info += `<div><strong>Vulnerable Packages:</strong> ${scanData.packages.length}</div>`;
1684
+ }
1685
+ if (scanData.sastFindings && scanData.sastFindings.length > 0) {
1686
+ info += `<div><strong>SAST Issues:</strong> ${scanData.sastFindings.length}</div>`;
1687
+ }
1688
+ if (scanData.toolsUsed) {
1689
+ info += `<div><strong>Tools:</strong> ${scanData.toolsUsed.join(', ')}</div>`;
1690
+ }
1691
+ info += `</div>`;
1692
+
1693
+ // Show detailed findings if any
1694
+ if (scanData.secrets && scanData.secrets.length > 0) {
1695
+ info += `<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #ff004444;">`;
1696
+ info += `<div style="color: #ff0044; font-weight: bold; margin-bottom: 5px;">SECRETS DETECTED:</div>`;
1697
+ scanData.secrets.slice(0, 5).forEach(s => {
1698
+ info += `<div style="font-size: 9px; color: #ff6666; margin: 2px 0;">
1699
+ ${s.type}: ${s.file}:${s.line}
1700
+ </div>`;
1701
+ });
1702
+ if (scanData.secrets.length > 5) {
1703
+ info += `<div style="font-size: 9px; color: #888;">...and ${scanData.secrets.length - 5} more</div>`;
1704
+ }
1705
+ info += `</div>`;
1706
+ }
1707
+
1708
+ document.getElementById('selectedAuditInfo').innerHTML = info;
1709
+
1710
+ // Update stats bar with this audit's findings
1711
+ document.getElementById('statCritical').textContent = summary.critical;
1712
+ document.getElementById('statHigh').textContent = summary.high;
1713
+ document.getElementById('statMedium').textContent = summary.medium;
1714
+ document.getElementById('statLow').textContent = summary.low;
1715
+
1716
+ showToast(`Loaded audit: ${audit.type} scan`);
1717
+ } else {
1718
+ showToast('Audit not found', true);
1719
+ }
1720
+ } catch (err) {
1721
+ console.error('Failed to load audit:', err);
1722
+ showToast('Failed to load audit: ' + err.message, true);
1723
+ }
1724
+
1725
+ showLoading(false);
1726
+ refreshAuditHistory(); // Refresh to update selection highlight
1727
+ }
1728
+
1729
+ function clearSelectedAudit() {
1730
+ selectedAuditId = null;
1731
+ document.getElementById('selectedAuditPanel').style.display = 'none';
1732
+ // Reset stats
1733
+ document.getElementById('statCritical').textContent = '0';
1734
+ document.getElementById('statHigh').textContent = '0';
1735
+ document.getElementById('statMedium').textContent = '0';
1736
+ document.getElementById('statLow').textContent = '0';
1737
+ refreshAuditHistory();
1738
+ }
1739
+
1740
+ // ========== SETTINGS FUNCTIONS ==========
1741
+ let settingsDirty = false;
1742
+
1743
+ async function openSettingsModal() {
1744
+ document.getElementById('settingsModal').style.display = 'flex';
1745
+ settingsDirty = false;
1746
+ document.getElementById('saveSettingsBtn').disabled = true;
1747
+
1748
+ // Load current settings from server
1749
+ try {
1750
+ const res = await fetch(`${AURA_URL}/settings`);
1751
+ if (res.ok) {
1752
+ const data = await res.json();
1753
+ populateSettings(data.settings);
1754
+ }
1755
+ } catch (err) {
1756
+ console.error('Failed to load settings:', err);
1757
+ showToast('Failed to load settings', true);
1758
+ }
1759
+ }
1760
+
1761
+ function closeSettingsModal() {
1762
+ document.getElementById('settingsModal').style.display = 'none';
1763
+ }
1764
+
1765
+ function switchSettingsTab(tab) {
1766
+ // Update tab buttons
1767
+ document.querySelectorAll('.settings-tab').forEach(btn => {
1768
+ btn.classList.toggle('active', btn.dataset.tab === tab);
1769
+ });
1770
+
1771
+ // Show/hide panels
1772
+ document.querySelectorAll('.settings-panel').forEach(panel => {
1773
+ panel.style.display = 'none';
1774
+ });
1775
+ document.getElementById(`settings-${tab}`).style.display = 'block';
1776
+ }
1777
+
1778
+ function markSettingsDirty() {
1779
+ settingsDirty = true;
1780
+ document.getElementById('saveSettingsBtn').disabled = false;
1781
+ }
1782
+
1783
+ function populateSettings(settings) {
1784
+ // AWS settings
1785
+ document.getElementById('aws-enabled').checked = settings['aws.enabled'] === 'true';
1786
+ document.getElementById('aws-region').value = settings['aws.region'] || 'us-east-1';
1787
+ document.getElementById('aws-profile').value = settings['aws.profile'] || '';
1788
+ document.getElementById('aws-iam').checked = settings['aws.services.iam'] !== 'false';
1789
+ document.getElementById('aws-s3').checked = settings['aws.services.s3'] !== 'false';
1790
+ document.getElementById('aws-ec2').checked = settings['aws.services.ec2'] !== 'false';
1791
+ document.getElementById('aws-lambda').checked = settings['aws.services.lambda'] !== 'false';
1792
+ document.getElementById('aws-rds').checked = settings['aws.services.rds'] !== 'false';
1793
+
1794
+ // Slack settings
1795
+ document.getElementById('slack-enabled').checked = settings['notifications.slack.enabled'] === 'true';
1796
+ document.getElementById('slack-webhook').value = settings['notifications.slack.webhookUrl'] || '';
1797
+ document.getElementById('slack-channel').value = settings['notifications.slack.channel'] || '';
1798
+
1799
+ // Discord settings
1800
+ document.getElementById('discord-enabled').checked = settings['notifications.discord.enabled'] === 'true';
1801
+ document.getElementById('discord-webhook').value = settings['notifications.discord.webhookUrl'] || '';
1802
+
1803
+ // Webhook settings
1804
+ document.getElementById('webhook-enabled').checked = settings['notifications.webhook.enabled'] === 'true';
1805
+ document.getElementById('webhook-url').value = settings['notifications.webhook.url'] || '';
1806
+ document.getElementById('webhook-headers').value = settings['notifications.webhook.headers'] || '';
1807
+
1808
+ // Update status indicator
1809
+ const enabledCount = [
1810
+ settings['aws.enabled'] === 'true',
1811
+ settings['notifications.slack.enabled'] === 'true',
1812
+ settings['notifications.discord.enabled'] === 'true',
1813
+ settings['notifications.webhook.enabled'] === 'true'
1814
+ ].filter(Boolean).length;
1815
+
1816
+ document.getElementById('settingsStatus').textContent = enabledCount > 0 ? `(${enabledCount} active)` : '';
1817
+ }
1818
+
1819
+ async function saveSettings() {
1820
+ const settings = {
1821
+ // AWS settings
1822
+ 'aws.enabled': document.getElementById('aws-enabled').checked.toString(),
1823
+ 'aws.region': document.getElementById('aws-region').value,
1824
+ 'aws.profile': document.getElementById('aws-profile').value,
1825
+ 'aws.services.iam': document.getElementById('aws-iam').checked.toString(),
1826
+ 'aws.services.s3': document.getElementById('aws-s3').checked.toString(),
1827
+ 'aws.services.ec2': document.getElementById('aws-ec2').checked.toString(),
1828
+ 'aws.services.lambda': document.getElementById('aws-lambda').checked.toString(),
1829
+ 'aws.services.rds': document.getElementById('aws-rds').checked.toString(),
1830
+
1831
+ // Slack settings
1832
+ 'notifications.slack.enabled': document.getElementById('slack-enabled').checked.toString(),
1833
+ 'notifications.slack.webhookUrl': document.getElementById('slack-webhook').value,
1834
+ 'notifications.slack.channel': document.getElementById('slack-channel').value,
1835
+
1836
+ // Discord settings
1837
+ 'notifications.discord.enabled': document.getElementById('discord-enabled').checked.toString(),
1838
+ 'notifications.discord.webhookUrl': document.getElementById('discord-webhook').value,
1839
+
1840
+ // Webhook settings
1841
+ 'notifications.webhook.enabled': document.getElementById('webhook-enabled').checked.toString(),
1842
+ 'notifications.webhook.url': document.getElementById('webhook-url').value,
1843
+ 'notifications.webhook.headers': document.getElementById('webhook-headers').value
1844
+ };
1845
+
1846
+ try {
1847
+ const res = await fetch(`${AURA_URL}/settings`, {
1848
+ method: 'POST',
1849
+ headers: { 'Content-Type': 'application/json' },
1850
+ body: JSON.stringify({ settings })
1851
+ });
1852
+
1853
+ if (res.ok) {
1854
+ showToast('Settings saved');
1855
+ settingsDirty = false;
1856
+ document.getElementById('saveSettingsBtn').disabled = true;
1857
+ closeSettingsModal();
1858
+
1859
+ // Update status indicator
1860
+ const enabledCount = [
1861
+ settings['aws.enabled'] === 'true',
1862
+ settings['notifications.slack.enabled'] === 'true',
1863
+ settings['notifications.discord.enabled'] === 'true',
1864
+ settings['notifications.webhook.enabled'] === 'true'
1865
+ ].filter(Boolean).length;
1866
+ document.getElementById('settingsStatus').textContent = enabledCount > 0 ? `(${enabledCount} active)` : '';
1867
+ } else {
1868
+ showToast('Failed to save settings', true);
1869
+ }
1870
+ } catch (err) {
1871
+ console.error('Failed to save settings:', err);
1872
+ showToast('Failed to save settings', true);
1873
+ }
1874
+ }
1875
+
1876
+ async function testNotification(channel) {
1877
+ showLoading(true);
1878
+ try {
1879
+ const res = await fetch(`${AURA_URL}/notifications/test`, {
1880
+ method: 'POST',
1881
+ headers: { 'Content-Type': 'application/json' },
1882
+ body: JSON.stringify({ channel })
1883
+ });
1884
+
1885
+ const data = await res.json();
1886
+ if (data.success) {
1887
+ showToast(`${channel} test sent successfully`);
1888
+ } else {
1889
+ showToast(`${channel} test failed: ${data.error}`, true);
1890
+ }
1891
+ } catch (err) {
1892
+ showToast(`${channel} test failed`, true);
1893
+ }
1894
+ showLoading(false);
1895
+ }
1896
+
1897
+ async function runAWSScan() {
1898
+ // First save the settings
1899
+ await saveSettings();
1900
+
1901
+ showLoading(true);
1902
+ try {
1903
+ // Get AWS settings
1904
+ const region = document.getElementById('aws-region').value;
1905
+ const services = [];
1906
+ if (document.getElementById('aws-iam').checked) services.push('iam');
1907
+ if (document.getElementById('aws-s3').checked) services.push('s3');
1908
+ if (document.getElementById('aws-ec2').checked) services.push('ec2');
1909
+ if (document.getElementById('aws-lambda').checked) services.push('lambda');
1910
+ if (document.getElementById('aws-rds').checked) services.push('rds');
1911
+
1912
+ const res = await fetch(`${AURA_URL}/tools`, {
1913
+ method: 'POST',
1914
+ headers: { 'Content-Type': 'application/json' },
1915
+ body: JSON.stringify({
1916
+ tool: 'scan-aws',
1917
+ arguments: { region, services }
1918
+ })
1919
+ });
1920
+
1921
+ if (res.ok) {
1922
+ const data = await res.json();
1923
+ showToast('AWS scan started');
1924
+ closeSettingsModal();
1925
+ // Refresh audit history to show results
1926
+ setTimeout(refreshAuditHistory, 2000);
1927
+ } else {
1928
+ showToast('AWS scan failed', true);
1929
+ }
1930
+ } catch (err) {
1931
+ console.error('AWS scan error:', err);
1932
+ showToast('AWS scan failed', true);
1933
+ }
1934
+ showLoading(false);
1935
+ }
1936
+
1937
+ // ========== UI HELPERS ==========
1938
+ function showLoading(show) {
1939
+ document.getElementById('loading').classList.toggle('active', show);
1940
+ }
1941
+
1942
+ function showToast(message, isError = false) {
1943
+ const container = document.getElementById('toast-container');
1944
+ const toast = document.createElement('div');
1945
+ toast.className = `toast ${isError ? 'error' : ''}`;
1946
+ toast.textContent = message;
1947
+ container.appendChild(toast);
1948
+ setTimeout(() => toast.remove(), 4000);
1949
+ }
1950
+
1951
+ function onWindowResize() {
1952
+ camera.aspect = window.innerWidth / window.innerHeight;
1953
+ camera.updateProjectionMatrix();
1954
+ renderer.setSize(window.innerWidth, window.innerHeight);
1955
+ }
1956
+
1957
+ function onMouseMove(event) {
1958
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
1959
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
1960
+
1961
+ // Raycast for tooltips
1962
+ raycaster.setFromCamera(mouse, camera);
1963
+ const intersects = raycaster.intersectObjects(scene.children, true);
1964
+
1965
+ const tooltip = document.getElementById('tooltip');
1966
+
1967
+ if (intersects.length > 0) {
1968
+ let obj = intersects[0].object;
1969
+ while (obj.parent && !obj.userData.type) {
1970
+ obj = obj.parent;
1971
+ }
1972
+
1973
+ if (obj.userData.type) {
1974
+ tooltip.classList.add('visible');
1975
+ tooltip.style.left = event.clientX + 15 + 'px';
1976
+ tooltip.style.top = event.clientY + 15 + 'px';
1977
+ document.getElementById('tooltipTitle').textContent = obj.userData.name || obj.userData.type;
1978
+ document.getElementById('tooltipContent').innerHTML = `
1979
+ <div>${obj.userData.desc || ''}</div>
1980
+ ${obj.userData.status ? `<div class="module-status">Status: ${obj.userData.status.toUpperCase()}</div>` : ''}
1981
+ `;
1982
+ return;
1983
+ }
1984
+ }
1985
+
1986
+ tooltip.classList.remove('visible');
1987
+ }
1988
+
1989
+ // ========== MODULE MANAGEMENT ==========
1990
+ let selectedModule = null;
1991
+ let moduleEnabled = {};
1992
+
1993
+ // Initialize all modules as enabled
1994
+ MODULES.forEach(mod => moduleEnabled[mod.id] = true);
1995
+
1996
+ function onMouseClick(event) {
1997
+ // Ignore clicks on UI elements
1998
+ if (event.target.closest('.ui-layer') || event.target.closest('#module-modal')) return;
1999
+
2000
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
2001
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
2002
+
2003
+ raycaster.setFromCamera(mouse, camera);
2004
+ const intersects = raycaster.intersectObjects(scene.children, true);
2005
+
2006
+ if (intersects.length > 0) {
2007
+ let obj = intersects[0].object;
2008
+ while (obj.parent && !obj.userData.type) {
2009
+ obj = obj.parent;
2010
+ }
2011
+
2012
+ if (obj.userData.type === 'module') {
2013
+ selectModule(obj.userData.id);
2014
+ return;
2015
+ }
2016
+
2017
+ if (obj.userData.type === 'auditor') {
2018
+ selectModule(null);
2019
+ showToast('Security Auditor selected - central analysis node');
2020
+ return;
2021
+ }
2022
+ }
2023
+
2024
+ // Clicked empty space - deselect
2025
+ selectModule(null);
2026
+ }
2027
+
2028
+ function selectModule(moduleId) {
2029
+ // Clear previous selection visual
2030
+ if (selectedModule && moduleNodes[selectedModule]) {
2031
+ moduleNodes[selectedModule].traverse(child => {
2032
+ if (child.isMesh && child.name?.startsWith('module-')) {
2033
+ child.material.emissiveIntensity = 0.2;
2034
+ }
2035
+ });
2036
+ }
2037
+
2038
+ selectedModule = moduleId;
2039
+
2040
+ if (moduleId && moduleNodes[moduleId]) {
2041
+ // Highlight selected module
2042
+ moduleNodes[moduleId].traverse(child => {
2043
+ if (child.isMesh && child.name?.startsWith('module-')) {
2044
+ child.material.emissiveIntensity = 0.8;
2045
+ }
2046
+ });
2047
+
2048
+ const mod = MODULES.find(m => m.id === moduleId);
2049
+ document.getElementById('selectedModuleInfo').style.display = 'block';
2050
+ document.getElementById('selectedModuleName').textContent = mod?.name || moduleId;
2051
+ showToast(`Selected: ${mod?.name || moduleId}`);
2052
+ } else {
2053
+ document.getElementById('selectedModuleInfo').style.display = 'none';
2054
+ }
2055
+ }
2056
+
2057
+ function openModuleManager() {
2058
+ document.getElementById('modalTitle').textContent = 'MANAGE MODULES';
2059
+ document.getElementById('modalBody').innerHTML = `
2060
+ <p style="font-size: 11px; color: #888; margin-bottom: 15px;">Toggle modules on/off to include them in security audits.</p>
2061
+ ${MODULES.map(mod => `
2062
+ <div class="module-toggle ${moduleEnabled[mod.id] ? '' : 'disabled'}" onclick="toggleModule('${mod.id}')">
2063
+ <div>
2064
+ <strong style="color: #${mod.color.toString(16).padStart(6, '0')}">${mod.name}</strong>
2065
+ <div style="font-size: 10px; color: #666;">${mod.desc}</div>
2066
+ </div>
2067
+ <div class="toggle-switch ${moduleEnabled[mod.id] ? 'active' : ''}" id="toggle-${mod.id}"></div>
2068
+ </div>
2069
+ `).join('')}
2070
+ `;
2071
+ document.getElementById('module-modal').classList.add('visible');
2072
+ }
2073
+
2074
+ function addNewModule() {
2075
+ document.getElementById('modalTitle').textContent = 'ADD NEW MODULE';
2076
+ document.getElementById('modalBody').innerHTML = `
2077
+ <div class="form-group">
2078
+ <label>Module ID (lowercase, no spaces)</label>
2079
+ <input type="text" id="newModuleId" placeholder="e.g., logging" pattern="[a-z0-9_-]+">
2080
+ </div>
2081
+ <div class="form-group">
2082
+ <label>Display Name</label>
2083
+ <input type="text" id="newModuleName" placeholder="e.g., LOGGING">
2084
+ </div>
2085
+ <div class="form-group">
2086
+ <label>Description</label>
2087
+ <input type="text" id="newModuleDesc" placeholder="e.g., Audit Logging & Events">
2088
+ </div>
2089
+ <div class="form-group">
2090
+ <label>Color</label>
2091
+ <select id="newModuleColor">
2092
+ <option value="0x00ff88">Green</option>
2093
+ <option value="0x00aaff">Blue</option>
2094
+ <option value="0xffaa00">Orange</option>
2095
+ <option value="0xff00ff">Purple</option>
2096
+ <option value="0xff0088">Pink</option>
2097
+ <option value="0xffff00">Yellow</option>
2098
+ <option value="0x00ffff">Cyan</option>
2099
+ </select>
2100
+ </div>
2101
+ <div class="form-group">
2102
+ <label>Connect To (existing module)</label>
2103
+ <select id="newModuleConnect">
2104
+ <option value="">-- No Connection --</option>
2105
+ ${MODULES.map(m => `<option value="${m.id}">${m.name}</option>`).join('')}
2106
+ </select>
2107
+ </div>
2108
+ `;
2109
+ document.getElementById('module-modal').classList.add('visible');
2110
+ }
2111
+
2112
+ function toggleModule(moduleId) {
2113
+ moduleEnabled[moduleId] = !moduleEnabled[moduleId];
2114
+
2115
+ const toggle = document.getElementById(`toggle-${moduleId}`);
2116
+ const toggleParent = toggle?.parentElement;
2117
+
2118
+ if (toggle) {
2119
+ toggle.classList.toggle('active', moduleEnabled[moduleId]);
2120
+ }
2121
+ if (toggleParent) {
2122
+ toggleParent.classList.toggle('disabled', !moduleEnabled[moduleId]);
2123
+ }
2124
+
2125
+ // Visual feedback in 3D
2126
+ if (moduleNodes[moduleId]) {
2127
+ moduleNodes[moduleId].visible = moduleEnabled[moduleId];
2128
+ }
2129
+
2130
+ // Update connections
2131
+ connectionLines.forEach(line => {
2132
+ if (line.userData.to === moduleId || line.userData.from === moduleId) {
2133
+ line.visible = moduleEnabled[moduleId];
2134
+ }
2135
+ });
2136
+ }
2137
+
2138
+ function removeSelectedModule() {
2139
+ if (!selectedModule) {
2140
+ showToast('No module selected', true);
2141
+ return;
2142
+ }
2143
+
2144
+ const modIndex = MODULES.findIndex(m => m.id === selectedModule);
2145
+ if (modIndex === -1) return;
2146
+
2147
+ // Remove from scene
2148
+ if (moduleNodes[selectedModule]) {
2149
+ scene.remove(moduleNodes[selectedModule]);
2150
+ delete moduleNodes[selectedModule];
2151
+ }
2152
+
2153
+ // Remove connections
2154
+ connectionLines = connectionLines.filter(line => {
2155
+ if (line.userData.to === selectedModule || line.userData.from === selectedModule) {
2156
+ scene.remove(line);
2157
+ return false;
2158
+ }
2159
+ return true;
2160
+ });
2161
+
2162
+ // Remove from MODULES array
2163
+ MODULES.splice(modIndex, 1);
2164
+ delete moduleEnabled[selectedModule];
2165
+
2166
+ showToast(`Removed module: ${selectedModule}`);
2167
+ selectModule(null);
2168
+ updateModuleStatusPanel([]);
2169
+ }
2170
+
2171
+ function saveModuleChanges() {
2172
+ const modalTitle = document.getElementById('modalTitle').textContent;
2173
+
2174
+ if (modalTitle === 'ADD NEW MODULE') {
2175
+ const id = document.getElementById('newModuleId')?.value?.toLowerCase().replace(/[^a-z0-9_-]/g, '');
2176
+ const name = document.getElementById('newModuleName')?.value?.toUpperCase();
2177
+ const desc = document.getElementById('newModuleDesc')?.value;
2178
+ const color = parseInt(document.getElementById('newModuleColor')?.value || '0x00ff88');
2179
+ const connectTo = document.getElementById('newModuleConnect')?.value;
2180
+
2181
+ if (!id || !name) {
2182
+ showToast('Module ID and Name are required', true);
2183
+ return;
2184
+ }
2185
+
2186
+ if (MODULES.find(m => m.id === id)) {
2187
+ showToast('Module ID already exists', true);
2188
+ return;
2189
+ }
2190
+
2191
+ // Calculate position (find empty spot)
2192
+ const angle = MODULES.length * (Math.PI / 4);
2193
+ const radius = 18;
2194
+ const pos = [
2195
+ Math.cos(angle) * radius,
2196
+ 0,
2197
+ Math.sin(angle) * radius
2198
+ ];
2199
+
2200
+ // Add to MODULES array
2201
+ const newMod = { id, name, desc, pos, color };
2202
+ MODULES.push(newMod);
2203
+ moduleEnabled[id] = true;
2204
+
2205
+ // Create 3D node
2206
+ createSingleModuleNode(newMod);
2207
+
2208
+ // Add audit connection
2209
+ const auditLine = createConnectionLine(
2210
+ new THREE.Vector3(0, 5, 0),
2211
+ new THREE.Vector3(pos[0], pos[1] + 2, pos[2]),
2212
+ 0x00aaff,
2213
+ true
2214
+ );
2215
+ auditLine.userData = { type: 'audit-connection', to: id };
2216
+ connectionLines.push(auditLine);
2217
+ scene.add(auditLine);
2218
+
2219
+ // Add module-to-module connection if specified
2220
+ if (connectTo) {
2221
+ const targetMod = MODULES.find(m => m.id === connectTo);
2222
+ if (targetMod) {
2223
+ const dataLine = createConnectionLine(
2224
+ new THREE.Vector3(pos[0], pos[1] + 2, pos[2]),
2225
+ new THREE.Vector3(targetMod.pos[0], targetMod.pos[1] + 2, targetMod.pos[2]),
2226
+ 0x00ff8844,
2227
+ false
2228
+ );
2229
+ dataLine.userData = { type: 'data-flow', from: id, to: connectTo, label: 'Data' };
2230
+ connectionLines.push(dataLine);
2231
+ scene.add(dataLine);
2232
+ CONNECTIONS.push({ from: id, to: connectTo, label: 'Data' });
2233
+ }
2234
+ }
2235
+
2236
+ showToast(`Added module: ${name}`);
2237
+ updateModuleStatusPanel([]);
2238
+ }
2239
+
2240
+ closeModal();
2241
+ }
2242
+
2243
+ function createSingleModuleNode(mod) {
2244
+ const group = new THREE.Group();
2245
+
2246
+ const boxGeom = new THREE.BoxGeometry(5, 3, 5);
2247
+ const boxMat = new THREE.MeshPhongMaterial({
2248
+ color: mod.color,
2249
+ emissive: mod.color,
2250
+ emissiveIntensity: 0.2
2251
+ });
2252
+ const box = new THREE.Mesh(boxGeom, boxMat);
2253
+ box.name = `module-${mod.id}`;
2254
+ group.add(box);
2255
+
2256
+ const indicatorGeom = new THREE.CylinderGeometry(0.5, 0.5, 0.3, 16);
2257
+ const indicatorMat = new THREE.MeshBasicMaterial({ color: 0x00ff88 });
2258
+ const indicator = new THREE.Mesh(indicatorGeom, indicatorMat);
2259
+ indicator.position.y = 1.8;
2260
+ indicator.name = `indicator-${mod.id}`;
2261
+ group.add(indicator);
2262
+
2263
+ const labelSprite = createTextSprite(mod.name, mod.color);
2264
+ labelSprite.position.y = 4;
2265
+ labelSprite.scale.set(6, 3, 1);
2266
+ group.add(labelSprite);
2267
+
2268
+ group.position.set(mod.pos[0], mod.pos[1] + 2, mod.pos[2]);
2269
+ group.userData = { type: 'module', id: mod.id, name: mod.name, desc: mod.desc, status: 'secure' };
2270
+ moduleNodes[mod.id] = group;
2271
+ scene.add(group);
2272
+ }
2273
+
2274
+ function closeModal() {
2275
+ document.getElementById('module-modal').classList.remove('visible');
2276
+ }
2277
+
2278
+ // ========== ANIMATION LOOP ==========
2279
+ let time = 0;
2280
+
2281
+ function animate() {
2282
+ requestAnimationFrame(animate);
2283
+ time += 0.01;
2284
+
2285
+ // Rotate auditor rings
2286
+ auditorNode.children.forEach(child => {
2287
+ if (child.name?.startsWith('auditor-ring')) {
2288
+ child.rotation.z += 0.01;
2289
+ }
2290
+ if (child.name === 'auditor-core') {
2291
+ child.rotation.y += 0.02;
2292
+ child.position.y = 5 + Math.sin(time * 2) * 0.3;
2293
+ }
2294
+ });
2295
+
2296
+ // Pulse finding markers
2297
+ findingMarkers.forEach((marker, i) => {
2298
+ marker.rotation.y += 0.03;
2299
+ marker.position.y = 6 + Math.sin(time * 3 + i) * 0.5;
2300
+ const scale = 1 + Math.sin(time * 4 + i) * 0.2;
2301
+ marker.scale.set(scale, scale, scale);
2302
+ });
2303
+
2304
+ // Animate particles
2305
+ const particles = scene.getObjectByName('particles');
2306
+ if (particles) {
2307
+ particles.rotation.y += 0.0003;
2308
+ const positions = particles.geometry.attributes.position.array;
2309
+ for (let i = 0; i < positions.length; i += 3) {
2310
+ positions[i + 1] += 0.01;
2311
+ if (positions[i + 1] > 40) positions[i + 1] = 0;
2312
+ }
2313
+ particles.geometry.attributes.position.needsUpdate = true;
2314
+ }
2315
+
2316
+ controls.update();
2317
+ renderer.render(scene, camera);
2318
+ }
2319
+
2320
+ // ========== PROJECT SCANNING ==========
2321
+ let currentScanDetails = null;
2322
+ let scanMode = 'local';
2323
+
2324
+ // Switch between local and git scan modes
2325
+ function setScanMode(mode) {
2326
+ scanMode = mode;
2327
+ document.getElementById('localInput').style.display = mode === 'local' ? 'block' : 'none';
2328
+ document.getElementById('gitInput').style.display = mode === 'git' ? 'block' : 'none';
2329
+ document.getElementById('tabLocal').className = mode === 'local' ? 'btn' : 'btn secondary';
2330
+ document.getElementById('tabGit').className = mode === 'git' ? 'btn' : 'btn secondary';
2331
+ }
2332
+
2333
+ // Get project name from path or URL
2334
+ function getProjectName(pathOrUrl) {
2335
+ if (!pathOrUrl) return 'Project';
2336
+ // Handle git URLs
2337
+ if (pathOrUrl.includes('github.com') || pathOrUrl.includes('gitlab.com') || pathOrUrl.endsWith('.git')) {
2338
+ const match = pathOrUrl.match(/\/([^\/]+?)(\.git)?$/);
2339
+ return match ? match[1] : 'GitRepo';
2340
+ }
2341
+ // Handle local paths
2342
+ const parts = pathOrUrl.replace(/\\/g, '/').split('/').filter(p => p);
2343
+ return parts[parts.length - 1] || 'Project';
2344
+ }
2345
+
2346
+ // Main scan function
2347
+ async function doScan() {
2348
+ const targetPath = document.getElementById('scanPath').value.trim();
2349
+ if (!targetPath) {
2350
+ showToast('Enter a directory path', true);
2351
+ return;
2352
+ }
2353
+ await performScan(targetPath);
2354
+ }
2355
+
2356
+ // Scan a Git repository
2357
+ async function scanGitRepo() {
2358
+ const gitUrl = document.getElementById('gitUrl').value.trim();
2359
+ if (!gitUrl) {
2360
+ showToast('Enter a Git URL', true);
2361
+ return;
2362
+ }
2363
+
2364
+ const statusEl = document.getElementById('scanStatus');
2365
+ statusEl.innerHTML = '<span style="color: #00aaff;">Scanning remote repository...</span>';
2366
+ showLoading(true);
2367
+
2368
+ // Clear map before scanning
2369
+ try {
2370
+ clearMap();
2371
+ } catch (e) {
2372
+ console.warn('clearMap error:', e);
2373
+ }
2374
+
2375
+ try {
2376
+ // Call backend to scan via API (no clone)
2377
+ const res = await fetch(`${AURA_URL}/tools`, {
2378
+ method: 'POST',
2379
+ headers: { 'Content-Type': 'application/json' },
2380
+ body: JSON.stringify({
2381
+ tool: 'scan-local',
2382
+ arguments: { gitUrl: gitUrl }
2383
+ })
2384
+ });
2385
+
2386
+ const data = await res.json();
2387
+ console.log('Git scan response:', data);
2388
+
2389
+ if (data.result && data.result.scan_details) {
2390
+ await handleScanResult(data.result, gitUrl);
2391
+ } else if (data.error) {
2392
+ statusEl.innerHTML = `<span style="color: #ff4444;">Error: ${data.error}</span>`;
2393
+ showToast('Git scan failed', true);
2394
+ } else {
2395
+ statusEl.innerHTML = '<span style="color: #ff4444;">Scan failed</span>';
2396
+ }
2397
+ } catch (err) {
2398
+ console.error('Git scan error:', err);
2399
+ statusEl.innerHTML = `<span style="color: #ff4444;">Error: ${err.message}</span>`;
2400
+ showToast('Git scan error', true);
2401
+ }
2402
+
2403
+ showLoading(false);
2404
+ }
2405
+
2406
+ // Perform local scan
2407
+ async function performScan(targetPath) {
2408
+ const statusEl = document.getElementById('scanStatus');
2409
+ statusEl.innerHTML = '<span style="color: #00aaff;">Scanning...</span>';
2410
+ showLoading(true);
2411
+
2412
+ // Clear map first
2413
+ try {
2414
+ clearMap();
2415
+ } catch (e) {
2416
+ console.warn('clearMap error:', e);
2417
+ }
2418
+
2419
+ try {
2420
+ console.log('Starting scan of:', targetPath);
2421
+
2422
+ const res = await fetch(`${AURA_URL}/tools`, {
2423
+ method: 'POST',
2424
+ headers: { 'Content-Type': 'application/json' },
2425
+ body: JSON.stringify({
2426
+ tool: 'scan-local',
2427
+ arguments: { targetPath: targetPath }
2428
+ })
2429
+ });
2430
+
2431
+ const data = await res.json();
2432
+ console.log('Scan response:', data);
2433
+
2434
+ if (data.result && data.result.scan_details) {
2435
+ await handleScanResult(data.result, targetPath);
2436
+ } else if (data.error) {
2437
+ statusEl.innerHTML = `<span style="color: #ff4444;">Error: ${data.error}</span>`;
2438
+ showToast('Scan failed', true);
2439
+ } else {
2440
+ statusEl.innerHTML = '<span style="color: #ff4444;">No scan results</span>';
2441
+ }
2442
+ } catch (err) {
2443
+ console.error('Scan error:', err);
2444
+ statusEl.innerHTML = `<span style="color: #ff4444;">Error: ${err.message}</span>`;
2445
+ showToast('Scan error', true);
2446
+ }
2447
+
2448
+ showLoading(false);
2449
+ }
2450
+
2451
+ // Handle scan result
2452
+ async function handleScanResult(result, sourcePath) {
2453
+ const details = result.scan_details;
2454
+ currentScanDetails = details;
2455
+
2456
+ const projectName = getProjectName(sourcePath);
2457
+
2458
+ // Store project
2459
+ scannedProjects[projectName] = {
2460
+ path: details.path,
2461
+ scanResult: result,
2462
+ details: details
2463
+ };
2464
+ activeProject = projectName;
2465
+
2466
+ // Update UI
2467
+ updateProjectTabs();
2468
+ updateAuditorState(result.agent_state || 'idle');
2469
+ updateFindings(result.events || []);
2470
+ showScanSummary(details);
2471
+
2472
+ // Build the map from discovered items
2473
+ try {
2474
+ updateModulesFromScan(details);
2475
+ } catch (e) {
2476
+ console.warn('updateModulesFromScan error:', e);
2477
+ }
2478
+
2479
+ showToast(`Scanned: ${projectName}`);
2480
+ }
2481
+
2482
+ // Switch between scanned projects
2483
+ function switchProject(projectName) {
2484
+ if (!scannedProjects[projectName]) return;
2485
+
2486
+ activeProject = projectName;
2487
+ const project = scannedProjects[projectName];
2488
+
2489
+ try { clearMap(); } catch(e) { console.warn(e); }
2490
+
2491
+ currentScanDetails = project.details;
2492
+ updateAuditorState(project.scanResult.agent_state || 'idle');
2493
+ updateFindings(project.scanResult.events || []);
2494
+ showScanSummary(project.details);
2495
+
2496
+ try { updateModulesFromScan(project.details); } catch(e) { console.warn(e); }
2497
+
2498
+ updateProjectTabs();
2499
+ showToast(`Switched to: ${projectName}`);
2500
+ }
2501
+
2502
+ // Update project tabs UI
2503
+ function updateProjectTabs() {
2504
+ const projectNames = Object.keys(scannedProjects);
2505
+ const tabsContainer = document.getElementById('projectTabs');
2506
+ const tabList = document.getElementById('projectTabList');
2507
+
2508
+ if (projectNames.length === 0) {
2509
+ tabsContainer.style.display = 'none';
2510
+ return;
2511
+ }
2512
+
2513
+ tabsContainer.style.display = 'block';
2514
+ tabList.innerHTML = projectNames.map(name => `
2515
+ <button onclick="switchProject('${name}')" style="
2516
+ padding: 4px 8px; font-size: 10px; cursor: pointer; font-family: inherit;
2517
+ background: ${name === activeProject ? '#00ff88' : 'rgba(0,20,10,0.8)'};
2518
+ color: ${name === activeProject ? '#000' : '#00ff88'};
2519
+ border: 1px solid #00ff88;
2520
+ ">${name}</button>
2521
+ `).join('');
2522
+ }
2523
+
2524
+ // Show scan summary
2525
+ function showScanSummary(details) {
2526
+ const servicesCount = details.discovered_services?.length || 0;
2527
+ const modulesCount = details.discovered_modules?.length || 0;
2528
+ const sastCount = details.sast_findings || 0;
2529
+ const toolsUsed = details.tools_used || [];
2530
+
2531
+ document.getElementById('scanStatus').innerHTML = `
2532
+ <div style="color: #00ff88; font-weight: bold;">Scan Complete</div>
2533
+ <div style="margin-top: 5px; font-size: 10px; color: #666; word-break: break-all;">${details.path}</div>
2534
+ <div style="margin-top: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 5px;">
2535
+ <div>Secrets: <span style="color: ${details.secrets_found > 0 ? '#ff4444' : '#00ff88'}">${details.secrets_found || 0}</span></div>
2536
+ <div>Modules: <span style="color: #88ff00;">${modulesCount}</span></div>
2537
+ <div>Services: <span style="color: #00aaff;">${servicesCount}</span></div>
2538
+ <div>Packages: ${details.packages_scanned || 0}</div>
2539
+ <div>SAST: <span style="color: ${sastCount > 0 ? '#ffaa00' : '#00ff88'}">${sastCount}</span></div>
2540
+ <div>Vulns: <span style="color: ${details.package_vulns > 0 ? '#ff4444' : '#00ff88'}">${details.package_vulns || 0}</span></div>
2541
+ </div>
2542
+ ${toolsUsed.length > 0 ? `<div style="margin-top: 8px; font-size: 10px; color: #00aaff;">
2543
+ Tools: ${toolsUsed.join(', ')}
2544
+ </div>` : ''}
2545
+ ${modulesCount > 0 ? `<div style="margin-top: 5px; font-size: 10px; color: #666;">
2546
+ ${details.discovered_modules.slice(0,4).map(m => m.name).join(', ')}${modulesCount > 4 ? '...' : ''}
2547
+ </div>` : ''}
2548
+ `;
2549
+ }
2550
+
2551
+ // Legacy compatibility
2552
+ async function runLocalScan() { await doScan(); }
2553
+ async function scanProject() { await doScan(); }
2554
+ async function addProject() { await doScan(); }
2555
+
2556
+ // Service type to color mapping
2557
+ const SERVICE_COLORS = {
2558
+ database: 0x00aaff,
2559
+ cache: 0xff6600,
2560
+ cloud: 0xff00ff,
2561
+ api: 0xffaa00,
2562
+ messaging: 0x00ffaa,
2563
+ storage: 0x00ff88,
2564
+ auth: 0x00ff00,
2565
+ monitoring: 0x888888
2566
+ };
2567
+
2568
+ function updateModulesFromScan(scanDetails) {
2569
+ console.log('[VISUALIZER] updateModulesFromScan called', scanDetails);
2570
+
2571
+ // Reset all modules to secure first
2572
+ MODULES.forEach(mod => updateModuleStatus(mod.id, 'secure', 'low'));
2573
+
2574
+ if (!scanDetails.raw_findings) {
2575
+ console.log('[VISUALIZER] No raw_findings in scan details');
2576
+ // Still update the status panel
2577
+ refreshModuleStatusPanel();
2578
+ return;
2579
+ }
2580
+
2581
+ const findings = scanDetails.raw_findings;
2582
+ console.log('[VISUALIZER] Processing findings:', findings.secrets?.length, 'secrets');
2583
+
2584
+ // Update SECRETS module if env files or secrets found
2585
+ if (findings.secrets?.length > 0 || findings.envFiles?.some(e => e.hasSecrets)) {
2586
+ const hasCritical = findings.secrets?.some(s => s.severity === 'critical');
2587
+ console.log('[VISUALIZER] Setting SECRETS to:', hasCritical ? 'CRITICAL' : 'WARNING');
2588
+ updateModuleStatus('secrets', hasCritical ? 'critical' : 'warning', hasCritical ? 'critical' : 'high');
2589
+ }
2590
+
2591
+ // Check what types of secrets were found and update relevant modules
2592
+ findings.secrets?.forEach(secret => {
2593
+ const type = (secret.type || '').toLowerCase();
2594
+ if (type.includes('aws') || type.includes('connection')) {
2595
+ updateModuleStatus('infra', 'critical', 'critical');
2596
+ }
2597
+ if (type.includes('password') || type.includes('token')) {
2598
+ updateModuleStatus('auth', 'warning', 'high');
2599
+ }
2600
+ if (type.includes('database') || type.includes('postgres') || type.includes('mysql')) {
2601
+ updateModuleStatus('database', 'critical', 'critical');
2602
+ }
2603
+ });
2604
+
2605
+ // Update from package vulnerabilities
2606
+ findings.packages?.forEach(pkg => {
2607
+ if (pkg.severity === 'critical' || pkg.severity === 'high') {
2608
+ updateModuleStatus('api', 'warning', pkg.severity);
2609
+ }
2610
+ });
2611
+
2612
+ // Update from SAST findings (semgrep etc.)
2613
+ findings.sastFindings?.forEach(finding => {
2614
+ const rule = finding.rule?.toLowerCase() || '';
2615
+ const message = finding.message?.toLowerCase() || '';
2616
+ const sev = finding.severity?.toLowerCase() || 'warning';
2617
+ const severity = sev === 'error' ? 'critical' : sev === 'warning' ? 'high' : 'medium';
2618
+
2619
+ // Map SAST findings to modules based on rule type
2620
+ if (rule.includes('sql') || rule.includes('injection') || message.includes('sql')) {
2621
+ updateModuleStatus('database', 'critical', 'critical');
2622
+ }
2623
+ if (rule.includes('xss') || rule.includes('csrf') || message.includes('xss')) {
2624
+ updateModuleStatus('api', 'warning', severity);
2625
+ }
2626
+ if (rule.includes('auth') || rule.includes('password') || rule.includes('jwt')) {
2627
+ updateModuleStatus('auth', 'warning', severity);
2628
+ }
2629
+ if (rule.includes('secrets') || rule.includes('hardcoded') || rule.includes('credential')) {
2630
+ updateModuleStatus('secrets', 'critical', 'critical');
2631
+ }
2632
+ if (rule.includes('command') || rule.includes('exec') || rule.includes('shell')) {
2633
+ updateModuleStatus('infra', 'critical', 'critical');
2634
+ }
2635
+ });
2636
+
2637
+ // Refresh the status panel to show current module states
2638
+ refreshModuleStatusPanel();
2639
+
2640
+ // DYNAMIC MAP BUILDING: Add discovered services as new nodes
2641
+ if (scanDetails.discovered_services && scanDetails.discovered_services.length > 0) {
2642
+ addDiscoveredServicesToMap(scanDetails.discovered_services);
2643
+ }
2644
+
2645
+ // DYNAMIC MAP BUILDING: Add discovered code modules/directories
2646
+ if (scanDetails.discovered_modules && scanDetails.discovered_modules.length > 0) {
2647
+ addDiscoveredModulesToMap(scanDetails.discovered_modules);
2648
+ }
2649
+ }
2650
+
2651
+ function addDiscoveredServicesToMap(services) {
2652
+ let addedCount = 0;
2653
+
2654
+ services.forEach((service, index) => {
2655
+ // Skip if module already exists
2656
+ if (moduleNodes[service.id] || MODULES.find(m => m.id === service.id)) {
2657
+ // Just update its status based on severity
2658
+ if (moduleNodes[service.id]) {
2659
+ const status = service.severity === 'critical' ? 'critical' :
2660
+ service.severity === 'high' ? 'warning' : 'secure';
2661
+ updateModuleStatus(service.id, status, service.severity);
2662
+ }
2663
+ return;
2664
+ }
2665
+
2666
+ // Calculate position for new module (spiral outward)
2667
+ const baseModuleCount = MODULES.length;
2668
+ const angle = (baseModuleCount + addedCount) * (Math.PI / 5);
2669
+ const radius = 22 + Math.floor(addedCount / 8) * 5;
2670
+ const pos = [
2671
+ Math.cos(angle) * radius,
2672
+ 0,
2673
+ Math.sin(angle) * radius
2674
+ ];
2675
+
2676
+ // Get color based on service type
2677
+ const color = SERVICE_COLORS[service.type] || 0x00ff88;
2678
+
2679
+ // Create new module definition
2680
+ const newMod = {
2681
+ id: service.id,
2682
+ name: service.name.toUpperCase(),
2683
+ desc: `${service.type.toUpperCase()} - Found in ${service.source}`,
2684
+ pos,
2685
+ color
2686
+ };
2687
+
2688
+ MODULES.push(newMod);
2689
+ moduleEnabled[service.id] = true;
2690
+
2691
+ // Create 3D node
2692
+ createSingleModuleNode(newMod);
2693
+
2694
+ // Add audit connection from central auditor
2695
+ const auditLine = createConnectionLine(
2696
+ new THREE.Vector3(0, 5, 0),
2697
+ new THREE.Vector3(pos[0], pos[1] + 2, pos[2]),
2698
+ 0x00aaff,
2699
+ true
2700
+ );
2701
+ auditLine.userData = { type: 'audit-connection', to: service.id };
2702
+ connectionLines.push(auditLine);
2703
+ scene.add(auditLine);
2704
+
2705
+ // Connect to related existing modules based on type
2706
+ connectServiceToRelatedModules(service, pos);
2707
+
2708
+ // Set initial status based on severity
2709
+ const status = service.severity === 'critical' ? 'critical' :
2710
+ service.severity === 'high' ? 'warning' : 'secure';
2711
+ updateModuleStatus(service.id, status, service.severity);
2712
+
2713
+ addedCount++;
2714
+ showToast(`Discovered: ${service.name} (${service.type})`);
2715
+ });
2716
+
2717
+ if (addedCount > 0) {
2718
+ updateModuleStatusPanel(currentFindings);
2719
+ console.log(`Added ${addedCount} discovered services to map`);
2720
+ }
2721
+ }
2722
+
2723
+ function connectServiceToRelatedModules(service, pos) {
2724
+ // Connect databases to the DATABASE module
2725
+ if (service.type === 'database' && moduleNodes['database']) {
2726
+ const dbMod = MODULES.find(m => m.id === 'database');
2727
+ if (dbMod) {
2728
+ const line = createConnectionLine(
2729
+ new THREE.Vector3(pos[0], pos[1] + 2, pos[2]),
2730
+ new THREE.Vector3(dbMod.pos[0], dbMod.pos[1] + 2, dbMod.pos[2]),
2731
+ 0x00aaff44,
2732
+ false
2733
+ );
2734
+ line.userData = { type: 'data-flow', from: service.id, to: 'database', label: 'Data' };
2735
+ connectionLines.push(line);
2736
+ scene.add(line);
2737
+ }
2738
+ }
2739
+
2740
+ // Connect auth services to AUTH module
2741
+ if (service.type === 'auth' && moduleNodes['auth']) {
2742
+ const authMod = MODULES.find(m => m.id === 'auth');
2743
+ if (authMod) {
2744
+ const line = createConnectionLine(
2745
+ new THREE.Vector3(pos[0], pos[1] + 2, pos[2]),
2746
+ new THREE.Vector3(authMod.pos[0], authMod.pos[1] + 2, authMod.pos[2]),
2747
+ 0x00ff8844,
2748
+ false
2749
+ );
2750
+ line.userData = { type: 'data-flow', from: service.id, to: 'auth', label: 'Auth' };
2751
+ connectionLines.push(line);
2752
+ scene.add(line);
2753
+ }
2754
+ }
2755
+
2756
+ // Connect cloud services to INFRA module
2757
+ if ((service.type === 'cloud' || service.type === 'storage') && moduleNodes['infra']) {
2758
+ const infraMod = MODULES.find(m => m.id === 'infra');
2759
+ if (infraMod) {
2760
+ const line = createConnectionLine(
2761
+ new THREE.Vector3(pos[0], pos[1] + 2, pos[2]),
2762
+ new THREE.Vector3(infraMod.pos[0], infraMod.pos[1] + 2, infraMod.pos[2]),
2763
+ 0xff00ff44,
2764
+ false
2765
+ );
2766
+ line.userData = { type: 'data-flow', from: service.id, to: 'infra', label: 'Cloud' };
2767
+ connectionLines.push(line);
2768
+ scene.add(line);
2769
+ }
2770
+ }
2771
+
2772
+ // Connect API services to API module
2773
+ if (service.type === 'api' && moduleNodes['api']) {
2774
+ const apiMod = MODULES.find(m => m.id === 'api');
2775
+ if (apiMod) {
2776
+ const line = createConnectionLine(
2777
+ new THREE.Vector3(pos[0], pos[1] + 2, pos[2]),
2778
+ new THREE.Vector3(apiMod.pos[0], apiMod.pos[1] + 2, apiMod.pos[2]),
2779
+ 0xffaa0044,
2780
+ false
2781
+ );
2782
+ line.userData = { type: 'data-flow', from: service.id, to: 'api', label: 'API' };
2783
+ connectionLines.push(line);
2784
+ scene.add(line);
2785
+ }
2786
+ }
2787
+ }
2788
+
2789
+ // Colors for discovered code module types
2790
+ const MODULE_TYPE_COLORS = {
2791
+ source: 0x00ff88, // Green - source code
2792
+ component: 0x00aaff, // Blue - UI components
2793
+ service: 0xff00ff, // Magenta - services
2794
+ api: 0xffaa00, // Orange - API
2795
+ lib: 0x88ff00, // Lime - libraries
2796
+ config: 0xffff00, // Yellow - config
2797
+ test: 0x00ffff, // Cyan - tests
2798
+ infra: 0xff00aa, // Pink - infrastructure
2799
+ docs: 0x8888ff // Light blue - docs
2800
+ };
2801
+
2802
+ function addDiscoveredModulesToMap(modules) {
2803
+ let addedCount = 0;
2804
+
2805
+ modules.forEach((mod, index) => {
2806
+ // Skip if module already exists
2807
+ if (moduleNodes[mod.id] || MODULES.find(m => m.id === mod.id)) {
2808
+ return;
2809
+ }
2810
+
2811
+ // Calculate position for new module (different ring from services)
2812
+ const baseCount = MODULES.length + addedCount;
2813
+ const angle = baseCount * (Math.PI / 6) + Math.PI / 12; // Offset to not overlap with services
2814
+ const radius = 18 + Math.floor(addedCount / 10) * 6;
2815
+ const pos = [
2816
+ Math.cos(angle) * radius,
2817
+ -2, // Slightly lower than services
2818
+ Math.sin(angle) * radius
2819
+ ];
2820
+
2821
+ // Get color based on module type
2822
+ const color = MODULE_TYPE_COLORS[mod.type] || 0x00ff88;
2823
+
2824
+ // Create new module definition
2825
+ const newMod = {
2826
+ id: mod.id,
2827
+ name: mod.name.toUpperCase(),
2828
+ desc: `${mod.type.toUpperCase()} - ${mod.fileCount} files`,
2829
+ pos,
2830
+ color
2831
+ };
2832
+
2833
+ MODULES.push(newMod);
2834
+ moduleEnabled[mod.id] = true;
2835
+
2836
+ // Create 3D node
2837
+ createSingleModuleNode(newMod);
2838
+
2839
+ // Add audit connection from central auditor
2840
+ const auditLine = createConnectionLine(
2841
+ new THREE.Vector3(0, 5, 0),
2842
+ new THREE.Vector3(pos[0], pos[1] + 2, pos[2]),
2843
+ 0x00aaff,
2844
+ true
2845
+ );
2846
+ auditLine.userData = { type: 'audit-connection', to: mod.id };
2847
+ connectionLines.push(auditLine);
2848
+ scene.add(auditLine);
2849
+
2850
+ // Connect modules based on their imports
2851
+ connectModuleByImports(mod, pos);
2852
+
2853
+ // Set status as secure by default (code modules aren't inherently insecure)
2854
+ updateModuleStatus(mod.id, 'secure', 'info');
2855
+
2856
+ addedCount++;
2857
+ showToast(`Mapped: ${mod.name} (${mod.type})`);
2858
+ });
2859
+
2860
+ if (addedCount > 0) {
2861
+ updateModuleStatusPanel(currentFindings);
2862
+ console.log(`Added ${addedCount} discovered code modules to map`);
2863
+ }
2864
+ }
2865
+
2866
+ function connectModuleByImports(mod, pos) {
2867
+ // Connect to modules this one imports from
2868
+ if (mod.imports && mod.imports.length > 0) {
2869
+ mod.imports.forEach(importName => {
2870
+ const targetMod = MODULES.find(m =>
2871
+ m.id === importName.toLowerCase() ||
2872
+ m.name.toLowerCase() === importName.toLowerCase()
2873
+ );
2874
+ if (targetMod && moduleNodes[targetMod.id]) {
2875
+ const line = createConnectionLine(
2876
+ new THREE.Vector3(pos[0], pos[1] + 2, pos[2]),
2877
+ new THREE.Vector3(targetMod.pos[0], targetMod.pos[1] + 2, targetMod.pos[2]),
2878
+ 0x00ff8844,
2879
+ false
2880
+ );
2881
+ line.userData = { type: 'import', from: mod.id, to: targetMod.id, label: 'imports' };
2882
+ connectionLines.push(line);
2883
+ scene.add(line);
2884
+ }
2885
+ });
2886
+ }
2887
+ }
2888
+
2889
+ // ========== INITIALIZE ==========
2890
+ // Expose functions to window for button onclick handlers
2891
+ window.runPreset = runPreset;
2892
+ window.runLocalScan = runLocalScan;
2893
+ window.doScan = doScan;
2894
+ window.scanProject = scanProject;
2895
+ window.addProject = addProject;
2896
+ window.switchProject = switchProject;
2897
+ window.setScanMode = setScanMode;
2898
+ window.scanGitRepo = scanGitRepo;
2899
+ window.openModuleManager = openModuleManager;
2900
+ window.addNewModule = addNewModule;
2901
+ window.toggleModule = toggleModule;
2902
+ window.removeSelectedModule = removeSelectedModule;
2903
+ window.saveModuleChanges = saveModuleChanges;
2904
+ window.closeModal = closeModal;
2905
+ window.refreshAuditHistory = refreshAuditHistory;
2906
+ window.selectAudit = selectAudit;
2907
+ window.clearSelectedAudit = clearSelectedAudit;
2908
+ window.openSettingsModal = openSettingsModal;
2909
+ window.closeSettingsModal = closeSettingsModal;
2910
+ window.switchSettingsTab = switchSettingsTab;
2911
+ window.markSettingsDirty = markSettingsDirty;
2912
+ window.saveSettings = saveSettings;
2913
+ window.testNotification = testNotification;
2914
+ window.runAWSScan = runAWSScan;
2915
+
2916
+ initScene();
2917
+ animate();
2918
+ pollAura();
2919
+ refreshAuditHistory();
2920
+ connectWebSocket(); // Connect to WebSocket for real-time updates
2921
+ setInterval(pollAura, POLL_INTERVAL);
2922
+ setInterval(refreshAuditHistory, POLL_INTERVAL * 2); // Refresh history less frequently
2923
+
2924
+ // Add click handler for module selection
2925
+ window.addEventListener('click', onMouseClick);
2926
+
2927
+ console.log('Aura Control Plane initialized');
2928
+ console.log('WebSocket: ws://127.0.0.1:3001 (real-time updates)');
2929
+ console.log('Enter a directory path and click "Scan Directory" to map your project.');
2930
+ console.log('Use "+ Add" to scan multiple projects and switch between them.');
2931
+ </script>
2932
+ </body>
2933
+ </html>