sapper-ai 0.5.0 → 0.6.1
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/dist/auth.d.ts +11 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +102 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +316 -32
- package/dist/harden.d.ts +28 -0
- package/dist/harden.d.ts.map +1 -0
- package/dist/harden.js +309 -0
- package/dist/mcp/jsonc.d.ts +3 -0
- package/dist/mcp/jsonc.d.ts.map +1 -0
- package/dist/mcp/jsonc.js +119 -0
- package/dist/mcp/wrapConfig.d.ts +22 -0
- package/dist/mcp/wrapConfig.d.ts.map +1 -0
- package/dist/mcp/wrapConfig.js +192 -0
- package/dist/policyYaml.d.ts +3 -0
- package/dist/policyYaml.d.ts.map +1 -0
- package/dist/policyYaml.js +27 -0
- package/dist/postinstall.d.ts.map +1 -1
- package/dist/postinstall.js +11 -2
- package/dist/quarantine.d.ts +13 -0
- package/dist/quarantine.d.ts.map +1 -0
- package/dist/quarantine.js +22 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +1061 -59
- package/dist/scan.d.ts +15 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +179 -178
- package/dist/utils/env.d.ts +3 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +25 -0
- package/dist/utils/format.d.ts +22 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +97 -0
- package/dist/utils/fs.d.ts +7 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +47 -0
- package/dist/utils/repoRoot.d.ts +2 -0
- package/dist/utils/repoRoot.d.ts.map +1 -0
- package/dist/utils/repoRoot.js +20 -0
- package/dist/utils/semver.d.ts +2 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +7 -0
- package/package.json +5 -7
package/dist/report.js
CHANGED
|
@@ -45,8 +45,11 @@ body {
|
|
|
45
45
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
46
46
|
background: var(--bg-primary);
|
|
47
47
|
color: var(--text-primary);
|
|
48
|
+
line-height: 1.45;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
button, input { font: inherit; }
|
|
52
|
+
|
|
50
53
|
header {
|
|
51
54
|
position: sticky;
|
|
52
55
|
top: 0;
|
|
@@ -69,6 +72,16 @@ header {
|
|
|
69
72
|
font-size: 12px;
|
|
70
73
|
color: var(--text-secondary);
|
|
71
74
|
text-align: right;
|
|
75
|
+
min-width: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.meta-scope {
|
|
79
|
+
display: inline-block;
|
|
80
|
+
max-width: 520px;
|
|
81
|
+
vertical-align: bottom;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
text-overflow: ellipsis;
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
#theme-toggle {
|
|
@@ -87,33 +100,54 @@ header {
|
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
.summary {
|
|
90
|
-
display:
|
|
91
|
-
|
|
92
|
-
gap:
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-wrap: nowrap;
|
|
105
|
+
gap: 10px;
|
|
106
|
+
overflow-x: auto;
|
|
107
|
+
overflow-y: hidden;
|
|
108
|
+
padding-bottom: 4px;
|
|
109
|
+
scrollbar-gutter: stable;
|
|
110
|
+
-webkit-overflow-scrolling: touch;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.summary:focus-visible {
|
|
114
|
+
outline: 2px solid var(--accent);
|
|
115
|
+
outline-offset: 4px;
|
|
116
|
+
border-radius: 16px;
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
.metric-card {
|
|
96
120
|
background: var(--bg-secondary);
|
|
97
121
|
border: 1px solid var(--border);
|
|
98
122
|
border-radius: 14px;
|
|
99
|
-
padding: 12px;
|
|
123
|
+
padding: 10px 12px;
|
|
124
|
+
flex: 1 0 150px;
|
|
125
|
+
min-width: 150px;
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
.metric-card .label {
|
|
103
129
|
display: block;
|
|
104
|
-
font-size:
|
|
130
|
+
font-size: 11px;
|
|
105
131
|
color: var(--text-secondary);
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
text-overflow: ellipsis;
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
.metric-card .value {
|
|
109
138
|
display: block;
|
|
110
|
-
margin-top:
|
|
111
|
-
font-size:
|
|
139
|
+
margin-top: 4px;
|
|
140
|
+
font-size: 18px;
|
|
112
141
|
font-variant-numeric: tabular-nums;
|
|
142
|
+
white-space: nowrap;
|
|
113
143
|
}
|
|
114
144
|
|
|
115
145
|
.metric-card .value.danger { color: var(--risk-critical); }
|
|
116
146
|
|
|
147
|
+
.metric-card[data-metric="coverage"] .value {
|
|
148
|
+
font-size: 16px;
|
|
149
|
+
}
|
|
150
|
+
|
|
117
151
|
.chart {
|
|
118
152
|
margin-top: 12px;
|
|
119
153
|
background: var(--bg-secondary);
|
|
@@ -181,28 +215,140 @@ header {
|
|
|
181
215
|
gap: 8px;
|
|
182
216
|
}
|
|
183
217
|
|
|
218
|
+
.toggle.inline { margin-top: 0; }
|
|
219
|
+
|
|
220
|
+
.tree-actions {
|
|
221
|
+
margin-top: 10px;
|
|
222
|
+
display: flex;
|
|
223
|
+
flex-wrap: wrap;
|
|
224
|
+
align-items: center;
|
|
225
|
+
gap: 8px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.mini-btn {
|
|
229
|
+
border: 1px solid var(--border);
|
|
230
|
+
background: transparent;
|
|
231
|
+
color: var(--text-primary);
|
|
232
|
+
padding: 6px 10px;
|
|
233
|
+
border-radius: 999px;
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.mini-btn:hover { background: var(--bg-tertiary); }
|
|
239
|
+
|
|
240
|
+
.mini-btn:focus-visible,
|
|
241
|
+
#theme-toggle:focus-visible,
|
|
242
|
+
#tree-search:focus-visible,
|
|
243
|
+
#tree summary:focus-visible,
|
|
244
|
+
.file-btn:focus-visible,
|
|
245
|
+
.match-btn:focus-visible {
|
|
246
|
+
outline: 2px solid var(--accent);
|
|
247
|
+
outline-offset: 2px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.tree-root {
|
|
251
|
+
margin-top: 10px;
|
|
252
|
+
font-size: 12px;
|
|
253
|
+
color: var(--text-secondary);
|
|
254
|
+
word-break: break-all;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.tree-empty {
|
|
258
|
+
padding: 10px;
|
|
259
|
+
border: 1px dashed var(--border);
|
|
260
|
+
border-radius: 12px;
|
|
261
|
+
color: var(--text-secondary);
|
|
262
|
+
font-size: 12px;
|
|
263
|
+
}
|
|
264
|
+
|
|
184
265
|
#tree {
|
|
185
266
|
padding: 10px;
|
|
186
267
|
overflow: auto;
|
|
187
268
|
flex: 1;
|
|
188
269
|
}
|
|
189
270
|
|
|
190
|
-
details {
|
|
271
|
+
#tree details {
|
|
191
272
|
border-radius: 10px;
|
|
192
273
|
}
|
|
193
|
-
|
|
274
|
+
|
|
275
|
+
#tree details > summary::-webkit-details-marker { display: none; }
|
|
276
|
+
|
|
277
|
+
#tree summary {
|
|
194
278
|
cursor: pointer;
|
|
195
279
|
padding: 8px 10px;
|
|
196
280
|
border-radius: 10px;
|
|
197
281
|
color: var(--text-primary);
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: 8px;
|
|
285
|
+
list-style: none;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#tree summary:hover { background: var(--bg-tertiary); }
|
|
289
|
+
|
|
290
|
+
.tree-icon {
|
|
291
|
+
width: 16px;
|
|
292
|
+
height: 16px;
|
|
293
|
+
display: inline-flex;
|
|
294
|
+
align-items: center;
|
|
295
|
+
justify-content: center;
|
|
296
|
+
flex: 0 0 auto;
|
|
297
|
+
color: var(--text-muted);
|
|
298
|
+
}
|
|
299
|
+
.tree-icon svg { width: 16px; height: 16px; fill: currentColor; }
|
|
300
|
+
.tree-spacer { width: 14px; height: 14px; flex: 0 0 auto; }
|
|
301
|
+
|
|
302
|
+
#tree details > summary .chev {
|
|
303
|
+
width: 14px;
|
|
304
|
+
height: 14px;
|
|
305
|
+
display: inline-flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
justify-content: center;
|
|
308
|
+
border-radius: 6px;
|
|
309
|
+
color: var(--text-secondary);
|
|
310
|
+
font-size: 12px;
|
|
311
|
+
flex: 0 0 auto;
|
|
312
|
+
transition: transform 140ms ease;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#tree details[open] > summary .chev { transform: rotate(90deg); }
|
|
316
|
+
|
|
317
|
+
#tree details > summary .dirname { flex: 1 1 auto; }
|
|
318
|
+
|
|
319
|
+
#tree details > summary .dir-badge {
|
|
320
|
+
font-size: 11px;
|
|
321
|
+
padding: 2px 8px;
|
|
322
|
+
border-radius: 999px;
|
|
323
|
+
border: 1px solid var(--border);
|
|
324
|
+
color: var(--text-secondary);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#tree details > summary .dir-badge.warn {
|
|
328
|
+
border-color: rgba(245, 158, 11, 0.6);
|
|
329
|
+
color: #f59e0b;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#tree details > summary .dir-badge.danger {
|
|
333
|
+
border-color: rgba(239, 68, 68, 0.65);
|
|
334
|
+
color: #ef4444;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#tree details > div {
|
|
338
|
+
margin-left: 14px;
|
|
339
|
+
padding-left: 10px;
|
|
340
|
+
border-left: 1px solid var(--border);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#tree > div > details > div {
|
|
344
|
+
margin-left: 0;
|
|
198
345
|
}
|
|
199
|
-
summary:hover { background: var(--bg-tertiary); }
|
|
200
346
|
|
|
201
347
|
.file-btn {
|
|
202
348
|
width: 100%;
|
|
203
349
|
text-align: left;
|
|
204
350
|
display: flex;
|
|
205
|
-
align-items:
|
|
351
|
+
align-items: flex-start;
|
|
206
352
|
gap: 10px;
|
|
207
353
|
padding: 8px 10px;
|
|
208
354
|
border: 1px solid transparent;
|
|
@@ -219,6 +365,27 @@ summary:hover { background: var(--bg-tertiary); }
|
|
|
219
365
|
border-left: 3px solid var(--accent);
|
|
220
366
|
}
|
|
221
367
|
|
|
368
|
+
.file-btn .fname {
|
|
369
|
+
flex: 1 1 auto;
|
|
370
|
+
min-width: 0;
|
|
371
|
+
display: flex;
|
|
372
|
+
flex-direction: column;
|
|
373
|
+
gap: 2px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.file-btn .fname > span {
|
|
377
|
+
overflow: hidden;
|
|
378
|
+
text-overflow: ellipsis;
|
|
379
|
+
white-space: nowrap;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.file-btn .fname .sub {
|
|
383
|
+
font-size: 11px;
|
|
384
|
+
color: var(--text-secondary);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.tree-spacer { width: 14px; flex: 0 0 auto; }
|
|
388
|
+
|
|
222
389
|
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
|
223
390
|
.dot.critical { background: var(--risk-critical); }
|
|
224
391
|
.dot.high { background: var(--risk-high); }
|
|
@@ -232,6 +399,7 @@ summary:hover { background: var(--bg-tertiary); }
|
|
|
232
399
|
}
|
|
233
400
|
|
|
234
401
|
.file-header { padding-bottom: 12px; border-bottom: 1px solid var(--border); }
|
|
402
|
+
.file-path-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
235
403
|
.file-path { font-size: 12px; color: var(--text-secondary); word-break: break-all; }
|
|
236
404
|
.file-name { margin-top: 6px; font-size: 18px; font-weight: 700; }
|
|
237
405
|
.metrics { margin-top: 10px; display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; color: var(--text-secondary); }
|
|
@@ -242,6 +410,7 @@ summary:hover { background: var(--bg-tertiary); }
|
|
|
242
410
|
.section { margin-top: 14px; }
|
|
243
411
|
.section h3 { margin: 0 0 8px; font-size: 13px; color: var(--text-secondary); }
|
|
244
412
|
.patterns ul { margin: 0; padding-left: 18px; }
|
|
413
|
+
.reasons ul { margin: 0; padding-left: 18px; }
|
|
245
414
|
|
|
246
415
|
pre {
|
|
247
416
|
margin: 0;
|
|
@@ -258,6 +427,74 @@ code {
|
|
|
258
427
|
white-space: pre;
|
|
259
428
|
}
|
|
260
429
|
|
|
430
|
+
mark.hl {
|
|
431
|
+
color: inherit;
|
|
432
|
+
border-radius: 6px;
|
|
433
|
+
padding: 0 2px;
|
|
434
|
+
text-decoration-line: underline;
|
|
435
|
+
text-decoration-style: wavy;
|
|
436
|
+
text-decoration-thickness: 2px;
|
|
437
|
+
text-underline-offset: 3px;
|
|
438
|
+
background: linear-gradient(to bottom, transparent 62%, rgba(245, 158, 11, 0.14) 62%);
|
|
439
|
+
text-decoration-color: rgba(245, 158, 11, 0.85);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
mark.hl[data-sev="high"] {
|
|
443
|
+
background: linear-gradient(to bottom, transparent 62%, rgba(239, 68, 68, 0.14) 62%);
|
|
444
|
+
text-decoration-color: rgba(239, 68, 68, 0.9);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
mark.hl[data-sev="medium"] {
|
|
448
|
+
background: linear-gradient(to bottom, transparent 62%, rgba(245, 158, 11, 0.14) 62%);
|
|
449
|
+
text-decoration-color: rgba(245, 158, 11, 0.85);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
mark.hl.active {
|
|
453
|
+
text-decoration-thickness: 3px;
|
|
454
|
+
filter: saturate(1.2);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.match-list { display: flex; flex-direction: column; gap: 6px; }
|
|
458
|
+
.match-btn {
|
|
459
|
+
text-align: left;
|
|
460
|
+
border: 1px solid var(--border);
|
|
461
|
+
background: transparent;
|
|
462
|
+
color: var(--text-primary);
|
|
463
|
+
padding: 8px 10px;
|
|
464
|
+
border-radius: 10px;
|
|
465
|
+
cursor: pointer;
|
|
466
|
+
font-size: 12px;
|
|
467
|
+
display: flex;
|
|
468
|
+
align-items: baseline;
|
|
469
|
+
justify-content: space-between;
|
|
470
|
+
gap: 10px;
|
|
471
|
+
}
|
|
472
|
+
.match-btn:hover { background: var(--bg-tertiary); }
|
|
473
|
+
.loc { color: var(--text-secondary); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
474
|
+
|
|
475
|
+
.muted { color: var(--text-secondary); font-size: 12px; margin: 8px 0 0; }
|
|
476
|
+
|
|
477
|
+
.match-context-list { margin-top: 10px; display: flex; flex-direction: column; gap: 10px; }
|
|
478
|
+
.match-context {
|
|
479
|
+
border: 1px solid var(--border);
|
|
480
|
+
background: var(--bg-secondary);
|
|
481
|
+
border-radius: 14px;
|
|
482
|
+
padding: 10px;
|
|
483
|
+
}
|
|
484
|
+
.match-meta { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
485
|
+
.match-needle { font-size: 12px; color: var(--text-secondary); margin-top: 4px; word-break: break-word; }
|
|
486
|
+
.match-context pre { margin-top: 8px; }
|
|
487
|
+
|
|
488
|
+
.file-btn:focus-visible,
|
|
489
|
+
#tree summary:focus-visible,
|
|
490
|
+
#tree-search:focus-visible,
|
|
491
|
+
#theme-toggle:focus-visible,
|
|
492
|
+
.mini-btn:focus-visible,
|
|
493
|
+
.match-btn:focus-visible {
|
|
494
|
+
outline: 2px solid var(--accent);
|
|
495
|
+
outline-offset: 2px;
|
|
496
|
+
}
|
|
497
|
+
|
|
261
498
|
@media (max-width: 1024px) {
|
|
262
499
|
.main { grid-template-columns: 40% 60%; }
|
|
263
500
|
}
|
|
@@ -265,15 +502,78 @@ code {
|
|
|
265
502
|
@media (max-width: 768px) {
|
|
266
503
|
header { height: auto; padding: 12px 14px; gap: 10px; flex-wrap: wrap; }
|
|
267
504
|
.container { padding: 14px; }
|
|
268
|
-
.summary {
|
|
505
|
+
.summary { gap: 8px; }
|
|
506
|
+
.metric-card { flex-basis: 140px; min-width: 140px; }
|
|
269
507
|
.main { grid-template-columns: 1fr; }
|
|
270
508
|
#tree { max-height: 260px; }
|
|
271
509
|
}
|
|
272
510
|
|
|
511
|
+
@media (prefers-contrast: more) {
|
|
512
|
+
:root, [data-theme="dark"] {
|
|
513
|
+
--border: #4b4b4b;
|
|
514
|
+
--text-secondary: #d1d1d1;
|
|
515
|
+
--text-muted: #a0a0a0;
|
|
516
|
+
}
|
|
517
|
+
[data-theme="light"] {
|
|
518
|
+
--border: #111827;
|
|
519
|
+
--text-secondary: #374151;
|
|
520
|
+
--text-muted: #4b5563;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@media (forced-colors: active) {
|
|
525
|
+
.metric-card,
|
|
526
|
+
.panel,
|
|
527
|
+
pre,
|
|
528
|
+
#theme-toggle,
|
|
529
|
+
#tree-search,
|
|
530
|
+
.mini-btn {
|
|
531
|
+
border-color: CanvasText;
|
|
532
|
+
}
|
|
533
|
+
.metric-card .value.danger { color: CanvasText; }
|
|
534
|
+
.badge,
|
|
535
|
+
.badge.block,
|
|
536
|
+
.badge.allow {
|
|
537
|
+
border-color: CanvasText;
|
|
538
|
+
color: CanvasText;
|
|
539
|
+
}
|
|
540
|
+
.file-btn.active {
|
|
541
|
+
outline: 2px solid Highlight;
|
|
542
|
+
outline-offset: 2px;
|
|
543
|
+
border-color: Highlight;
|
|
544
|
+
border-left-color: Highlight;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
273
548
|
@media print {
|
|
274
549
|
* { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
|
275
550
|
header { position: static; }
|
|
276
551
|
.main { grid-template-columns: 1fr; }
|
|
552
|
+
.summary { overflow: visible; flex-wrap: wrap; padding-bottom: 0; }
|
|
553
|
+
.metric-card { min-width: auto; flex: 1 1 220px; }
|
|
554
|
+
.metric-card .label,
|
|
555
|
+
.metric-card .value {
|
|
556
|
+
white-space: normal;
|
|
557
|
+
overflow: visible;
|
|
558
|
+
text-overflow: clip;
|
|
559
|
+
}
|
|
560
|
+
.meta-scope { max-width: none; white-space: normal; }
|
|
561
|
+
#tree,
|
|
562
|
+
.detail-inner,
|
|
563
|
+
pre {
|
|
564
|
+
overflow: visible;
|
|
565
|
+
max-height: none;
|
|
566
|
+
}
|
|
567
|
+
pre { white-space: pre-wrap; }
|
|
568
|
+
code { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
569
|
+
#theme-toggle,
|
|
570
|
+
.controls,
|
|
571
|
+
.tree-actions,
|
|
572
|
+
#tree-search,
|
|
573
|
+
.mini-btn,
|
|
574
|
+
.toggle {
|
|
575
|
+
display: none !important;
|
|
576
|
+
}
|
|
277
577
|
}
|
|
278
578
|
`.trim();
|
|
279
579
|
}
|
|
@@ -281,6 +581,75 @@ function generateJs() {
|
|
|
281
581
|
return `
|
|
282
582
|
const el = (sel) => document.querySelector(sel);
|
|
283
583
|
|
|
584
|
+
const ICON_FOLDER =
|
|
585
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 4l2 2h8a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2h6z"/></svg>';
|
|
586
|
+
const ICON_FILE =
|
|
587
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6zm0 2.5L19.5 10H14V4.5z"/></svg>';
|
|
588
|
+
|
|
589
|
+
function prefersReducedMotion() {
|
|
590
|
+
return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function normalizePath(p) {
|
|
594
|
+
return String(p || '').replace(/\\\\/g, '/');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function computeBasePath(findings, targetStr) {
|
|
598
|
+
const paths = [];
|
|
599
|
+
for (const f of findings || []) {
|
|
600
|
+
const fp = normalizePath(f && f.filePath);
|
|
601
|
+
if (fp) paths.push(fp);
|
|
602
|
+
}
|
|
603
|
+
if (paths.length === 0) return '';
|
|
604
|
+
|
|
605
|
+
const targets = String(targetStr || '')
|
|
606
|
+
.split(',')
|
|
607
|
+
.map((s) => s.trim())
|
|
608
|
+
.filter(Boolean)
|
|
609
|
+
.map(normalizePath);
|
|
610
|
+
|
|
611
|
+
if (targets.length === 1) {
|
|
612
|
+
const t = targets[0].replace(/\\/+$/, '');
|
|
613
|
+
if (t && paths.every((p) => p === t || p.startsWith(t + '/'))) {
|
|
614
|
+
return t + '/';
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const segs = paths.map((p) => p.split('/').filter(Boolean));
|
|
619
|
+
let prefix = segs[0].slice();
|
|
620
|
+
for (let i = 1; i < segs.length; i++) {
|
|
621
|
+
const s = segs[i];
|
|
622
|
+
let j = 0;
|
|
623
|
+
while (j < prefix.length && j < s.length && prefix[j] === s[j]) j += 1;
|
|
624
|
+
prefix = prefix.slice(0, j);
|
|
625
|
+
if (prefix.length === 0) break;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (prefix.length < 2) return '';
|
|
629
|
+
const lead = paths[0].startsWith('/') ? '/' : '';
|
|
630
|
+
return lead + prefix.join('/') + '/';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function toDisplayPath(filePath, basePath) {
|
|
634
|
+
const full = normalizePath(filePath);
|
|
635
|
+
const base = normalizePath(basePath);
|
|
636
|
+
if (base && full.startsWith(base)) {
|
|
637
|
+
return full.slice(base.length).replace(/^\\/+/, '');
|
|
638
|
+
}
|
|
639
|
+
return full.replace(/^\\/+/, '');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function fileDisplayInfo(finding, basePath) {
|
|
643
|
+
const fullPath = String(finding && finding.filePath ? finding.filePath : '');
|
|
644
|
+
const normalizedFull = normalizePath(fullPath);
|
|
645
|
+
const displayPath = toDisplayPath(normalizedFull, basePath);
|
|
646
|
+
const displayName =
|
|
647
|
+
displayPath.split('/').filter(Boolean).pop() ||
|
|
648
|
+
normalizedFull.split('/').filter(Boolean).pop() ||
|
|
649
|
+
normalizedFull;
|
|
650
|
+
return { displayPath, displayName };
|
|
651
|
+
}
|
|
652
|
+
|
|
284
653
|
function escapeHtml(text) {
|
|
285
654
|
return String(text)
|
|
286
655
|
.replace(/&/g, '&')
|
|
@@ -304,11 +673,39 @@ function debounce(fn, ms) {
|
|
|
304
673
|
};
|
|
305
674
|
}
|
|
306
675
|
|
|
307
|
-
function
|
|
308
|
-
|
|
676
|
+
function getDirOpenState() {
|
|
677
|
+
if (!window.__DIR_OPEN) window.__DIR_OPEN = {};
|
|
678
|
+
return window.__DIR_OPEN;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function buildFileTree(findings, basePath) {
|
|
682
|
+
function mkNode(name) {
|
|
683
|
+
return {
|
|
684
|
+
name,
|
|
685
|
+
files: [],
|
|
686
|
+
dirs: new Map(),
|
|
687
|
+
stats: { total: 0, threats: 0, maxRisk: 0 },
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function bump(node, finding) {
|
|
692
|
+
const r = Number(finding && finding.risk);
|
|
693
|
+
const risk = Number.isFinite(r) ? r : 0;
|
|
694
|
+
node.stats.total += 1;
|
|
695
|
+
if (risk >= 0.5) node.stats.threats += 1;
|
|
696
|
+
if (risk > node.stats.maxRisk) node.stats.maxRisk = risk;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const root = mkNode('');
|
|
309
700
|
for (const f of findings) {
|
|
310
|
-
const
|
|
701
|
+
const info = fileDisplayInfo(f, basePath);
|
|
702
|
+
f.__displayPath = info.displayPath;
|
|
703
|
+
f.__displayName = info.displayName;
|
|
704
|
+
|
|
705
|
+
const parts = String(info.displayPath).split('/').filter(Boolean);
|
|
706
|
+
if (parts.length === 0) continue;
|
|
311
707
|
let node = root;
|
|
708
|
+
bump(node, f);
|
|
312
709
|
for (let i = 0; i < parts.length; i++) {
|
|
313
710
|
const part = parts[i];
|
|
314
711
|
const isFile = i === parts.length - 1;
|
|
@@ -316,51 +713,113 @@ function buildFileTree(findings) {
|
|
|
316
713
|
node.files.push(f);
|
|
317
714
|
} else {
|
|
318
715
|
if (!node.dirs.has(part)) {
|
|
319
|
-
node.dirs.set(part,
|
|
716
|
+
node.dirs.set(part, mkNode(part));
|
|
320
717
|
}
|
|
321
718
|
node = node.dirs.get(part);
|
|
719
|
+
bump(node, f);
|
|
322
720
|
}
|
|
323
721
|
}
|
|
324
722
|
}
|
|
325
723
|
return root;
|
|
326
724
|
}
|
|
327
725
|
|
|
328
|
-
function renderTreeNode(node, opts) {
|
|
726
|
+
function renderTreeNode(node, opts, parentPath) {
|
|
329
727
|
const container = document.createElement('div');
|
|
330
728
|
|
|
331
729
|
const dirEntries = Array.from(node.dirs.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
332
730
|
for (const dir of dirEntries) {
|
|
333
731
|
const details = document.createElement('details');
|
|
334
|
-
|
|
732
|
+
const dirPath = (parentPath ? parentPath + '/' : '') + String(dir.name);
|
|
733
|
+
details.setAttribute('data-dir', dirPath);
|
|
734
|
+
const openState = getDirOpenState();
|
|
735
|
+
if (opts && opts.query) {
|
|
736
|
+
details.open = true;
|
|
737
|
+
} else if (Object.prototype.hasOwnProperty.call(openState, dirPath)) {
|
|
738
|
+
details.open = !!openState[dirPath];
|
|
739
|
+
} else {
|
|
740
|
+
details.open = parentPath === '';
|
|
741
|
+
}
|
|
742
|
+
details.addEventListener('toggle', () => {
|
|
743
|
+
const state = getDirOpenState();
|
|
744
|
+
state[dirPath] = details.open;
|
|
745
|
+
});
|
|
746
|
+
|
|
335
747
|
const summary = document.createElement('summary');
|
|
336
|
-
summary.
|
|
748
|
+
summary.title = dirPath;
|
|
749
|
+
|
|
750
|
+
const chev = document.createElement('span');
|
|
751
|
+
chev.className = 'chev';
|
|
752
|
+
chev.textContent = '>';
|
|
753
|
+
summary.appendChild(chev);
|
|
754
|
+
|
|
755
|
+
const icon = document.createElement('span');
|
|
756
|
+
icon.className = 'tree-icon';
|
|
757
|
+
icon.innerHTML = ICON_FOLDER;
|
|
758
|
+
summary.appendChild(icon);
|
|
759
|
+
|
|
760
|
+
const dirname = document.createElement('span');
|
|
761
|
+
dirname.className = 'dirname';
|
|
762
|
+
dirname.textContent = String(dir.name);
|
|
763
|
+
summary.appendChild(dirname);
|
|
764
|
+
|
|
765
|
+
const badge = document.createElement('span');
|
|
766
|
+
const threats = Number(dir.stats && dir.stats.threats) || 0;
|
|
767
|
+
const total = Number(dir.stats && dir.stats.total) || 0;
|
|
768
|
+
const maxRisk = Number(dir.stats && dir.stats.maxRisk) || 0;
|
|
769
|
+
badge.className = 'dir-badge' + (maxRisk >= 0.8 ? ' danger' : threats > 0 ? ' warn' : '');
|
|
770
|
+
badge.title = threats > 0 ? (threats + ' threats') : (total + ' files');
|
|
771
|
+
badge.textContent = threats > 0 ? (threats + '/' + total) : String(total);
|
|
772
|
+
summary.appendChild(badge);
|
|
773
|
+
|
|
337
774
|
details.appendChild(summary);
|
|
338
|
-
details.appendChild(renderTreeNode(dir, opts));
|
|
775
|
+
details.appendChild(renderTreeNode(dir, opts, dirPath));
|
|
339
776
|
container.appendChild(details);
|
|
340
777
|
}
|
|
341
778
|
|
|
342
|
-
const files = node.files
|
|
779
|
+
const files = node.files
|
|
780
|
+
.slice()
|
|
781
|
+
.sort((a, b) => String(a.__displayPath || a.filePath).localeCompare(String(b.__displayPath || b.filePath)));
|
|
343
782
|
for (const f of files) {
|
|
344
|
-
const threat = Number(f.risk) >= 0.5;
|
|
345
|
-
if (opts.threatsOnly && !threat) continue;
|
|
346
|
-
if (opts.query) {
|
|
347
|
-
const name = String(f.filePath).split('/').pop() || '';
|
|
348
|
-
if (!name.toLowerCase().includes(opts.query.toLowerCase())) continue;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
783
|
const btn = document.createElement('button');
|
|
352
784
|
btn.className = 'file-btn';
|
|
353
785
|
btn.type = 'button';
|
|
354
786
|
btn.setAttribute('data-file', String(f.filePath));
|
|
355
787
|
btn.setAttribute('role', 'treeitem');
|
|
356
788
|
btn.tabIndex = -1;
|
|
789
|
+
btn.title = String(f.__displayPath || f.filePath);
|
|
790
|
+
|
|
791
|
+
const spacer = document.createElement('span');
|
|
792
|
+
spacer.className = 'tree-spacer';
|
|
793
|
+
btn.appendChild(spacer);
|
|
357
794
|
|
|
358
795
|
const dot = document.createElement('span');
|
|
359
796
|
dot.className = 'dot ' + riskLevel(Number(f.risk));
|
|
360
797
|
btn.appendChild(dot);
|
|
361
798
|
|
|
799
|
+
const icon = document.createElement('span');
|
|
800
|
+
icon.className = 'tree-icon';
|
|
801
|
+
icon.innerHTML = ICON_FILE;
|
|
802
|
+
btn.appendChild(icon);
|
|
803
|
+
|
|
362
804
|
const label = document.createElement('span');
|
|
363
|
-
label.
|
|
805
|
+
label.className = 'fname';
|
|
806
|
+
const displayName =
|
|
807
|
+
String(f.__displayName || '').trim() ||
|
|
808
|
+
String(f.filePath).split('/').pop() ||
|
|
809
|
+
String(f.filePath);
|
|
810
|
+
|
|
811
|
+
const base = document.createElement('span');
|
|
812
|
+
base.textContent = displayName;
|
|
813
|
+
label.appendChild(base);
|
|
814
|
+
|
|
815
|
+
const dp = String(f.__displayPath || '');
|
|
816
|
+
const parent = dp.split('/').slice(0, -1).join('/');
|
|
817
|
+
if (parent) {
|
|
818
|
+
const sub = document.createElement('span');
|
|
819
|
+
sub.className = 'sub';
|
|
820
|
+
sub.textContent = parent;
|
|
821
|
+
label.appendChild(sub);
|
|
822
|
+
}
|
|
364
823
|
btn.appendChild(label);
|
|
365
824
|
|
|
366
825
|
btn.addEventListener('click', () => handleFileClick(String(f.filePath)));
|
|
@@ -370,32 +829,367 @@ function renderTreeNode(node, opts) {
|
|
|
370
829
|
return container;
|
|
371
830
|
}
|
|
372
831
|
|
|
832
|
+
function uniqStrings(values) {
|
|
833
|
+
const out = [];
|
|
834
|
+
const seen = new Set();
|
|
835
|
+
for (const v of values) {
|
|
836
|
+
const s = String(v || '').trim();
|
|
837
|
+
if (!s) continue;
|
|
838
|
+
const k = s.toLowerCase();
|
|
839
|
+
if (seen.has(k)) continue;
|
|
840
|
+
seen.add(k);
|
|
841
|
+
out.push(s);
|
|
842
|
+
}
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function extractQuotedSegments(text) {
|
|
847
|
+
const out = [];
|
|
848
|
+
const s = String(text || '');
|
|
849
|
+
const re = /\\x60([^\\x60]{3,120})\\x60|"([^"]{3,120})"|'([^']{3,120})'/g;
|
|
850
|
+
let m;
|
|
851
|
+
while ((m = re.exec(s))) {
|
|
852
|
+
out.push(m[1] || m[2] || m[3] || '');
|
|
853
|
+
}
|
|
854
|
+
return out;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function buildHighlightNeedles(patterns, reasons) {
|
|
858
|
+
const needles = [];
|
|
859
|
+
|
|
860
|
+
const add = (value) => {
|
|
861
|
+
const s = String(value || '').trim();
|
|
862
|
+
if (!s) return;
|
|
863
|
+
needles.push(s);
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
for (const p of patterns || []) {
|
|
867
|
+
add(p);
|
|
868
|
+
const parts = String(p || '').split(/[^a-zA-Z0-9_-]+/).filter(Boolean);
|
|
869
|
+
for (const part of parts) {
|
|
870
|
+
if (part.length >= 6 || /[\\d_-]/.test(part)) add(part);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
for (const r of reasons || []) {
|
|
875
|
+
for (const q of extractQuotedSegments(r)) add(q);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const unique = uniqStrings(needles).filter((s) => s.length >= 3 && s.length <= 160);
|
|
879
|
+
return unique.slice(0, 24);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function indexToLineCol(text, index) {
|
|
883
|
+
const s = String(text || '');
|
|
884
|
+
const n = Math.max(0, Math.min(s.length, Number(index) || 0));
|
|
885
|
+
let line = 1;
|
|
886
|
+
let col = 1;
|
|
887
|
+
for (let i = 0; i < n; i++) {
|
|
888
|
+
if (s.charCodeAt(i) === 10) {
|
|
889
|
+
line += 1;
|
|
890
|
+
col = 1;
|
|
891
|
+
} else {
|
|
892
|
+
col += 1;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return { line, col };
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function normalizeRuleMatches(ruleMatches) {
|
|
899
|
+
const out = [];
|
|
900
|
+
const seen = new Set();
|
|
901
|
+
for (const m of ruleMatches || []) {
|
|
902
|
+
if (!m || typeof m !== 'object') continue;
|
|
903
|
+
const label = typeof m.label === 'string' ? m.label : '';
|
|
904
|
+
const severity = m.severity === 'high' ? 'high' : 'medium';
|
|
905
|
+
const matchText = typeof m.matchText === 'string' ? m.matchText : '';
|
|
906
|
+
const context = typeof m.context === 'string' ? m.context : '';
|
|
907
|
+
if (!label || !matchText) continue;
|
|
908
|
+
const key = label + '\\n' + matchText.toLowerCase();
|
|
909
|
+
if (seen.has(key)) continue;
|
|
910
|
+
seen.add(key);
|
|
911
|
+
out.push({ label, severity, matchText, context });
|
|
912
|
+
if (out.length >= 24) break;
|
|
913
|
+
}
|
|
914
|
+
return out;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function highlightPlainText(text, needle) {
|
|
918
|
+
const s = String(text || '');
|
|
919
|
+
const n = String(needle || '').trim();
|
|
920
|
+
if (!s) return '';
|
|
921
|
+
if (!n) return escapeHtml(s);
|
|
922
|
+
|
|
923
|
+
const lower = s.toLowerCase();
|
|
924
|
+
const nl = n.toLowerCase();
|
|
925
|
+
|
|
926
|
+
let html = '';
|
|
927
|
+
let pos = 0;
|
|
928
|
+
let from = 0;
|
|
929
|
+
let count = 0;
|
|
930
|
+
const MAX = 8;
|
|
931
|
+
|
|
932
|
+
while (count < MAX) {
|
|
933
|
+
const idx = lower.indexOf(nl, from);
|
|
934
|
+
if (idx === -1) break;
|
|
935
|
+
html += escapeHtml(s.slice(pos, idx));
|
|
936
|
+
html += '<mark class="hl">' + escapeHtml(s.slice(idx, idx + nl.length)) + '</mark>';
|
|
937
|
+
pos = idx + nl.length;
|
|
938
|
+
from = pos;
|
|
939
|
+
count += 1;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
html += escapeHtml(s.slice(pos));
|
|
943
|
+
return html;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function highlightSnippet(snippet, ruleMatches, patterns, reasons) {
|
|
947
|
+
const text = String(snippet || '');
|
|
948
|
+
const rms = normalizeRuleMatches(ruleMatches);
|
|
949
|
+
const missing = [];
|
|
950
|
+
|
|
951
|
+
if (rms.length > 0) {
|
|
952
|
+
if (text.length === 0) {
|
|
953
|
+
for (const rm of rms) {
|
|
954
|
+
missing.push({
|
|
955
|
+
label: rm.label,
|
|
956
|
+
severity: rm.severity,
|
|
957
|
+
matchText: rm.matchText,
|
|
958
|
+
contextHtml: highlightPlainText(rm.context || '', rm.matchText),
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
return { html: escapeHtml(text), matches: [], missing };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const lower = text.toLowerCase();
|
|
965
|
+
const occ = [];
|
|
966
|
+
const found = new Set();
|
|
967
|
+
const MAX_MATCHES = 160;
|
|
968
|
+
const MAX_PER_NEEDLE = 24;
|
|
969
|
+
|
|
970
|
+
for (const rm of rms) {
|
|
971
|
+
const needle = String(rm.matchText || '').trim();
|
|
972
|
+
if (!needle || needle.length < 3 || needle.length > 200) continue;
|
|
973
|
+
const nl = needle.toLowerCase();
|
|
974
|
+
let from = 0;
|
|
975
|
+
let count = 0;
|
|
976
|
+
while (count < MAX_PER_NEEDLE && occ.length < MAX_MATCHES) {
|
|
977
|
+
const idx = lower.indexOf(nl, from);
|
|
978
|
+
if (idx === -1) break;
|
|
979
|
+
occ.push({ start: idx, end: idx + nl.length, label: rm.label, severity: rm.severity });
|
|
980
|
+
found.add(rm.label + '\\n' + nl);
|
|
981
|
+
from = idx + Math.max(1, nl.length);
|
|
982
|
+
count += 1;
|
|
983
|
+
}
|
|
984
|
+
if (occ.length >= MAX_MATCHES) break;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
for (const rm of rms) {
|
|
988
|
+
const nl = String(rm.matchText || '').trim().toLowerCase();
|
|
989
|
+
if (!nl) continue;
|
|
990
|
+
const key = rm.label + '\\n' + nl;
|
|
991
|
+
if (found.has(key)) continue;
|
|
992
|
+
missing.push({
|
|
993
|
+
label: rm.label,
|
|
994
|
+
severity: rm.severity,
|
|
995
|
+
matchText: rm.matchText,
|
|
996
|
+
contextHtml: highlightPlainText(rm.context || '', rm.matchText),
|
|
997
|
+
});
|
|
998
|
+
if (missing.length >= 24) break;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (occ.length === 0) {
|
|
1002
|
+
return { html: escapeHtml(text), matches: [], missing };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
occ.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
1006
|
+
|
|
1007
|
+
const merged = [];
|
|
1008
|
+
for (const o of occ) {
|
|
1009
|
+
const last = merged[merged.length - 1];
|
|
1010
|
+
if (!last || o.start > last.end) {
|
|
1011
|
+
merged.push({ start: o.start, end: o.end, labels: [o.label], severity: o.severity });
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
if (o.end > last.end) last.end = o.end;
|
|
1015
|
+
if (last.labels.indexOf(o.label) === -1) last.labels.push(o.label);
|
|
1016
|
+
if (o.severity === 'high') last.severity = 'high';
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
let html = '';
|
|
1020
|
+
let pos = 0;
|
|
1021
|
+
const matches = [];
|
|
1022
|
+
for (let i = 0; i < merged.length; i++) {
|
|
1023
|
+
const r = merged[i];
|
|
1024
|
+
html += escapeHtml(text.slice(pos, r.start));
|
|
1025
|
+
const id = 'm' + i;
|
|
1026
|
+
const seg = text.slice(r.start, r.end);
|
|
1027
|
+
const labelText = Array.isArray(r.labels) ? r.labels.slice(0, 4).join(', ') : '';
|
|
1028
|
+
const labelAttr = labelText ? (' data-label=\"' + escapeHtml(labelText) + '\" title=\"' + escapeHtml(labelText) + '\"') : '';
|
|
1029
|
+
const sevAttr = r.severity ? (' data-sev=\"' + escapeHtml(String(r.severity)) + '\"') : '';
|
|
1030
|
+
html += '<mark class=\"hl\" id=\"' + id + '\"' + labelAttr + sevAttr + '>' + escapeHtml(seg) + '</mark>';
|
|
1031
|
+
|
|
1032
|
+
const lc = indexToLineCol(text, r.start);
|
|
1033
|
+
const previewBase = seg.replace(/\\s+/g, ' ').trim().slice(0, 70);
|
|
1034
|
+
const preview = labelText ? ('[' + labelText + '] ' + previewBase) : previewBase;
|
|
1035
|
+
matches.push({ id, start: r.start, loc: lc.line + ':' + lc.col, preview });
|
|
1036
|
+
pos = r.end;
|
|
1037
|
+
}
|
|
1038
|
+
html += escapeHtml(text.slice(pos));
|
|
1039
|
+
|
|
1040
|
+
return { html, matches, missing };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Fallback for older scan results without ruleMatches.
|
|
1044
|
+
const needles = buildHighlightNeedles(patterns, reasons);
|
|
1045
|
+
if (needles.length === 0 || text.length === 0) {
|
|
1046
|
+
return { html: escapeHtml(text), matches: [], missing: [] };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const lower = text.toLowerCase();
|
|
1050
|
+
const ranges = [];
|
|
1051
|
+
const MAX_MATCHES = 120;
|
|
1052
|
+
const MAX_PER_NEEDLE = 20;
|
|
1053
|
+
|
|
1054
|
+
for (const needle of needles) {
|
|
1055
|
+
const n = String(needle || '').toLowerCase();
|
|
1056
|
+
if (!n) continue;
|
|
1057
|
+
let from = 0;
|
|
1058
|
+
let count = 0;
|
|
1059
|
+
while (count < MAX_PER_NEEDLE && ranges.length < MAX_MATCHES) {
|
|
1060
|
+
const idx = lower.indexOf(n, from);
|
|
1061
|
+
if (idx === -1) break;
|
|
1062
|
+
ranges.push({ start: idx, end: idx + n.length });
|
|
1063
|
+
from = idx + Math.max(1, n.length);
|
|
1064
|
+
count += 1;
|
|
1065
|
+
}
|
|
1066
|
+
if (ranges.length >= MAX_MATCHES) break;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (ranges.length === 0) {
|
|
1070
|
+
return { html: escapeHtml(text), matches: [], missing: [] };
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
ranges.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
1074
|
+
|
|
1075
|
+
const merged = [];
|
|
1076
|
+
for (const r of ranges) {
|
|
1077
|
+
const last = merged[merged.length - 1];
|
|
1078
|
+
if (!last || r.start > last.end) {
|
|
1079
|
+
merged.push({ start: r.start, end: r.end });
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (r.end > last.end) last.end = r.end;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
let html = '';
|
|
1086
|
+
let pos = 0;
|
|
1087
|
+
const matches = [];
|
|
1088
|
+
for (let i = 0; i < merged.length; i++) {
|
|
1089
|
+
const r = merged[i];
|
|
1090
|
+
html += escapeHtml(text.slice(pos, r.start));
|
|
1091
|
+
const id = 'm' + i;
|
|
1092
|
+
const seg = text.slice(r.start, r.end);
|
|
1093
|
+
html += '<mark class=\"hl\" id=\"' + id + '\">' + escapeHtml(seg) + '</mark>';
|
|
1094
|
+
|
|
1095
|
+
const lc = indexToLineCol(text, r.start);
|
|
1096
|
+
const preview = seg.replace(/\\s+/g, ' ').trim().slice(0, 80);
|
|
1097
|
+
matches.push({ id, start: r.start, loc: lc.line + ':' + lc.col, preview });
|
|
1098
|
+
pos = r.end;
|
|
1099
|
+
}
|
|
1100
|
+
html += escapeHtml(text.slice(pos));
|
|
1101
|
+
|
|
1102
|
+
return { html, matches, missing: [] };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function setActiveHighlight(detail, mark) {
|
|
1106
|
+
if (!detail || !mark) return;
|
|
1107
|
+
detail.querySelectorAll('mark.hl.active').forEach((m) => m.classList.remove('active'));
|
|
1108
|
+
mark.classList.add('active');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
373
1111
|
function handleFileClick(filePath) {
|
|
374
1112
|
const finding = (SCAN_DATA.findings || []).find((f) => f.filePath === filePath);
|
|
375
1113
|
if (!finding) return;
|
|
376
1114
|
|
|
1115
|
+
window.__ACTIVE_FILE = filePath;
|
|
1116
|
+
|
|
377
1117
|
document.querySelectorAll('.file-btn').forEach((b) => b.classList.remove('active'));
|
|
378
|
-
|
|
379
|
-
|
|
1118
|
+
let active = null;
|
|
1119
|
+
document.querySelectorAll('.file-btn').forEach((b) => {
|
|
1120
|
+
if (b.getAttribute('data-file') === filePath) active = b;
|
|
1121
|
+
});
|
|
1122
|
+
if (active) {
|
|
1123
|
+
active.classList.add('active');
|
|
1124
|
+
try { active.scrollIntoView({ block: 'nearest' }); } catch {}
|
|
1125
|
+
}
|
|
380
1126
|
|
|
381
1127
|
const detail = el('#detail');
|
|
382
|
-
|
|
1128
|
+
if (!detail) return;
|
|
1129
|
+
|
|
1130
|
+
const basePath = String(window.__TREE_STATE && window.__TREE_STATE.basePath ? window.__TREE_STATE.basePath : '');
|
|
1131
|
+
const info = fileDisplayInfo(finding, basePath);
|
|
1132
|
+
const displayPath = String(info.displayPath || filePath);
|
|
1133
|
+
const name = String(info.displayName || normalizePath(filePath).split('/').pop() || filePath);
|
|
383
1134
|
const patterns = Array.isArray(finding.patterns) ? finding.patterns : [];
|
|
1135
|
+
const reasons = Array.isArray(finding.reasons) ? finding.reasons : [];
|
|
384
1136
|
const detectors = Array.isArray(finding.detectors) ? finding.detectors : [];
|
|
1137
|
+
const ruleMatches = Array.isArray(finding.ruleMatches) ? finding.ruleMatches : [];
|
|
385
1138
|
const badgeClass = finding.action === 'block' ? 'block' : 'allow';
|
|
386
1139
|
const badgeText = String(finding.action || '').toUpperCase();
|
|
387
1140
|
|
|
388
1141
|
const patternsHtml = patterns.length
|
|
389
1142
|
? patterns.map((p) => '<li>' + escapeHtml(p) + '</li>').join('')
|
|
390
1143
|
: '<li>None</li>';
|
|
1144
|
+
const reasonsHtml = reasons.length
|
|
1145
|
+
? reasons.map((r) => '<li>' + escapeHtml(r) + '</li>').join('')
|
|
1146
|
+
: '<li>None</li>';
|
|
391
1147
|
const detectorsHtml = detectors.length
|
|
392
1148
|
? detectors.map((d) => '<span class="badge">' + escapeHtml(d) + '</span>').join('')
|
|
393
1149
|
: '<span class="badge">none</span>';
|
|
394
1150
|
|
|
1151
|
+
const hl = highlightSnippet(finding.snippet || '', ruleMatches, patterns, reasons);
|
|
1152
|
+
const matchButtonsHtml = hl.matches.length
|
|
1153
|
+
? hl.matches.map((m, i) =>
|
|
1154
|
+
'<button class="match-btn" type="button" data-jump="' + m.id + '">' +
|
|
1155
|
+
'<span>' + escapeHtml(String(i + 1) + '. ' + (m.preview || 'match')) + '</span>' +
|
|
1156
|
+
'<span class="loc">' + escapeHtml(m.loc) + '</span>' +
|
|
1157
|
+
'</button>'
|
|
1158
|
+
).join('')
|
|
1159
|
+
: '';
|
|
1160
|
+
|
|
1161
|
+
const missingHtml = hl.missing && hl.missing.length
|
|
1162
|
+
? '<div class="match-context-list">' +
|
|
1163
|
+
hl.missing.map((m) =>
|
|
1164
|
+
'<div class="match-context">' +
|
|
1165
|
+
'<div class="match-meta">' +
|
|
1166
|
+
'<span>' + escapeHtml(String(m.label || 'match')) + '</span>' +
|
|
1167
|
+
'<span class="loc">' + escapeHtml(String(m.severity || '')) + '</span>' +
|
|
1168
|
+
'</div>' +
|
|
1169
|
+
(m.matchText ? '<div class="match-needle">' + escapeHtml(String(m.matchText)) + '</div>' : '') +
|
|
1170
|
+
'<pre><code>' + String(m.contextHtml || '') + '</code></pre>' +
|
|
1171
|
+
'</div>'
|
|
1172
|
+
).join('') +
|
|
1173
|
+
'</div>'
|
|
1174
|
+
: '';
|
|
1175
|
+
|
|
1176
|
+
const matchesSectionHtml =
|
|
1177
|
+
(matchButtonsHtml || missingHtml)
|
|
1178
|
+
? '<div class="section matches">' +
|
|
1179
|
+
'<h3>Matches</h3>' +
|
|
1180
|
+
(matchButtonsHtml
|
|
1181
|
+
? '<div class="match-list">' + matchButtonsHtml + '</div>'
|
|
1182
|
+
: '<p class="muted">No matches found in snippet (it may be truncated). See contexts below.</p>') +
|
|
1183
|
+
missingHtml +
|
|
1184
|
+
'</div>'
|
|
1185
|
+
: '';
|
|
1186
|
+
|
|
395
1187
|
detail.innerHTML =
|
|
396
1188
|
'<div class="detail-inner">' +
|
|
397
1189
|
'<div class="file-header">' +
|
|
398
|
-
'<div class="file-path">' +
|
|
1190
|
+
'<div class="file-path-row">' +
|
|
1191
|
+
'<div class="file-path" title="' + escapeHtml(filePath) + '">' + escapeHtml(displayPath) + '</div>' +
|
|
1192
|
+
'</div>' +
|
|
399
1193
|
'<div class="file-name">' + escapeHtml(name) + '</div>' +
|
|
400
1194
|
'<div class="metrics">' +
|
|
401
1195
|
'<span>Risk: <span class="badge">' + Number(finding.risk).toFixed(2) + '</span></span>' +
|
|
@@ -407,9 +1201,14 @@ function handleFileClick(filePath) {
|
|
|
407
1201
|
'<h3>Detected Patterns</h3>' +
|
|
408
1202
|
'<ul>' + patternsHtml + '</ul>' +
|
|
409
1203
|
'</div>' +
|
|
1204
|
+
'<div class="section reasons">' +
|
|
1205
|
+
'<h3>Reasons</h3>' +
|
|
1206
|
+
'<ul>' + reasonsHtml + '</ul>' +
|
|
1207
|
+
'</div>' +
|
|
1208
|
+
matchesSectionHtml +
|
|
410
1209
|
'<div class="section snippet">' +
|
|
411
1210
|
'<h3>Code Snippet</h3>' +
|
|
412
|
-
'<pre><code>' +
|
|
1211
|
+
'<pre><code>' + hl.html + '</code></pre>' +
|
|
413
1212
|
'</div>' +
|
|
414
1213
|
'<div class="section detectors">' +
|
|
415
1214
|
'<h3>Detectors</h3>' +
|
|
@@ -420,6 +1219,31 @@ function handleFileClick(filePath) {
|
|
|
420
1219
|
'<p>' + escapeHtml(finding.aiAnalysis || '') + '</p>' +
|
|
421
1220
|
'</div>' +
|
|
422
1221
|
'</div>';
|
|
1222
|
+
|
|
1223
|
+
const inner = detail.querySelector('.detail-inner');
|
|
1224
|
+
if (inner) {
|
|
1225
|
+
inner.scrollTop = 0;
|
|
1226
|
+
inner.scrollLeft = 0;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const firstMark = detail.querySelector('mark.hl');
|
|
1230
|
+
if (firstMark) setActiveHighlight(detail, firstMark);
|
|
1231
|
+
|
|
1232
|
+
try {
|
|
1233
|
+
const next = '#file=' + encodeURIComponent(filePath);
|
|
1234
|
+
if (location.hash !== next) location.hash = next;
|
|
1235
|
+
} catch {}
|
|
1236
|
+
|
|
1237
|
+
const autoJump = el('#auto-jump');
|
|
1238
|
+
if (
|
|
1239
|
+
autoJump &&
|
|
1240
|
+
autoJump.checked &&
|
|
1241
|
+
window.matchMedia &&
|
|
1242
|
+
window.matchMedia('(max-width: 768px)').matches
|
|
1243
|
+
) {
|
|
1244
|
+
try { detail.focus({ preventScroll: true }); } catch {}
|
|
1245
|
+
try { detail.scrollIntoView({ behavior: prefersReducedMotion() ? 'auto' : 'smooth', block: 'start' }); } catch {}
|
|
1246
|
+
}
|
|
423
1247
|
}
|
|
424
1248
|
|
|
425
1249
|
function handleSearch(query) {
|
|
@@ -484,12 +1308,14 @@ function renderChart() {
|
|
|
484
1308
|
}
|
|
485
1309
|
|
|
486
1310
|
function renderTree(partial) {
|
|
487
|
-
const state = window.__TREE_STATE || { query: '', riskFilter: null };
|
|
1311
|
+
const state = window.__TREE_STATE || { query: '', riskFilter: null, basePath: '' };
|
|
488
1312
|
window.__TREE_STATE = Object.assign({}, state, partial);
|
|
489
1313
|
|
|
490
1314
|
const findings = Array.isArray(SCAN_DATA.findings) ? SCAN_DATA.findings : [];
|
|
491
1315
|
const threatsOnlyEl = el('#threats-only');
|
|
492
1316
|
const threatsOnly = threatsOnlyEl ? !!threatsOnlyEl.checked : true;
|
|
1317
|
+
const query = String(window.__TREE_STATE.query || '').trim().toLowerCase();
|
|
1318
|
+
const basePath = String(window.__TREE_STATE.basePath || '');
|
|
493
1319
|
|
|
494
1320
|
let visible = findings;
|
|
495
1321
|
if (window.__TREE_STATE.riskFilter) {
|
|
@@ -503,32 +1329,96 @@ function renderTree(partial) {
|
|
|
503
1329
|
});
|
|
504
1330
|
}
|
|
505
1331
|
|
|
506
|
-
|
|
1332
|
+
if (threatsOnly) {
|
|
1333
|
+
visible = visible.filter((f) => Number(f.risk) >= 0.5);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
for (const f of visible) {
|
|
1337
|
+
const info = fileDisplayInfo(f, basePath);
|
|
1338
|
+
f.__displayPath = info.displayPath;
|
|
1339
|
+
f.__displayName = info.displayName;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (query) {
|
|
1343
|
+
visible = visible.filter((f) => {
|
|
1344
|
+
const dp = String(f.__displayPath || '').toLowerCase();
|
|
1345
|
+
const dn = String(f.__displayName || '').toLowerCase();
|
|
1346
|
+
return dp.includes(query) || dn.includes(query);
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
|
|
507
1350
|
const tree = el('#tree');
|
|
508
1351
|
if (!tree) return;
|
|
509
1352
|
tree.innerHTML = '';
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1353
|
+
if (visible.length === 0) {
|
|
1354
|
+
tree.innerHTML = '<div class="tree-empty">No files match the current filters.</div>';
|
|
1355
|
+
} else {
|
|
1356
|
+
const treeRoot = buildFileTree(visible, basePath);
|
|
1357
|
+
const node = renderTreeNode(treeRoot, { query }, '');
|
|
1358
|
+
tree.appendChild(node);
|
|
1359
|
+
|
|
1360
|
+
const first = tree.querySelector('.file-btn');
|
|
1361
|
+
if (first) first.tabIndex = 0;
|
|
1362
|
+
}
|
|
513
1363
|
|
|
514
|
-
const
|
|
515
|
-
if (
|
|
1364
|
+
const root = el('#tree-root');
|
|
1365
|
+
if (root) {
|
|
1366
|
+
const parts = [];
|
|
1367
|
+
const baseLabel = String(basePath || '').replace(/\\/+$/, '');
|
|
1368
|
+
if (baseLabel) parts.push('Root: ' + baseLabel);
|
|
1369
|
+
else if (SCAN_DATA && SCAN_DATA.target) parts.push('Target: ' + String(SCAN_DATA.target));
|
|
1370
|
+
parts.push('Showing: ' + String(visible.length) + '/' + String(findings.length));
|
|
1371
|
+
if (window.__TREE_STATE.riskFilter) parts.push('Risk: ' + String(window.__TREE_STATE.riskFilter));
|
|
1372
|
+
if (threatsOnly) parts.push('Threats only');
|
|
1373
|
+
if (query) parts.push('Search: ' + query);
|
|
1374
|
+
root.textContent = parts.join(' | ');
|
|
1375
|
+
root.hidden = parts.length === 0;
|
|
1376
|
+
}
|
|
516
1377
|
}
|
|
517
1378
|
|
|
518
1379
|
function installKeyboardNav() {
|
|
519
1380
|
document.addEventListener('keydown', (e) => {
|
|
1381
|
+
const target = e.target && e.target.nodeType === 1 ? e.target : null;
|
|
1382
|
+
const tag = target && target.tagName ? String(target.tagName).toUpperCase() : '';
|
|
1383
|
+
const isTyping = tag === 'INPUT' || tag === 'TEXTAREA' || (target && target.isContentEditable);
|
|
1384
|
+
|
|
1385
|
+
if (!isTyping && e.key === '/' && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
1386
|
+
const search = el('#tree-search');
|
|
1387
|
+
if (search) {
|
|
1388
|
+
e.preventDefault();
|
|
1389
|
+
search.focus();
|
|
1390
|
+
if (search.select) search.select();
|
|
1391
|
+
}
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
if (e.key === 'Escape') {
|
|
1396
|
+
const search = el('#tree-search');
|
|
1397
|
+
if (search && typeof search.value === 'string' && search.value.length > 0) {
|
|
1398
|
+
e.preventDefault();
|
|
1399
|
+
search.value = '';
|
|
1400
|
+
handleSearch('');
|
|
1401
|
+
try { search.focus(); } catch {}
|
|
1402
|
+
}
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if (isTyping) return;
|
|
1407
|
+
|
|
520
1408
|
const items = Array.from(document.querySelectorAll('.file-btn'));
|
|
521
1409
|
if (items.length === 0) return;
|
|
522
1410
|
const active = document.activeElement;
|
|
523
1411
|
const idx = items.indexOf(active);
|
|
524
1412
|
|
|
525
1413
|
if (e.key === 'ArrowDown') {
|
|
1414
|
+
if (idx === -1) return;
|
|
526
1415
|
e.preventDefault();
|
|
527
1416
|
const next = items[Math.min(items.length - 1, Math.max(0, idx + 1))] || items[0];
|
|
528
1417
|
next.focus();
|
|
529
1418
|
return;
|
|
530
1419
|
}
|
|
531
1420
|
if (e.key === 'ArrowUp') {
|
|
1421
|
+
if (idx === -1) return;
|
|
532
1422
|
e.preventDefault();
|
|
533
1423
|
const next = items[Math.max(0, idx - 1)] || items[0];
|
|
534
1424
|
next.focus();
|
|
@@ -561,13 +1451,99 @@ function bootstrap() {
|
|
|
561
1451
|
const threatsOnly = el('#threats-only');
|
|
562
1452
|
if (threatsOnly) threatsOnly.addEventListener('change', handleThreatsOnlyToggle);
|
|
563
1453
|
|
|
1454
|
+
const findings = Array.isArray(SCAN_DATA.findings) ? SCAN_DATA.findings : [];
|
|
1455
|
+
const basePath = computeBasePath(findings, SCAN_DATA && SCAN_DATA.target);
|
|
1456
|
+
const state = window.__TREE_STATE || { query: '', riskFilter: null };
|
|
1457
|
+
window.__TREE_STATE = Object.assign({}, state, { basePath });
|
|
1458
|
+
|
|
1459
|
+
const autoJump = el('#auto-jump');
|
|
1460
|
+
if (autoJump) {
|
|
1461
|
+
try {
|
|
1462
|
+
const saved = localStorage.getItem('sapper-auto-jump');
|
|
1463
|
+
if (saved === '0' || saved === '1') autoJump.checked = saved === '1';
|
|
1464
|
+
} catch {}
|
|
1465
|
+
|
|
1466
|
+
autoJump.addEventListener('change', () => {
|
|
1467
|
+
try { localStorage.setItem('sapper-auto-jump', autoJump.checked ? '1' : '0'); } catch {}
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function setAllDirsOpen(open) {
|
|
1472
|
+
const state = getDirOpenState();
|
|
1473
|
+
document.querySelectorAll('#tree details').forEach((d) => {
|
|
1474
|
+
d.open = open;
|
|
1475
|
+
const dir = d.getAttribute('data-dir');
|
|
1476
|
+
if (dir) state[dir] = open;
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const expandAll = el('#expand-all');
|
|
1481
|
+
if (expandAll) expandAll.addEventListener('click', () => setAllDirsOpen(true));
|
|
1482
|
+
|
|
1483
|
+
const collapseAll = el('#collapse-all');
|
|
1484
|
+
if (collapseAll) collapseAll.addEventListener('click', () => setAllDirsOpen(false));
|
|
1485
|
+
|
|
1486
|
+
try {
|
|
1487
|
+
const findings = Array.isArray(SCAN_DATA && SCAN_DATA.findings) ? SCAN_DATA.findings : [];
|
|
1488
|
+
const basePath = computeBasePath(findings, SCAN_DATA && SCAN_DATA.target);
|
|
1489
|
+
const state = window.__TREE_STATE || { query: '', riskFilter: null, basePath: '' };
|
|
1490
|
+
if (!state.basePath) state.basePath = basePath;
|
|
1491
|
+
window.__TREE_STATE = state;
|
|
1492
|
+
} catch {}
|
|
1493
|
+
|
|
564
1494
|
renderChart();
|
|
565
1495
|
renderTree({ query: '' });
|
|
566
1496
|
installKeyboardNav();
|
|
567
1497
|
|
|
568
1498
|
const detail = el('#detail');
|
|
569
1499
|
if (detail) {
|
|
1500
|
+
detail.addEventListener('click', (e) => {
|
|
1501
|
+
const t = e.target && e.target.nodeType === 1 ? e.target : null;
|
|
1502
|
+
if (!t) return;
|
|
1503
|
+
|
|
1504
|
+
const btn = t.closest ? t.closest('.match-btn') : null;
|
|
1505
|
+
if (btn) {
|
|
1506
|
+
const id = btn.getAttribute('data-jump');
|
|
1507
|
+
if (!id) return;
|
|
1508
|
+
const mark = document.getElementById(id);
|
|
1509
|
+
if (!mark) return;
|
|
1510
|
+
setActiveHighlight(detail, mark);
|
|
1511
|
+
try {
|
|
1512
|
+
mark.scrollIntoView({
|
|
1513
|
+
behavior: prefersReducedMotion() ? 'auto' : 'smooth',
|
|
1514
|
+
block: 'center',
|
|
1515
|
+
inline: 'nearest',
|
|
1516
|
+
});
|
|
1517
|
+
} catch {
|
|
1518
|
+
try { mark.scrollIntoView(); } catch {}
|
|
1519
|
+
}
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (t.matches && t.matches('mark.hl')) {
|
|
1524
|
+
setActiveHighlight(detail, t);
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
|
|
570
1528
|
detail.innerHTML = '<div class="detail-inner"><div class="file-path">Select a file to view details</div></div>';
|
|
1529
|
+
|
|
1530
|
+
try {
|
|
1531
|
+
const h = String(location.hash || '');
|
|
1532
|
+
if (h.startsWith('#file=')) {
|
|
1533
|
+
const fp = decodeURIComponent(h.slice('#file='.length));
|
|
1534
|
+
if (fp) handleFileClick(fp);
|
|
1535
|
+
}
|
|
1536
|
+
} catch {}
|
|
1537
|
+
|
|
1538
|
+
window.addEventListener('hashchange', () => {
|
|
1539
|
+
try {
|
|
1540
|
+
const h = String(location.hash || '');
|
|
1541
|
+
if (!h.startsWith('#file=')) return;
|
|
1542
|
+
const fp = decodeURIComponent(h.slice('#file='.length));
|
|
1543
|
+
if (!fp || window.__ACTIVE_FILE === fp) return;
|
|
1544
|
+
handleFileClick(fp);
|
|
1545
|
+
} catch {}
|
|
1546
|
+
});
|
|
571
1547
|
}
|
|
572
1548
|
}
|
|
573
1549
|
|
|
@@ -578,32 +1554,52 @@ function renderHeader(result) {
|
|
|
578
1554
|
return `
|
|
579
1555
|
<header>
|
|
580
1556
|
<div class="logo">SapperAI Scan Report</div>
|
|
581
|
-
<div class="meta">Scanned: ${escapeHtml(result.timestamp)} | Scope: ${escapeHtml(result.scope)}</div>
|
|
1557
|
+
<div class="meta">Scanned: ${escapeHtml(result.timestamp)} | Scope: <span class="meta-scope" title="${escapeHtml(result.scope)}">${escapeHtml(result.scope)}</span></div>
|
|
582
1558
|
<button id="theme-toggle" type="button">Dark/Light</button>
|
|
583
1559
|
</header>
|
|
584
1560
|
`.trim();
|
|
585
1561
|
}
|
|
586
1562
|
function renderSummary(result) {
|
|
587
|
-
const total = result.summary
|
|
588
|
-
const
|
|
1563
|
+
const total = result.summary?.totalFiles ?? 0;
|
|
1564
|
+
const eligible = result.summary?.eligibleFiles ?? 0;
|
|
1565
|
+
const scanned = result.summary?.scannedFiles ?? 0;
|
|
1566
|
+
const threats = result.summary?.threats ?? 0;
|
|
589
1567
|
const maxRisk = result.findings.reduce((m, f) => Math.max(m, f.risk), 0);
|
|
1568
|
+
const coverageEligible = eligible > 0 ? (scanned / eligible) * 100 : 0;
|
|
1569
|
+
const coverageTotal = total > 0 ? (scanned / total) * 100 : 0;
|
|
1570
|
+
const coverageValue = `${coverageEligible.toFixed(1)}% / ${coverageTotal.toFixed(2)}%`;
|
|
1571
|
+
const coverageTitle = eligible > 0 && total > 0
|
|
1572
|
+
? `Coverage: ${coverageEligible.toFixed(1)}% eligible (${scanned.toLocaleString()}/${eligible.toLocaleString()}) · ${coverageTotal.toFixed(2)}% total (${scanned.toLocaleString()}/${total.toLocaleString()})`
|
|
1573
|
+
: 'Coverage: N/A';
|
|
590
1574
|
return `
|
|
591
|
-
<section class="summary">
|
|
592
|
-
<div class="metric-card">
|
|
593
|
-
<span class="label">Total
|
|
594
|
-
<span class="value">${total.toLocaleString()}</span>
|
|
1575
|
+
<section class="summary" tabindex="0" role="region" aria-label="Scan summary metrics">
|
|
1576
|
+
<div class="metric-card" data-metric="total">
|
|
1577
|
+
<span class="label" title="Total files">Total</span>
|
|
1578
|
+
<span class="value" title="${total.toLocaleString()}">${total.toLocaleString()}</span>
|
|
1579
|
+
</div>
|
|
1580
|
+
<div class="metric-card" data-metric="eligible">
|
|
1581
|
+
<span class="label" title="Eligible (Config-like)">Eligible</span>
|
|
1582
|
+
<span class="value" title="${eligible.toLocaleString()}">${eligible.toLocaleString()}</span>
|
|
1583
|
+
</div>
|
|
1584
|
+
<div class="metric-card" data-metric="scanned">
|
|
1585
|
+
<span class="label" title="Scanned files">Scanned</span>
|
|
1586
|
+
<span class="value" title="${scanned.toLocaleString()}">${scanned.toLocaleString()}</span>
|
|
1587
|
+
</div>
|
|
1588
|
+
<div class="metric-card" data-metric="coverage">
|
|
1589
|
+
<span class="label" title="Coverage (eligible / total)">Coverage</span>
|
|
1590
|
+
<span class="value" title="${escapeHtml(coverageTitle)}" aria-label="${escapeHtml(coverageTitle)}">${coverageValue}</span>
|
|
595
1591
|
</div>
|
|
596
|
-
<div class="metric-card">
|
|
597
|
-
<span class="label">Threats</span>
|
|
598
|
-
<span class="value danger">${threats.toLocaleString()}</span>
|
|
1592
|
+
<div class="metric-card" data-metric="threats">
|
|
1593
|
+
<span class="label" title="Threats">Threats</span>
|
|
1594
|
+
<span class="value danger" title="${threats.toLocaleString()}">${threats.toLocaleString()}</span>
|
|
599
1595
|
</div>
|
|
600
|
-
<div class="metric-card">
|
|
601
|
-
<span class="label">Max
|
|
602
|
-
<span class="value">${maxRisk.toFixed(2)}</span>
|
|
1596
|
+
<div class="metric-card" data-metric="maxRisk">
|
|
1597
|
+
<span class="label" title="Max risk">Max risk</span>
|
|
1598
|
+
<span class="value" title="${maxRisk.toFixed(2)}">${maxRisk.toFixed(2)}</span>
|
|
603
1599
|
</div>
|
|
604
|
-
<div class="metric-card">
|
|
605
|
-
<span class="label">AI
|
|
606
|
-
<span class="value">${result.ai ? '
|
|
1600
|
+
<div class="metric-card" data-metric="ai">
|
|
1601
|
+
<span class="label" title="AI scan">AI scan</span>
|
|
1602
|
+
<span class="value" title="${result.ai ? 'On' : 'Off'}">${result.ai ? 'On' : 'Off'}</span>
|
|
607
1603
|
</div>
|
|
608
1604
|
</section>
|
|
609
1605
|
|
|
@@ -620,11 +1616,17 @@ function renderMainContent(result) {
|
|
|
620
1616
|
<aside class="panel file-tree">
|
|
621
1617
|
<div class="controls">
|
|
622
1618
|
<input type="text" placeholder="Search files..." id="tree-search" />
|
|
623
|
-
<
|
|
1619
|
+
<div class="tree-actions">
|
|
1620
|
+
<label class="toggle inline"><input type="checkbox" id="threats-only" checked /> Threats only (${threatsCount})</label>
|
|
1621
|
+
<label class="toggle inline"><input type="checkbox" id="auto-jump" checked /> Auto-jump to details</label>
|
|
1622
|
+
<button class="mini-btn" type="button" id="expand-all">Expand all</button>
|
|
1623
|
+
<button class="mini-btn" type="button" id="collapse-all">Collapse all</button>
|
|
1624
|
+
</div>
|
|
1625
|
+
<div class="tree-root" id="tree-root" hidden></div>
|
|
624
1626
|
</div>
|
|
625
1627
|
<div id="tree" role="tree"></div>
|
|
626
1628
|
</aside>
|
|
627
|
-
<main class="panel detail-panel" id="detail"></main>
|
|
1629
|
+
<main class="panel detail-panel" id="detail" tabindex="-1"></main>
|
|
628
1630
|
</section>
|
|
629
1631
|
`.trim();
|
|
630
1632
|
}
|