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.
- package/LICENSE +21 -0
- package/README.md +446 -0
- package/deploy/AWS-DEPLOYMENT.md +358 -0
- package/deploy/terraform/main.tf +362 -0
- package/deploy/terraform/terraform.tfvars.example +6 -0
- package/dist/agents/base.d.ts +44 -0
- package/dist/agents/base.js +96 -0
- package/dist/agents/index.d.ts +14 -0
- package/dist/agents/index.js +17 -0
- package/dist/agents/policy/evaluator.d.ts +15 -0
- package/dist/agents/policy/evaluator.js +183 -0
- package/dist/agents/policy/index.d.ts +12 -0
- package/dist/agents/policy/index.js +15 -0
- package/dist/agents/policy/validator.d.ts +15 -0
- package/dist/agents/policy/validator.js +182 -0
- package/dist/agents/scanners/gitleaks.d.ts +14 -0
- package/dist/agents/scanners/gitleaks.js +155 -0
- package/dist/agents/scanners/grype.d.ts +14 -0
- package/dist/agents/scanners/grype.js +109 -0
- package/dist/agents/scanners/index.d.ts +15 -0
- package/dist/agents/scanners/index.js +27 -0
- package/dist/agents/scanners/npm-audit.d.ts +13 -0
- package/dist/agents/scanners/npm-audit.js +129 -0
- package/dist/agents/scanners/semgrep.d.ts +14 -0
- package/dist/agents/scanners/semgrep.js +131 -0
- package/dist/agents/scanners/trivy.d.ts +14 -0
- package/dist/agents/scanners/trivy.js +122 -0
- package/dist/agents/types.d.ts +137 -0
- package/dist/agents/types.js +91 -0
- package/dist/auditor/index.d.ts +3 -0
- package/dist/auditor/index.js +2 -0
- package/dist/auditor/pipeline.d.ts +19 -0
- package/dist/auditor/pipeline.js +240 -0
- package/dist/auditor/validator.d.ts +17 -0
- package/dist/auditor/validator.js +58 -0
- package/dist/aura/client.d.ts +29 -0
- package/dist/aura/client.js +125 -0
- package/dist/aura/index.d.ts +4 -0
- package/dist/aura/index.js +2 -0
- package/dist/aura/server.d.ts +45 -0
- package/dist/aura/server.js +343 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +1433 -0
- package/dist/client/index.d.ts +41 -0
- package/dist/client/index.js +170 -0
- package/dist/compliance/index.d.ts +40 -0
- package/dist/compliance/index.js +292 -0
- package/dist/database/index.d.ts +77 -0
- package/dist/database/index.js +395 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +762 -0
- package/dist/integrations/aura-scanner.d.ts +69 -0
- package/dist/integrations/aura-scanner.js +155 -0
- package/dist/integrations/aws-scanner.d.ts +63 -0
- package/dist/integrations/aws-scanner.js +624 -0
- package/dist/integrations/config.d.ts +69 -0
- package/dist/integrations/config.js +212 -0
- package/dist/integrations/github.d.ts +45 -0
- package/dist/integrations/github.js +201 -0
- package/dist/integrations/gitlab.d.ts +36 -0
- package/dist/integrations/gitlab.js +110 -0
- package/dist/integrations/index.d.ts +11 -0
- package/dist/integrations/index.js +11 -0
- package/dist/integrations/local-scanner.d.ts +146 -0
- package/dist/integrations/local-scanner.js +1654 -0
- package/dist/integrations/notifications.d.ts +99 -0
- package/dist/integrations/notifications.js +305 -0
- package/dist/integrations/scanners.d.ts +57 -0
- package/dist/integrations/scanners.js +217 -0
- package/dist/integrations/slop-scanner.d.ts +69 -0
- package/dist/integrations/slop-scanner.js +155 -0
- package/dist/integrations/webhook.d.ts +37 -0
- package/dist/integrations/webhook.js +256 -0
- package/dist/orchestrator/index.d.ts +72 -0
- package/dist/orchestrator/index.js +187 -0
- package/dist/output/index.d.ts +152 -0
- package/dist/output/index.js +399 -0
- package/dist/pipeline/index.d.ts +72 -0
- package/dist/pipeline/index.js +313 -0
- package/dist/sbom/index.d.ts +94 -0
- package/dist/sbom/index.js +298 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/input.schema.d.ts +87 -0
- package/dist/schemas/input.schema.js +44 -0
- package/dist/schemas/output.schema.d.ts +115 -0
- package/dist/schemas/output.schema.js +64 -0
- package/dist/serve-visualizer.d.ts +2 -0
- package/dist/serve-visualizer.js +78 -0
- package/dist/slop/client.d.ts +29 -0
- package/dist/slop/client.js +125 -0
- package/dist/slop/index.d.ts +4 -0
- package/dist/slop/index.js +2 -0
- package/dist/slop/server.d.ts +45 -0
- package/dist/slop/server.js +343 -0
- package/dist/types/events.d.ts +62 -0
- package/dist/types/events.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/visualizer/index.d.ts +4 -0
- package/dist/visualizer/index.js +181 -0
- package/dist/websocket/index.d.ts +88 -0
- package/dist/websocket/index.js +195 -0
- package/dist/zones/index.d.ts +7 -0
- package/dist/zones/index.js +7 -0
- package/dist/zones/manager.d.ts +101 -0
- package/dist/zones/manager.js +304 -0
- package/dist/zones/types.d.ts +78 -0
- package/dist/zones/types.js +33 -0
- package/package.json +84 -0
- package/visualizer/app.js +0 -0
- package/visualizer/index-minimal.html +1771 -0
- package/visualizer/index.html +2933 -0
- package/visualizer/landing.html +1328 -0
- 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 <kbd>Scroll</kbd> Zoom <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>
|