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,1771 @@
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>aurasecurity</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
9
+
10
+ * { margin: 0; padding: 0; box-sizing: border-box; }
11
+
12
+ :root {
13
+ --bg-primary: #0d1117;
14
+ --bg-secondary: #161b22;
15
+ --bg-tertiary: #21262d;
16
+ --border: #30363d;
17
+ --border-light: #484f58;
18
+ --text-primary: #e6edf3;
19
+ --text-secondary: #8b949e;
20
+ --text-muted: #6e7681;
21
+ --accent: #58a6ff;
22
+ --accent-subtle: rgba(56, 139, 253, 0.15);
23
+ --critical: #f85149;
24
+ --critical-subtle: rgba(248, 81, 73, 0.15);
25
+ --high: #db6d28;
26
+ --high-subtle: rgba(219, 109, 40, 0.15);
27
+ --warning: #d29922;
28
+ --warning-subtle: rgba(210, 153, 34, 0.15);
29
+ --success: #3fb950;
30
+ --success-subtle: rgba(63, 185, 80, 0.15);
31
+ }
32
+
33
+ body {
34
+ background: var(--bg-primary);
35
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
36
+ color: var(--text-primary);
37
+ overflow: hidden;
38
+ min-height: 100vh;
39
+ }
40
+
41
+ #canvas-container {
42
+ position: fixed;
43
+ top: 0;
44
+ left: 0;
45
+ width: 100%;
46
+ height: 100%;
47
+ z-index: 1;
48
+ }
49
+
50
+ /* 3D Hover Tooltip */
51
+ #tooltip3d {
52
+ position: fixed;
53
+ z-index: 200;
54
+ background: var(--bg-secondary);
55
+ border: 1px solid var(--border);
56
+ border-radius: 8px;
57
+ padding: 10px 14px;
58
+ pointer-events: none;
59
+ opacity: 0;
60
+ transition: opacity 0.15s;
61
+ max-width: 280px;
62
+ }
63
+
64
+ #tooltip3d.visible {
65
+ opacity: 1;
66
+ }
67
+
68
+ #tooltip3d .tooltip-name {
69
+ font-weight: 600;
70
+ color: var(--text-primary);
71
+ margin-bottom: 4px;
72
+ font-size: 13px;
73
+ }
74
+
75
+ #tooltip3d .tooltip-stats {
76
+ display: flex;
77
+ gap: 12px;
78
+ font-size: 12px;
79
+ color: var(--text-secondary);
80
+ }
81
+
82
+ #tooltip3d .tooltip-stat {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 4px;
86
+ }
87
+
88
+ #tooltip3d .tooltip-stat.critical { color: var(--critical); }
89
+ #tooltip3d .tooltip-stat.warning { color: var(--warning); }
90
+ #tooltip3d .tooltip-stat.safe { color: var(--success); }
91
+
92
+ #tooltip3d .tooltip-hint {
93
+ font-size: 11px;
94
+ color: var(--text-muted);
95
+ margin-top: 6px;
96
+ border-top: 1px solid var(--border);
97
+ padding-top: 6px;
98
+ }
99
+
100
+ /* 3D Navigation Breadcrumb */
101
+ #nav3d {
102
+ position: fixed;
103
+ bottom: 80px;
104
+ left: 50%;
105
+ transform: translateX(-50%);
106
+ z-index: 150;
107
+ background: var(--bg-secondary);
108
+ border: 1px solid var(--border);
109
+ border-radius: 8px;
110
+ padding: 8px 16px;
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 8px;
114
+ font-size: 13px;
115
+ opacity: 0;
116
+ transition: opacity 0.2s;
117
+ pointer-events: none;
118
+ }
119
+
120
+ #nav3d.visible {
121
+ opacity: 1;
122
+ pointer-events: auto;
123
+ }
124
+
125
+ #nav3d .nav-item {
126
+ color: var(--text-secondary);
127
+ cursor: pointer;
128
+ padding: 4px 8px;
129
+ border-radius: 4px;
130
+ transition: all 0.15s;
131
+ }
132
+
133
+ #nav3d .nav-item:hover {
134
+ background: var(--bg-tertiary);
135
+ color: var(--text-primary);
136
+ }
137
+
138
+ #nav3d .nav-item.active {
139
+ color: var(--accent);
140
+ font-weight: 500;
141
+ }
142
+
143
+ #nav3d .nav-sep {
144
+ color: var(--text-muted);
145
+ }
146
+
147
+ #nav3d .back-btn {
148
+ background: var(--bg-tertiary);
149
+ border: 1px solid var(--border);
150
+ color: var(--text-primary);
151
+ padding: 6px 12px;
152
+ border-radius: 4px;
153
+ cursor: pointer;
154
+ font-family: inherit;
155
+ font-size: 12px;
156
+ margin-right: 8px;
157
+ transition: all 0.15s;
158
+ }
159
+
160
+ #nav3d .back-btn:hover {
161
+ background: var(--accent);
162
+ border-color: var(--accent);
163
+ }
164
+
165
+ /* Panels */
166
+ .panel {
167
+ position: fixed;
168
+ z-index: 100;
169
+ background: var(--bg-secondary);
170
+ border: 1px solid var(--border);
171
+ border-radius: 8px;
172
+ padding: 16px;
173
+ }
174
+
175
+ /* Header */
176
+ #header {
177
+ top: 0;
178
+ left: 0;
179
+ right: 0;
180
+ height: 56px;
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: space-between;
184
+ padding: 0 20px;
185
+ background: var(--bg-secondary);
186
+ border-radius: 0;
187
+ border-top: none;
188
+ border-left: none;
189
+ border-right: none;
190
+ }
191
+
192
+ .logo {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 10px;
196
+ font-size: 14px;
197
+ font-weight: 600;
198
+ }
199
+
200
+ .logo-icon {
201
+ width: 28px;
202
+ height: 28px;
203
+ background: var(--accent);
204
+ border-radius: 6px;
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: center;
208
+ font-size: 14px;
209
+ font-weight: 600;
210
+ color: white;
211
+ }
212
+
213
+ .nav-tabs {
214
+ display: flex;
215
+ gap: 4px;
216
+ background: var(--bg-tertiary);
217
+ padding: 4px;
218
+ border-radius: 6px;
219
+ }
220
+
221
+ .nav-tab {
222
+ padding: 6px 16px;
223
+ background: transparent;
224
+ border: none;
225
+ color: var(--text-secondary);
226
+ font-family: inherit;
227
+ font-size: 13px;
228
+ font-weight: 500;
229
+ cursor: pointer;
230
+ transition: all 0.15s;
231
+ border-radius: 4px;
232
+ }
233
+
234
+ .nav-tab:hover {
235
+ color: var(--text-primary);
236
+ background: var(--bg-secondary);
237
+ }
238
+
239
+ .nav-tab.active {
240
+ background: var(--bg-secondary);
241
+ color: var(--text-primary);
242
+ }
243
+
244
+ .header-stats {
245
+ display: flex;
246
+ gap: 16px;
247
+ font-size: 13px;
248
+ }
249
+
250
+ .header-stat {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 6px;
254
+ color: var(--text-secondary);
255
+ }
256
+
257
+ .stat-value {
258
+ font-weight: 600;
259
+ color: var(--text-primary);
260
+ }
261
+
262
+ .stat-value.critical { color: var(--critical); }
263
+ .stat-value.warning { color: var(--warning); }
264
+
265
+ /* Left Panel - Scanner */
266
+ #scanner-panel {
267
+ top: 72px;
268
+ left: 16px;
269
+ width: 320px;
270
+ max-height: calc(100vh - 88px);
271
+ overflow-y: auto;
272
+ }
273
+
274
+ .section-header {
275
+ font-size: 12px;
276
+ font-weight: 600;
277
+ color: var(--text-secondary);
278
+ text-transform: uppercase;
279
+ letter-spacing: 0.5px;
280
+ margin-bottom: 12px;
281
+ padding-bottom: 8px;
282
+ border-bottom: 1px solid var(--border);
283
+ }
284
+
285
+ .input-group {
286
+ margin-bottom: 12px;
287
+ }
288
+
289
+ .input-group label {
290
+ display: block;
291
+ font-size: 12px;
292
+ font-weight: 500;
293
+ color: var(--text-secondary);
294
+ margin-bottom: 6px;
295
+ }
296
+
297
+ .input-group input {
298
+ width: 100%;
299
+ padding: 10px 12px;
300
+ background: var(--bg-primary);
301
+ border: 1px solid var(--border);
302
+ border-radius: 6px;
303
+ color: var(--text-primary);
304
+ font-family: inherit;
305
+ font-size: 13px;
306
+ transition: border-color 0.15s;
307
+ }
308
+
309
+ .input-group input:focus {
310
+ outline: none;
311
+ border-color: var(--accent);
312
+ }
313
+
314
+ .input-group input::placeholder {
315
+ color: var(--text-muted);
316
+ }
317
+
318
+ .btn {
319
+ width: 100%;
320
+ padding: 10px 16px;
321
+ background: var(--accent);
322
+ border: none;
323
+ border-radius: 6px;
324
+ color: white;
325
+ font-family: inherit;
326
+ font-size: 13px;
327
+ font-weight: 500;
328
+ cursor: pointer;
329
+ transition: all 0.15s;
330
+ margin-top: 8px;
331
+ }
332
+
333
+ .btn:hover {
334
+ background: #4c9aff;
335
+ }
336
+
337
+ .btn:disabled {
338
+ opacity: 0.6;
339
+ cursor: not-allowed;
340
+ }
341
+
342
+ .btn-secondary {
343
+ background: var(--bg-tertiary);
344
+ color: var(--text-primary);
345
+ }
346
+
347
+ .btn-secondary:hover {
348
+ background: var(--border);
349
+ }
350
+
351
+ .btn-danger {
352
+ background: var(--critical-subtle);
353
+ color: var(--critical);
354
+ }
355
+
356
+ .btn-danger:hover {
357
+ background: rgba(248, 81, 73, 0.25);
358
+ }
359
+
360
+ /* Scanned repos list */
361
+ .repo-list {
362
+ margin-top: 16px;
363
+ }
364
+
365
+ .repo-item {
366
+ display: flex;
367
+ align-items: center;
368
+ gap: 12px;
369
+ padding: 12px;
370
+ background: var(--bg-primary);
371
+ border: 1px solid var(--border);
372
+ border-radius: 6px;
373
+ margin-bottom: 8px;
374
+ cursor: pointer;
375
+ transition: all 0.15s;
376
+ }
377
+
378
+ .repo-item:hover {
379
+ border-color: var(--border-light);
380
+ }
381
+
382
+ .repo-item.selected {
383
+ border-color: var(--accent);
384
+ background: var(--accent-subtle);
385
+ }
386
+
387
+ .repo-status {
388
+ width: 8px;
389
+ height: 8px;
390
+ border-radius: 50%;
391
+ flex-shrink: 0;
392
+ }
393
+
394
+ .repo-status.critical { background: var(--critical); }
395
+ .repo-status.warning { background: var(--warning); }
396
+ .repo-status.safe { background: var(--success); }
397
+
398
+ .repo-info { flex: 1; min-width: 0; }
399
+ .repo-name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
400
+ .repo-path { font-size: 11px; color: var(--text-muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
401
+ .repo-stats { font-size: 11px; color: var(--text-secondary); }
402
+
403
+ /* Right Panel - Details */
404
+ #details-panel {
405
+ top: 72px;
406
+ right: 16px;
407
+ width: 360px;
408
+ max-height: calc(100vh - 88px);
409
+ overflow-y: auto;
410
+ }
411
+
412
+ .selected-repo-header {
413
+ display: none;
414
+ padding: 12px;
415
+ background: var(--bg-primary);
416
+ border-radius: 6px;
417
+ margin-bottom: 16px;
418
+ }
419
+
420
+ .selected-repo-header.visible { display: block; }
421
+ .selected-repo-name { font-size: 14px; font-weight: 500; }
422
+ .selected-repo-path { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
423
+
424
+ .finding-list {
425
+ display: flex;
426
+ flex-direction: column;
427
+ gap: 8px;
428
+ }
429
+
430
+ .finding-item {
431
+ padding: 12px;
432
+ background: var(--bg-primary);
433
+ border: 1px solid var(--border);
434
+ border-radius: 6px;
435
+ border-left: 3px solid;
436
+ }
437
+
438
+ .finding-item.critical { border-left-color: var(--critical); background: var(--critical-subtle); }
439
+ .finding-item.high { border-left-color: var(--high); background: var(--high-subtle); }
440
+ .finding-item.medium { border-left-color: var(--warning); background: var(--warning-subtle); }
441
+ .finding-item.low { border-left-color: var(--success); background: var(--success-subtle); }
442
+
443
+ .finding-header {
444
+ display: flex;
445
+ justify-content: space-between;
446
+ align-items: center;
447
+ margin-bottom: 6px;
448
+ }
449
+
450
+ .finding-severity {
451
+ font-size: 10px;
452
+ font-weight: 600;
453
+ text-transform: uppercase;
454
+ letter-spacing: 0.5px;
455
+ }
456
+
457
+ .finding-item.critical .finding-severity { color: var(--critical); }
458
+ .finding-item.high .finding-severity { color: var(--high); }
459
+ .finding-item.medium .finding-severity { color: var(--warning); }
460
+ .finding-item.low .finding-severity { color: var(--success); }
461
+
462
+ .finding-type {
463
+ font-size: 10px;
464
+ color: var(--text-muted);
465
+ background: var(--bg-tertiary);
466
+ padding: 2px 6px;
467
+ border-radius: 3px;
468
+ }
469
+
470
+ .finding-message {
471
+ font-size: 13px;
472
+ color: var(--text-primary);
473
+ margin-bottom: 6px;
474
+ line-height: 1.4;
475
+ }
476
+
477
+ .finding-file {
478
+ font-size: 11px;
479
+ color: var(--text-muted);
480
+ font-family: 'SF Mono', Monaco, monospace;
481
+ }
482
+
483
+ /* Reports Panel */
484
+ #reports-panel {
485
+ top: 72px;
486
+ left: 50%;
487
+ transform: translateX(-50%);
488
+ width: 900px;
489
+ max-width: calc(100% - 40px);
490
+ max-height: calc(100vh - 88px);
491
+ overflow-y: auto;
492
+ display: none;
493
+ }
494
+
495
+ #reports-panel.active { display: block; }
496
+
497
+ .report-actions {
498
+ display: flex;
499
+ gap: 8px;
500
+ margin-bottom: 16px;
501
+ }
502
+
503
+ .report-actions .btn {
504
+ width: auto;
505
+ padding: 8px 14px;
506
+ }
507
+
508
+ .report-table {
509
+ width: 100%;
510
+ border-collapse: collapse;
511
+ font-size: 13px;
512
+ }
513
+
514
+ .report-table th,
515
+ .report-table td {
516
+ padding: 12px;
517
+ text-align: left;
518
+ border-bottom: 1px solid var(--border);
519
+ }
520
+
521
+ .report-table th {
522
+ color: var(--text-secondary);
523
+ font-weight: 500;
524
+ font-size: 12px;
525
+ }
526
+
527
+ .report-table tr:hover td {
528
+ background: var(--bg-tertiary);
529
+ }
530
+
531
+ /* View Info */
532
+ #view-info {
533
+ bottom: 20px;
534
+ left: 50%;
535
+ transform: translateX(-50%);
536
+ padding: 8px 16px;
537
+ font-size: 12px;
538
+ text-align: center;
539
+ background: var(--bg-tertiary);
540
+ color: var(--text-secondary);
541
+ border-radius: 6px;
542
+ }
543
+
544
+ #view-info kbd {
545
+ background: var(--bg-secondary);
546
+ border: 1px solid var(--border);
547
+ padding: 2px 6px;
548
+ border-radius: 4px;
549
+ font-size: 11px;
550
+ color: var(--text-primary);
551
+ }
552
+
553
+ /* Empty state */
554
+ .empty-state {
555
+ text-align: center;
556
+ padding: 32px 16px;
557
+ color: var(--text-muted);
558
+ }
559
+
560
+ .empty-state-icon {
561
+ font-size: 32px;
562
+ margin-bottom: 8px;
563
+ opacity: 0.5;
564
+ }
565
+
566
+ .empty-state p {
567
+ font-size: 13px;
568
+ }
569
+
570
+ /* Log output */
571
+ .log-output {
572
+ background: var(--bg-primary);
573
+ border: 1px solid var(--border);
574
+ border-radius: 6px;
575
+ padding: 12px;
576
+ font-size: 12px;
577
+ font-family: 'SF Mono', Monaco, monospace;
578
+ max-height: 140px;
579
+ overflow-y: auto;
580
+ margin-top: 12px;
581
+ }
582
+
583
+ .log-line {
584
+ margin-bottom: 4px;
585
+ color: var(--text-secondary);
586
+ }
587
+
588
+ .log-line.success { color: var(--success); }
589
+ .log-line.error { color: var(--critical); }
590
+
591
+ /* Scrollbar */
592
+ ::-webkit-scrollbar { width: 8px; }
593
+ ::-webkit-scrollbar-track { background: transparent; }
594
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
595
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
596
+ </style>
597
+ </head>
598
+ <body>
599
+ <!-- Three.js 3D Canvas -->
600
+ <div id="canvas-container"></div>
601
+
602
+ <!-- 3D Hover Tooltip -->
603
+ <div id="tooltip3d">
604
+ <div class="tooltip-name"></div>
605
+ <div class="tooltip-stats">
606
+ <span class="tooltip-stat critical"><span class="count">0</span> Secrets</span>
607
+ <span class="tooltip-stat warning"><span class="count">0</span> Vulns</span>
608
+ </div>
609
+ <div class="tooltip-hint">Click to view findings in 3D</div>
610
+ </div>
611
+
612
+ <!-- 3D Navigation Breadcrumb -->
613
+ <div id="nav3d">
614
+ <button class="back-btn" onclick="navigateBack()">← Back</button>
615
+ <span class="nav-item" onclick="navigateToLevel(0)">All Repos</span>
616
+ <span class="nav-sep">/</span>
617
+ <span class="nav-item nav-repo"></span>
618
+ <span class="nav-sep nav-sev-sep" style="display:none">/</span>
619
+ <span class="nav-item nav-severity" style="display:none"></span>
620
+ </div>
621
+
622
+ <!-- Header -->
623
+ <header id="header" class="panel">
624
+ <div class="logo">
625
+ <div class="logo-icon">S</div>
626
+ <span>aurasecurity</span>
627
+ </div>
628
+
629
+ <div class="nav-tabs">
630
+ <button class="nav-tab active" onclick="showView('scan')">Scan</button>
631
+ <button class="nav-tab" onclick="showView('reports')">Reports</button>
632
+ </div>
633
+
634
+ <div class="header-stats">
635
+ <div class="header-stat">
636
+ <span class="stat-value critical" id="totalCritical">0</span>
637
+ <span>Secrets</span>
638
+ </div>
639
+ <div class="header-stat">
640
+ <span class="stat-value warning" id="totalWarning">0</span>
641
+ <span>Vulns</span>
642
+ </div>
643
+ <div class="header-stat">
644
+ <span class="stat-value" id="totalRepos">0</span>
645
+ <span>Scanned</span>
646
+ </div>
647
+ </div>
648
+ </header>
649
+
650
+ <!-- Scanner Panel (Left) -->
651
+ <div id="scanner-panel" class="panel">
652
+ <div class="section-header">New Scan</div>
653
+
654
+ <div class="input-group">
655
+ <label>Local Path</label>
656
+ <input type="text" id="scanPath" placeholder="/path/to/project">
657
+ </div>
658
+
659
+ <div class="input-group">
660
+ <label>Git Repository URL</label>
661
+ <input type="text" id="gitUrl" placeholder="https://github.com/user/repo">
662
+ </div>
663
+
664
+ <button class="btn" id="scanBtn" onclick="executeScan()">
665
+ Start Scan
666
+ </button>
667
+
668
+ <div class="log-output" id="logOutput">
669
+ <div class="log-line">Ready to scan</div>
670
+ </div>
671
+
672
+ <div class="section-header" style="margin-top: 20px;">Scanned Targets</div>
673
+
674
+ <div class="repo-list" id="repoList">
675
+ <div class="empty-state">
676
+ <div class="empty-state-icon">📁</div>
677
+ <p>No targets scanned yet</p>
678
+ </div>
679
+ </div>
680
+ </div>
681
+
682
+ <!-- Details Panel (Right) -->
683
+ <div id="details-panel" class="panel">
684
+ <div class="section-header">Findings</div>
685
+
686
+ <div class="selected-repo-header" id="selectedRepoInfo">
687
+ <div class="selected-repo-name" id="selectedRepoName">-</div>
688
+ <div class="selected-repo-path" id="selectedRepoPath">-</div>
689
+ </div>
690
+
691
+ <div class="finding-list" id="findingList">
692
+ <div class="empty-state">
693
+ <div class="empty-state-icon">🔍</div>
694
+ <p>Select a target to view findings</p>
695
+ </div>
696
+ </div>
697
+ </div>
698
+
699
+ <!-- Reports Panel (Center, hidden by default) -->
700
+ <div id="reports-panel" class="panel">
701
+ <div class="section-header">Scan Reports</div>
702
+
703
+ <div class="report-actions">
704
+ <button class="btn" onclick="exportReports('json')">Export JSON</button>
705
+ <button class="btn btn-secondary" onclick="exportReports('csv')">Export CSV</button>
706
+ <button class="btn btn-danger" onclick="clearAllReports()">Clear All</button>
707
+ </div>
708
+
709
+ <table class="report-table">
710
+ <thead>
711
+ <tr>
712
+ <th>Timestamp</th>
713
+ <th>Target</th>
714
+ <th>Type</th>
715
+ <th>Secrets</th>
716
+ <th>Vulns</th>
717
+ <th>Status</th>
718
+ <th>Actions</th>
719
+ </tr>
720
+ </thead>
721
+ <tbody id="reportTableBody">
722
+ <tr>
723
+ <td colspan="7" style="text-align: center; color: var(--text-muted);">No reports yet</td>
724
+ </tr>
725
+ </tbody>
726
+ </table>
727
+ </div>
728
+
729
+ <!-- View Info -->
730
+ <div id="view-info" class="panel">
731
+ <kbd>Drag</kbd> Rotate &nbsp; <kbd>Scroll</kbd> Zoom &nbsp; <kbd>Click</kbd> Select node
732
+ </div>
733
+
734
+ <!-- Scripts -->
735
+ <script type="importmap">
736
+ {
737
+ "imports": {
738
+ "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
739
+ "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
740
+ }
741
+ }
742
+ </script>
743
+
744
+ <script type="module">
745
+ import * as THREE from 'three';
746
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
747
+
748
+ // ========== STATE ==========
749
+ const state = {
750
+ repos: [],
751
+ selectedRepo: null,
752
+ reports: [],
753
+ currentView: 'scan',
754
+ drillLevel: 0, // 0=repos, 1=severities, 2=findings
755
+ selectedSeverity: null // 'critical', 'high', 'medium', 'low'
756
+ };
757
+
758
+ // Auto-detect API URL: use localhost for local dev, same origin for deployed
759
+ const isLocal = window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost';
760
+ // Local: direct to port 3000, AWS: use nginx proxy on same origin
761
+ const AURA_URL = isLocal ? 'http://127.0.0.1:3000' : window.location.origin;
762
+
763
+ // ========== THREE.JS SETUP ==========
764
+ let scene, camera, renderer, controls;
765
+ let repoNodes = [];
766
+ let centralNode;
767
+ let raycaster, mouse;
768
+ let findingNodes = []; // 3D nodes for vulnerabilities/secrets
769
+ let findingLines = []; // Connection lines for findings
770
+ let hoveredNode = null; // Currently hovered repo node
771
+
772
+ function initScene() {
773
+ scene = new THREE.Scene();
774
+ scene.background = new THREE.Color(0x0d1117);
775
+
776
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
777
+ camera.position.set(0, 25, 45);
778
+
779
+ renderer = new THREE.WebGLRenderer({ antialias: true });
780
+ renderer.setSize(window.innerWidth, window.innerHeight);
781
+ document.getElementById('canvas-container').appendChild(renderer.domElement);
782
+
783
+ controls = new OrbitControls(camera, renderer.domElement);
784
+ controls.enableDamping = true;
785
+ controls.dampingFactor = 0.05;
786
+ controls.maxPolarAngle = Math.PI / 2.1;
787
+ controls.minDistance = 15;
788
+ controls.maxDistance = 80;
789
+
790
+ raycaster = new THREE.Raycaster();
791
+ mouse = new THREE.Vector2();
792
+
793
+ // Lights
794
+ const ambientLight = new THREE.AmbientLight(0x404040, 1);
795
+ scene.add(ambientLight);
796
+
797
+ const mainLight = new THREE.DirectionalLight(0xffffff, 0.8);
798
+ mainLight.position.set(10, 20, 10);
799
+ scene.add(mainLight);
800
+
801
+ const fillLight = new THREE.DirectionalLight(0x58a6ff, 0.3);
802
+ fillLight.position.set(-10, 10, -10);
803
+ scene.add(fillLight);
804
+
805
+ createGrid();
806
+ createCentralHub();
807
+
808
+ window.addEventListener('resize', onResize);
809
+ renderer.domElement.addEventListener('click', onMouseClick);
810
+ renderer.domElement.addEventListener('mousemove', onMouseMove);
811
+
812
+ animate();
813
+ }
814
+
815
+ function createGrid() {
816
+ const gridHelper = new THREE.GridHelper(60, 30, 0x30363d, 0x21262d);
817
+ scene.add(gridHelper);
818
+ }
819
+
820
+ function createCentralHub() {
821
+ const group = new THREE.Group();
822
+
823
+ // Core sphere
824
+ const coreGeom = new THREE.IcosahedronGeometry(2.5, 1);
825
+ const coreMat = new THREE.MeshPhongMaterial({
826
+ color: 0x58a6ff,
827
+ emissive: 0x58a6ff,
828
+ emissiveIntensity: 0.2,
829
+ flatShading: true
830
+ });
831
+ const core = new THREE.Mesh(coreGeom, coreMat);
832
+ group.add(core);
833
+
834
+ // Outer ring
835
+ const ringGeom = new THREE.TorusGeometry(4, 0.15, 8, 32);
836
+ const ringMat = new THREE.MeshBasicMaterial({
837
+ color: 0x58a6ff,
838
+ transparent: true,
839
+ opacity: 0.4
840
+ });
841
+ const ring = new THREE.Mesh(ringGeom, ringMat);
842
+ ring.rotation.x = Math.PI / 2;
843
+ group.add(ring);
844
+
845
+ group.position.y = 4;
846
+ centralNode = group;
847
+ scene.add(group);
848
+ }
849
+
850
+ function addRepoNode(repo) {
851
+ const existingNode = repoNodes.find(n => n.userData.repoId === repo.id);
852
+ if (existingNode) {
853
+ updateNodeColor(existingNode, repo);
854
+ return existingNode;
855
+ }
856
+
857
+ const group = new THREE.Group();
858
+
859
+ // Determine color based on threat level
860
+ let color = 0x3fb950; // success/safe
861
+ if (repo.secrets > 0 || repo.vulns > 10) color = 0xf85149; // critical
862
+ else if (repo.vulns > 0) color = 0xd29922; // warning
863
+
864
+ // Base platform
865
+ const baseGeom = new THREE.CylinderGeometry(1.8, 2, 0.3, 6);
866
+ const baseMat = new THREE.MeshPhongMaterial({
867
+ color: 0x21262d,
868
+ flatShading: true
869
+ });
870
+ const base = new THREE.Mesh(baseGeom, baseMat);
871
+ group.add(base);
872
+
873
+ // Main block
874
+ const blockGeom = new THREE.BoxGeometry(2, 2.5, 2);
875
+ const blockMat = new THREE.MeshPhongMaterial({
876
+ color: color,
877
+ flatShading: true
878
+ });
879
+ const block = new THREE.Mesh(blockGeom, blockMat);
880
+ block.position.y = 1.5;
881
+ group.add(block);
882
+
883
+ // Top indicator
884
+ const topGeom = new THREE.ConeGeometry(0.5, 0.8, 4);
885
+ const topMat = new THREE.MeshPhongMaterial({
886
+ color: color,
887
+ emissive: color,
888
+ emissiveIntensity: 0.3
889
+ });
890
+ const top = new THREE.Mesh(topGeom, topMat);
891
+ top.position.y = 3.2;
892
+ group.add(top);
893
+
894
+ // Position in circle
895
+ const angle = (repoNodes.length / 8) * Math.PI * 2;
896
+ const radius = 12 + Math.floor(repoNodes.length / 8) * 7;
897
+ group.position.set(
898
+ Math.cos(angle) * radius,
899
+ 0,
900
+ Math.sin(angle) * radius
901
+ );
902
+
903
+ group.userData = {
904
+ repoId: repo.id,
905
+ repoName: repo.name,
906
+ color: color
907
+ };
908
+
909
+ // Connection line
910
+ const lineGeom = new THREE.BufferGeometry().setFromPoints([
911
+ group.position.clone().add(new THREE.Vector3(0, 1.5, 0)),
912
+ centralNode.position.clone()
913
+ ]);
914
+ const lineMat = new THREE.LineBasicMaterial({
915
+ color: color,
916
+ transparent: true,
917
+ opacity: 0.3
918
+ });
919
+ const line = new THREE.Line(lineGeom, lineMat);
920
+ scene.add(line);
921
+ group.userData.connectionLine = line;
922
+
923
+ repoNodes.push(group);
924
+ scene.add(group);
925
+
926
+ return group;
927
+ }
928
+
929
+ function updateNodeColor(node, repo) {
930
+ let color = 0x3fb950;
931
+ if (repo.secrets > 0 || repo.vulns > 10) color = 0xf85149;
932
+ else if (repo.vulns > 0) color = 0xd29922;
933
+
934
+ node.children.forEach((child, i) => {
935
+ if (i > 0 && child.material) {
936
+ child.material.color.setHex(color);
937
+ if (child.material.emissive) child.material.emissive.setHex(color);
938
+ }
939
+ });
940
+
941
+ if (node.userData.connectionLine) {
942
+ node.userData.connectionLine.material.color.setHex(color);
943
+ }
944
+
945
+ node.userData.color = color;
946
+ }
947
+
948
+ // Severity colors and labels
949
+ const SEVERITY_CONFIG = {
950
+ critical: { color: 0xf85149, label: 'Critical', icon: 'octahedron' },
951
+ high: { color: 0xdb6d28, label: 'High', icon: 'box' },
952
+ medium: { color: 0xd29922, label: 'Medium', icon: 'sphere' },
953
+ low: { color: 0x58a6ff, label: 'Low', icon: 'cone' }
954
+ };
955
+
956
+ // Clear all finding nodes from the scene
957
+ function clearFindingNodes() {
958
+ findingNodes.forEach(node => {
959
+ scene.remove(node);
960
+ if (node.geometry) node.geometry.dispose();
961
+ if (node.material) node.material.dispose();
962
+ });
963
+ findingNodes = [];
964
+
965
+ findingLines.forEach(line => {
966
+ scene.remove(line);
967
+ if (line.geometry) line.geometry.dispose();
968
+ if (line.material) line.material.dispose();
969
+ });
970
+ findingLines = [];
971
+ }
972
+
973
+ // Update navigation breadcrumb
974
+ function updateNav3D() {
975
+ const nav = document.getElementById('nav3d');
976
+ const repoName = document.querySelector('#nav3d .nav-repo');
977
+ const sevSep = document.querySelector('#nav3d .nav-sev-sep');
978
+ const sevName = document.querySelector('#nav3d .nav-severity');
979
+
980
+ if (state.drillLevel === 0) {
981
+ nav.classList.remove('visible');
982
+ } else {
983
+ nav.classList.add('visible');
984
+ const repo = state.repos.find(r => r.id === state.selectedRepo);
985
+ repoName.textContent = repo?.name || '';
986
+ repoName.classList.toggle('active', state.drillLevel === 1);
987
+
988
+ if (state.drillLevel === 2 && state.selectedSeverity) {
989
+ sevSep.style.display = 'inline';
990
+ sevName.style.display = 'inline';
991
+ sevName.textContent = SEVERITY_CONFIG[state.selectedSeverity]?.label || state.selectedSeverity;
992
+ sevName.classList.add('active');
993
+ repoName.classList.remove('active');
994
+ } else {
995
+ sevSep.style.display = 'none';
996
+ sevName.style.display = 'none';
997
+ }
998
+ }
999
+ }
1000
+
1001
+ // Navigate back one level
1002
+ function navigateBack() {
1003
+ if (state.drillLevel === 2) {
1004
+ // Go back to severity view
1005
+ state.drillLevel = 1;
1006
+ state.selectedSeverity = null;
1007
+ const repo = state.repos.find(r => r.id === state.selectedRepo);
1008
+ const repoNode = repoNodes.find(n => n.userData.repoId === state.selectedRepo);
1009
+ if (repo && repoNode) {
1010
+ showSeverityNodes(repo, repoNode);
1011
+ }
1012
+ } else if (state.drillLevel === 1) {
1013
+ // Go back to repo view
1014
+ state.drillLevel = 0;
1015
+ state.selectedRepo = null;
1016
+ clearFindingNodes();
1017
+ document.getElementById('selectedRepoInfo').classList.remove('visible');
1018
+ repoNodes.forEach(node => node.scale.setScalar(1));
1019
+ }
1020
+ updateNav3D();
1021
+ }
1022
+ window.navigateBack = navigateBack;
1023
+
1024
+ // Navigate to specific level
1025
+ function navigateToLevel(level) {
1026
+ if (level === 0) {
1027
+ state.drillLevel = 0;
1028
+ state.selectedRepo = null;
1029
+ state.selectedSeverity = null;
1030
+ clearFindingNodes();
1031
+ document.getElementById('selectedRepoInfo').classList.remove('visible');
1032
+ repoNodes.forEach(node => node.scale.setScalar(1));
1033
+ } else if (level === 1 && state.selectedRepo) {
1034
+ state.drillLevel = 1;
1035
+ state.selectedSeverity = null;
1036
+ const repo = state.repos.find(r => r.id === state.selectedRepo);
1037
+ const repoNode = repoNodes.find(n => n.userData.repoId === state.selectedRepo);
1038
+ if (repo && repoNode) {
1039
+ showSeverityNodes(repo, repoNode);
1040
+ }
1041
+ }
1042
+ updateNav3D();
1043
+ }
1044
+ window.navigateToLevel = navigateToLevel;
1045
+
1046
+ // LEVEL 1: Show severity category nodes around a repo
1047
+ function showSeverityNodes(repo, repoNode) {
1048
+ clearFindingNodes();
1049
+ state.drillLevel = 1;
1050
+ updateNav3D();
1051
+
1052
+ if (!repo.findings || repo.findings.length === 0) return;
1053
+
1054
+ const repoPos = repoNode.position.clone();
1055
+ repoPos.y += 2;
1056
+
1057
+ // Count findings by severity
1058
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
1059
+ repo.findings.forEach(f => {
1060
+ const sev = f.type === 'SECRET' ? 'critical' : (f.severity?.toLowerCase() || 'medium');
1061
+ if (counts[sev] !== undefined) counts[sev]++;
1062
+ else counts.medium++;
1063
+ });
1064
+
1065
+ // Filter to only severities with findings
1066
+ const severities = ['critical', 'high', 'medium', 'low'];
1067
+ const activeSeverities = severities.filter(sev => counts[sev] > 0);
1068
+ const radius = 6;
1069
+
1070
+ // Position nodes evenly based on how many we actually have
1071
+ activeSeverities.forEach((sev, i) => {
1072
+ const count = counts[sev];
1073
+ const config = SEVERITY_CONFIG[sev];
1074
+ // Distribute evenly around the circle based on active count
1075
+ const angle = (i / activeSeverities.length) * Math.PI * 2 - Math.PI / 2;
1076
+
1077
+ // Create severity node based on icon type
1078
+ let geom;
1079
+ const size = 0.8 + Math.min(count / 10, 1) * 0.5; // Scale by count
1080
+ switch (config.icon) {
1081
+ case 'octahedron':
1082
+ geom = new THREE.OctahedronGeometry(size, 0);
1083
+ break;
1084
+ case 'box':
1085
+ geom = new THREE.BoxGeometry(size * 1.5, size * 1.5, size * 1.5);
1086
+ break;
1087
+ case 'cone':
1088
+ geom = new THREE.ConeGeometry(size, size * 1.5, 6);
1089
+ break;
1090
+ default:
1091
+ geom = new THREE.SphereGeometry(size, 12, 12);
1092
+ }
1093
+
1094
+ const mat = new THREE.MeshPhongMaterial({
1095
+ color: config.color,
1096
+ emissive: config.color,
1097
+ emissiveIntensity: 0.3,
1098
+ flatShading: true
1099
+ });
1100
+
1101
+ const mesh = new THREE.Mesh(geom, mat);
1102
+ mesh.position.set(
1103
+ repoPos.x + Math.cos(angle) * radius,
1104
+ repoPos.y + 2,
1105
+ repoPos.z + Math.sin(angle) * radius
1106
+ );
1107
+
1108
+ mesh.userData = {
1109
+ isSeverityNode: true,
1110
+ severity: sev,
1111
+ count: count,
1112
+ label: `${config.label} (${count})`
1113
+ };
1114
+
1115
+ scene.add(mesh);
1116
+ findingNodes.push(mesh);
1117
+
1118
+ // Create connection line
1119
+ const lineGeom = new THREE.BufferGeometry().setFromPoints([
1120
+ mesh.position.clone(),
1121
+ repoPos.clone().add(new THREE.Vector3(0, 1, 0))
1122
+ ]);
1123
+ const lineMat = new THREE.LineBasicMaterial({
1124
+ color: config.color,
1125
+ transparent: true,
1126
+ opacity: 0.6
1127
+ });
1128
+ const line = new THREE.Line(lineGeom, lineMat);
1129
+ scene.add(line);
1130
+ findingLines.push(line);
1131
+
1132
+ // Add count label as sprite
1133
+ const canvas = document.createElement('canvas');
1134
+ canvas.width = 128;
1135
+ canvas.height = 64;
1136
+ const ctx = canvas.getContext('2d');
1137
+ ctx.fillStyle = '#' + config.color.toString(16).padStart(6, '0');
1138
+ ctx.font = 'bold 32px Inter, sans-serif';
1139
+ ctx.textAlign = 'center';
1140
+ ctx.fillText(count.toString(), 64, 40);
1141
+
1142
+ const texture = new THREE.CanvasTexture(canvas);
1143
+ const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true });
1144
+ const sprite = new THREE.Sprite(spriteMat);
1145
+ sprite.position.copy(mesh.position);
1146
+ sprite.position.y += 1.5;
1147
+ sprite.scale.set(2, 1, 1);
1148
+ scene.add(sprite);
1149
+ findingNodes.push(sprite);
1150
+ });
1151
+ }
1152
+
1153
+ // LEVEL 2: Show individual findings for a severity
1154
+ function showFindingsForSeverity(repo, repoNode, severity) {
1155
+ clearFindingNodes();
1156
+ state.drillLevel = 2;
1157
+ state.selectedSeverity = severity;
1158
+ updateNav3D();
1159
+
1160
+ const repoPos = repoNode.position.clone();
1161
+ repoPos.y += 2;
1162
+
1163
+ // Filter findings by severity
1164
+ const findings = repo.findings.filter(f => {
1165
+ const fSev = f.type === 'SECRET' ? 'critical' : (f.severity?.toLowerCase() || 'medium');
1166
+ return fSev === severity;
1167
+ }).slice(0, 25); // Limit for performance
1168
+
1169
+ const config = SEVERITY_CONFIG[severity];
1170
+ const radius = 5;
1171
+
1172
+ findings.forEach((finding, i) => {
1173
+ const angle = (i / findings.length) * Math.PI * 2;
1174
+ const yOffset = Math.sin(i * 0.5) * 2;
1175
+
1176
+ // Create finding node
1177
+ const size = 0.35;
1178
+ const geom = finding.type === 'SECRET'
1179
+ ? new THREE.OctahedronGeometry(size, 0)
1180
+ : new THREE.SphereGeometry(size, 8, 8);
1181
+ const mat = new THREE.MeshPhongMaterial({
1182
+ color: config.color,
1183
+ emissive: config.color,
1184
+ emissiveIntensity: 0.5,
1185
+ flatShading: true
1186
+ });
1187
+
1188
+ const mesh = new THREE.Mesh(geom, mat);
1189
+ mesh.position.set(
1190
+ repoPos.x + Math.cos(angle) * radius,
1191
+ repoPos.y + yOffset + 2,
1192
+ repoPos.z + Math.sin(angle) * radius
1193
+ );
1194
+
1195
+ mesh.userData = {
1196
+ isFindingNode: true,
1197
+ finding: finding
1198
+ };
1199
+
1200
+ scene.add(mesh);
1201
+ findingNodes.push(mesh);
1202
+
1203
+ // Create connection line
1204
+ const lineGeom = new THREE.BufferGeometry().setFromPoints([
1205
+ mesh.position.clone(),
1206
+ repoPos.clone().add(new THREE.Vector3(0, 1.5, 0))
1207
+ ]);
1208
+ const lineMat = new THREE.LineBasicMaterial({
1209
+ color: config.color,
1210
+ transparent: true,
1211
+ opacity: 0.4
1212
+ });
1213
+ const line = new THREE.Line(lineGeom, lineMat);
1214
+ scene.add(line);
1215
+ findingLines.push(line);
1216
+ });
1217
+
1218
+ // Create central severity indicator
1219
+ const centralGeom = new THREE.TorusGeometry(2, 0.2, 8, 24);
1220
+ const centralMat = new THREE.MeshPhongMaterial({
1221
+ color: config.color,
1222
+ emissive: config.color,
1223
+ emissiveIntensity: 0.3
1224
+ });
1225
+ const centralRing = new THREE.Mesh(centralGeom, centralMat);
1226
+ centralRing.position.copy(repoPos);
1227
+ centralRing.position.y += 2;
1228
+ centralRing.rotation.x = Math.PI / 2;
1229
+ scene.add(centralRing);
1230
+ findingNodes.push(centralRing);
1231
+ }
1232
+
1233
+ function animate() {
1234
+ requestAnimationFrame(animate);
1235
+
1236
+ const time = Date.now() * 0.001;
1237
+
1238
+ // Gentle rotation of central hub
1239
+ if (centralNode) {
1240
+ centralNode.children[0].rotation.y += 0.003;
1241
+ centralNode.children[1].rotation.z += 0.005;
1242
+ }
1243
+
1244
+ // Subtle float for repo nodes
1245
+ repoNodes.forEach((node, i) => {
1246
+ node.position.y = Math.sin(time * 0.5 + i) * 0.15;
1247
+ });
1248
+
1249
+ // Animate finding nodes - gentle orbit and pulse
1250
+ findingNodes.forEach((node, i) => {
1251
+ node.rotation.y += 0.02;
1252
+ node.rotation.x += 0.01;
1253
+ // Pulse effect
1254
+ const scale = 1 + Math.sin(time * 3 + i) * 0.1;
1255
+ node.scale.setScalar(scale);
1256
+ });
1257
+
1258
+ controls.update();
1259
+ renderer.render(scene, camera);
1260
+ }
1261
+
1262
+ function onResize() {
1263
+ camera.aspect = window.innerWidth / window.innerHeight;
1264
+ camera.updateProjectionMatrix();
1265
+ renderer.setSize(window.innerWidth, window.innerHeight);
1266
+ }
1267
+
1268
+ function onMouseMove(event) {
1269
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
1270
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
1271
+
1272
+ raycaster.setFromCamera(mouse, camera);
1273
+
1274
+ // Check for intersections with finding nodes first, then repo nodes
1275
+ const allClickables = [...findingNodes.filter(n => n.userData.isSeverityNode || n.userData.isFindingNode), ...repoNodes];
1276
+ const intersects = raycaster.intersectObjects(allClickables, true);
1277
+
1278
+ const tooltip = document.getElementById('tooltip3d');
1279
+
1280
+ if (intersects.length > 0) {
1281
+ document.body.style.cursor = 'pointer';
1282
+ let target = intersects[0].object;
1283
+
1284
+ // Check if it's a severity node
1285
+ if (target.userData.isSeverityNode) {
1286
+ tooltip.querySelector('.tooltip-name').textContent = target.userData.label;
1287
+ tooltip.querySelector('.tooltip-stats').innerHTML = `
1288
+ <span class="tooltip-stat">Click to view findings</span>
1289
+ `;
1290
+ tooltip.querySelector('.tooltip-hint').textContent = 'Drill down into ' + target.userData.label;
1291
+ tooltip.style.left = (event.clientX + 15) + 'px';
1292
+ tooltip.style.top = (event.clientY + 15) + 'px';
1293
+ tooltip.classList.add('visible');
1294
+ return;
1295
+ }
1296
+
1297
+ // Check if it's a finding node
1298
+ if (target.userData.isFindingNode) {
1299
+ const f = target.userData.finding;
1300
+ tooltip.querySelector('.tooltip-name').textContent = f.message || f.type;
1301
+ tooltip.querySelector('.tooltip-stats').innerHTML = `
1302
+ <span class="tooltip-stat">${f.type}</span>
1303
+ `;
1304
+ tooltip.querySelector('.tooltip-hint').textContent = f.file ? f.file + (f.line ? ':' + f.line : '') : '';
1305
+ tooltip.style.left = (event.clientX + 15) + 'px';
1306
+ tooltip.style.top = (event.clientY + 15) + 'px';
1307
+ tooltip.classList.add('visible');
1308
+ return;
1309
+ }
1310
+
1311
+ // Find the repo node group
1312
+ while (target.parent && !target.userData.repoId) {
1313
+ target = target.parent;
1314
+ }
1315
+
1316
+ if (target.userData.repoId && target !== hoveredNode) {
1317
+ hoveredNode = target;
1318
+ const repo = state.repos.find(r => r.id === target.userData.repoId);
1319
+
1320
+ if (repo) {
1321
+ // Update tooltip content
1322
+ tooltip.querySelector('.tooltip-name').textContent = repo.name;
1323
+ tooltip.querySelector('.tooltip-stats').innerHTML = `
1324
+ <span class="tooltip-stat critical"><span class="count">${repo.secrets}</span> Secrets</span>
1325
+ <span class="tooltip-stat warning"><span class="count">${repo.vulns}</span> Vulns</span>
1326
+ `;
1327
+ tooltip.querySelector('.tooltip-hint').textContent = 'Click to view severity breakdown';
1328
+
1329
+ // Update colors based on status
1330
+ const critStat = tooltip.querySelector('.tooltip-stat.critical');
1331
+ const warnStat = tooltip.querySelector('.tooltip-stat.warning');
1332
+ if (critStat) critStat.style.color = repo.secrets > 0 ? 'var(--critical)' : 'var(--text-muted)';
1333
+ if (warnStat) warnStat.style.color = repo.vulns > 0 ? 'var(--warning)' : 'var(--text-muted)';
1334
+ }
1335
+ }
1336
+
1337
+ // Position tooltip near cursor
1338
+ tooltip.style.left = (event.clientX + 15) + 'px';
1339
+ tooltip.style.top = (event.clientY + 15) + 'px';
1340
+ tooltip.classList.add('visible');
1341
+
1342
+ } else {
1343
+ document.body.style.cursor = 'default';
1344
+ tooltip.classList.remove('visible');
1345
+ hoveredNode = null;
1346
+ }
1347
+ }
1348
+
1349
+ function onMouseClick(event) {
1350
+ raycaster.setFromCamera(mouse, camera);
1351
+
1352
+ // Check for intersections with finding nodes first, then repo nodes
1353
+ const allClickables = [...findingNodes.filter(n => n.userData.isSeverityNode), ...repoNodes];
1354
+ const intersects = raycaster.intersectObjects(allClickables, true);
1355
+
1356
+ if (intersects.length > 0) {
1357
+ let target = intersects[0].object;
1358
+
1359
+ // Check if clicking a severity node (drill down to findings)
1360
+ if (target.userData.isSeverityNode) {
1361
+ const repo = state.repos.find(r => r.id === state.selectedRepo);
1362
+ const repoNode = repoNodes.find(n => n.userData.repoId === state.selectedRepo);
1363
+ if (repo && repoNode) {
1364
+ showFindingsForSeverity(repo, repoNode, target.userData.severity);
1365
+ }
1366
+ return;
1367
+ }
1368
+
1369
+ // Otherwise, find the repo node group
1370
+ while (target.parent && !target.userData.repoId) {
1371
+ target = target.parent;
1372
+ }
1373
+ if (target.userData.repoId) {
1374
+ selectRepo(target.userData.repoId);
1375
+ }
1376
+ }
1377
+ }
1378
+
1379
+ // ========== UI FUNCTIONS ==========
1380
+ function showView(view) {
1381
+ state.currentView = view;
1382
+ document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1383
+ event.target.classList.add('active');
1384
+
1385
+ document.getElementById('scanner-panel').style.display = view === 'scan' ? 'block' : 'none';
1386
+ document.getElementById('details-panel').style.display = view === 'scan' ? 'block' : 'none';
1387
+ document.getElementById('reports-panel').classList.toggle('active', view === 'reports');
1388
+ document.getElementById('view-info').style.display = view === 'scan' ? 'block' : 'none';
1389
+
1390
+ if (view === 'reports') updateReportsTable();
1391
+ }
1392
+ window.showView = showView;
1393
+
1394
+ function log(message, type = 'info') {
1395
+ const output = document.getElementById('logOutput');
1396
+ const line = document.createElement('div');
1397
+ line.className = `log-line ${type}`;
1398
+ line.textContent = message;
1399
+ output.appendChild(line);
1400
+ output.scrollTop = output.scrollHeight;
1401
+ }
1402
+
1403
+ async function executeScan() {
1404
+ const localPath = document.getElementById('scanPath').value.trim();
1405
+ const gitUrl = document.getElementById('gitUrl').value.trim();
1406
+
1407
+ if (!localPath && !gitUrl) {
1408
+ log('Error: No target specified', 'error');
1409
+ return;
1410
+ }
1411
+
1412
+ const scanBtn = document.getElementById('scanBtn');
1413
+ scanBtn.disabled = true;
1414
+ scanBtn.textContent = 'Scanning...';
1415
+
1416
+ const args = { scanSecrets: true, scanPackages: true };
1417
+ let targetName = '';
1418
+ let targetType = '';
1419
+
1420
+ if (gitUrl) {
1421
+ args.gitUrl = gitUrl;
1422
+ targetName = gitUrl.split('/').slice(-2).join('/').replace('.git', '');
1423
+ targetType = 'git';
1424
+ log(`Scanning: ${gitUrl}`);
1425
+ } else {
1426
+ args.targetPath = localPath;
1427
+ targetName = localPath.split(/[/\\]/).pop() || localPath;
1428
+ targetType = 'local';
1429
+ log(`Scanning: ${localPath}`);
1430
+ }
1431
+
1432
+ try {
1433
+ const res = await fetch(`${AURA_URL}/tools`, {
1434
+ method: 'POST',
1435
+ headers: { 'Content-Type': 'application/json' },
1436
+ body: JSON.stringify({ tool: 'scan-local', arguments: args })
1437
+ });
1438
+
1439
+ const data = await res.json();
1440
+ console.log('API Response:', JSON.stringify(data, null, 2));
1441
+
1442
+ if (data.result?.error) {
1443
+ throw new Error(data.result.error);
1444
+ }
1445
+
1446
+ const scanData = data.result?.scan_details || data.result || data;
1447
+ console.log('scanData:', JSON.stringify({
1448
+ secrets_found: scanData.secrets_found,
1449
+ package_vulns: scanData.package_vulns,
1450
+ raw_findings_keys: Object.keys(scanData.raw_findings || {})
1451
+ }, null, 2));
1452
+ const repoPath = gitUrl || localPath;
1453
+
1454
+ // Check if this repo was already scanned (update instead of duplicate)
1455
+ const existingRepo = state.repos.find(r => r.path === repoPath);
1456
+ const existingReportIdx = state.reports.findIndex(r => r.path === repoPath);
1457
+
1458
+ const repo = {
1459
+ id: existingRepo?.id || Date.now().toString(),
1460
+ name: targetName,
1461
+ path: repoPath,
1462
+ type: targetType,
1463
+ secrets: scanData.secrets_found || 0,
1464
+ vulns: scanData.package_vulns || 0,
1465
+ findings: [],
1466
+ timestamp: new Date().toISOString(),
1467
+ status: 'complete',
1468
+ rawData: scanData
1469
+ };
1470
+
1471
+ // Extract findings from all sources
1472
+ const rf = scanData.raw_findings || {};
1473
+
1474
+ // Secrets - use actual severity from scanner
1475
+ if (rf.secrets) {
1476
+ rf.secrets.forEach(s => {
1477
+ repo.findings.push({
1478
+ severity: s.severity?.toLowerCase() || 'critical',
1479
+ type: 'SECRET',
1480
+ message: `${s.type || 'Secret'} detected`,
1481
+ file: s.file,
1482
+ line: s.line
1483
+ });
1484
+ });
1485
+ }
1486
+
1487
+ // Package vulnerabilities (backend uses 'packages' not 'packageVulns')
1488
+ if (rf.packages) {
1489
+ rf.packages.forEach(v => {
1490
+ repo.findings.push({
1491
+ severity: v.severity?.toLowerCase() || 'medium',
1492
+ type: 'VULN',
1493
+ message: v.title || v.vulnId || v.name,
1494
+ file: `${v.name}@${v.version}`
1495
+ });
1496
+ });
1497
+ }
1498
+
1499
+ // SAST findings
1500
+ if (rf.sastFindings) {
1501
+ rf.sastFindings.forEach(s => {
1502
+ repo.findings.push({
1503
+ severity: s.severity?.toLowerCase() || 'medium',
1504
+ type: 'CODE',
1505
+ message: s.message || s.rule,
1506
+ file: s.file,
1507
+ line: s.line
1508
+ });
1509
+ });
1510
+ }
1511
+
1512
+ // IaC findings (Terraform, Kubernetes, etc.)
1513
+ if (rf.iacFindings) {
1514
+ rf.iacFindings.forEach(i => {
1515
+ repo.findings.push({
1516
+ severity: i.severity?.toLowerCase() || 'medium',
1517
+ type: 'IAC',
1518
+ message: i.title || i.checkId,
1519
+ file: i.file
1520
+ });
1521
+ });
1522
+ }
1523
+
1524
+ // Dockerfile findings
1525
+ if (rf.dockerfileFindings) {
1526
+ rf.dockerfileFindings.forEach(d => {
1527
+ repo.findings.push({
1528
+ severity: d.severity?.toLowerCase() || 'medium',
1529
+ type: 'DOCKER',
1530
+ message: d.message || d.code,
1531
+ file: d.file,
1532
+ line: d.line
1533
+ });
1534
+ });
1535
+ }
1536
+
1537
+ // Update or add repo to state
1538
+ if (existingRepo) {
1539
+ // Update existing repo
1540
+ const idx = state.repos.findIndex(r => r.id === existingRepo.id);
1541
+ if (idx !== -1) state.repos[idx] = repo;
1542
+
1543
+ // Update existing report
1544
+ if (existingReportIdx !== -1) state.reports[existingReportIdx] = repo;
1545
+
1546
+ // Update existing 3D node
1547
+ const existingNode = repoNodes.find(n => n.userData.repoId === repo.id);
1548
+ if (existingNode) {
1549
+ updateNodeColor(existingNode, repo);
1550
+ }
1551
+
1552
+ log(`Updated: ${repo.secrets} secrets, ${repo.vulns} vulnerabilities`, repo.secrets > 0 ? 'error' : 'success');
1553
+ } else {
1554
+ // Add new repo
1555
+ state.repos.push(repo);
1556
+ state.reports.push(repo);
1557
+ addRepoNode(repo);
1558
+
1559
+ log(`Complete: ${repo.secrets} secrets, ${repo.vulns} vulnerabilities`, repo.secrets > 0 ? 'error' : 'success');
1560
+ }
1561
+ updateUI();
1562
+ selectRepo(repo.id);
1563
+
1564
+ } catch (err) {
1565
+ console.error('Scan error:', err);
1566
+ log(`Error: ${err.message}`, 'error');
1567
+ }
1568
+
1569
+ scanBtn.disabled = false;
1570
+ scanBtn.textContent = 'Start Scan';
1571
+ }
1572
+ window.executeScan = executeScan;
1573
+
1574
+ function selectRepo(repoId) {
1575
+ state.selectedRepo = repoId;
1576
+ updateUI();
1577
+
1578
+ let selectedNode = null;
1579
+ repoNodes.forEach(node => {
1580
+ const isSelected = node.userData.repoId === repoId;
1581
+ node.scale.setScalar(isSelected ? 1.15 : 1);
1582
+ if (isSelected) selectedNode = node;
1583
+ });
1584
+
1585
+ document.querySelectorAll('.repo-item').forEach(item => {
1586
+ item.classList.toggle('selected', item.dataset.id === repoId);
1587
+ });
1588
+
1589
+ const repo = state.repos.find(r => r.id === repoId);
1590
+ if (repo) {
1591
+ document.getElementById('selectedRepoInfo').classList.add('visible');
1592
+ document.getElementById('selectedRepoName').textContent = repo.name;
1593
+ document.getElementById('selectedRepoPath').textContent = repo.path;
1594
+ updateFindingsList(repo);
1595
+
1596
+ // Show severity breakdown in 3D view (Level 1)
1597
+ if (selectedNode) {
1598
+ showSeverityNodes(repo, selectedNode);
1599
+ }
1600
+ } else {
1601
+ // No repo selected, clear finding nodes
1602
+ state.drillLevel = 0;
1603
+ clearFindingNodes();
1604
+ updateNav3D();
1605
+ }
1606
+ }
1607
+ window.selectRepo = selectRepo;
1608
+
1609
+ function updateUI() {
1610
+ let totalCrit = 0, totalWarn = 0;
1611
+ state.repos.forEach(r => {
1612
+ totalCrit += r.secrets;
1613
+ totalWarn += r.vulns;
1614
+ });
1615
+
1616
+ document.getElementById('totalCritical').textContent = totalCrit;
1617
+ document.getElementById('totalWarning').textContent = totalWarn;
1618
+ document.getElementById('totalRepos').textContent = state.repos.length;
1619
+
1620
+ const repoList = document.getElementById('repoList');
1621
+ if (state.repos.length === 0) {
1622
+ repoList.innerHTML = `
1623
+ <div class="empty-state">
1624
+ <div class="empty-state-icon">📁</div>
1625
+ <p>No targets scanned yet</p>
1626
+ </div>
1627
+ `;
1628
+ } else {
1629
+ repoList.innerHTML = state.repos.map(r => {
1630
+ let status = 'safe';
1631
+ if (r.secrets > 0) status = 'critical';
1632
+ else if (r.vulns > 0) status = 'warning';
1633
+
1634
+ return `
1635
+ <div class="repo-item ${state.selectedRepo === r.id ? 'selected' : ''}"
1636
+ data-id="${r.id}" onclick="selectRepo('${r.id}')">
1637
+ <div class="repo-status ${status}"></div>
1638
+ <div class="repo-info">
1639
+ <div class="repo-name">${r.name}</div>
1640
+ <div class="repo-path">${r.type === 'git' ? '🌐' : '📁'} ${r.path}</div>
1641
+ </div>
1642
+ <div class="repo-stats">${r.secrets}S / ${r.vulns}V</div>
1643
+ </div>
1644
+ `;
1645
+ }).join('');
1646
+ }
1647
+ }
1648
+
1649
+ function updateFindingsList(repo) {
1650
+ const container = document.getElementById('findingList');
1651
+
1652
+ if (!repo || repo.findings.length === 0) {
1653
+ container.innerHTML = `
1654
+ <div class="empty-state">
1655
+ <div class="empty-state-icon">✓</div>
1656
+ <p>${repo ? 'No findings for this target' : 'Select a target to view findings'}</p>
1657
+ </div>
1658
+ `;
1659
+ return;
1660
+ }
1661
+
1662
+ container.innerHTML = repo.findings.map(f => `
1663
+ <div class="finding-item ${f.severity}">
1664
+ <div class="finding-header">
1665
+ <span class="finding-severity">${f.severity}</span>
1666
+ <span class="finding-type">${f.type}</span>
1667
+ </div>
1668
+ <div class="finding-message">${f.message}</div>
1669
+ ${f.file ? `<div class="finding-file">${f.file}${f.line ? ':' + f.line : ''}</div>` : ''}
1670
+ </div>
1671
+ `).join('');
1672
+ }
1673
+
1674
+ function updateReportsTable() {
1675
+ const tbody = document.getElementById('reportTableBody');
1676
+
1677
+ if (state.reports.length === 0) {
1678
+ tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: var(--text-muted);">No reports yet</td></tr>';
1679
+ return;
1680
+ }
1681
+
1682
+ tbody.innerHTML = state.reports.map(r => `
1683
+ <tr>
1684
+ <td>${new Date(r.timestamp).toLocaleString()}</td>
1685
+ <td>${r.name}</td>
1686
+ <td>${r.type.toUpperCase()}</td>
1687
+ <td style="color: ${r.secrets > 0 ? 'var(--critical)' : 'var(--success)'}">${r.secrets}</td>
1688
+ <td style="color: ${r.vulns > 0 ? 'var(--warning)' : 'var(--success)'}">${r.vulns}</td>
1689
+ <td>${r.status}</td>
1690
+ <td>
1691
+ <button class="btn btn-secondary" style="width: auto; padding: 4px 10px; font-size: 11px;" onclick="viewReport('${r.id}')">View</button>
1692
+ </td>
1693
+ </tr>
1694
+ `).join('');
1695
+ }
1696
+
1697
+ function viewReport(repoId) {
1698
+ showView('scan');
1699
+ document.querySelector('.nav-tab').classList.add('active');
1700
+ document.querySelectorAll('.nav-tab')[1].classList.remove('active');
1701
+ selectRepo(repoId);
1702
+ }
1703
+ window.viewReport = viewReport;
1704
+
1705
+ function exportReports(format) {
1706
+ if (state.reports.length === 0) {
1707
+ log('No reports to export', 'error');
1708
+ return;
1709
+ }
1710
+
1711
+ let content, filename, type;
1712
+
1713
+ if (format === 'json') {
1714
+ content = JSON.stringify(state.reports, null, 2);
1715
+ filename = `aura-audit-${Date.now()}.json`;
1716
+ type = 'application/json';
1717
+ } else {
1718
+ const headers = ['Timestamp', 'Name', 'Path', 'Type', 'Secrets', 'Vulns', 'Status'];
1719
+ const rows = state.reports.map(r =>
1720
+ [r.timestamp, r.name, r.path, r.type, r.secrets, r.vulns, r.status].join(',')
1721
+ );
1722
+ content = [headers.join(','), ...rows].join('\n');
1723
+ filename = `aura-audit-${Date.now()}.csv`;
1724
+ type = 'text/csv';
1725
+ }
1726
+
1727
+ const blob = new Blob([content], { type });
1728
+ const url = URL.createObjectURL(blob);
1729
+ const a = document.createElement('a');
1730
+ a.href = url;
1731
+ a.download = filename;
1732
+ a.click();
1733
+ URL.revokeObjectURL(url);
1734
+
1735
+ log(`Exported ${state.reports.length} reports`, 'success');
1736
+ }
1737
+ window.exportReports = exportReports;
1738
+
1739
+ function clearAllReports() {
1740
+ if (confirm('Clear all scan reports?')) {
1741
+ state.reports = [];
1742
+ state.repos = [];
1743
+ state.selectedRepo = null;
1744
+
1745
+ repoNodes.forEach(node => {
1746
+ if (node.userData.connectionLine) scene.remove(node.userData.connectionLine);
1747
+ scene.remove(node);
1748
+ });
1749
+ repoNodes = [];
1750
+
1751
+ updateUI();
1752
+ updateReportsTable();
1753
+ document.getElementById('selectedRepoInfo').classList.remove('visible');
1754
+ document.getElementById('findingList').innerHTML = `
1755
+ <div class="empty-state">
1756
+ <div class="empty-state-icon">🔍</div>
1757
+ <p>Select a target to view findings</p>
1758
+ </div>
1759
+ `;
1760
+
1761
+ log('All reports cleared');
1762
+ }
1763
+ }
1764
+ window.clearAllReports = clearAllReports;
1765
+
1766
+ // ========== INIT ==========
1767
+ initScene();
1768
+ log('Ready to scan');
1769
+ </script>
1770
+ </body>
1771
+ </html>