@panguard-ai/panguard-guard 1.6.0 → 1.7.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/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +128 -29
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -14
- package/dist/config.js.map +1 -1
- package/dist/daemon/index.js +20 -18
- package/dist/daemon/index.js.map +1 -1
- package/dist/dashboard/dashboard.html +2428 -205
- package/dist/dashboard/index.d.ts +104 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +710 -58
- package/dist/dashboard/index.js.map +1 -1
- package/dist/engines/atr-engine.d.ts +24 -2
- package/dist/engines/atr-engine.d.ts.map +1 -1
- package/dist/engines/atr-engine.js +80 -20
- package/dist/engines/atr-engine.js.map +1 -1
- package/dist/event-processor.js +6 -4
- package/dist/event-processor.js.map +1 -1
- package/dist/guard-engine.d.ts +41 -1
- package/dist/guard-engine.d.ts.map +1 -1
- package/dist/guard-engine.js +124 -19
- package/dist/guard-engine.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/investigation/index.js +3 -3
- package/dist/investigation/index.js.map +1 -1
- package/dist/llm-detect.d.ts +19 -0
- package/dist/llm-detect.d.ts.map +1 -1
- package/dist/llm-detect.js +72 -5
- package/dist/llm-detect.js.map +1 -1
- package/dist/memory/baseline.d.ts.map +1 -1
- package/dist/memory/baseline.js +4 -7
- package/dist/memory/baseline.js.map +1 -1
- package/dist/net/validate-outbound-url.d.ts +33 -0
- package/dist/net/validate-outbound-url.d.ts.map +1 -0
- package/dist/net/validate-outbound-url.js +95 -0
- package/dist/net/validate-outbound-url.js.map +1 -0
- package/dist/notify/webhook.d.ts.map +1 -1
- package/dist/notify/webhook.js +10 -0
- package/dist/notify/webhook.js.map +1 -1
- package/dist/redact.d.ts +35 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +39 -0
- package/dist/redact.js.map +1 -0
- package/dist/response/file-quarantine.d.ts +2 -0
- package/dist/response/file-quarantine.d.ts.map +1 -1
- package/dist/response/file-quarantine.js +35 -9
- package/dist/response/file-quarantine.js.map +1 -1
- package/dist/rule-loader.d.ts +14 -1
- package/dist/rule-loader.d.ts.map +1 -1
- package/dist/rule-loader.js +62 -4
- package/dist/rule-loader.js.map +1 -1
- package/dist/rule-sync.d.ts +22 -0
- package/dist/rule-sync.d.ts.map +1 -1
- package/dist/rule-sync.js +152 -66
- package/dist/rule-sync.js.map +1 -1
- package/dist/threat-cloud/index.d.ts +11 -14
- package/dist/threat-cloud/index.d.ts.map +1 -1
- package/dist/threat-cloud/index.js +24 -24
- package/dist/threat-cloud/index.js.map +1 -1
- package/dist/threat-cloud/tc-key-provisioner.d.ts.map +1 -1
- package/dist/threat-cloud/tc-key-provisioner.js +19 -1
- package/dist/threat-cloud/tc-key-provisioner.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +12 -12
|
@@ -12,14 +12,15 @@
|
|
|
12
12
|
/>
|
|
13
13
|
<style>
|
|
14
14
|
:root {
|
|
15
|
-
--sage: #8b9a8e;
|
|
15
|
+
--sage: #8b9a8e; /* brand identity / presence */
|
|
16
|
+
--emerald: #34d399; /* action layer: CTAs, positive/active, updates */
|
|
16
17
|
--bg-base: #1a1614;
|
|
17
18
|
--bg-card: #242220;
|
|
18
19
|
--bg-raised: #2e2c2a;
|
|
19
20
|
--border: #3a3836;
|
|
20
21
|
--text-primary: #f5f1e8;
|
|
21
22
|
--text-muted: #a09a94;
|
|
22
|
-
--ok: #
|
|
23
|
+
--ok: #34d399;
|
|
23
24
|
--warn: #fbbf24;
|
|
24
25
|
--danger: #ef4444;
|
|
25
26
|
--orange: #f97316;
|
|
@@ -63,8 +64,8 @@
|
|
|
63
64
|
gap: 10px;
|
|
64
65
|
}
|
|
65
66
|
.sb-brand svg {
|
|
66
|
-
width:
|
|
67
|
-
height:
|
|
67
|
+
width: 32px;
|
|
68
|
+
height: 32px;
|
|
68
69
|
flex-shrink: 0;
|
|
69
70
|
}
|
|
70
71
|
.sb-brand span {
|
|
@@ -74,6 +75,25 @@
|
|
|
74
75
|
color: var(--text-primary);
|
|
75
76
|
letter-spacing: 0.5px;
|
|
76
77
|
}
|
|
78
|
+
.sb-brand-text {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: 4px;
|
|
82
|
+
min-width: 0;
|
|
83
|
+
}
|
|
84
|
+
.sb-brand .brand-tier {
|
|
85
|
+
font-size: 9px;
|
|
86
|
+
font-weight: 700;
|
|
87
|
+
letter-spacing: 0.8px;
|
|
88
|
+
text-transform: uppercase;
|
|
89
|
+
color: var(--sage);
|
|
90
|
+
background: rgba(139, 154, 142, 0.14);
|
|
91
|
+
border: 1px solid rgba(139, 154, 142, 0.3);
|
|
92
|
+
border-radius: 4px;
|
|
93
|
+
padding: 1px 6px;
|
|
94
|
+
align-self: flex-start;
|
|
95
|
+
line-height: 1.6;
|
|
96
|
+
}
|
|
77
97
|
.sb-nav {
|
|
78
98
|
flex: 1;
|
|
79
99
|
padding: 12px 0;
|
|
@@ -282,6 +302,224 @@
|
|
|
282
302
|
border-left-color: var(--ok);
|
|
283
303
|
}
|
|
284
304
|
|
|
305
|
+
/* Readable event cards (replaces the raw JSON dump rows). A solo dev
|
|
306
|
+
scans these, not parses them: severity, plain summary, time. */
|
|
307
|
+
.ev-card {
|
|
308
|
+
display: flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
gap: 12px;
|
|
311
|
+
padding: 11px 16px;
|
|
312
|
+
border-bottom: 1px solid var(--border);
|
|
313
|
+
border-left: 3px solid var(--text-muted);
|
|
314
|
+
}
|
|
315
|
+
.ev-card:last-child {
|
|
316
|
+
border-bottom: none;
|
|
317
|
+
}
|
|
318
|
+
.ev-card .ev-sev {
|
|
319
|
+
width: 8px;
|
|
320
|
+
height: 8px;
|
|
321
|
+
border-radius: 50%;
|
|
322
|
+
background: var(--text-muted);
|
|
323
|
+
flex-shrink: 0;
|
|
324
|
+
}
|
|
325
|
+
.ev-card .ev-body {
|
|
326
|
+
flex: 1;
|
|
327
|
+
min-width: 0;
|
|
328
|
+
}
|
|
329
|
+
.ev-card .ev-title {
|
|
330
|
+
font-size: 13px;
|
|
331
|
+
color: var(--text-primary);
|
|
332
|
+
font-weight: 500;
|
|
333
|
+
white-space: nowrap;
|
|
334
|
+
overflow: hidden;
|
|
335
|
+
text-overflow: ellipsis;
|
|
336
|
+
}
|
|
337
|
+
.ev-card .ev-meta {
|
|
338
|
+
font-size: 11px;
|
|
339
|
+
color: var(--text-muted);
|
|
340
|
+
margin-top: 2px;
|
|
341
|
+
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
|
342
|
+
white-space: nowrap;
|
|
343
|
+
overflow: hidden;
|
|
344
|
+
text-overflow: ellipsis;
|
|
345
|
+
}
|
|
346
|
+
.ev-card .ev-sevtag {
|
|
347
|
+
font-size: 10px;
|
|
348
|
+
font-weight: 700;
|
|
349
|
+
text-transform: uppercase;
|
|
350
|
+
letter-spacing: 0.5px;
|
|
351
|
+
color: var(--text-muted);
|
|
352
|
+
flex-shrink: 0;
|
|
353
|
+
}
|
|
354
|
+
.ev-card .ev-time {
|
|
355
|
+
font-size: 11px;
|
|
356
|
+
color: var(--text-muted);
|
|
357
|
+
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
|
358
|
+
flex-shrink: 0;
|
|
359
|
+
}
|
|
360
|
+
.ev-card.critical,
|
|
361
|
+
.ev-card.malicious {
|
|
362
|
+
border-left-color: var(--danger);
|
|
363
|
+
}
|
|
364
|
+
.ev-card.critical .ev-sev,
|
|
365
|
+
.ev-card.malicious .ev-sev {
|
|
366
|
+
background: var(--danger);
|
|
367
|
+
}
|
|
368
|
+
.ev-card.critical .ev-sevtag,
|
|
369
|
+
.ev-card.malicious .ev-sevtag {
|
|
370
|
+
color: var(--danger);
|
|
371
|
+
}
|
|
372
|
+
.ev-card.high {
|
|
373
|
+
border-left-color: var(--orange);
|
|
374
|
+
}
|
|
375
|
+
.ev-card.high .ev-sev {
|
|
376
|
+
background: var(--orange);
|
|
377
|
+
}
|
|
378
|
+
.ev-card.high .ev-sevtag {
|
|
379
|
+
color: var(--orange);
|
|
380
|
+
}
|
|
381
|
+
.ev-card.medium,
|
|
382
|
+
.ev-card.suspicious {
|
|
383
|
+
border-left-color: var(--warn);
|
|
384
|
+
}
|
|
385
|
+
.ev-card.medium .ev-sev,
|
|
386
|
+
.ev-card.suspicious .ev-sev {
|
|
387
|
+
background: var(--warn);
|
|
388
|
+
}
|
|
389
|
+
.ev-card.medium .ev-sevtag,
|
|
390
|
+
.ev-card.suspicious .ev-sevtag {
|
|
391
|
+
color: var(--warn);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* First-run welcome guide */
|
|
395
|
+
.welcome {
|
|
396
|
+
position: relative;
|
|
397
|
+
background: linear-gradient(135deg, rgba(52, 211, 153, 0.1), rgba(139, 154, 142, 0.04));
|
|
398
|
+
border: 1px solid var(--border);
|
|
399
|
+
border-radius: var(--radius);
|
|
400
|
+
padding: 22px 24px;
|
|
401
|
+
margin-bottom: 20px;
|
|
402
|
+
box-shadow: var(--glow);
|
|
403
|
+
}
|
|
404
|
+
.welcome-x {
|
|
405
|
+
position: absolute;
|
|
406
|
+
top: 12px;
|
|
407
|
+
right: 14px;
|
|
408
|
+
background: transparent;
|
|
409
|
+
border: 0;
|
|
410
|
+
color: var(--text-muted);
|
|
411
|
+
font-size: 22px;
|
|
412
|
+
line-height: 1;
|
|
413
|
+
cursor: pointer;
|
|
414
|
+
padding: 2px 6px;
|
|
415
|
+
}
|
|
416
|
+
.welcome-x:hover {
|
|
417
|
+
color: var(--text-primary);
|
|
418
|
+
}
|
|
419
|
+
.welcome-head {
|
|
420
|
+
display: flex;
|
|
421
|
+
align-items: center;
|
|
422
|
+
gap: 10px;
|
|
423
|
+
margin-bottom: 8px;
|
|
424
|
+
}
|
|
425
|
+
.welcome-title {
|
|
426
|
+
font-family: 'Space Grotesk', 'Noto Sans TC', sans-serif;
|
|
427
|
+
font-size: 18px;
|
|
428
|
+
font-weight: 700;
|
|
429
|
+
color: var(--text-primary);
|
|
430
|
+
}
|
|
431
|
+
.welcome-lead {
|
|
432
|
+
font-size: 13px;
|
|
433
|
+
color: var(--text-primary);
|
|
434
|
+
line-height: 1.65;
|
|
435
|
+
max-width: 72ch;
|
|
436
|
+
margin-bottom: 18px;
|
|
437
|
+
}
|
|
438
|
+
.welcome-grid {
|
|
439
|
+
display: grid;
|
|
440
|
+
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
|
|
441
|
+
gap: 24px;
|
|
442
|
+
}
|
|
443
|
+
@media (max-width: 720px) {
|
|
444
|
+
.welcome-grid {
|
|
445
|
+
grid-template-columns: 1fr;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
.welcome-col-h {
|
|
449
|
+
font-size: 11px;
|
|
450
|
+
font-weight: 700;
|
|
451
|
+
text-transform: uppercase;
|
|
452
|
+
letter-spacing: 0.5px;
|
|
453
|
+
color: var(--text-muted);
|
|
454
|
+
margin-bottom: 10px;
|
|
455
|
+
}
|
|
456
|
+
.welcome-opt {
|
|
457
|
+
font-weight: 500;
|
|
458
|
+
text-transform: none;
|
|
459
|
+
letter-spacing: 0;
|
|
460
|
+
color: var(--sage);
|
|
461
|
+
}
|
|
462
|
+
.welcome-list {
|
|
463
|
+
list-style: none;
|
|
464
|
+
padding: 0;
|
|
465
|
+
margin: 0;
|
|
466
|
+
font-size: 13px;
|
|
467
|
+
line-height: 1.9;
|
|
468
|
+
color: var(--text-muted);
|
|
469
|
+
}
|
|
470
|
+
.welcome-list strong {
|
|
471
|
+
color: var(--text-primary);
|
|
472
|
+
font-weight: 600;
|
|
473
|
+
}
|
|
474
|
+
.welcome-step {
|
|
475
|
+
background: var(--bg-raised);
|
|
476
|
+
border: 1px solid var(--border);
|
|
477
|
+
border-radius: var(--radius);
|
|
478
|
+
padding: 12px 14px;
|
|
479
|
+
margin-bottom: 10px;
|
|
480
|
+
}
|
|
481
|
+
.welcome-step:last-child {
|
|
482
|
+
margin-bottom: 0;
|
|
483
|
+
}
|
|
484
|
+
.welcome-step-h {
|
|
485
|
+
font-size: 13px;
|
|
486
|
+
font-weight: 700;
|
|
487
|
+
color: var(--text-primary);
|
|
488
|
+
margin-bottom: 4px;
|
|
489
|
+
}
|
|
490
|
+
.welcome-step-b {
|
|
491
|
+
font-size: 12px;
|
|
492
|
+
color: var(--text-muted);
|
|
493
|
+
line-height: 1.6;
|
|
494
|
+
margin-bottom: 8px;
|
|
495
|
+
}
|
|
496
|
+
.welcome-step-b strong {
|
|
497
|
+
color: var(--text-primary);
|
|
498
|
+
font-weight: 600;
|
|
499
|
+
}
|
|
500
|
+
.welcome-foot {
|
|
501
|
+
display: flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
gap: 14px;
|
|
504
|
+
margin-top: 18px;
|
|
505
|
+
flex-wrap: wrap;
|
|
506
|
+
}
|
|
507
|
+
.welcome-later {
|
|
508
|
+
background: transparent;
|
|
509
|
+
border: 0;
|
|
510
|
+
color: var(--text-muted);
|
|
511
|
+
font-size: 12px;
|
|
512
|
+
cursor: pointer;
|
|
513
|
+
text-decoration: underline;
|
|
514
|
+
}
|
|
515
|
+
.welcome-later:hover {
|
|
516
|
+
color: var(--text-primary);
|
|
517
|
+
}
|
|
518
|
+
.welcome-foot-note {
|
|
519
|
+
font-size: 11px;
|
|
520
|
+
color: var(--text-muted);
|
|
521
|
+
}
|
|
522
|
+
|
|
285
523
|
/* Tables */
|
|
286
524
|
.tb {
|
|
287
525
|
width: 100%;
|
|
@@ -389,7 +627,7 @@
|
|
|
389
627
|
font-family: inherit;
|
|
390
628
|
}
|
|
391
629
|
.btn-primary {
|
|
392
|
-
background: var(--
|
|
630
|
+
background: var(--emerald);
|
|
393
631
|
color: var(--bg-base);
|
|
394
632
|
}
|
|
395
633
|
.btn-primary:hover {
|
|
@@ -573,7 +811,7 @@
|
|
|
573
811
|
flex-shrink: 0;
|
|
574
812
|
}
|
|
575
813
|
.toggle.on {
|
|
576
|
-
background: var(--
|
|
814
|
+
background: var(--emerald);
|
|
577
815
|
}
|
|
578
816
|
.toggle::after {
|
|
579
817
|
content: '';
|
|
@@ -606,6 +844,145 @@
|
|
|
606
844
|
font-weight: 600;
|
|
607
845
|
}
|
|
608
846
|
|
|
847
|
+
/* Collective-defense value/privacy explainer */
|
|
848
|
+
.cd-grid {
|
|
849
|
+
display: grid;
|
|
850
|
+
grid-template-columns: 1fr 1fr;
|
|
851
|
+
gap: 12px;
|
|
852
|
+
margin: 14px 0;
|
|
853
|
+
}
|
|
854
|
+
@media (max-width: 720px) {
|
|
855
|
+
.cd-grid {
|
|
856
|
+
grid-template-columns: 1fr;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
.cd-box {
|
|
860
|
+
background: var(--bg-raised);
|
|
861
|
+
border: 1px solid var(--border);
|
|
862
|
+
border-radius: var(--radius);
|
|
863
|
+
padding: 14px 16px;
|
|
864
|
+
}
|
|
865
|
+
.cd-box-title {
|
|
866
|
+
font-size: 12px;
|
|
867
|
+
font-weight: 700;
|
|
868
|
+
text-transform: uppercase;
|
|
869
|
+
letter-spacing: 0.04em;
|
|
870
|
+
color: var(--text-muted);
|
|
871
|
+
margin-bottom: 8px;
|
|
872
|
+
}
|
|
873
|
+
.cd-box-title.cd-good {
|
|
874
|
+
color: var(--ok);
|
|
875
|
+
}
|
|
876
|
+
.cd-list {
|
|
877
|
+
margin: 0;
|
|
878
|
+
padding-left: 18px;
|
|
879
|
+
font-size: 13px;
|
|
880
|
+
line-height: 1.65;
|
|
881
|
+
color: var(--text-muted);
|
|
882
|
+
}
|
|
883
|
+
.cd-list li {
|
|
884
|
+
margin-bottom: 4px;
|
|
885
|
+
}
|
|
886
|
+
.cd-control {
|
|
887
|
+
display: flex;
|
|
888
|
+
align-items: center;
|
|
889
|
+
gap: 14px;
|
|
890
|
+
padding: 12px 0 4px;
|
|
891
|
+
}
|
|
892
|
+
.cd-cli {
|
|
893
|
+
margin-top: 12px;
|
|
894
|
+
font-size: 12px;
|
|
895
|
+
color: var(--text-muted);
|
|
896
|
+
}
|
|
897
|
+
/* Copy-paste command line */
|
|
898
|
+
.cmd-box {
|
|
899
|
+
display: flex;
|
|
900
|
+
align-items: center;
|
|
901
|
+
gap: 8px;
|
|
902
|
+
margin-top: 6px;
|
|
903
|
+
background: var(--bg-raised);
|
|
904
|
+
border: 1px solid var(--border);
|
|
905
|
+
border-radius: var(--radius);
|
|
906
|
+
padding: 8px 10px;
|
|
907
|
+
}
|
|
908
|
+
.cmd-box code {
|
|
909
|
+
flex: 1;
|
|
910
|
+
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
|
911
|
+
font-size: 12px;
|
|
912
|
+
color: var(--text-primary);
|
|
913
|
+
white-space: pre-wrap;
|
|
914
|
+
word-break: break-all;
|
|
915
|
+
}
|
|
916
|
+
.cmd-copy {
|
|
917
|
+
flex-shrink: 0;
|
|
918
|
+
background: transparent;
|
|
919
|
+
border: 1px solid var(--border);
|
|
920
|
+
color: var(--sage);
|
|
921
|
+
border-radius: 6px;
|
|
922
|
+
padding: 4px 10px;
|
|
923
|
+
font-size: 11px;
|
|
924
|
+
font-weight: 600;
|
|
925
|
+
cursor: pointer;
|
|
926
|
+
transition:
|
|
927
|
+
border-color 0.15s,
|
|
928
|
+
color 0.15s;
|
|
929
|
+
}
|
|
930
|
+
.cmd-copy:hover {
|
|
931
|
+
border-color: var(--sage);
|
|
932
|
+
}
|
|
933
|
+
.cmd-copy.copied {
|
|
934
|
+
color: var(--ok);
|
|
935
|
+
border-color: var(--ok);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/* Optional alert-channel disclosure */
|
|
939
|
+
.notify-summary {
|
|
940
|
+
cursor: pointer;
|
|
941
|
+
font-size: 13px;
|
|
942
|
+
font-weight: 600;
|
|
943
|
+
color: var(--sage);
|
|
944
|
+
list-style: none;
|
|
945
|
+
user-select: none;
|
|
946
|
+
padding: 4px 0;
|
|
947
|
+
}
|
|
948
|
+
.notify-summary::-webkit-details-marker {
|
|
949
|
+
display: none;
|
|
950
|
+
}
|
|
951
|
+
.notify-summary::before {
|
|
952
|
+
content: '▸ ';
|
|
953
|
+
color: var(--text-muted);
|
|
954
|
+
}
|
|
955
|
+
details[open] .notify-summary::before {
|
|
956
|
+
content: '▾ ';
|
|
957
|
+
}
|
|
958
|
+
.nc {
|
|
959
|
+
background: var(--bg-raised);
|
|
960
|
+
border: 1px solid var(--border);
|
|
961
|
+
border-radius: var(--radius);
|
|
962
|
+
padding: 12px 14px;
|
|
963
|
+
}
|
|
964
|
+
.nc-head {
|
|
965
|
+
display: flex;
|
|
966
|
+
align-items: baseline;
|
|
967
|
+
gap: 8px;
|
|
968
|
+
margin-bottom: 4px;
|
|
969
|
+
}
|
|
970
|
+
.nc-name {
|
|
971
|
+
font-size: 13px;
|
|
972
|
+
font-weight: 700;
|
|
973
|
+
color: var(--text-primary);
|
|
974
|
+
}
|
|
975
|
+
.nc-state {
|
|
976
|
+
font-size: 11px;
|
|
977
|
+
font-weight: 600;
|
|
978
|
+
}
|
|
979
|
+
.nc-what {
|
|
980
|
+
font-size: 12px;
|
|
981
|
+
color: var(--text-muted);
|
|
982
|
+
line-height: 1.55;
|
|
983
|
+
margin-bottom: 8px;
|
|
984
|
+
}
|
|
985
|
+
|
|
609
986
|
/* Spinner */
|
|
610
987
|
.spin {
|
|
611
988
|
display: inline-block;
|
|
@@ -791,29 +1168,138 @@
|
|
|
791
1168
|
color: var(--ok);
|
|
792
1169
|
}
|
|
793
1170
|
|
|
794
|
-
/*
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
gap: 6px;
|
|
799
|
-
font-size: 12px;
|
|
800
|
-
color: var(--text-muted);
|
|
801
|
-
text-decoration: none;
|
|
802
|
-
padding: 6px 10px;
|
|
803
|
-
border-radius: 8px;
|
|
804
|
-
border: 1px dashed var(--border);
|
|
805
|
-
transition: all 0.15s;
|
|
1171
|
+
/* Neutral default + connecting state for the persistent status dot, so it
|
|
1172
|
+
never reads green before a real armed status loads. */
|
|
1173
|
+
.pbar .pb-dot {
|
|
1174
|
+
background: var(--text-muted);
|
|
806
1175
|
}
|
|
807
|
-
.
|
|
808
|
-
|
|
809
|
-
|
|
1176
|
+
.pbar .pb-dot.warn {
|
|
1177
|
+
background: var(--warn);
|
|
1178
|
+
box-shadow: 0 0 6px rgba(251, 191, 36, 0.4);
|
|
810
1179
|
}
|
|
811
1180
|
|
|
812
|
-
/*
|
|
813
|
-
|
|
1181
|
+
/* Connection guidance banner: shown only when Guard is unreachable. The
|
|
1182
|
+
single honest answer to "why is everything blank?" with the one fix. */
|
|
1183
|
+
.conn-banner {
|
|
1184
|
+
display: flex;
|
|
1185
|
+
align-items: center;
|
|
1186
|
+
gap: 12px;
|
|
1187
|
+
flex-wrap: wrap;
|
|
1188
|
+
padding: 12px 16px;
|
|
1189
|
+
margin-bottom: 16px;
|
|
1190
|
+
border: 1px solid var(--danger);
|
|
1191
|
+
border-radius: 10px;
|
|
1192
|
+
background: rgba(239, 68, 68, 0.08);
|
|
1193
|
+
font-size: 13px;
|
|
1194
|
+
line-height: 1.5;
|
|
1195
|
+
color: var(--text-primary);
|
|
1196
|
+
}
|
|
1197
|
+
.conn-banner .cb-text {
|
|
1198
|
+
flex: 1;
|
|
1199
|
+
min-width: 200px;
|
|
1200
|
+
}
|
|
1201
|
+
.conn-banner .conn-cmd {
|
|
814
1202
|
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
|
815
|
-
font-size:
|
|
816
|
-
|
|
1203
|
+
font-size: 13px;
|
|
1204
|
+
background: var(--bg-base);
|
|
1205
|
+
border: 1px solid var(--border);
|
|
1206
|
+
border-radius: 6px;
|
|
1207
|
+
padding: 4px 10px;
|
|
1208
|
+
color: var(--emerald);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/* One fused posture hero: state (left) + score with one-click fixes
|
|
1212
|
+
(right), collapsing to one column on narrow screens. */
|
|
1213
|
+
.hero-fused {
|
|
1214
|
+
display: grid;
|
|
1215
|
+
grid-template-columns: 1fr auto;
|
|
1216
|
+
grid-template-areas: 'main score' 'fixes fixes';
|
|
1217
|
+
gap: 18px 32px;
|
|
1218
|
+
align-items: start;
|
|
1219
|
+
}
|
|
1220
|
+
.hp-main {
|
|
1221
|
+
grid-area: main;
|
|
1222
|
+
min-width: 0;
|
|
1223
|
+
}
|
|
1224
|
+
.hp-score {
|
|
1225
|
+
grid-area: score;
|
|
1226
|
+
text-align: center;
|
|
1227
|
+
min-width: 140px;
|
|
1228
|
+
}
|
|
1229
|
+
.hp-fixes {
|
|
1230
|
+
grid-area: fixes;
|
|
1231
|
+
display: flex;
|
|
1232
|
+
flex-direction: column;
|
|
1233
|
+
gap: 8px;
|
|
1234
|
+
}
|
|
1235
|
+
.hp-fixes:empty {
|
|
1236
|
+
display: none;
|
|
1237
|
+
}
|
|
1238
|
+
.hp-state {
|
|
1239
|
+
display: flex;
|
|
1240
|
+
align-items: center;
|
|
1241
|
+
gap: 12px;
|
|
1242
|
+
flex-wrap: wrap;
|
|
1243
|
+
}
|
|
1244
|
+
.hp-sub {
|
|
1245
|
+
font-size: 14px;
|
|
1246
|
+
color: var(--text-primary);
|
|
1247
|
+
line-height: 1.6;
|
|
1248
|
+
margin-top: 10px;
|
|
1249
|
+
max-width: 56ch;
|
|
1250
|
+
}
|
|
1251
|
+
.hp-tagline {
|
|
1252
|
+
font-size: 12px;
|
|
1253
|
+
color: var(--text-muted);
|
|
1254
|
+
margin-top: 14px;
|
|
1255
|
+
letter-spacing: 0.2px;
|
|
1256
|
+
}
|
|
1257
|
+
.score-num {
|
|
1258
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
1259
|
+
font-size: 56px;
|
|
1260
|
+
font-weight: 700;
|
|
1261
|
+
line-height: 1;
|
|
1262
|
+
color: var(--emerald);
|
|
1263
|
+
}
|
|
1264
|
+
.score-cap {
|
|
1265
|
+
font-size: 11px;
|
|
1266
|
+
letter-spacing: 1px;
|
|
1267
|
+
color: var(--text-muted);
|
|
1268
|
+
text-transform: uppercase;
|
|
1269
|
+
margin-top: 4px;
|
|
1270
|
+
}
|
|
1271
|
+
.score-track {
|
|
1272
|
+
height: 8px;
|
|
1273
|
+
border-radius: 6px;
|
|
1274
|
+
background: var(--bg-raised);
|
|
1275
|
+
overflow: hidden;
|
|
1276
|
+
margin: 12px 0 6px;
|
|
1277
|
+
}
|
|
1278
|
+
.score-fill {
|
|
1279
|
+
height: 100%;
|
|
1280
|
+
width: 0;
|
|
1281
|
+
background: var(--emerald);
|
|
1282
|
+
transition: width 0.4s;
|
|
1283
|
+
}
|
|
1284
|
+
.score-label {
|
|
1285
|
+
font-size: 12px;
|
|
1286
|
+
color: var(--text-muted);
|
|
1287
|
+
}
|
|
1288
|
+
@media (max-width: 720px) {
|
|
1289
|
+
.hero-fused {
|
|
1290
|
+
grid-template-columns: 1fr;
|
|
1291
|
+
grid-template-areas: 'score' 'main' 'fixes';
|
|
1292
|
+
}
|
|
1293
|
+
.hp-score {
|
|
1294
|
+
text-align: left;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/* Threat Cloud flywheel */
|
|
1299
|
+
.flywheel {
|
|
1300
|
+
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
|
1301
|
+
font-size: 12px;
|
|
1302
|
+
color: var(--text-muted);
|
|
817
1303
|
background: var(--bg-raised);
|
|
818
1304
|
border: 1px solid var(--border);
|
|
819
1305
|
border-radius: var(--radius);
|
|
@@ -828,13 +1314,36 @@
|
|
|
828
1314
|
<!-- Sidebar (220px fixed) -->
|
|
829
1315
|
<div class="sb">
|
|
830
1316
|
<div class="sb-brand">
|
|
831
|
-
|
|
1317
|
+
<!-- Official PanGuard brand mark — the full 5-path 3D cube-shield (three
|
|
1318
|
+
interlocking shield faces + top bracket), matching packages/website
|
|
1319
|
+
BrandLogo.tsx. The dashboard previously shipped only one of the five
|
|
1320
|
+
paths, so it rendered as a flat blob. fg = sage, bg = sidebar cutout. -->
|
|
1321
|
+
<svg viewBox="385 323 1278 1403" fill="none" aria-label="PanGuard AI">
|
|
832
1322
|
<path
|
|
833
|
-
fill="
|
|
1323
|
+
fill="var(--sage)"
|
|
834
1324
|
d="M 1021.5 830.423 C 1026.54 829.911 1045.81 832.659 1051.64 833.401 C 1072.38 835.99 1093.1 838.746 1113.8 841.667 L 1329.99 871.428 L 1329.95 1306.51 L 1330 1422.21 C 1330.01 1450.54 1332.08 1491.81 1325.18 1518.04 C 1318.79 1541.46 1305.62 1562.47 1287.31 1578.42 C 1269.98 1593.59 1234.62 1610.78 1212.96 1622.97 C 1151.4 1657.6 1087.44 1689.5 1026.42 1725.08 L 1024.58 1725.72 C 1020.72 1724.57 1005.67 1715.74 1001.44 1713.38 C 986.676 1705.08 971.857 1696.88 956.982 1688.79 L 836.342 1623.07 C 818.678 1613.37 800.537 1603.48 783.285 1593.93 C 741.183 1570.62 718.225 1528.6 718.162 1480.8 C 718.14 1463.85 718.093 1446.28 718.119 1429.07 L 718.13 1313.33 L 718.15 871.408 C 819.171 858.462 920.068 841.835 1021.5 830.423 z"
|
|
835
1325
|
/>
|
|
1326
|
+
<path
|
|
1327
|
+
fill="var(--bg-card)"
|
|
1328
|
+
d="M 1022.52 931.436 C 1027.43 930.745 1072.31 937.152 1079.51 938.107 C 1129.15 944.531 1178.76 951.264 1228.32 958.306 L 1228.23 1353.88 L 1228.35 1443.73 C 1228.37 1464.29 1233.5 1493.24 1214.51 1505.36 C 1195.74 1517.34 1175.98 1527.71 1156.43 1538.41 C 1112.57 1561.92 1068.87 1585.75 1025.35 1609.89 L 1024.59 1610.05 C 1019.31 1608.51 1009.91 1602.52 1004.79 1599.72 C 950.803 1570.14 896.481 1541.36 842.686 1511.4 C 829.113 1503.85 820.756 1496.4 820.195 1479.81 C 819.565 1461.16 819.816 1442.57 819.824 1423.9 L 819.867 1314.91 L 819.664 958.376 C 837.727 956.927 860.873 952.653 879.255 950.388 C 926.512 944.567 975.292 936.455 1022.52 931.436 z"
|
|
1329
|
+
/>
|
|
1330
|
+
<path
|
|
1331
|
+
fill="var(--sage)"
|
|
1332
|
+
d="M 687.515 577.4 C 696.504 576.451 715.718 579.584 725.68 580.913 L 782.266 588.553 C 853.62 598.25 926.853 607.634 997.939 618.387 L 997.872 779.654 C 966.188 784.487 928.734 789.954 897.039 793.352 L 897.073 705.399 C 831.524 696.853 760.432 685.286 694.176 678.356 C 683.151 677.203 646.195 683.753 632.643 685.485 L 487.112 705.406 C 488.77 841.757 487.098 980.17 487.357 1116.69 L 487.339 1188.98 C 487.213 1243.94 479.328 1246.69 530.666 1273.69 C 574.625 1298.03 623.372 1322.43 666.093 1347.35 C 665.454 1385.26 666.06 1424.82 666.087 1462.87 C 649.609 1452.6 628.321 1441.62 610.88 1432.12 L 500.871 1372.76 C 476.603 1359.56 440.159 1342.09 421.535 1323.57 C 404.11 1306.02 392.433 1283.59 388.055 1259.25 C 384.254 1237.81 385.323 1203.28 385.354 1180.66 L 385.478 1064.48 L 385.312 618.209 C 414.868 615.376 445.833 609.518 475.437 605.863 C 545.54 597.208 617.46 584.737 687.515 577.4 z"
|
|
1333
|
+
/>
|
|
1334
|
+
<path
|
|
1335
|
+
fill="var(--sage)"
|
|
1336
|
+
d="M 1348.89 577.369 C 1360.73 575.936 1415.83 584.528 1431.77 586.589 C 1508.75 596.536 1585.62 608.685 1662.67 618.335 L 1662.73 1028.87 L 1662.69 1154.09 C 1662.67 1223.3 1674.33 1293.75 1609.64 1337.92 C 1596.01 1347.22 1581.75 1354.57 1567.33 1362.44 L 1512.89 1391.87 L 1382 1462.6 C 1383.76 1427.96 1382.35 1382.83 1382.21 1347.24 C 1420 1324.81 1467.93 1301.58 1507.58 1279.5 C 1524.53 1270.36 1558.74 1256.3 1560.1 1235.39 C 1561.42 1215.18 1560.95 1193.69 1560.9 1173.32 L 1560.87 1066.29 L 1560.68 705.345 C 1543.39 703.65 1521.51 700.068 1504.1 697.576 C 1473.23 693.054 1442.33 688.801 1411.39 684.819 C 1399.03 683.204 1369.09 678.563 1357.99 678.482 C 1343.65 678.377 1310.02 683.839 1294.14 685.904 C 1246.36 692.092 1198.63 698.627 1150.95 705.51 C 1151.37 734.567 1151.1 764.255 1151.14 793.361 C 1118.48 789.558 1083.43 784.206 1050.79 779.597 L 1050.69 617.847 L 1348.89 577.369 z"
|
|
1337
|
+
/>
|
|
1338
|
+
<path
|
|
1339
|
+
fill="var(--sage)"
|
|
1340
|
+
d="M 1017.32 323.354 C 1027.15 321.591 1105.66 333.344 1122.51 335.603 C 1166.74 341.267 1210.92 347.328 1255.05 353.786 C 1279.68 357.183 1305.51 360.23 1329.92 364.192 L 1329.98 526.684 C 1296.55 530.372 1261.73 535.721 1228.25 540.29 C 1228.4 510.591 1228.39 480.892 1228.23 451.193 C 1219.27 449.729 1209.61 448.427 1200.57 447.329 C 1144.13 440.467 1086.78 429.864 1030.24 424.572 C 1018.71 422.967 967.642 430.978 952.027 433.125 L 819.579 450.968 C 820.252 479.945 819.726 511.22 819.879 540.382 C 785.991 535.397 752.051 530.778 718.064 526.526 L 717.99 364.358 C 745.543 359.872 774.642 356.332 802.417 352.399 L 1017.32 323.354 z"
|
|
1341
|
+
/>
|
|
836
1342
|
</svg>
|
|
837
|
-
<
|
|
1343
|
+
<div class="sb-brand-text">
|
|
1344
|
+
<span class="sb-wordmark">PanGuard</span>
|
|
1345
|
+
<span class="brand-tier" id="brand-tier">Community</span>
|
|
1346
|
+
</div>
|
|
838
1347
|
</div>
|
|
839
1348
|
<div class="sb-nav">
|
|
840
1349
|
<div class="ni on" data-tab="overview">
|
|
@@ -861,6 +1370,20 @@
|
|
|
861
1370
|
/></svg
|
|
862
1371
|
><span data-i18n="n_sk">Skills</span>
|
|
863
1372
|
</div>
|
|
1373
|
+
<div class="ni" data-tab="coverage">
|
|
1374
|
+
<svg viewBox="0 0 24 24">
|
|
1375
|
+
<path
|
|
1376
|
+
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"
|
|
1377
|
+
/></svg
|
|
1378
|
+
><span>Coverage</span>
|
|
1379
|
+
</div>
|
|
1380
|
+
<div class="ni" data-tab="runtime">
|
|
1381
|
+
<svg viewBox="0 0 24 24">
|
|
1382
|
+
<path
|
|
1383
|
+
d="M13 3l-2 0 0 10 2 0 0-10zm4.83 2.17l-1.42 1.42A6.92 6.92 0 0119 12a7 7 0 11-11.41-5.42L6.17 5.17A9 9 0 1021 12a8.94 8.94 0 00-3.17-6.83z"
|
|
1384
|
+
/></svg
|
|
1385
|
+
><span>Runtime</span>
|
|
1386
|
+
</div>
|
|
864
1387
|
<div class="ni" data-tab="tcloud">
|
|
865
1388
|
<svg viewBox="0 0 24 24">
|
|
866
1389
|
<path
|
|
@@ -894,6 +1417,15 @@
|
|
|
894
1417
|
>COMMUNITY</span
|
|
895
1418
|
>
|
|
896
1419
|
</div>
|
|
1420
|
+
<div class="ws-b" style="margin-top: 8px">
|
|
1421
|
+
<span
|
|
1422
|
+
id="welcome-reopen"
|
|
1423
|
+
role="button"
|
|
1424
|
+
tabindex="0"
|
|
1425
|
+
style="font-size: 11px; color: var(--sage); cursor: pointer"
|
|
1426
|
+
>Show welcome guide</span
|
|
1427
|
+
>
|
|
1428
|
+
</div>
|
|
897
1429
|
</div>
|
|
898
1430
|
</div>
|
|
899
1431
|
|
|
@@ -901,44 +1433,156 @@
|
|
|
901
1433
|
<div class="mn">
|
|
902
1434
|
<!-- Protection status bar (always visible) -->
|
|
903
1435
|
<div class="pbar" id="pbar">
|
|
904
|
-
<div class="pb-dot
|
|
905
|
-
<span class="pb-txt" id="pb-txt">
|
|
906
|
-
<span id="pb-detail"
|
|
1436
|
+
<div class="pb-dot" id="pb-dot"></div>
|
|
1437
|
+
<span class="pb-txt" id="pb-txt">Connecting…</span>
|
|
1438
|
+
<span id="pb-detail">checking protection status…</span>
|
|
1439
|
+
</div>
|
|
1440
|
+
|
|
1441
|
+
<!-- Connection guidance: hidden unless Guard is unreachable. Replaces the
|
|
1442
|
+
old dead end where everything sat on a stale green "CONNECTING…". -->
|
|
1443
|
+
<div class="conn-banner" id="conn-banner" style="display: none">
|
|
1444
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--danger)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1445
|
+
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
1446
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
1447
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
1448
|
+
</svg>
|
|
1449
|
+
<div class="cb-text">
|
|
1450
|
+
<strong>Guard isn't running on this machine.</strong>
|
|
1451
|
+
Your agents aren't being watched right now. Start protection:
|
|
1452
|
+
</div>
|
|
1453
|
+
<code class="conn-cmd">pga up</code>
|
|
1454
|
+
<button type="button" class="btn btn-secondary btn-sm" id="conn-copy">Copy</button>
|
|
907
1455
|
</div>
|
|
908
1456
|
|
|
909
1457
|
<!-- ===== PAGE 1: Overview ===== -->
|
|
910
1458
|
<div class="pg on" id="p-overview">
|
|
911
1459
|
<div class="pt"><em data-i18n="t_ov">Overview</em></div>
|
|
912
1460
|
|
|
913
|
-
<!--
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1461
|
+
<!-- First-run welcome guide: positive first-contact orientation. Hidden by
|
|
1462
|
+
default; shown ONCE on the first LIVE authenticated status (gated by a
|
|
1463
|
+
localStorage flag + the single-source-of-truth live signal, so it can
|
|
1464
|
+
never paint on top of the "Guard not running" banner or the 401 overlay).
|
|
1465
|
+
Dismissible, never nags; reopenable from the sidebar. -->
|
|
1466
|
+
<div
|
|
1467
|
+
class="welcome"
|
|
1468
|
+
id="welcome-guide"
|
|
1469
|
+
style="display: none"
|
|
1470
|
+
role="region"
|
|
1471
|
+
aria-label="Welcome to PanGuard Guard"
|
|
1472
|
+
>
|
|
1473
|
+
<button type="button" class="welcome-x" id="welcome-x" aria-label="Close welcome guide">
|
|
1474
|
+
×
|
|
1475
|
+
</button>
|
|
1476
|
+
<div class="welcome-head">
|
|
1477
|
+
<span class="dot dot-ok"></span>
|
|
1478
|
+
<span class="welcome-title">You're protected. Here's the lay of the land.</span>
|
|
919
1479
|
</div>
|
|
920
|
-
<div
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1480
|
+
<div class="welcome-lead">
|
|
1481
|
+
PanGuard Guard is watching the AI agents on this machine. Two detection layers run
|
|
1482
|
+
automatically and need no setup: <strong>Layer A</strong> (proven ATR pattern rules) and
|
|
1483
|
+
<strong>Layer B</strong> (behavioural heuristics). They run fully on-device — nothing
|
|
1484
|
+
about your activity leaves this machine. Everything below is here only when you want it.
|
|
1485
|
+
</div>
|
|
1486
|
+
|
|
1487
|
+
<div class="welcome-grid">
|
|
1488
|
+
<div class="welcome-col">
|
|
1489
|
+
<div class="welcome-col-h">Where things are</div>
|
|
1490
|
+
<ul class="welcome-list">
|
|
1491
|
+
<li><strong>Overview</strong> — am I safe right now? (this page)</li>
|
|
1492
|
+
<li><strong>Threats</strong> — anything Guard has caught</li>
|
|
1493
|
+
<li><strong>Coverage</strong> — which agent apps are protected</li>
|
|
1494
|
+
<li><strong>Rules · Skills · Runtime</strong> — reference, not chores</li>
|
|
1495
|
+
<li><strong>Settings</strong> — mode, the optional model, alerts</li>
|
|
1496
|
+
</ul>
|
|
1497
|
+
</div>
|
|
1498
|
+
<div class="welcome-col">
|
|
1499
|
+
<div class="welcome-col-h">
|
|
1500
|
+
Optional next steps <span class="welcome-opt">— both off by default</span>
|
|
1501
|
+
</div>
|
|
1502
|
+
|
|
1503
|
+
<div class="welcome-step">
|
|
1504
|
+
<div class="welcome-step-h">Add a second opinion (Layer C)</div>
|
|
1505
|
+
<div class="welcome-step-b">
|
|
1506
|
+
Optionally connect your own LLM for an advisory read on anything Layers A and B
|
|
1507
|
+
flag. It only ever <strong>flags for review — it can never block on its own</strong>.
|
|
1508
|
+
Runs on already-flagged events only, so a cloud key is a few cents a month; a local
|
|
1509
|
+
model (Ollama) is free. <strong>Your key is entered in the terminal, never here</strong>,
|
|
1510
|
+
and is stored encrypted on this machine.
|
|
1511
|
+
</div>
|
|
1512
|
+
<div class="cmd-box">
|
|
1513
|
+
<code id="welcome-ai-cmd">pga guard ai</code>
|
|
1514
|
+
<button class="cmd-copy" data-copy-target="welcome-ai-cmd" type="button">
|
|
1515
|
+
Copy
|
|
1516
|
+
</button>
|
|
1517
|
+
</div>
|
|
1518
|
+
</div>
|
|
1519
|
+
|
|
1520
|
+
<div class="welcome-step">
|
|
1521
|
+
<div class="welcome-step-h">Join collective defense</div>
|
|
1522
|
+
<div class="welcome-step-b">
|
|
1523
|
+
Optionally let the attacks Guard blocks here help draft new open rules for everyone —
|
|
1524
|
+
and get the community's new rules back sooner. Shares only a rule ID and a one-way
|
|
1525
|
+
hash; <strong>never your prompts, code, files, keys, IP, or hostname</strong>. Off
|
|
1526
|
+
until you turn it on.
|
|
1527
|
+
</div>
|
|
1528
|
+
<button type="button" class="btn btn-secondary btn-sm" data-goto="tcloud">
|
|
1529
|
+
Open Threat Cloud →
|
|
1530
|
+
</button>
|
|
1531
|
+
</div>
|
|
1532
|
+
</div>
|
|
927
1533
|
</div>
|
|
1534
|
+
|
|
1535
|
+
<div class="welcome-foot">
|
|
1536
|
+
<button type="button" class="btn btn-primary btn-sm" id="welcome-done">Got it</button>
|
|
1537
|
+
<button type="button" class="welcome-later" id="welcome-later">Dismiss for now</button>
|
|
1538
|
+
<span class="welcome-foot-note">
|
|
1539
|
+
<code>pga</code> commands run in your terminal. Reopen this anytime from the sidebar.
|
|
1540
|
+
</span>
|
|
1541
|
+
</div>
|
|
1542
|
+
</div>
|
|
1543
|
+
|
|
1544
|
+
<!-- One fused posture hero: the single answer to "am I safe right now?".
|
|
1545
|
+
Left = honest state + a plain-English line; right = the posture score
|
|
1546
|
+
with one-click fixes. Starts NEUTRAL (never a static green "Active");
|
|
1547
|
+
setConnState()/updateStatus() drive every element from one source of
|
|
1548
|
+
truth, and a 401 swaps in the unauthenticated screen instead. All IDs
|
|
1549
|
+
the JS drives are preserved (DESIGN.md §2.1: lead with one decision). -->
|
|
1550
|
+
<div class="hero hero-fused" id="posture-card">
|
|
1551
|
+
<div class="hp-main">
|
|
1552
|
+
<div class="hp-state">
|
|
1553
|
+
<span class="live-dot" id="hero-dot"></span>
|
|
1554
|
+
<span class="hero-status" id="hero-status">Connecting…</span>
|
|
1555
|
+
<span class="badge badge-off" id="hero-badge">Connecting…</span>
|
|
1556
|
+
</div>
|
|
1557
|
+
<div class="hp-sub" id="hero-desc">Checking Guard status on this machine…</div>
|
|
1558
|
+
<div class="hp-tagline">
|
|
1559
|
+
PanGuard Community — free, on-device protection for your AI agents.
|
|
1560
|
+
</div>
|
|
1561
|
+
</div>
|
|
1562
|
+
<div class="hp-score">
|
|
1563
|
+
<div class="score-num" id="posture-score">--</div>
|
|
1564
|
+
<div class="score-cap">Posture</div>
|
|
1565
|
+
<div class="score-track"><div class="score-fill" id="posture-bar"></div></div>
|
|
1566
|
+
<div class="score-label" id="posture-label">Checking…</div>
|
|
1567
|
+
</div>
|
|
1568
|
+
<div class="hp-fixes" id="posture-deductions"></div>
|
|
928
1569
|
</div>
|
|
929
1570
|
|
|
930
1571
|
<!-- 4 KPI cards -->
|
|
931
1572
|
<div class="cg">
|
|
932
|
-
<div class="cd">
|
|
1573
|
+
<div class="cd" title="Total ATR detection rules currently loaded and enforcing on this machine.">
|
|
933
1574
|
<div class="cl" data-i18n="k_rules">Rules Active</div>
|
|
934
1575
|
<div class="cv sg" id="v-rules">--</div>
|
|
935
1576
|
</div>
|
|
936
|
-
<div class="cd">
|
|
1577
|
+
<div class="cd" title="Threats Guard has detected on this machine.">
|
|
937
1578
|
<div class="cl" data-i18n="k_threats">Threats Detected</div>
|
|
938
1579
|
<div class="cv sg" id="v-threats">0</div>
|
|
939
1580
|
</div>
|
|
940
|
-
<div
|
|
941
|
-
|
|
1581
|
+
<div
|
|
1582
|
+
class="cd"
|
|
1583
|
+
title="Detection rules pulled from the Threat Cloud network, included in your active rule set."
|
|
1584
|
+
>
|
|
1585
|
+
<div class="cl" data-i18n="k_tc">Threat Cloud rules synced</div>
|
|
942
1586
|
<div class="cv sg" id="v-tcsync">0</div>
|
|
943
1587
|
</div>
|
|
944
1588
|
</div>
|
|
@@ -946,12 +1590,15 @@
|
|
|
946
1590
|
<!-- Detection layers status (renamed Tier → Layer A/B/C) -->
|
|
947
1591
|
<div class="fs">
|
|
948
1592
|
<h3>
|
|
949
|
-
<span class="dot dot-ok"
|
|
1593
|
+
<span class="dot dot-ok" id="layers-dot"></span>
|
|
1594
|
+
<span data-i18n="layer_title">Detection Layers</span>
|
|
950
1595
|
</h3>
|
|
951
1596
|
<div class="desc">
|
|
952
|
-
Layers A and B are deterministic and always on. Layer C is optional — bring your own
|
|
953
|
-
|
|
954
|
-
|
|
1597
|
+
Layers A and B are deterministic and always on. Layer C is optional — bring your own LLM
|
|
1598
|
+
(cloud API or local model). It is advisory (flags for review, never auto-blocks) and
|
|
1599
|
+
stays off until you connect one. It runs only on events A and B already flagged, not on
|
|
1600
|
+
every action, so cloud use is typically a few cents a month — and a local model (Ollama)
|
|
1601
|
+
is free and keeps everything on your machine.
|
|
955
1602
|
</div>
|
|
956
1603
|
<div class="layer-row">
|
|
957
1604
|
<span class="layer-label">Layer A — Pattern match (ATR rules)</span>
|
|
@@ -964,10 +1611,45 @@
|
|
|
964
1611
|
<span class="layer-status on" id="lb-st">Active</span>
|
|
965
1612
|
</div>
|
|
966
1613
|
<div class="layer-row">
|
|
967
|
-
<span class="layer-label"
|
|
1614
|
+
<span class="layer-label"
|
|
1615
|
+
>Layer C — Semantic verdict (optional · advisory)</span
|
|
1616
|
+
>
|
|
968
1617
|
<div class="layer-bar"><div class="layer-fill" id="lc-bar"></div></div>
|
|
969
1618
|
<span class="layer-status" id="lc-st">Off · bring your own</span>
|
|
970
1619
|
</div>
|
|
1620
|
+
<!-- Layer C call-to-action: a discoverable two-state affordance that
|
|
1621
|
+
replaces the old silent server-filled hint. Not connected -> why +
|
|
1622
|
+
"Set up Layer C" deep-linking to the Settings section. Connected ->
|
|
1623
|
+
a calm confirmation. Driven from status.layers.c.state. -->
|
|
1624
|
+
<div
|
|
1625
|
+
class="lc-cta"
|
|
1626
|
+
id="lc-cta"
|
|
1627
|
+
style="
|
|
1628
|
+
margin-top: 10px;
|
|
1629
|
+
display: flex;
|
|
1630
|
+
align-items: center;
|
|
1631
|
+
justify-content: space-between;
|
|
1632
|
+
gap: 12px;
|
|
1633
|
+
flex-wrap: wrap;
|
|
1634
|
+
padding: 10px 12px;
|
|
1635
|
+
background: var(--bg-raised);
|
|
1636
|
+
border: 1px solid var(--border);
|
|
1637
|
+
border-radius: 10px;
|
|
1638
|
+
"
|
|
1639
|
+
>
|
|
1640
|
+
<div
|
|
1641
|
+
class="lc-cta-text"
|
|
1642
|
+
id="lc-cta-text"
|
|
1643
|
+
style="font-size: 12px; color: var(--text-muted); line-height: 1.6; flex: 1; min-width: 220px"
|
|
1644
|
+
>
|
|
1645
|
+
Optional: connect your own LLM for a second opinion on flagged events — catches novel
|
|
1646
|
+
attacks A and B miss. Advisory only: it flags for review, never auto-blocks. Free with a
|
|
1647
|
+
local model, or a few cents a month with a cloud key.
|
|
1648
|
+
</div>
|
|
1649
|
+
<button type="button" class="btn btn-secondary btn-sm" id="lc-cta-btn">
|
|
1650
|
+
Set up Layer C
|
|
1651
|
+
</button>
|
|
1652
|
+
</div>
|
|
971
1653
|
</div>
|
|
972
1654
|
|
|
973
1655
|
<!-- Uptime counter -->
|
|
@@ -1017,69 +1699,43 @@
|
|
|
1017
1699
|
"
|
|
1018
1700
|
>
|
|
1019
1701
|
<div class="flt">
|
|
1020
|
-
<button
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
>
|
|
1025
|
-
All
|
|
1026
|
-
</button>
|
|
1027
|
-
<button
|
|
1028
|
-
class="fb"
|
|
1029
|
-
data-sev="critical"
|
|
1030
|
-
data-i18n="critical"
|
|
1031
|
-
>
|
|
1032
|
-
Critical
|
|
1033
|
-
</button>
|
|
1034
|
-
<button
|
|
1035
|
-
class="fb"
|
|
1036
|
-
data-sev="high"
|
|
1037
|
-
data-i18n="high"
|
|
1038
|
-
>
|
|
1039
|
-
High
|
|
1040
|
-
</button>
|
|
1041
|
-
<button
|
|
1042
|
-
class="fb"
|
|
1043
|
-
data-sev="medium"
|
|
1044
|
-
data-i18n="medium"
|
|
1045
|
-
>
|
|
1046
|
-
Medium
|
|
1047
|
-
</button>
|
|
1048
|
-
<button class="fb" data-sev="low" data-i18n="low">
|
|
1049
|
-
Low
|
|
1050
|
-
</button>
|
|
1702
|
+
<button class="fb on" data-sev="all" data-i18n="all">All</button>
|
|
1703
|
+
<button class="fb" data-sev="critical" data-i18n="critical">Critical</button>
|
|
1704
|
+
<button class="fb" data-sev="high" data-i18n="high">High</button>
|
|
1705
|
+
<button class="fb" data-sev="medium" data-i18n="medium">Medium</button>
|
|
1706
|
+
<button class="fb" data-sev="low" data-i18n="low">Low</button>
|
|
1051
1707
|
</div>
|
|
1052
1708
|
<div style="display: flex; gap: 8px; align-items: center">
|
|
1053
1709
|
<button
|
|
1054
1710
|
class="btn btn-secondary btn-sm"
|
|
1055
|
-
data-tier-gate="pilot"
|
|
1056
1711
|
id="btn-sarif"
|
|
1057
|
-
|
|
1712
|
+
title="Download a .sarif.json of detected threats (the standard format GitHub and code scanners read). Saved to your computer — nothing is uploaded, and it never includes your source code."
|
|
1058
1713
|
>
|
|
1059
1714
|
<span data-i18n="exp_sarif">Export SARIF</span>
|
|
1060
1715
|
</button>
|
|
1061
1716
|
<button
|
|
1062
1717
|
class="btn btn-primary btn-sm"
|
|
1063
|
-
data-tier-gate="pilot"
|
|
1064
1718
|
id="btn-evidence"
|
|
1065
|
-
|
|
1719
|
+
title="Download a JSON evidence report of detections (rule IDs, timestamps, severities, matched patterns). Saved to your computer — nothing is uploaded, and it never includes your source code."
|
|
1066
1720
|
>
|
|
1067
1721
|
<span data-i18n="exp_evidence">Generate Evidence Pack</span>
|
|
1068
1722
|
</button>
|
|
1069
|
-
<a
|
|
1070
|
-
class="upsell"
|
|
1071
|
-
data-tier-upsell="pilot"
|
|
1072
|
-
href="https://panguard.ai/pricing"
|
|
1073
|
-
target="_blank"
|
|
1074
|
-
rel="noopener"
|
|
1075
|
-
style="display: none"
|
|
1076
|
-
data-i18n="upsell_pilot"
|
|
1077
|
-
>
|
|
1078
|
-
Upgrade to Pilot — unlock SARIF + Evidence Pack
|
|
1079
|
-
</a>
|
|
1080
1723
|
</div>
|
|
1081
1724
|
</div>
|
|
1082
1725
|
|
|
1726
|
+
<div class="pd" style="margin: -6px 0 14px; font-size: 12px">
|
|
1727
|
+
Export your detected threats as a file you can keep or share. SARIF is a standard format
|
|
1728
|
+
GitHub and code scanners read; the Evidence Pack is a self-contained JSON report. Both
|
|
1729
|
+
download to your computer — nothing is uploaded, and neither includes your source code or
|
|
1730
|
+
file contents.
|
|
1731
|
+
</div>
|
|
1732
|
+
|
|
1733
|
+
<!-- Suppression summary (anti alert-fatigue: only high-confidence shown by default) -->
|
|
1734
|
+
<div
|
|
1735
|
+
id="th-summary"
|
|
1736
|
+
style="display: none; font-size: 12px; color: var(--text-secondary); margin: 4px 0 10px"
|
|
1737
|
+
></div>
|
|
1738
|
+
|
|
1083
1739
|
<!-- Sortable threat table -->
|
|
1084
1740
|
<table class="tb">
|
|
1085
1741
|
<thead>
|
|
@@ -1087,6 +1743,7 @@
|
|
|
1087
1743
|
<th data-sort="timestamp" class="sortable">Timestamp</th>
|
|
1088
1744
|
<th data-sort="ruleId" class="sortable">Rule ID</th>
|
|
1089
1745
|
<th data-sort="severity" class="sortable">Severity</th>
|
|
1746
|
+
<th data-sort="confidence" class="sortable">Confidence</th>
|
|
1090
1747
|
<th>Matched Pattern</th>
|
|
1091
1748
|
<th data-sort="action" class="sortable">Action</th>
|
|
1092
1749
|
</tr>
|
|
@@ -1105,10 +1762,28 @@
|
|
|
1105
1762
|
<div class="pg" id="p-rules">
|
|
1106
1763
|
<div class="pt"><em data-i18n="t_ru">Rules</em></div>
|
|
1107
1764
|
<div class="pd" data-i18n="d_ru">
|
|
1108
|
-
All loaded ATR detection rules.
|
|
1109
|
-
|
|
1765
|
+
All loaded ATR detection rules. The bundled rule set is checked daily against npm; updates
|
|
1766
|
+
are surfaced below and applied only when you run <code>pga upgrade</code> — rules are the
|
|
1767
|
+
engine's trust root and are never auto-pulled. Use search and filters to find specific
|
|
1768
|
+
rules.
|
|
1110
1769
|
</div>
|
|
1111
1770
|
|
|
1771
|
+
<!-- Rule-update banner: surfaces rule-sync's daily notify-only check.
|
|
1772
|
+
Hidden unless a newer published rule set is available. -->
|
|
1773
|
+
<div
|
|
1774
|
+
id="rule-update-banner"
|
|
1775
|
+
style="
|
|
1776
|
+
display: none;
|
|
1777
|
+
margin: 0 0 16px;
|
|
1778
|
+
padding: 12px 16px;
|
|
1779
|
+
border: 1px solid var(--emerald, #34d399);
|
|
1780
|
+
border-radius: 10px;
|
|
1781
|
+
background: rgba(52, 211, 153, 0.08);
|
|
1782
|
+
font-size: 13px;
|
|
1783
|
+
line-height: 1.6;
|
|
1784
|
+
"
|
|
1785
|
+
></div>
|
|
1786
|
+
|
|
1112
1787
|
<div class="cg">
|
|
1113
1788
|
<div class="cd">
|
|
1114
1789
|
<div class="cl" data-i18n="ru_total">Total Rules</div>
|
|
@@ -1255,18 +1930,105 @@
|
|
|
1255
1930
|
<th data-i18n="sk_name">Name</th>
|
|
1256
1931
|
<th data-i18n="sk_platform">Platform</th>
|
|
1257
1932
|
<th data-i18n="sk_status">Status</th>
|
|
1933
|
+
<th data-i18n="sk_actions">Actions</th>
|
|
1258
1934
|
</tr>
|
|
1259
1935
|
</thead>
|
|
1260
1936
|
<tbody id="sk-tb"></tbody>
|
|
1261
1937
|
</table>
|
|
1938
|
+
|
|
1939
|
+
<!-- Quarantined files — see what's quarantined and restore it. The
|
|
1940
|
+
quarantine confirm dialog promises restorability; this honors it.
|
|
1941
|
+
Hidden when nothing is quarantined. -->
|
|
1942
|
+
<div id="qz-section" style="display: none; margin-top: 28px">
|
|
1943
|
+
<div class="st" style="margin-bottom: 12px">Quarantined Files</div>
|
|
1944
|
+
<table class="tb">
|
|
1945
|
+
<thead>
|
|
1946
|
+
<tr>
|
|
1947
|
+
<th>Original path</th>
|
|
1948
|
+
<th>Reason</th>
|
|
1949
|
+
<th>Quarantined</th>
|
|
1950
|
+
<th>Actions</th>
|
|
1951
|
+
</tr>
|
|
1952
|
+
</thead>
|
|
1953
|
+
<tbody id="qz-tb"></tbody>
|
|
1954
|
+
</table>
|
|
1955
|
+
</div>
|
|
1956
|
+
</div>
|
|
1957
|
+
|
|
1958
|
+
<!-- ===== Coverage ===== -->
|
|
1959
|
+
<div class="pg" id="p-coverage">
|
|
1960
|
+
<div class="pt"><em>Coverage</em></div>
|
|
1961
|
+
<div class="pd">
|
|
1962
|
+
Which AI agent platforms Guard can see on this machine, and whether each is wired up. A
|
|
1963
|
+
detected-but-not-connected platform is running <strong>unprotected</strong> — connect it
|
|
1964
|
+
with <code>pga init</code>.
|
|
1965
|
+
</div>
|
|
1966
|
+
<div class="cg">
|
|
1967
|
+
<div class="cd">
|
|
1968
|
+
<div class="cl">Detected</div>
|
|
1969
|
+
<div class="cv sg" id="cov-detected">--</div>
|
|
1970
|
+
</div>
|
|
1971
|
+
<div class="cd">
|
|
1972
|
+
<div class="cl">Protected</div>
|
|
1973
|
+
<div class="cv" id="cov-configured" style="color: var(--emerald)">--</div>
|
|
1974
|
+
</div>
|
|
1975
|
+
<div class="cd">
|
|
1976
|
+
<div class="cl">Unprotected</div>
|
|
1977
|
+
<div class="cv" id="cov-unprotected">--</div>
|
|
1978
|
+
</div>
|
|
1979
|
+
</div>
|
|
1980
|
+
<div id="cov-loading" class="empty">
|
|
1981
|
+
<span class="spin"></span> <span>Detecting platforms…</span>
|
|
1982
|
+
</div>
|
|
1983
|
+
<table class="tb" id="cov-table" style="display: none">
|
|
1984
|
+
<thead>
|
|
1985
|
+
<tr>
|
|
1986
|
+
<th>Platform</th>
|
|
1987
|
+
<th>Status</th>
|
|
1988
|
+
</tr>
|
|
1989
|
+
</thead>
|
|
1990
|
+
<tbody id="cov-tb"></tbody>
|
|
1991
|
+
</table>
|
|
1992
|
+
</div>
|
|
1993
|
+
|
|
1994
|
+
<!-- ===== Runtime ===== -->
|
|
1995
|
+
<div class="pg" id="p-runtime">
|
|
1996
|
+
<div class="pt"><em>Runtime</em></div>
|
|
1997
|
+
<div class="pd">
|
|
1998
|
+
Live decisions from the MCP proxy as your agents call tools. Quiet when healthy — a row
|
|
1999
|
+
appears only on a real evaluation. <strong>allow</strong> = passed,
|
|
2000
|
+
<strong>deny</strong> = blocked on a proven rule, <strong>ask</strong> = flagged for you
|
|
2001
|
+
to decide.
|
|
2002
|
+
</div>
|
|
2003
|
+
<div id="rt-loading" class="empty">
|
|
2004
|
+
<span class="spin"></span> <span>Loading runtime verdicts…</span>
|
|
2005
|
+
</div>
|
|
2006
|
+
<div id="rt-empty" class="empty" style="display: none">
|
|
2007
|
+
No proxy activity yet. Verdicts appear here when the MCP proxy evaluates a tool call.
|
|
2008
|
+
</div>
|
|
2009
|
+
<table class="tb" id="rt-table" style="display: none">
|
|
2010
|
+
<thead>
|
|
2011
|
+
<tr>
|
|
2012
|
+
<th>Time</th>
|
|
2013
|
+
<th>Outcome</th>
|
|
2014
|
+
<th>Tool / reason</th>
|
|
2015
|
+
<th>Confidence</th>
|
|
2016
|
+
</tr>
|
|
2017
|
+
</thead>
|
|
2018
|
+
<tbody id="rt-tb"></tbody>
|
|
2019
|
+
</table>
|
|
1262
2020
|
</div>
|
|
1263
2021
|
|
|
1264
2022
|
<!-- ===== PAGE 5: Threat Cloud ===== -->
|
|
1265
2023
|
<div class="pg" id="p-tcloud">
|
|
1266
2024
|
<div class="pt"><em data-i18n="t_tc">Threat Cloud</em></div>
|
|
1267
2025
|
<div class="pd" data-i18n="d_tc">
|
|
1268
|
-
Threat Cloud is PanGuard's anonymous threat
|
|
1269
|
-
|
|
2026
|
+
Threat Cloud is PanGuard's anonymous threat-intelligence network. It is
|
|
2027
|
+
<strong>off by default — until you turn on Collective Defense below, nothing leaves this
|
|
2028
|
+
machine.</strong> When you do enable it, your endpoint uploads anonymized threat signatures
|
|
2029
|
+
(no code, no file contents). New detection rules ship in the open ATR package — Guard
|
|
2030
|
+
checks once a day and tells you when an upgrade is available. It never auto-pulls rule logic
|
|
2031
|
+
from the network, so a compromised relay can't change what your engine enforces.
|
|
1270
2032
|
</div>
|
|
1271
2033
|
|
|
1272
2034
|
<div id="tc-loading" class="empty">
|
|
@@ -1316,20 +2078,54 @@
|
|
|
1316
2078
|
|
|
1317
2079
|
<div class="st" data-i18n="tc_flywheel">Flywheel</div>
|
|
1318
2080
|
<div class="flywheel" id="tc-flywheel">
|
|
1319
|
-
<strong>Your endpoint</strong> detects
|
|
1320
|
-
<strong>Threat Cloud</strong> aggregates
|
|
1321
|
-
<strong>
|
|
2081
|
+
<strong>Your endpoint</strong> detects a threat →
|
|
2082
|
+
<strong>Threat Cloud</strong> aggregates it anonymously → it strengthens the open
|
|
2083
|
+
<strong>ATR</strong> ruleset → you get the improvements through
|
|
2084
|
+
<strong>pga upgrade</strong> (a daily check flags when new rules are out).
|
|
1322
2085
|
</div>
|
|
1323
2086
|
|
|
1324
2087
|
<div class="fs" style="margin-top: 20px">
|
|
1325
|
-
<h3
|
|
1326
|
-
<div class="desc"
|
|
1327
|
-
When
|
|
1328
|
-
|
|
2088
|
+
<h3>Collective defense</h3>
|
|
2089
|
+
<div class="desc">
|
|
2090
|
+
Off by default. When you turn it on, every attack Guard blocks on this machine can
|
|
2091
|
+
become a new ATR rule that protects everyone — and you get the community's new rules
|
|
2092
|
+
back faster. More sensors means quicker detection for all of us.
|
|
2093
|
+
</div>
|
|
2094
|
+
|
|
2095
|
+
<div class="cd-grid">
|
|
2096
|
+
<div class="cd-box">
|
|
2097
|
+
<div class="cd-box-title cd-good">What turning it on does for you</div>
|
|
2098
|
+
<ul class="cd-list">
|
|
2099
|
+
<li>Your blocked patterns help draft new open ATR rules.</li>
|
|
2100
|
+
<li>You receive community rule updates sooner.</li>
|
|
2101
|
+
<li>You can turn it off again at any time.</li>
|
|
2102
|
+
</ul>
|
|
2103
|
+
</div>
|
|
2104
|
+
<div class="cd-box">
|
|
2105
|
+
<div class="cd-box-title">What is shared (and what is never shared)</div>
|
|
2106
|
+
<ul class="cd-list">
|
|
2107
|
+
<li>Shared: the matched rule ID, a one-way hash of the payload, and the source type.</li>
|
|
2108
|
+
<li>Never shared: your prompts, code, file contents, keys, IP address, or hostname.</li>
|
|
2109
|
+
<li>A random install ID only — raw samples never leave this machine.</li>
|
|
2110
|
+
</ul>
|
|
2111
|
+
</div>
|
|
1329
2112
|
</div>
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
<
|
|
2113
|
+
|
|
2114
|
+
<div class="cd-control">
|
|
2115
|
+
<div class="toggle" id="tc-toggle" role="switch" aria-checked="false" tabindex="0"
|
|
2116
|
+
aria-label="Collective defense"></div>
|
|
2117
|
+
<div>
|
|
2118
|
+
<div id="tc-toggle-label" style="font-size: 14px; font-weight: 600">--</div>
|
|
2119
|
+
<div id="tc-toggle-sub" style="font-size: 12px; color: var(--text-muted)"></div>
|
|
2120
|
+
</div>
|
|
2121
|
+
</div>
|
|
2122
|
+
|
|
2123
|
+
<div class="cd-cli">
|
|
2124
|
+
Prefer the command line? This does the same thing:
|
|
2125
|
+
<div class="cmd-box">
|
|
2126
|
+
<code id="tc-cli-cmd">pga config set telemetry true</code>
|
|
2127
|
+
<button class="cmd-copy" data-copy-target="tc-cli-cmd" type="button">Copy</button>
|
|
2128
|
+
</div>
|
|
1333
2129
|
</div>
|
|
1334
2130
|
</div>
|
|
1335
2131
|
</div>
|
|
@@ -1343,20 +2139,198 @@
|
|
|
1343
2139
|
<div class="fs">
|
|
1344
2140
|
<h3>Guard Mode</h3>
|
|
1345
2141
|
<div class="desc">
|
|
1346
|
-
Learning
|
|
1347
|
-
|
|
2142
|
+
Three modes. <strong>Learning</strong> builds a baseline and only logs.
|
|
2143
|
+
<strong>Report-only</strong> detects and records everything but never acts (recommended
|
|
2144
|
+
for new deployments). <strong>Protection</strong> actively blocks — but only using the
|
|
2145
|
+
responses armed in your enforcement policy (by default that's nothing destructive: it
|
|
2146
|
+
detects and notifies). Saving applies to new activity right away; it won't interrupt
|
|
2147
|
+
actions already in progress.
|
|
1348
2148
|
</div>
|
|
1349
|
-
<div
|
|
1350
|
-
|
|
1351
|
-
|
|
2149
|
+
<div
|
|
2150
|
+
id="st-mode-seg"
|
|
2151
|
+
role="radiogroup"
|
|
2152
|
+
aria-label="Guard mode"
|
|
2153
|
+
style="
|
|
2154
|
+
display: inline-flex;
|
|
2155
|
+
border: 1px solid var(--border, #2e2a27);
|
|
2156
|
+
border-radius: 8px;
|
|
2157
|
+
overflow: hidden;
|
|
2158
|
+
"
|
|
2159
|
+
>
|
|
2160
|
+
<button
|
|
2161
|
+
type="button"
|
|
2162
|
+
class="seg-btn"
|
|
2163
|
+
data-mode="learning"
|
|
2164
|
+
role="radio"
|
|
2165
|
+
aria-checked="false"
|
|
2166
|
+
style="
|
|
2167
|
+
padding: 8px 16px;
|
|
2168
|
+
background: transparent;
|
|
2169
|
+
color: var(--text-muted);
|
|
2170
|
+
border: 0;
|
|
2171
|
+
cursor: pointer;
|
|
2172
|
+
font-size: 13px;
|
|
2173
|
+
font-weight: 600;
|
|
2174
|
+
"
|
|
2175
|
+
>
|
|
2176
|
+
Learning
|
|
2177
|
+
</button>
|
|
2178
|
+
<button
|
|
2179
|
+
type="button"
|
|
2180
|
+
class="seg-btn"
|
|
2181
|
+
data-mode="report-only"
|
|
2182
|
+
role="radio"
|
|
2183
|
+
aria-checked="false"
|
|
2184
|
+
style="
|
|
2185
|
+
padding: 8px 16px;
|
|
2186
|
+
background: transparent;
|
|
2187
|
+
color: var(--text-muted);
|
|
2188
|
+
border: 0;
|
|
2189
|
+
border-left: 1px solid var(--border, #2e2a27);
|
|
2190
|
+
cursor: pointer;
|
|
2191
|
+
font-size: 13px;
|
|
2192
|
+
font-weight: 600;
|
|
2193
|
+
"
|
|
2194
|
+
>
|
|
2195
|
+
Report-only
|
|
2196
|
+
</button>
|
|
2197
|
+
<button
|
|
2198
|
+
type="button"
|
|
2199
|
+
class="seg-btn"
|
|
2200
|
+
data-mode="protection"
|
|
2201
|
+
role="radio"
|
|
2202
|
+
aria-checked="false"
|
|
2203
|
+
style="
|
|
2204
|
+
padding: 8px 16px;
|
|
2205
|
+
background: transparent;
|
|
2206
|
+
color: var(--text-muted);
|
|
2207
|
+
border: 0;
|
|
2208
|
+
border-left: 1px solid var(--border, #2e2a27);
|
|
2209
|
+
cursor: pointer;
|
|
2210
|
+
font-size: 13px;
|
|
2211
|
+
font-weight: 600;
|
|
2212
|
+
"
|
|
2213
|
+
>
|
|
2214
|
+
Protection
|
|
2215
|
+
</button>
|
|
2216
|
+
</div>
|
|
2217
|
+
<div
|
|
2218
|
+
id="st-mode-desc"
|
|
2219
|
+
style="margin-top: 8px; font-size: 12px; color: var(--text-muted); line-height: 1.6"
|
|
2220
|
+
></div>
|
|
2221
|
+
</div>
|
|
2222
|
+
|
|
2223
|
+
<!-- ===== Semantic layer (Layer C) — optional advisory LLM =====
|
|
2224
|
+
The headline "how do I connect a model" surface. Self-contained (does
|
|
2225
|
+
NOT depend on a server string). No key field by design — the key is
|
|
2226
|
+
entered only in the terminal via `pga guard ai`. -->
|
|
2227
|
+
<div class="fs" id="p-settings-layerc">
|
|
2228
|
+
<h3>
|
|
2229
|
+
<span class="dot dot-off" id="lc-set-dot"></span>
|
|
2230
|
+
Semantic layer (Layer C)
|
|
2231
|
+
<span
|
|
2232
|
+
id="lc-set-state"
|
|
2233
|
+
style="margin-left: auto; font-size: 12px; font-weight: 600; color: var(--text-muted)"
|
|
2234
|
+
>Off · optional</span
|
|
2235
|
+
>
|
|
2236
|
+
</h3>
|
|
2237
|
+
<div class="desc">
|
|
2238
|
+
An optional second opinion. Layers A and B (the always-on ATR pattern rules and
|
|
2239
|
+
heuristics) do the blocking. Layer C adds your own LLM to read flagged events in plain
|
|
2240
|
+
language and catch novel attacks the deterministic layers miss. It is
|
|
2241
|
+
<strong>advisory only</strong> — it flags events for your review and
|
|
2242
|
+
<strong>can never block on its own</strong>. It runs only on events A or B already
|
|
2243
|
+
flagged, not on every action, so a cloud model typically costs a few cents a month, and a
|
|
2244
|
+
local model is free.
|
|
2245
|
+
</div>
|
|
2246
|
+
|
|
2247
|
+
<div class="cd-grid">
|
|
2248
|
+
<div class="cd-box">
|
|
2249
|
+
<div class="cd-box-title cd-good">Path 1 — Local model (free & fully private)</div>
|
|
2250
|
+
<ul class="cd-list">
|
|
2251
|
+
<li>Runs a model on this machine with Ollama.</li>
|
|
2252
|
+
<li>No API key, no cost — nothing leaves your computer.</li>
|
|
2253
|
+
<li>Best if you want maximum privacy.</li>
|
|
2254
|
+
</ul>
|
|
2255
|
+
</div>
|
|
2256
|
+
<div class="cd-box">
|
|
2257
|
+
<div class="cd-box-title">Path 2 — Cloud model (bring your own key)</div>
|
|
2258
|
+
<ul class="cd-list">
|
|
2259
|
+
<li>Use an OpenAI or Anthropic key you already have.</li>
|
|
2260
|
+
<li>Typically a few cents a month (flagged events only).</li>
|
|
2261
|
+
<li>Your key is sent only to that provider — never to PanGuard.</li>
|
|
2262
|
+
</ul>
|
|
2263
|
+
</div>
|
|
2264
|
+
</div>
|
|
2265
|
+
|
|
2266
|
+
<div style="margin-top: 14px">
|
|
2267
|
+
<div style="font-size: 13px; font-weight: 600; margin-bottom: 6px">How to connect</div>
|
|
2268
|
+
<div class="desc" style="margin-bottom: 8px">
|
|
2269
|
+
Run this one command in your terminal. It walks you through picking a model and, for a
|
|
2270
|
+
cloud model, prompts for your key privately.
|
|
2271
|
+
<strong>You never type the key into this dashboard.</strong>
|
|
2272
|
+
</div>
|
|
2273
|
+
<div class="cmd-box">
|
|
2274
|
+
<code id="lc-cmd">pga guard ai</code>
|
|
2275
|
+
<button class="cmd-copy" type="button" data-copy-target="lc-cmd">Copy</button>
|
|
2276
|
+
</div>
|
|
2277
|
+
<div class="desc" style="margin-top: 8px">
|
|
2278
|
+
When it finishes, restart Guard with
|
|
2279
|
+
<code>pga guard restart</code> — this panel will then show Layer C as connected.
|
|
2280
|
+
</div>
|
|
2281
|
+
</div>
|
|
2282
|
+
|
|
2283
|
+
<details style="margin-top: 14px">
|
|
2284
|
+
<summary class="notify-summary">How your API key is protected</summary>
|
|
2285
|
+
<ul class="cd-list" style="margin-top: 10px; padding-left: 18px">
|
|
2286
|
+
<li>
|
|
2287
|
+
<strong>Typed privately in the terminal.</strong> The key is read with the screen
|
|
2288
|
+
echo off, so it never shows on screen, never lands in your shell history, and is never
|
|
2289
|
+
passed as a command argument.
|
|
2290
|
+
</li>
|
|
2291
|
+
<li>
|
|
2292
|
+
<strong>Never entered in the browser.</strong> This dashboard deliberately has no
|
|
2293
|
+
field for your key — a localhost web page is a weaker place to handle a secret than
|
|
2294
|
+
your terminal.
|
|
2295
|
+
</li>
|
|
2296
|
+
<li>
|
|
2297
|
+
<strong>Encrypted at rest.</strong> Stored at
|
|
2298
|
+
<code>~/.panguard/llm.enc</code> with AES-256-GCM, file permissions
|
|
2299
|
+
<code>0600</code> (only you can read it).
|
|
2300
|
+
</li>
|
|
2301
|
+
<li>
|
|
2302
|
+
<strong>Locked to this machine.</strong> The encryption key is derived from this
|
|
2303
|
+
machine and user, so a copied file is useless anywhere else.
|
|
2304
|
+
</li>
|
|
2305
|
+
<li>
|
|
2306
|
+
<strong>Sent only to your provider.</strong> Your key goes only to the model provider
|
|
2307
|
+
you chose. It is never sent to PanGuard.
|
|
2308
|
+
</li>
|
|
2309
|
+
</ul>
|
|
2310
|
+
</details>
|
|
2311
|
+
|
|
2312
|
+
<div style="margin-top: 14px">
|
|
2313
|
+
<div style="font-size: 13px; font-weight: 600; margin-bottom: 6px">
|
|
2314
|
+
Turn it off / remove your key
|
|
2315
|
+
</div>
|
|
2316
|
+
<div class="desc" style="margin-bottom: 8px">
|
|
2317
|
+
Turns Layer C off and deletes the stored config (including any key) from this machine.
|
|
2318
|
+
</div>
|
|
2319
|
+
<div class="cmd-box">
|
|
2320
|
+
<code id="lc-cmd-remove">pga guard ai --remove</code>
|
|
2321
|
+
<button class="cmd-copy" type="button" data-copy-target="lc-cmd-remove">Copy</button>
|
|
2322
|
+
</div>
|
|
1352
2323
|
</div>
|
|
1353
2324
|
</div>
|
|
1354
2325
|
|
|
1355
2326
|
<div class="fs">
|
|
1356
2327
|
<h3 data-i18n="st_tc">Threat Cloud Endpoint</h3>
|
|
1357
2328
|
<div class="desc" data-i18n="st_tc_desc">
|
|
1358
|
-
|
|
1359
|
-
|
|
2329
|
+
The server Guard talks to for Threat Cloud. <strong>Most people should leave this
|
|
2330
|
+
blank</strong> — Guard uses the default (tc.panguard.ai). Change it only if you run your
|
|
2331
|
+
own private or air-gapped relay: when Collective Defense is on, anonymized signatures and
|
|
2332
|
+
the daily rule-update check go to whatever URL is set here. Your prompts, code, and keys
|
|
2333
|
+
are never sent regardless.
|
|
1360
2334
|
</div>
|
|
1361
2335
|
<div class="fr">
|
|
1362
2336
|
<label data-i18n="st_endpoint">Endpoint</label>
|
|
@@ -1376,20 +2350,58 @@
|
|
|
1376
2350
|
</div>
|
|
1377
2351
|
</div>
|
|
1378
2352
|
|
|
2353
|
+
<div class="fs">
|
|
2354
|
+
<h3>Alerts</h3>
|
|
2355
|
+
<div class="info-box" style="border-left: 3px solid var(--ok); margin-bottom: 14px">
|
|
2356
|
+
<strong>You are already set up.</strong> Every threat Guard detects shows up right here
|
|
2357
|
+
in this dashboard — on the Overview and Threats tabs. No configuration needed.
|
|
2358
|
+
</div>
|
|
2359
|
+
|
|
2360
|
+
<details id="notify-details">
|
|
2361
|
+
<summary class="notify-summary">Send alerts somewhere else too? (optional)</summary>
|
|
2362
|
+
<div class="desc" style="margin-top: 10px">
|
|
2363
|
+
You can also push each alert to a chat or inbox you already watch. Pick one below,
|
|
2364
|
+
paste its line into <code>~/.panguard-guard/config.json</code> (or run
|
|
2365
|
+
<code>pga setup</code> for a guided walk-through), then restart Guard. Secrets are
|
|
2366
|
+
typed into that file, never into this dashboard. Heads-up: any channel you add sends
|
|
2367
|
+
alert summaries to that service (Slack's servers, your email provider, or your webhook
|
|
2368
|
+
URL). Alerts always stay visible here regardless.
|
|
2369
|
+
</div>
|
|
2370
|
+
<div id="st-notify-list" style="display: flex; flex-direction: column; gap: 10px"></div>
|
|
2371
|
+
</details>
|
|
2372
|
+
</div>
|
|
2373
|
+
|
|
2374
|
+
<div class="fs">
|
|
2375
|
+
<h3>
|
|
2376
|
+
Enforcement Policy
|
|
2377
|
+
<span style="font-weight: 400; color: var(--text-muted); font-size: 12px"
|
|
2378
|
+
>— what Protection mode is allowed to do</span
|
|
2379
|
+
>
|
|
2380
|
+
</h3>
|
|
2381
|
+
<div class="desc">
|
|
2382
|
+
This list is <strong>read-only here</strong> — an "armed" response only runs
|
|
2383
|
+
automatically when Guard Mode is set to Protection; in Learning or Report-only nothing
|
|
2384
|
+
automatic happens. To change what's armed, edit
|
|
2385
|
+
<code>enforcementPolicy</code> in <code>~/.panguard-guard/config.json</code> (or run
|
|
2386
|
+
<code>pga setup</code>) and restart Guard.
|
|
2387
|
+
</div>
|
|
2388
|
+
<div
|
|
2389
|
+
id="st-enforce-list"
|
|
2390
|
+
style="display: flex; flex-direction: column; gap: 6px; font-size: 13px"
|
|
2391
|
+
></div>
|
|
2392
|
+
</div>
|
|
1379
2393
|
|
|
1380
2394
|
<div style="display: flex; gap: 10px; margin-top: 8px">
|
|
1381
2395
|
<button class="btn btn-primary" id="btn-save">Save</button>
|
|
1382
|
-
<button class="btn btn-secondary" id="btn-reload">
|
|
1383
|
-
Reload
|
|
1384
|
-
</button>
|
|
2396
|
+
<button class="btn btn-secondary" id="btn-reload">Reload</button>
|
|
1385
2397
|
</div>
|
|
1386
2398
|
|
|
1387
2399
|
<div class="info-box" style="margin-top: 20px">
|
|
1388
2400
|
<strong data-i18n="st_license">License Tier:</strong> <span id="st-tier-display">--</span
|
|
1389
2401
|
><br />
|
|
1390
2402
|
<span data-i18n="st_license_desc"
|
|
1391
|
-
>Community is free and unlimited
|
|
1392
|
-
|
|
2403
|
+
>Community is free and unlimited — including SARIF and Evidence Pack export. No features
|
|
2404
|
+
are locked.</span
|
|
1393
2405
|
>
|
|
1394
2406
|
<br /><br />
|
|
1395
2407
|
<a
|
|
@@ -1398,7 +2410,7 @@
|
|
|
1398
2410
|
rel="noopener"
|
|
1399
2411
|
style="color: var(--sage)"
|
|
1400
2412
|
data-i18n="st_license_link"
|
|
1401
|
-
>
|
|
2413
|
+
>Compare editions →</a
|
|
1402
2414
|
>
|
|
1403
2415
|
</div>
|
|
1404
2416
|
</div>
|
|
@@ -1411,12 +2423,14 @@
|
|
|
1411
2423
|
/* i18n strings (EN / ZH) */
|
|
1412
2424
|
/* ============================================================ */
|
|
1413
2425
|
|
|
1414
|
-
|
|
1415
2426
|
var currentSevFilter = 'all';
|
|
1416
2427
|
var currentSort = { col: 'timestamp', dir: 'desc' };
|
|
1417
2428
|
var allThreats = [];
|
|
1418
2429
|
var allRules = [];
|
|
2430
|
+
var skillsData = [];
|
|
1419
2431
|
var licenseTier = 'community';
|
|
2432
|
+
var SUPPRESS_THRESHOLD = 50;
|
|
2433
|
+
var showSuppressed = false;
|
|
1420
2434
|
|
|
1421
2435
|
/* ============================================================ */
|
|
1422
2436
|
/* Utilities */
|
|
@@ -1429,6 +2443,28 @@
|
|
|
1429
2443
|
.replace(/"/g, '"')
|
|
1430
2444
|
.replace(/'/g, ''');
|
|
1431
2445
|
}
|
|
2446
|
+
/* Copy a command to the clipboard. Falls back to a manual-select prompt
|
|
2447
|
+
when the async clipboard API is unavailable (older browsers / non-secure
|
|
2448
|
+
contexts). The button briefly confirms so the user knows it worked. */
|
|
2449
|
+
function copyText(text, btn) {
|
|
2450
|
+
function ok() {
|
|
2451
|
+
if (!btn) return;
|
|
2452
|
+
var prev = btn.textContent;
|
|
2453
|
+
btn.textContent = 'Copied';
|
|
2454
|
+
btn.classList.add('copied');
|
|
2455
|
+
setTimeout(function () {
|
|
2456
|
+
btn.textContent = prev;
|
|
2457
|
+
btn.classList.remove('copied');
|
|
2458
|
+
}, 1500);
|
|
2459
|
+
}
|
|
2460
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2461
|
+
navigator.clipboard.writeText(text).then(ok, function () {
|
|
2462
|
+
window.prompt('Copy this command:', text);
|
|
2463
|
+
});
|
|
2464
|
+
} else {
|
|
2465
|
+
window.prompt('Copy this command:', text);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
1432
2468
|
function fmtUptime(ms) {
|
|
1433
2469
|
var s = Math.floor(ms / 1000);
|
|
1434
2470
|
if (s < 60) return s + 's';
|
|
@@ -1454,10 +2490,70 @@
|
|
|
1454
2490
|
if (r.status === 401) {
|
|
1455
2491
|
var wl = document.getElementById('wl');
|
|
1456
2492
|
if (wl) wl.textContent = 'Invalid token';
|
|
2493
|
+
// An unauthenticated visit (bare URL, expired/cleared cookie, wrong
|
|
2494
|
+
// token) used to silently no-op and leave a static green "Active"
|
|
2495
|
+
// badge — a dead end with no path forward. Render an explicit screen
|
|
2496
|
+
// that tells the user exactly how to get an authenticated URL.
|
|
2497
|
+
showUnauthenticated();
|
|
1457
2498
|
}
|
|
1458
2499
|
return r;
|
|
1459
2500
|
});
|
|
1460
2501
|
}
|
|
2502
|
+
/* Explicit unauthenticated state: the cookie that authorizes /api/* is
|
|
2503
|
+
minted only by opening the launch URL `pga up` prints. A bare visit has
|
|
2504
|
+
no cookie, so every /api/* call 401s. Instead of a fake-green dead end,
|
|
2505
|
+
show how to get an authenticated URL and neutralize the hero badge. */
|
|
2506
|
+
var _unauthShown = false;
|
|
2507
|
+
function showUnauthenticated() {
|
|
2508
|
+
if (_unauthShown) return;
|
|
2509
|
+
_unauthShown = true;
|
|
2510
|
+
// Neutralize the hero so it never reads green while unauthenticated.
|
|
2511
|
+
var hs = document.getElementById('hero-status');
|
|
2512
|
+
var hd = document.getElementById('hero-dot');
|
|
2513
|
+
var hb = document.getElementById('hero-badge');
|
|
2514
|
+
if (hs) {
|
|
2515
|
+
hs.textContent = 'NOT AUTHENTICATED';
|
|
2516
|
+
hs.style.color = 'var(--warn)';
|
|
2517
|
+
}
|
|
2518
|
+
if (hd) hd.style.background = 'var(--warn)';
|
|
2519
|
+
if (hb) {
|
|
2520
|
+
hb.textContent = 'Not authenticated';
|
|
2521
|
+
hb.className = 'badge badge-off';
|
|
2522
|
+
hb.title = 'Open the launch URL from `pga up` to authenticate this dashboard.';
|
|
2523
|
+
}
|
|
2524
|
+
var ov = document.getElementById('unauth-overlay');
|
|
2525
|
+
if (ov) {
|
|
2526
|
+
ov.style.display = 'flex';
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
ov = document.createElement('div');
|
|
2530
|
+
ov.id = 'unauth-overlay';
|
|
2531
|
+
ov.style.cssText =
|
|
2532
|
+
'position:fixed;inset:0;z-index:9999;display:flex;align-items:center;' +
|
|
2533
|
+
'justify-content:center;background:rgba(11,13,16,0.92);padding:24px';
|
|
2534
|
+
ov.innerHTML =
|
|
2535
|
+
'<div style="max-width:460px;background:var(--surface-2,#1b1f24);' +
|
|
2536
|
+
'border:1px solid var(--border,#2a2f36);border-radius:12px;padding:28px;' +
|
|
2537
|
+
'text-align:left;color:var(--text,#e6e8eb)">' +
|
|
2538
|
+
'<div style="font-size:16px;font-weight:600;margin-bottom:10px">' +
|
|
2539
|
+
'This dashboard is not authenticated</div>' +
|
|
2540
|
+
'<div style="font-size:13px;color:var(--text-muted,#9aa1a9);line-height:1.6;margin-bottom:16px">' +
|
|
2541
|
+
'For your security, the dashboard only accepts requests carrying a ' +
|
|
2542
|
+
'one-time launch token. Opening this URL directly does not include it, ' +
|
|
2543
|
+
'so live status and controls are unavailable.</div>' +
|
|
2544
|
+
'<div style="font-size:13px;color:var(--text,#e6e8eb);margin-bottom:8px">' +
|
|
2545
|
+
'To get an authenticated URL, run:</div>' +
|
|
2546
|
+
'<div style="background:var(--surface-3,#0f1216);border-radius:8px;padding:12px 14px;' +
|
|
2547
|
+
'font-family:ui-monospace,monospace;font-size:13px;color:var(--emerald,#34d399)">' +
|
|
2548
|
+
'pga up</div>' +
|
|
2549
|
+
'<div style="font-size:12px;color:var(--text-muted,#9aa1a9);line-height:1.6;margin-top:14px">' +
|
|
2550
|
+
'It prints a link like ' +
|
|
2551
|
+
'<code>http://127.0.0.1:<port>/?token=…</code> — open that ' +
|
|
2552
|
+
'link to authenticate. If Guard is already running, re-run ' +
|
|
2553
|
+
'<code>pga up</code> to reprint the authenticated link.</div>' +
|
|
2554
|
+
'</div>';
|
|
2555
|
+
document.body.appendChild(ov);
|
|
2556
|
+
}
|
|
1461
2557
|
|
|
1462
2558
|
/* ============================================================ */
|
|
1463
2559
|
/* i18n + language toggle */
|
|
@@ -1496,6 +2592,12 @@
|
|
|
1496
2592
|
if (tab === 'settings') {
|
|
1497
2593
|
loadSettings();
|
|
1498
2594
|
}
|
|
2595
|
+
if (tab === 'coverage') {
|
|
2596
|
+
loadCoverage();
|
|
2597
|
+
}
|
|
2598
|
+
if (tab === 'runtime') {
|
|
2599
|
+
loadRuntime();
|
|
2600
|
+
}
|
|
1499
2601
|
}
|
|
1500
2602
|
document.querySelectorAll('.ni').forEach(function (n) {
|
|
1501
2603
|
n.addEventListener('click', function () {
|
|
@@ -1504,20 +2606,15 @@
|
|
|
1504
2606
|
});
|
|
1505
2607
|
|
|
1506
2608
|
/* ============================================================ */
|
|
1507
|
-
/* License tier gating
|
|
2609
|
+
/* License tier label (no feature gating — everything is free) */
|
|
1508
2610
|
/* ============================================================ */
|
|
1509
2611
|
function applyTierGates(status) {
|
|
1510
2612
|
var tier = (status && status.license && status.license.tier) || 'community';
|
|
1511
2613
|
licenseTier = tier;
|
|
1512
|
-
var isPaid = tier === 'pilot' || tier === 'enterprise' || tier === 'pro';
|
|
1513
|
-
document.querySelectorAll('[data-tier-gate="pilot"]').forEach(function (el) {
|
|
1514
|
-
el.style.display = isPaid ? 'inline-flex' : 'none';
|
|
1515
|
-
});
|
|
1516
|
-
document.querySelectorAll('[data-tier-upsell="pilot"]').forEach(function (el) {
|
|
1517
|
-
el.style.display = isPaid ? 'none' : 'inline-flex';
|
|
1518
|
-
});
|
|
1519
2614
|
var badge = document.getElementById('lic-badge');
|
|
1520
2615
|
if (badge) badge.textContent = tier.toUpperCase();
|
|
2616
|
+
var brandTier = document.getElementById('brand-tier');
|
|
2617
|
+
if (brandTier) brandTier.textContent = tier.charAt(0).toUpperCase() + tier.slice(1);
|
|
1521
2618
|
var dispTier = document.getElementById('st-tier-display');
|
|
1522
2619
|
if (dispTier) dispTier.textContent = tier;
|
|
1523
2620
|
}
|
|
@@ -1525,15 +2622,143 @@
|
|
|
1525
2622
|
/* ============================================================ */
|
|
1526
2623
|
/* WebSocket */
|
|
1527
2624
|
/* ============================================================ */
|
|
2625
|
+
/* ----------------------------------------------------------------
|
|
2626
|
+
Connection state — ONE source of truth for "is Guard up?".
|
|
2627
|
+
Drives pbar + hero + posture + footer together so they never
|
|
2628
|
+
disagree. 'down' (Guard unreachable) is distinct from the 401
|
|
2629
|
+
unauthenticated screen and surfaces the start-it guidance. The
|
|
2630
|
+
old code left a stale green "CONNECTING…" forever when Guard was
|
|
2631
|
+
simply not running; this collapses that into one honest answer.
|
|
2632
|
+
---------------------------------------------------------------- */
|
|
2633
|
+
var _connState = '';
|
|
2634
|
+
var _connectTimer = null;
|
|
2635
|
+
var _downGrace = null;
|
|
2636
|
+
function markConnLive() {
|
|
2637
|
+
_connState = 'live';
|
|
2638
|
+
if (_connectTimer) {
|
|
2639
|
+
clearTimeout(_connectTimer);
|
|
2640
|
+
_connectTimer = null;
|
|
2641
|
+
}
|
|
2642
|
+
if (_downGrace) {
|
|
2643
|
+
clearTimeout(_downGrace);
|
|
2644
|
+
_downGrace = null;
|
|
2645
|
+
}
|
|
2646
|
+
var b = document.getElementById('conn-banner');
|
|
2647
|
+
if (b) b.style.display = 'none';
|
|
2648
|
+
var wl = document.getElementById('wl');
|
|
2649
|
+
var wd = document.getElementById('wd');
|
|
2650
|
+
if (wl && wl.textContent !== 'Invalid token') wl.textContent = 'Connected';
|
|
2651
|
+
if (wd) wd.classList.add('on');
|
|
2652
|
+
}
|
|
2653
|
+
// When Guard is unreachable we cannot claim any layer is "Active" — the
|
|
2654
|
+
// HTML defaults to Active and renderLayers() only runs on live status, so
|
|
2655
|
+
// neutralize the layer rows to avoid a residual false-green.
|
|
2656
|
+
function setLayersUnknown() {
|
|
2657
|
+
['la', 'lb', 'lc'].forEach(function (k) {
|
|
2658
|
+
var bar = document.getElementById(k + '-bar');
|
|
2659
|
+
var st = document.getElementById(k + '-st');
|
|
2660
|
+
if (bar) bar.className = 'layer-fill off';
|
|
2661
|
+
if (st) {
|
|
2662
|
+
st.textContent = '—';
|
|
2663
|
+
st.className = 'layer-status off';
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
var ld = document.getElementById('layers-dot');
|
|
2667
|
+
if (ld) ld.style.background = 'var(--text-muted)';
|
|
2668
|
+
}
|
|
2669
|
+
function setConnState(state) {
|
|
2670
|
+
if (state === 'live') {
|
|
2671
|
+
markConnLive();
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
if (state === _connState) return;
|
|
2675
|
+
_connState = state;
|
|
2676
|
+
setLayersUnknown();
|
|
2677
|
+
var dot = document.getElementById('pb-dot');
|
|
2678
|
+
var txt = document.getElementById('pb-txt');
|
|
2679
|
+
var det = document.getElementById('pb-detail');
|
|
2680
|
+
var hs = document.getElementById('hero-status');
|
|
2681
|
+
var hd = document.getElementById('hero-dot');
|
|
2682
|
+
var hb = document.getElementById('hero-badge');
|
|
2683
|
+
var hdsc = document.getElementById('hero-desc');
|
|
2684
|
+
var sc = document.getElementById('posture-score');
|
|
2685
|
+
var pl = document.getElementById('posture-label');
|
|
2686
|
+
var pbar = document.getElementById('posture-bar');
|
|
2687
|
+
var banner = document.getElementById('conn-banner');
|
|
2688
|
+
var wl = document.getElementById('wl');
|
|
2689
|
+
var wd = document.getElementById('wd');
|
|
2690
|
+
if (state === 'connecting') {
|
|
2691
|
+
if (dot) dot.className = 'pb-dot warn';
|
|
2692
|
+
if (txt) txt.textContent = 'Connecting…';
|
|
2693
|
+
if (hs) {
|
|
2694
|
+
hs.textContent = 'Connecting…';
|
|
2695
|
+
hs.style.color = 'var(--text-muted)';
|
|
2696
|
+
}
|
|
2697
|
+
if (hd) hd.style.background = 'var(--text-muted)';
|
|
2698
|
+
if (hb) {
|
|
2699
|
+
hb.textContent = 'Connecting…';
|
|
2700
|
+
hb.className = 'badge badge-off';
|
|
2701
|
+
}
|
|
2702
|
+
if (hdsc) hdsc.textContent = 'Checking Guard status on this machine…';
|
|
2703
|
+
if (banner) banner.style.display = 'none';
|
|
2704
|
+
if (wl && wl.textContent !== 'Invalid token') wl.textContent = 'Connecting…';
|
|
2705
|
+
if (wd) wd.classList.remove('on');
|
|
2706
|
+
} else if (state === 'down') {
|
|
2707
|
+
if (dot) dot.className = 'pb-dot bad';
|
|
2708
|
+
if (txt) txt.textContent = 'GUARD NOT RUNNING';
|
|
2709
|
+
if (det) det.textContent = '| start Guard to resume protection';
|
|
2710
|
+
if (hs) {
|
|
2711
|
+
hs.textContent = 'GUARD NOT RUNNING';
|
|
2712
|
+
hs.style.color = 'var(--danger)';
|
|
2713
|
+
}
|
|
2714
|
+
if (hd) hd.style.background = 'var(--danger)';
|
|
2715
|
+
if (hb) {
|
|
2716
|
+
hb.textContent = 'Not running';
|
|
2717
|
+
hb.className = 'badge badge-off';
|
|
2718
|
+
}
|
|
2719
|
+
if (hdsc)
|
|
2720
|
+
hdsc.textContent =
|
|
2721
|
+
'PanGuard Guard is not running on this machine, so your agents are not ' +
|
|
2722
|
+
'being watched right now.';
|
|
2723
|
+
if (sc) {
|
|
2724
|
+
sc.textContent = '--';
|
|
2725
|
+
sc.style.color = 'var(--danger)';
|
|
2726
|
+
}
|
|
2727
|
+
if (pl) pl.textContent = 'Guard isn’t running';
|
|
2728
|
+
if (pbar) pbar.style.width = '0';
|
|
2729
|
+
if (banner) banner.style.display = 'flex';
|
|
2730
|
+
if (wl && wl.textContent !== 'Invalid token') wl.textContent = 'Not running';
|
|
2731
|
+
if (wd) wd.classList.remove('on');
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
|
|
1528
2735
|
function connectWS() {
|
|
1529
2736
|
var ws = new WebSocket('ws://' + location.host + '/ws');
|
|
1530
2737
|
ws.onopen = function () {
|
|
1531
|
-
|
|
1532
|
-
|
|
2738
|
+
if (_downGrace) {
|
|
2739
|
+
clearTimeout(_downGrace);
|
|
2740
|
+
_downGrace = null;
|
|
2741
|
+
}
|
|
2742
|
+
var wd = document.getElementById('wd');
|
|
2743
|
+
var wl = document.getElementById('wl');
|
|
2744
|
+
if (wd) wd.classList.add('on');
|
|
2745
|
+
if (wl && wl.textContent !== 'Invalid token' && _connState !== 'down')
|
|
2746
|
+
wl.textContent = 'Connected';
|
|
1533
2747
|
};
|
|
1534
2748
|
ws.onclose = function () {
|
|
1535
|
-
document.getElementById('wd')
|
|
1536
|
-
|
|
2749
|
+
var wd = document.getElementById('wd');
|
|
2750
|
+
if (wd) wd.classList.remove('on');
|
|
2751
|
+
// If we were live, give a short grace for an immediate reconnect before
|
|
2752
|
+
// declaring Guard down — avoids flashing the banner on transient blips.
|
|
2753
|
+
if (_connState === 'live') {
|
|
2754
|
+
var wl = document.getElementById('wl');
|
|
2755
|
+
if (wl && wl.textContent !== 'Invalid token') wl.textContent = 'Reconnecting…';
|
|
2756
|
+
if (!_downGrace) {
|
|
2757
|
+
_downGrace = setTimeout(function () {
|
|
2758
|
+
if (_connState !== 'live') setConnState('down');
|
|
2759
|
+
}, 4000);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
1537
2762
|
setTimeout(connectWS, 3000);
|
|
1538
2763
|
};
|
|
1539
2764
|
ws.onmessage = function (e) {
|
|
@@ -1563,6 +2788,13 @@
|
|
|
1563
2788
|
}
|
|
1564
2789
|
function updateStatus(s) {
|
|
1565
2790
|
if (!s) return;
|
|
2791
|
+
markConnLive();
|
|
2792
|
+
// First-run welcome: only ever after a confirmed live+authenticated status,
|
|
2793
|
+
// so it can never appear over the "Guard not running" banner or 401 overlay.
|
|
2794
|
+
if (!_welcomeChecked) {
|
|
2795
|
+
_welcomeChecked = true;
|
|
2796
|
+
maybeShowWelcome();
|
|
2797
|
+
}
|
|
1566
2798
|
setKPI('v-rules', (s.atrRuleCount || 0).toLocaleString());
|
|
1567
2799
|
var th = s.threatsDetected || 0;
|
|
1568
2800
|
setKPI('v-threats', th);
|
|
@@ -1572,34 +2804,327 @@
|
|
|
1572
2804
|
setKPI('v-events', (s.eventsProcessed || 0).toLocaleString());
|
|
1573
2805
|
setKPI('v-mode', s.mode || '--');
|
|
1574
2806
|
|
|
2807
|
+
// Honest posture label: PROTECTED only when the daemon will actually ACT
|
|
2808
|
+
// (protection mode + an armed response). 'protection' mode with no armed
|
|
2809
|
+
// OS response is MONITORING (detect + notify), not green PROTECTED.
|
|
2810
|
+
var p = postureView(s);
|
|
1575
2811
|
var hs = document.getElementById('hero-status');
|
|
1576
2812
|
var hd = document.getElementById('hero-dot');
|
|
1577
2813
|
var hb = document.getElementById('hero-badge');
|
|
1578
2814
|
if (hs) {
|
|
1579
|
-
hs.textContent =
|
|
1580
|
-
hs.style.color =
|
|
2815
|
+
hs.textContent = p.label;
|
|
2816
|
+
hs.style.color = p.color;
|
|
2817
|
+
hs.title = p.detail;
|
|
1581
2818
|
}
|
|
1582
|
-
if (hd) hd.style.background =
|
|
2819
|
+
if (hd) hd.style.background = p.color;
|
|
1583
2820
|
if (hb) {
|
|
1584
|
-
hb.textContent =
|
|
1585
|
-
hb.className =
|
|
1586
|
-
|
|
2821
|
+
hb.textContent = p.label;
|
|
2822
|
+
hb.className = 'badge ' + p.badge;
|
|
2823
|
+
hb.title = p.detail;
|
|
1587
2824
|
}
|
|
2825
|
+
var hdsc = document.getElementById('hero-desc');
|
|
2826
|
+
if (hdsc && p.sub) hdsc.textContent = p.sub;
|
|
1588
2827
|
updatePBar(s);
|
|
2828
|
+
renderLayers(s.layers);
|
|
2829
|
+
_lastStatus = s;
|
|
2830
|
+
renderPosture(s);
|
|
1589
2831
|
applyTierGates(s);
|
|
1590
2832
|
}
|
|
2833
|
+
/* Posture score — one honest number from real local state, with each
|
|
2834
|
+
deduction offering a one-click jump to fix it. Formula (founder-set
|
|
2835
|
+
default): start 100; −min(threats*8,40) unresolved threats; −30 if the
|
|
2836
|
+
deterministic engine isn't actually running rules; −min(unprotected*5,20)
|
|
2837
|
+
detected-but-unconnected agent platforms. The optional semantic layer
|
|
2838
|
+
being off is NOT penalized — it is opt-in by design. */
|
|
2839
|
+
var _lastStatus = null;
|
|
2840
|
+
var _agentUnprotected = 0;
|
|
2841
|
+
function renderPosture(s) {
|
|
2842
|
+
if (!s) return;
|
|
2843
|
+
var score = 100;
|
|
2844
|
+
var ded = [];
|
|
2845
|
+
var threats = s.threatsDetected || 0;
|
|
2846
|
+
if (threats > 0) {
|
|
2847
|
+
var pt = Math.min(threats * 8, 40);
|
|
2848
|
+
score -= pt;
|
|
2849
|
+
ded.push({
|
|
2850
|
+
label: threats + ' unresolved threat' + (threats > 1 ? 's' : ''),
|
|
2851
|
+
pts: pt,
|
|
2852
|
+
goto: 'threats',
|
|
2853
|
+
btn: 'Review',
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
var la = s.layers && s.layers.a;
|
|
2857
|
+
if (la && la.state !== 'active') {
|
|
2858
|
+
score -= 30;
|
|
2859
|
+
ded.push({
|
|
2860
|
+
label: 'Detection engine: ' + (la.detail || 'not running'),
|
|
2861
|
+
pts: 30,
|
|
2862
|
+
goto: 'rules',
|
|
2863
|
+
btn: 'Inspect',
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
if (_agentUnprotected > 0) {
|
|
2867
|
+
var pa = Math.min(_agentUnprotected * 5, 20);
|
|
2868
|
+
score -= pa;
|
|
2869
|
+
ded.push({
|
|
2870
|
+
label:
|
|
2871
|
+
_agentUnprotected +
|
|
2872
|
+
' agent platform' +
|
|
2873
|
+
(_agentUnprotected > 1 ? 's' : '') +
|
|
2874
|
+
' unprotected',
|
|
2875
|
+
pts: pa,
|
|
2876
|
+
goto: 'coverage',
|
|
2877
|
+
btn: 'Connect',
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
// Honesty: 'protection' mode that arms NO automatic response is detect-and-
|
|
2881
|
+
// notify, not prevention. Don't let the score read 'Strong' while the daemon
|
|
2882
|
+
// would take no action. (report-only / learning make no such claim → no penalty.)
|
|
2883
|
+
if (s.enforcement && s.enforcement.mode === 'protection' && !s.enforcement.osActionsArmed) {
|
|
2884
|
+
score -= 10;
|
|
2885
|
+
ded.push({
|
|
2886
|
+
label: 'Protection mode on, but no automatic response is armed',
|
|
2887
|
+
pts: 10,
|
|
2888
|
+
goto: 'settings',
|
|
2889
|
+
btn: 'Configure',
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
score = Math.max(0, score);
|
|
2893
|
+
var color = score >= 85 ? 'var(--emerald)' : score >= 60 ? 'var(--warn)' : 'var(--danger)';
|
|
2894
|
+
var label =
|
|
2895
|
+
score >= 85
|
|
2896
|
+
? 'Strong — nothing needs your attention'
|
|
2897
|
+
: score >= 60
|
|
2898
|
+
? 'Fair — a few things to address'
|
|
2899
|
+
: 'At risk — action needed';
|
|
2900
|
+
var sc = document.getElementById('posture-score');
|
|
2901
|
+
if (sc) {
|
|
2902
|
+
sc.textContent = score;
|
|
2903
|
+
sc.style.color = color;
|
|
2904
|
+
sc.title =
|
|
2905
|
+
'Starts at 100. Points come off for unresolved threats, a detection engine that ' +
|
|
2906
|
+
'isn’t running, and agent platforms you haven’t connected. The optional semantic ' +
|
|
2907
|
+
'layer (Layer C) being off never lowers your score — it’s opt-in by design.';
|
|
2908
|
+
}
|
|
2909
|
+
var bar = document.getElementById('posture-bar');
|
|
2910
|
+
if (bar) {
|
|
2911
|
+
bar.style.width = score + '%';
|
|
2912
|
+
bar.style.background = color;
|
|
2913
|
+
}
|
|
2914
|
+
var lb = document.getElementById('posture-label');
|
|
2915
|
+
if (lb) lb.textContent = label;
|
|
2916
|
+
var dd = document.getElementById('posture-deductions');
|
|
2917
|
+
if (dd) {
|
|
2918
|
+
if (!ded.length) {
|
|
2919
|
+
dd.innerHTML =
|
|
2920
|
+
'<div style="font-size:12px;color:var(--text-muted)">Nothing needs your ' +
|
|
2921
|
+
'attention right now.</div>';
|
|
2922
|
+
} else {
|
|
2923
|
+
dd.innerHTML = '';
|
|
2924
|
+
ded.forEach(function (d) {
|
|
2925
|
+
var row = document.createElement('div');
|
|
2926
|
+
row.style.cssText =
|
|
2927
|
+
'display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px';
|
|
2928
|
+
row.innerHTML =
|
|
2929
|
+
'<span>' +
|
|
2930
|
+
esc(d.label) +
|
|
2931
|
+
' <span style="color:var(--danger)">−' +
|
|
2932
|
+
d.pts +
|
|
2933
|
+
'</span></span>' +
|
|
2934
|
+
'<button type="button" class="btn btn-secondary btn-sm" data-goto="' +
|
|
2935
|
+
d.goto +
|
|
2936
|
+
'">' +
|
|
2937
|
+
esc(d.btn) +
|
|
2938
|
+
'</button>';
|
|
2939
|
+
dd.appendChild(row);
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
/* Pull the unprotected-platform count for the posture score (Coverage data). */
|
|
2945
|
+
function refreshPostureAgents() {
|
|
2946
|
+
af('/api/agents')
|
|
2947
|
+
.then(function (r) {
|
|
2948
|
+
return r.json();
|
|
2949
|
+
})
|
|
2950
|
+
.then(function (d) {
|
|
2951
|
+
_agentUnprotected = Math.max(0, (d.detected || 0) - (d.configured || 0));
|
|
2952
|
+
if (_lastStatus) renderPosture(_lastStatus);
|
|
2953
|
+
})
|
|
2954
|
+
.catch(function () {
|
|
2955
|
+
/* leave at 0 */
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
/* Real per-layer health from the server (never hard-coded green). The
|
|
2959
|
+
server reports state per layer; Layer A goes red 'Degraded' if zero
|
|
2960
|
+
rules are loaded, and Layer C shows the BYO-LLM guidance inline. */
|
|
2961
|
+
function renderLayers(L) {
|
|
2962
|
+
if (!L) return;
|
|
2963
|
+
// Live status arrived: restore the section dot (cleared to neutral while
|
|
2964
|
+
// Guard was unreachable) so it reflects real per-layer health again.
|
|
2965
|
+
var ld = document.getElementById('layers-dot');
|
|
2966
|
+
if (ld) ld.style.background = '';
|
|
2967
|
+
var map = { a: 'la', b: 'lb', c: 'lc' };
|
|
2968
|
+
Object.keys(map).forEach(function (k) {
|
|
2969
|
+
var ly = L[k];
|
|
2970
|
+
if (!ly) return;
|
|
2971
|
+
var active = ly.state === 'active';
|
|
2972
|
+
var color =
|
|
2973
|
+
ly.state === 'active'
|
|
2974
|
+
? 'var(--ok)'
|
|
2975
|
+
: ly.state === 'degraded'
|
|
2976
|
+
? 'var(--danger)'
|
|
2977
|
+
: ly.state === 'idle'
|
|
2978
|
+
? 'var(--warn)'
|
|
2979
|
+
: 'var(--text-muted)';
|
|
2980
|
+
var label = active
|
|
2981
|
+
? 'Active'
|
|
2982
|
+
: ly.state === 'degraded'
|
|
2983
|
+
? 'Degraded'
|
|
2984
|
+
: ly.state === 'idle'
|
|
2985
|
+
? 'Idle'
|
|
2986
|
+
: ly.optional
|
|
2987
|
+
? 'Off · bring your own'
|
|
2988
|
+
: 'Off';
|
|
2989
|
+
var st = document.getElementById(map[k] + '-st');
|
|
2990
|
+
var bar = document.getElementById(map[k] + '-bar');
|
|
2991
|
+
if (st) {
|
|
2992
|
+
st.textContent = label;
|
|
2993
|
+
st.style.color = color;
|
|
2994
|
+
if (ly.detail) st.title = ly.detail;
|
|
2995
|
+
}
|
|
2996
|
+
if (bar) bar.className = 'layer-fill' + (active ? ' on' : '');
|
|
2997
|
+
});
|
|
2998
|
+
// Layer C call-to-action: a real two-state affordance (not a server-filled
|
|
2999
|
+
// hint). Not connected -> why + "Set up Layer C"; connected -> calm note.
|
|
3000
|
+
var cOn = L.c && L.c.state === 'active';
|
|
3001
|
+
var ctaText = document.getElementById('lc-cta-text');
|
|
3002
|
+
var ctaBtn = document.getElementById('lc-cta-btn');
|
|
3003
|
+
if (ctaText && ctaBtn) {
|
|
3004
|
+
if (cOn) {
|
|
3005
|
+
ctaText.textContent =
|
|
3006
|
+
'Layer C is connected and adding an advisory second opinion on flagged events. ' +
|
|
3007
|
+
'It never blocks on its own.';
|
|
3008
|
+
ctaBtn.textContent = 'Manage Layer C';
|
|
3009
|
+
} else {
|
|
3010
|
+
ctaText.textContent =
|
|
3011
|
+
'Optional: connect your own LLM for a second opinion on flagged events — catches ' +
|
|
3012
|
+
'novel attacks A and B miss. Advisory only: it flags for review, never auto-blocks. ' +
|
|
3013
|
+
'Free with a local model, or a few cents a month with a cloud key.';
|
|
3014
|
+
ctaBtn.textContent = 'Set up Layer C';
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
renderLayerCSettings(L);
|
|
3018
|
+
}
|
|
3019
|
+
/* Reflect Layer C connected/off state in the Settings section header. Defaults
|
|
3020
|
+
to Off in markup so it never reads green before real status loads. */
|
|
3021
|
+
function renderLayerCSettings(L) {
|
|
3022
|
+
var on = L && L.c && L.c.state === 'active';
|
|
3023
|
+
var dot = document.getElementById('lc-set-dot');
|
|
3024
|
+
var st = document.getElementById('lc-set-state');
|
|
3025
|
+
if (dot) dot.className = 'dot ' + (on ? 'dot-ok' : 'dot-off');
|
|
3026
|
+
if (st) {
|
|
3027
|
+
st.textContent = on ? 'Connected · advisory' : 'Off · optional';
|
|
3028
|
+
st.style.color = on ? 'var(--ok)' : 'var(--text-muted)';
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
/* ----------------------------------------------------------------
|
|
3033
|
+
First-run welcome guide. Shown once on the first LIVE authenticated
|
|
3034
|
+
status (called from updateStatus, after markConnLive), gated by a
|
|
3035
|
+
localStorage flag so it never nags and never paints over the down/
|
|
3036
|
+
unauth states. "Got it"/X persist; "Dismiss for now" returns next
|
|
3037
|
+
launch; the sidebar link reopens it.
|
|
3038
|
+
---------------------------------------------------------------- */
|
|
3039
|
+
var _welcomeChecked = false;
|
|
3040
|
+
function maybeShowWelcome() {
|
|
3041
|
+
try {
|
|
3042
|
+
if (localStorage.getItem('pg.welcome.dismissed') === '1') return;
|
|
3043
|
+
} catch (e) {
|
|
3044
|
+
/* private mode: fall through and show once for this page load */
|
|
3045
|
+
}
|
|
3046
|
+
var w = document.getElementById('welcome-guide');
|
|
3047
|
+
if (w) w.style.display = '';
|
|
3048
|
+
}
|
|
3049
|
+
function hideWelcome(persist) {
|
|
3050
|
+
var w = document.getElementById('welcome-guide');
|
|
3051
|
+
if (w) w.style.display = 'none';
|
|
3052
|
+
if (persist) {
|
|
3053
|
+
try {
|
|
3054
|
+
localStorage.setItem('pg.welcome.dismissed', '1');
|
|
3055
|
+
} catch (e) {
|
|
3056
|
+
/* ignore */
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
function reopenWelcome() {
|
|
3061
|
+
try {
|
|
3062
|
+
localStorage.removeItem('pg.welcome.dismissed');
|
|
3063
|
+
} catch (e) {
|
|
3064
|
+
/* ignore */
|
|
3065
|
+
}
|
|
3066
|
+
nav('overview');
|
|
3067
|
+
var w = document.getElementById('welcome-guide');
|
|
3068
|
+
if (w) w.style.display = '';
|
|
3069
|
+
}
|
|
3070
|
+
/* Map the server's honest enforcement posture to a label/color. PROTECTED
|
|
3071
|
+
(emerald) ONLY when the daemon will actually act; 'protection' mode with
|
|
3072
|
+
no armed response → MONITORING (amber), not a green over-claim. */
|
|
3073
|
+
function postureView(s) {
|
|
3074
|
+
var enf = s.enforcement || {
|
|
3075
|
+
posture: s.mode === 'protection' ? 'protected' : s.mode || 'learning',
|
|
3076
|
+
armedActions: [],
|
|
3077
|
+
};
|
|
3078
|
+
var MAP = {
|
|
3079
|
+
protected: {
|
|
3080
|
+
label: 'PROTECTED',
|
|
3081
|
+
color: 'var(--emerald)',
|
|
3082
|
+
badge: 'badge-protected',
|
|
3083
|
+
detail: 'Detecting and auto-responding (' + (enf.armedActions || []).join(', ') + ')',
|
|
3084
|
+
sub: 'Your AI agents are protected. Threats are detected and blocked automatically.',
|
|
3085
|
+
},
|
|
3086
|
+
monitoring: {
|
|
3087
|
+
label: 'MONITORING',
|
|
3088
|
+
color: 'var(--warn)',
|
|
3089
|
+
badge: 'badge-learning',
|
|
3090
|
+
detail:
|
|
3091
|
+
'Detecting + notifying. No automatic OS response is armed — arm it under ' +
|
|
3092
|
+
'enforcementPolicy in config to auto-act.',
|
|
3093
|
+
sub: 'Watching every action and alerting you. No automatic blocking is armed yet.',
|
|
3094
|
+
},
|
|
3095
|
+
'report-only': {
|
|
3096
|
+
label: 'REPORT-ONLY',
|
|
3097
|
+
color: 'var(--sage)',
|
|
3098
|
+
badge: 'badge-learning',
|
|
3099
|
+
detail: 'Recording detections only; takes no action by design.',
|
|
3100
|
+
sub: 'Recording detections for your review. Guard takes no action by design.',
|
|
3101
|
+
},
|
|
3102
|
+
learning: {
|
|
3103
|
+
label: 'LEARNING',
|
|
3104
|
+
color: 'var(--warn)',
|
|
3105
|
+
badge: 'badge-learning',
|
|
3106
|
+
detail: 'Building a baseline of normal activity; logging only.',
|
|
3107
|
+
sub: 'Learning what normal looks like on your machine. Logging only for now.',
|
|
3108
|
+
},
|
|
3109
|
+
off: {
|
|
3110
|
+
label: 'OFFLINE',
|
|
3111
|
+
color: 'var(--danger)',
|
|
3112
|
+
badge: 'badge-learning',
|
|
3113
|
+
detail: 'Guard is not running.',
|
|
3114
|
+
sub: 'Guard is not running, so your agents are not being watched.',
|
|
3115
|
+
},
|
|
3116
|
+
};
|
|
3117
|
+
return MAP[enf.posture] || MAP.learning;
|
|
3118
|
+
}
|
|
1591
3119
|
function updatePBar(s) {
|
|
1592
3120
|
var dot = document.getElementById('pb-dot');
|
|
1593
3121
|
var txt = document.getElementById('pb-txt');
|
|
1594
3122
|
var det = document.getElementById('pb-detail');
|
|
1595
3123
|
if (!dot || !txt || !det) return;
|
|
1596
|
-
var running = s.mode === 'protection' || s.mode === 'learning';
|
|
3124
|
+
var running = s.mode === 'protection' || s.mode === 'learning' || s.mode === 'report-only';
|
|
3125
|
+
var p = postureView(s);
|
|
1597
3126
|
dot.className = 'pb-dot ' + (running ? 'ok' : 'bad');
|
|
1598
|
-
txt.textContent = running
|
|
1599
|
-
? s.mode === 'protection'
|
|
1600
|
-
? 'PROTECTED'
|
|
1601
|
-
: 'LEARNING'
|
|
1602
|
-
: 'OFFLINE';
|
|
3127
|
+
txt.textContent = running ? p.label : 'OFFLINE';
|
|
1603
3128
|
var rules = s.atrRuleCount || 0;
|
|
1604
3129
|
det.textContent =
|
|
1605
3130
|
'| ' +
|
|
@@ -1614,21 +3139,37 @@
|
|
|
1614
3139
|
if (emp) emp.style.display = 'none';
|
|
1615
3140
|
var log = document.getElementById('ev-log');
|
|
1616
3141
|
if (log) log.style.display = '';
|
|
1617
|
-
var
|
|
1618
|
-
var sev = (
|
|
1619
|
-
d.className = 'ei ' + sev;
|
|
3142
|
+
var data = (m && m.data) || {};
|
|
3143
|
+
var sev = String(data.severity || data.conclusion || 'benign').toLowerCase();
|
|
1620
3144
|
var t = m.timestamp ? m.timestamp.split('T')[1].split('.')[0] : '--:--:--';
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
3145
|
+
// Plain-English summary instead of a raw JSON dump — a solo dev scans
|
|
3146
|
+
// these. Prefer a human label; fall back to rule id / event type.
|
|
3147
|
+
var title =
|
|
3148
|
+
data.ruleName ||
|
|
3149
|
+
data.message ||
|
|
3150
|
+
data.ruleId ||
|
|
3151
|
+
(m.type === 'new_verdict' ? 'Action reviewed' : 'Event detected');
|
|
3152
|
+
var bits = [];
|
|
3153
|
+
if (data.action) bits.push(String(data.action));
|
|
3154
|
+
if (data.tool) bits.push(String(data.tool));
|
|
3155
|
+
if (data.matchedPattern) bits.push('matched ' + String(data.matchedPattern).slice(0, 56));
|
|
3156
|
+
var card = document.createElement('div');
|
|
3157
|
+
card.className = 'ev-card ' + sev;
|
|
3158
|
+
card.innerHTML =
|
|
3159
|
+
'<span class="ev-sev"></span>' +
|
|
3160
|
+
'<div class="ev-body">' +
|
|
3161
|
+
'<div class="ev-title">' +
|
|
3162
|
+
esc(String(title).slice(0, 140)) +
|
|
3163
|
+
'</div>' +
|
|
3164
|
+
(bits.length ? '<div class="ev-meta">' + esc(bits.join(' · ')) + '</div>' : '') +
|
|
3165
|
+
'</div>' +
|
|
3166
|
+
'<span class="ev-sevtag">' +
|
|
3167
|
+
esc(sev) +
|
|
1627
3168
|
'</span>' +
|
|
1628
|
-
'<span class="
|
|
1629
|
-
|
|
3169
|
+
'<span class="ev-time">' +
|
|
3170
|
+
t +
|
|
1630
3171
|
'</span>';
|
|
1631
|
-
log.prepend(
|
|
3172
|
+
log.prepend(card);
|
|
1632
3173
|
while (log.children.length > 20) log.removeChild(log.lastChild);
|
|
1633
3174
|
}
|
|
1634
3175
|
|
|
@@ -1642,8 +3183,12 @@
|
|
|
1642
3183
|
})
|
|
1643
3184
|
.then(updateStatus)
|
|
1644
3185
|
.catch(function () {
|
|
1645
|
-
|
|
3186
|
+
// Fetch failed (not a 401 — that path shows the unauthenticated
|
|
3187
|
+
// screen). On localhost this means Guard isn't reachable: surface
|
|
3188
|
+
// the one honest "not running" state instead of a silent no-op.
|
|
3189
|
+
if (!_unauthShown) setConnState('down');
|
|
1646
3190
|
});
|
|
3191
|
+
refreshPostureAgents();
|
|
1647
3192
|
}
|
|
1648
3193
|
|
|
1649
3194
|
function loadThreats() {
|
|
@@ -1653,15 +3198,32 @@
|
|
|
1653
3198
|
})
|
|
1654
3199
|
.then(function (verdicts) {
|
|
1655
3200
|
allThreats = (verdicts || []).map(function (v, i) {
|
|
3201
|
+
// Real ATR rule id lives in a rule_match evidence's data.ruleId —
|
|
3202
|
+
// evidence[0].source is the evidence TYPE (e.g. "rule_match"), not an id.
|
|
3203
|
+
var ruleMatch = (v.evidence || []).filter(function (e) {
|
|
3204
|
+
return e && e.source === 'rule_match' && e.data && e.data.ruleId;
|
|
3205
|
+
})[0];
|
|
3206
|
+
var ruleId = ruleMatch
|
|
3207
|
+
? ruleMatch.data.ruleId
|
|
3208
|
+
: v.mitreTechnique || v.evidence?.[0]?.source || 'heuristic-' + i;
|
|
3209
|
+
// Enforcement confidence (deterministic-only) drives the displayed
|
|
3210
|
+
// confidence; falls back to overall confidence.
|
|
3211
|
+
var conf =
|
|
3212
|
+
typeof v.enforcementConfidence === 'number'
|
|
3213
|
+
? v.enforcementConfidence
|
|
3214
|
+
: typeof v.confidence === 'number'
|
|
3215
|
+
? v.confidence
|
|
3216
|
+
: 0;
|
|
1656
3217
|
return {
|
|
1657
3218
|
timestamp: v.timestamp || new Date().toISOString(),
|
|
1658
|
-
ruleId:
|
|
3219
|
+
ruleId: ruleId,
|
|
1659
3220
|
severity:
|
|
1660
3221
|
v.conclusion === 'malicious'
|
|
1661
3222
|
? 'critical'
|
|
1662
3223
|
: v.conclusion === 'suspicious'
|
|
1663
3224
|
? 'medium'
|
|
1664
3225
|
: 'low',
|
|
3226
|
+
confidence: Math.round(conf),
|
|
1665
3227
|
pattern: (v.reasoning || '').slice(0, 160),
|
|
1666
3228
|
action: v.recommendedAction || 'log_only',
|
|
1667
3229
|
};
|
|
@@ -1682,10 +3244,44 @@
|
|
|
1682
3244
|
function renderThreats() {
|
|
1683
3245
|
var tb = document.getElementById('th-tb');
|
|
1684
3246
|
var emp = document.getElementById('th-empty');
|
|
3247
|
+
var sum = document.getElementById('th-summary');
|
|
1685
3248
|
tb.innerHTML = '';
|
|
1686
|
-
var
|
|
3249
|
+
var bySev = allThreats.filter(function (t) {
|
|
1687
3250
|
return currentSevFilter === 'all' || t.severity === currentSevFilter;
|
|
1688
3251
|
});
|
|
3252
|
+
// Anti alert-fatigue: low-confidence detections are suppressed from the
|
|
3253
|
+
// default view (the #1 reason solo devs mute a security tool). The count
|
|
3254
|
+
// stays visible and one click reveals them.
|
|
3255
|
+
var high = bySev.filter(function (t) {
|
|
3256
|
+
return t.confidence >= SUPPRESS_THRESHOLD;
|
|
3257
|
+
});
|
|
3258
|
+
var low = bySev.filter(function (t) {
|
|
3259
|
+
return t.confidence < SUPPRESS_THRESHOLD;
|
|
3260
|
+
});
|
|
3261
|
+
if (sum) {
|
|
3262
|
+
if (low.length > 0) {
|
|
3263
|
+
sum.style.display = '';
|
|
3264
|
+
sum.innerHTML =
|
|
3265
|
+
'Showing ' +
|
|
3266
|
+
high.length +
|
|
3267
|
+
' high-confidence · ' +
|
|
3268
|
+
'<span id="th-supp-toggle" style="text-decoration:underline;cursor:pointer">' +
|
|
3269
|
+
low.length +
|
|
3270
|
+
' low-confidence ' +
|
|
3271
|
+
(showSuppressed ? 'hidden (click to hide)' : 'hidden — show') +
|
|
3272
|
+
'</span>' +
|
|
3273
|
+
' <span style="color:var(--text-muted)">(still recorded — hidden by default to cut noise)</span>';
|
|
3274
|
+
var tg = document.getElementById('th-supp-toggle');
|
|
3275
|
+
if (tg)
|
|
3276
|
+
tg.addEventListener('click', function () {
|
|
3277
|
+
showSuppressed = !showSuppressed;
|
|
3278
|
+
renderThreats();
|
|
3279
|
+
});
|
|
3280
|
+
} else {
|
|
3281
|
+
sum.style.display = 'none';
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
var list = (showSuppressed ? bySev : high).slice();
|
|
1689
3285
|
list.sort(function (a, b) {
|
|
1690
3286
|
var av = a[currentSort.col],
|
|
1691
3287
|
bv = b[currentSort.col];
|
|
@@ -1699,6 +3295,7 @@
|
|
|
1699
3295
|
if (emp) emp.style.display = 'none';
|
|
1700
3296
|
list.forEach(function (t) {
|
|
1701
3297
|
var tr = document.createElement('tr');
|
|
3298
|
+
if (t.confidence < SUPPRESS_THRESHOLD) tr.style.opacity = '0.6';
|
|
1702
3299
|
var ts = t.timestamp.split('T')[1]
|
|
1703
3300
|
? t.timestamp.split('T')[1].split('.')[0]
|
|
1704
3301
|
: t.timestamp;
|
|
@@ -1715,6 +3312,9 @@
|
|
|
1715
3312
|
esc(t.severity) +
|
|
1716
3313
|
'</span></td>' +
|
|
1717
3314
|
'<td class="mono">' +
|
|
3315
|
+
t.confidence +
|
|
3316
|
+
'%</td>' +
|
|
3317
|
+
'<td class="mono">' +
|
|
1718
3318
|
esc(t.pattern) +
|
|
1719
3319
|
'</td>' +
|
|
1720
3320
|
'<td>' +
|
|
@@ -1760,7 +3360,7 @@
|
|
|
1760
3360
|
a.click();
|
|
1761
3361
|
document.body.removeChild(a);
|
|
1762
3362
|
URL.revokeObjectURL(url);
|
|
1763
|
-
toast('SARIF
|
|
3363
|
+
toast('SARIF file downloaded to your computer — nothing was uploaded.');
|
|
1764
3364
|
})
|
|
1765
3365
|
.catch(function () {
|
|
1766
3366
|
toast('Network error');
|
|
@@ -1785,14 +3385,45 @@
|
|
|
1785
3385
|
a.click();
|
|
1786
3386
|
document.body.removeChild(a);
|
|
1787
3387
|
URL.revokeObjectURL(url);
|
|
1788
|
-
toast('Evidence pack
|
|
3388
|
+
toast('Evidence pack downloaded to your computer — nothing was uploaded.');
|
|
1789
3389
|
})
|
|
1790
3390
|
.catch(function () {
|
|
1791
3391
|
toast('Network error');
|
|
1792
3392
|
});
|
|
1793
3393
|
}
|
|
1794
3394
|
|
|
3395
|
+
/* Surface the daily notify-only rule-update check (rule-sync writes it to
|
|
3396
|
+
rule-update.json). The server computed this 'newer rules published'
|
|
3397
|
+
signal but nothing read it. Notify-only — never auto-applies. */
|
|
3398
|
+
function loadRuleUpdate() {
|
|
3399
|
+
var b = document.getElementById('rule-update-banner');
|
|
3400
|
+
if (!b) return;
|
|
3401
|
+
af('/api/rule-update')
|
|
3402
|
+
.then(function (r) {
|
|
3403
|
+
return r.json();
|
|
3404
|
+
})
|
|
3405
|
+
.then(function (d) {
|
|
3406
|
+
if (d && d.updateAvailable && d.latestVersion) {
|
|
3407
|
+
b.innerHTML =
|
|
3408
|
+
'<strong>New detection rules available</strong> — agent-threat-rules ' +
|
|
3409
|
+
esc(d.currentVersion || '?') +
|
|
3410
|
+
' → ' +
|
|
3411
|
+
esc(d.latestVersion) +
|
|
3412
|
+
'. Run <code>pga upgrade</code> in your terminal when you\'re ready — your current ' +
|
|
3413
|
+
'rules keep protecting you until you do, and nothing changes automatically. Rules ' +
|
|
3414
|
+
'are verified by npm and never auto-applied.';
|
|
3415
|
+
b.style.display = '';
|
|
3416
|
+
} else {
|
|
3417
|
+
b.style.display = 'none';
|
|
3418
|
+
}
|
|
3419
|
+
})
|
|
3420
|
+
.catch(function () {
|
|
3421
|
+
b.style.display = 'none';
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
|
|
1795
3425
|
function loadRules() {
|
|
3426
|
+
loadRuleUpdate();
|
|
1796
3427
|
af('/api/rules')
|
|
1797
3428
|
.then(function (r) {
|
|
1798
3429
|
return r.json();
|
|
@@ -1886,7 +3517,110 @@
|
|
|
1886
3517
|
renderRules(f);
|
|
1887
3518
|
}
|
|
1888
3519
|
|
|
3520
|
+
/* Coverage — surfaces /api/agents (which platforms are detected + whether
|
|
3521
|
+
each is wired to the proxy). This data drove zero UI before; a detected
|
|
3522
|
+
but un-connected platform is running unprotected. */
|
|
3523
|
+
function loadCoverage() {
|
|
3524
|
+
var ld = document.getElementById('cov-loading');
|
|
3525
|
+
var tbl = document.getElementById('cov-table');
|
|
3526
|
+
var tb = document.getElementById('cov-tb');
|
|
3527
|
+
if (!tb) return;
|
|
3528
|
+
af('/api/agents')
|
|
3529
|
+
.then(function (r) {
|
|
3530
|
+
return r.json();
|
|
3531
|
+
})
|
|
3532
|
+
.then(function (d) {
|
|
3533
|
+
var agents = (d && d.agents) || [];
|
|
3534
|
+
var det = d.detected || 0;
|
|
3535
|
+
var cfg = d.configured || 0;
|
|
3536
|
+
setKPI('cov-detected', det);
|
|
3537
|
+
setKPI('cov-configured', cfg);
|
|
3538
|
+
var unprot = Math.max(0, det - cfg);
|
|
3539
|
+
setKPI('cov-unprotected', unprot);
|
|
3540
|
+
var ue = document.getElementById('cov-unprotected');
|
|
3541
|
+
if (ue) ue.style.color = unprot > 0 ? 'var(--warn)' : 'var(--text-muted)';
|
|
3542
|
+
if (!agents.length) {
|
|
3543
|
+
if (ld)
|
|
3544
|
+
ld.innerHTML =
|
|
3545
|
+
'<span style="color:var(--text-muted)">No agent platforms detected. Install ' +
|
|
3546
|
+
'one (Claude Code, Cursor, Cline…), then run <code>pga init</code>.</span>';
|
|
3547
|
+
if (tbl) tbl.style.display = 'none';
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
if (ld) ld.style.display = 'none';
|
|
3551
|
+
if (tbl) tbl.style.display = '';
|
|
3552
|
+
tb.innerHTML = '';
|
|
3553
|
+
agents.forEach(function (a) {
|
|
3554
|
+
var status = !a.detected
|
|
3555
|
+
? '<span style="color:var(--text-muted)">not installed</span>'
|
|
3556
|
+
: a.alreadyConfigured
|
|
3557
|
+
? '<span style="color:var(--emerald)">● protected</span>'
|
|
3558
|
+
: '<span style="color:var(--warn)">○ unprotected — run <code>pga init</code></span>';
|
|
3559
|
+
var tr = document.createElement('tr');
|
|
3560
|
+
tr.innerHTML = '<td>' + esc(a.name || a.id || '') + '</td><td>' + status + '</td>';
|
|
3561
|
+
tb.appendChild(tr);
|
|
3562
|
+
});
|
|
3563
|
+
})
|
|
3564
|
+
.catch(function () {
|
|
3565
|
+
if (ld)
|
|
3566
|
+
ld.innerHTML =
|
|
3567
|
+
'<span style="color:var(--text-muted)">Platform detection unavailable.</span>';
|
|
3568
|
+
});
|
|
3569
|
+
}
|
|
3570
|
+
/* Runtime — surfaces /api/proxy-verdicts (live MCP proxy decisions). Quiet
|
|
3571
|
+
when healthy; a row only appears on a real allow/deny/ask evaluation. */
|
|
3572
|
+
function loadRuntime() {
|
|
3573
|
+
var ld = document.getElementById('rt-loading');
|
|
3574
|
+
var emp = document.getElementById('rt-empty');
|
|
3575
|
+
var tbl = document.getElementById('rt-table');
|
|
3576
|
+
var tb = document.getElementById('rt-tb');
|
|
3577
|
+
if (!tb) return;
|
|
3578
|
+
af('/api/proxy-verdicts')
|
|
3579
|
+
.then(function (r) {
|
|
3580
|
+
return r.json();
|
|
3581
|
+
})
|
|
3582
|
+
.then(function (d) {
|
|
3583
|
+
var vs = (d && d.verdicts) || [];
|
|
3584
|
+
if (ld) ld.style.display = 'none';
|
|
3585
|
+
if (!vs.length) {
|
|
3586
|
+
if (emp) emp.style.display = '';
|
|
3587
|
+
if (tbl) tbl.style.display = 'none';
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
if (emp) emp.style.display = 'none';
|
|
3591
|
+
if (tbl) tbl.style.display = '';
|
|
3592
|
+
tb.innerHTML = '';
|
|
3593
|
+
vs.forEach(function (v) {
|
|
3594
|
+
var oc = (v.outcome || '').toLowerCase();
|
|
3595
|
+
var color =
|
|
3596
|
+
oc === 'deny' ? 'var(--danger)' : oc === 'ask' ? 'var(--warn)' : 'var(--emerald)';
|
|
3597
|
+
var t = v.timestamp ? (String(v.timestamp).split('T')[1] || '').split('.')[0] : '--';
|
|
3598
|
+
var reason =
|
|
3599
|
+
v.reason || (v.matchedRules && v.matchedRules.join(', ')) || v.tool || '';
|
|
3600
|
+
var conf = typeof v.confidence === 'number' ? v.confidence + '%' : '--';
|
|
3601
|
+
var tr = document.createElement('tr');
|
|
3602
|
+
tr.innerHTML =
|
|
3603
|
+
'<td style="font-size:12px">' +
|
|
3604
|
+
esc(t) +
|
|
3605
|
+
'</td><td><span style="color:' +
|
|
3606
|
+
color +
|
|
3607
|
+
';text-transform:uppercase;font-weight:600;font-size:12px">' +
|
|
3608
|
+
esc(oc || '?') +
|
|
3609
|
+
'</span></td><td style="font-size:12px">' +
|
|
3610
|
+
esc(String(reason).slice(0, 80)) +
|
|
3611
|
+
'</td><td style="font-size:12px">' +
|
|
3612
|
+
esc(conf) +
|
|
3613
|
+
'</td>';
|
|
3614
|
+
tb.appendChild(tr);
|
|
3615
|
+
});
|
|
3616
|
+
})
|
|
3617
|
+
.catch(function () {
|
|
3618
|
+
if (ld) ld.style.display = 'none';
|
|
3619
|
+
if (emp) emp.style.display = '';
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
1889
3622
|
function loadSkills() {
|
|
3623
|
+
loadQuarantined();
|
|
1890
3624
|
af('/api/installed-skills')
|
|
1891
3625
|
.then(function (r) {
|
|
1892
3626
|
return r.json();
|
|
@@ -1896,6 +3630,7 @@
|
|
|
1896
3630
|
var tbl = document.getElementById('sk-table');
|
|
1897
3631
|
var tb = document.getElementById('sk-tb');
|
|
1898
3632
|
tb.innerHTML = '';
|
|
3633
|
+
skillsData = d.skills || [];
|
|
1899
3634
|
if (!d.skills || d.skills.length === 0) {
|
|
1900
3635
|
if (ld)
|
|
1901
3636
|
ld.innerHTML = '<span style="color:var(--text-muted)">No skills detected.</span>';
|
|
@@ -1917,6 +3652,29 @@
|
|
|
1917
3652
|
: status === 'blocked'
|
|
1918
3653
|
? 'badge-critical'
|
|
1919
3654
|
: 'badge-w';
|
|
3655
|
+
var actBtn =
|
|
3656
|
+
'padding:3px 9px;font-size:11px;border-radius:6px;cursor:pointer;background:transparent;';
|
|
3657
|
+
var actionsCell = s.whitelisted
|
|
3658
|
+
? '<td style="white-space:nowrap">' +
|
|
3659
|
+
'<span style="color:var(--text-muted);font-size:12px;margin-right:8px">trusted</span>' +
|
|
3660
|
+
'<button type="button" class="act" data-act="unwhitelist" data-idx="' +
|
|
3661
|
+
i +
|
|
3662
|
+
'" style="' +
|
|
3663
|
+
actBtn +
|
|
3664
|
+
'border:0.5px solid var(--border);color:var(--text-secondary)">Un-trust</button>' +
|
|
3665
|
+
'</td>'
|
|
3666
|
+
: '<td style="white-space:nowrap">' +
|
|
3667
|
+
'<button type="button" class="act" data-act="quarantine" data-idx="' +
|
|
3668
|
+
i +
|
|
3669
|
+
'" style="' +
|
|
3670
|
+
actBtn +
|
|
3671
|
+
'border:0.5px solid var(--bad,#c0504d);color:var(--bad,#c0504d)">Quarantine</button> ' +
|
|
3672
|
+
'<button type="button" class="act" data-act="whitelist" data-idx="' +
|
|
3673
|
+
i +
|
|
3674
|
+
'" style="' +
|
|
3675
|
+
actBtn +
|
|
3676
|
+
'border:0.5px solid var(--border);color:var(--text-secondary)">Mark safe</button>' +
|
|
3677
|
+
'</td>';
|
|
1920
3678
|
var tr = document.createElement('tr');
|
|
1921
3679
|
tr.innerHTML =
|
|
1922
3680
|
'<td>' +
|
|
@@ -1934,7 +3692,8 @@
|
|
|
1934
3692
|
badgeCls +
|
|
1935
3693
|
'">' +
|
|
1936
3694
|
esc(status) +
|
|
1937
|
-
'</span></td>'
|
|
3695
|
+
'</span></td>' +
|
|
3696
|
+
actionsCell;
|
|
1938
3697
|
tb.appendChild(tr);
|
|
1939
3698
|
});
|
|
1940
3699
|
setKPI('sk-total', d.skills.length);
|
|
@@ -1955,13 +3714,24 @@
|
|
|
1955
3714
|
.then(function (d) {
|
|
1956
3715
|
document.getElementById('tc-loading').style.display = 'none';
|
|
1957
3716
|
document.getElementById('tc-content').style.display = '';
|
|
1958
|
-
|
|
3717
|
+
// Three honest states: sharing OFF by choice is "Off" (neutral), NOT a
|
|
3718
|
+
// red "Disconnected" failure. Red is reserved for enabled-but-unreachable.
|
|
1959
3719
|
var dot = document.getElementById('tc-conn-dot');
|
|
1960
|
-
if (dot) dot.className = 'dot ' + (isConnected ? 'dot-ok' : 'dot-bad');
|
|
1961
3720
|
var ctext = document.getElementById('tc-conn-text');
|
|
3721
|
+
var connState = !d.uploadEnabled
|
|
3722
|
+
? { label: 'Off', cls: 'dot-off', color: 'var(--text-muted)' }
|
|
3723
|
+
: d.connected
|
|
3724
|
+
? { label: 'Connected', cls: 'dot-ok', color: 'var(--ok)' }
|
|
3725
|
+
: { label: 'Not reachable', cls: 'dot-bad', color: 'var(--danger)' };
|
|
3726
|
+
if (dot) dot.className = 'dot ' + connState.cls;
|
|
1962
3727
|
if (ctext) {
|
|
1963
|
-
ctext.textContent =
|
|
1964
|
-
ctext.style.color =
|
|
3728
|
+
ctext.textContent = connState.label;
|
|
3729
|
+
ctext.style.color = connState.color;
|
|
3730
|
+
ctext.title = !d.uploadEnabled
|
|
3731
|
+
? 'Collective defense is off — your endpoint is not sharing or syncing. Turn it on below.'
|
|
3732
|
+
: d.connected
|
|
3733
|
+
? 'Connected to the Threat Cloud relay.'
|
|
3734
|
+
: 'Collective defense is on but the relay is not reachable right now.';
|
|
1965
3735
|
}
|
|
1966
3736
|
document.getElementById('tc-endpoint').textContent = d.endpoint || '--';
|
|
1967
3737
|
setKPI('tc-recv', d.totalRulesReceived || 0);
|
|
@@ -1974,19 +3744,35 @@
|
|
|
1974
3744
|
|
|
1975
3745
|
var tog = document.getElementById('tc-toggle');
|
|
1976
3746
|
var tlbl = document.getElementById('tc-toggle-label');
|
|
3747
|
+
var tsub = document.getElementById('tc-toggle-sub');
|
|
1977
3748
|
if (d.uploadEnabled) {
|
|
1978
|
-
if (tog)
|
|
3749
|
+
if (tog) {
|
|
3750
|
+
tog.classList.add('on');
|
|
3751
|
+
tog.setAttribute('aria-checked', 'true');
|
|
3752
|
+
}
|
|
1979
3753
|
if (tlbl) {
|
|
1980
|
-
tlbl.textContent = '
|
|
3754
|
+
tlbl.textContent = 'Collective defense is ON';
|
|
1981
3755
|
tlbl.style.color = 'var(--ok)';
|
|
1982
3756
|
}
|
|
3757
|
+
if (tsub)
|
|
3758
|
+
tsub.textContent =
|
|
3759
|
+
'You are helping defend the commons. Click to turn off — sharing stops immediately.';
|
|
1983
3760
|
} else {
|
|
1984
|
-
if (tog)
|
|
3761
|
+
if (tog) {
|
|
3762
|
+
tog.classList.remove('on');
|
|
3763
|
+
tog.setAttribute('aria-checked', 'false');
|
|
3764
|
+
}
|
|
1985
3765
|
if (tlbl) {
|
|
1986
|
-
tlbl.textContent = '
|
|
3766
|
+
tlbl.textContent = 'Collective defense is OFF';
|
|
1987
3767
|
tlbl.style.color = 'var(--text-muted)';
|
|
1988
3768
|
}
|
|
3769
|
+
if (tsub)
|
|
3770
|
+
tsub.textContent =
|
|
3771
|
+
'Nothing leaves your machine. Click to turn on and share anonymized signatures.';
|
|
1989
3772
|
}
|
|
3773
|
+
// Keep the CLI command in sync with the action the toggle will take.
|
|
3774
|
+
var cli = document.getElementById('tc-cli-cmd');
|
|
3775
|
+
if (cli) cli.textContent = 'pga config set telemetry ' + (d.uploadEnabled ? 'false' : 'true');
|
|
1990
3776
|
})
|
|
1991
3777
|
.catch(function () {
|
|
1992
3778
|
document.getElementById('tc-loading').innerHTML =
|
|
@@ -1996,17 +3782,36 @@
|
|
|
1996
3782
|
function toggleUpload() {
|
|
1997
3783
|
var tog = document.getElementById('tc-toggle');
|
|
1998
3784
|
var isOn = tog.classList.contains('on');
|
|
3785
|
+
// consentGiven flips collective defense BOTH on and off: the backend syncs
|
|
3786
|
+
// telemetryEnabled + threatCloudUploadEnabled and records/clears the consent
|
|
3787
|
+
// marker. Reversible either direction. Turning ON is the higher-consequence
|
|
3788
|
+
// direction (anonymized signatures start leaving the machine), so confirm it;
|
|
3789
|
+
// turning OFF is the safe direction and stays one click.
|
|
3790
|
+
if (!isOn) {
|
|
3791
|
+
var ok = window.confirm(
|
|
3792
|
+
'Turn on collective defense?\n\n' +
|
|
3793
|
+
'When Guard blocks an attack on this machine, it will share an anonymized signature ' +
|
|
3794
|
+
'(the matched rule ID + a one-way hash of the payload + the source type) with the ATR ' +
|
|
3795
|
+
'network.\n\nNever shared: your prompts, code, file contents, keys, IP address, or ' +
|
|
3796
|
+
'hostname.\n\nYou can turn this off anytime.'
|
|
3797
|
+
);
|
|
3798
|
+
if (!ok) return;
|
|
3799
|
+
}
|
|
1999
3800
|
af('/api/threat-cloud', {
|
|
2000
3801
|
method: 'POST',
|
|
2001
3802
|
headers: { 'Content-Type': 'application/json' },
|
|
2002
|
-
body: JSON.stringify({
|
|
3803
|
+
body: JSON.stringify({ consentGiven: !isOn }),
|
|
2003
3804
|
})
|
|
2004
3805
|
.then(function (r) {
|
|
2005
3806
|
return r.json();
|
|
2006
3807
|
})
|
|
2007
3808
|
.then(function (d) {
|
|
2008
3809
|
if (d.success) {
|
|
2009
|
-
toast(
|
|
3810
|
+
toast(
|
|
3811
|
+
!isOn
|
|
3812
|
+
? 'Collective defense ON — anonymized signatures will be shared.'
|
|
3813
|
+
: 'Collective defense OFF — nothing leaves this machine.'
|
|
3814
|
+
);
|
|
2010
3815
|
loadTCloud();
|
|
2011
3816
|
} else {
|
|
2012
3817
|
toast(d.error || 'Error');
|
|
@@ -2017,47 +3822,216 @@
|
|
|
2017
3822
|
});
|
|
2018
3823
|
}
|
|
2019
3824
|
|
|
3825
|
+
/* Guard mode is THREE states (learning / report-only / protection). The
|
|
3826
|
+
old binary toggle could not represent report-only and silently rewrote
|
|
3827
|
+
it to 'learning' on save — quietly downgrading enforcement. This 3-state
|
|
3828
|
+
segmented control preserves whatever the config actually is. */
|
|
3829
|
+
var selectedMode = 'learning';
|
|
3830
|
+
var _modePendingRestart = false;
|
|
3831
|
+
var MODE_INFO = {
|
|
3832
|
+
learning: {
|
|
3833
|
+
desc: 'builds a baseline of normal activity and only logs — no blocking.',
|
|
3834
|
+
color: 'var(--warn)',
|
|
3835
|
+
},
|
|
3836
|
+
'report-only': {
|
|
3837
|
+
desc: 'detects and records everything but never acts (recommended for new deployments).',
|
|
3838
|
+
color: 'var(--sage, #8b9a8e)',
|
|
3839
|
+
},
|
|
3840
|
+
protection: {
|
|
3841
|
+
desc: 'actively blocks detected threats per your enforcement policy.',
|
|
3842
|
+
color: 'var(--ok)',
|
|
3843
|
+
},
|
|
3844
|
+
};
|
|
3845
|
+
function renderModeSeg() {
|
|
3846
|
+
var seg = document.getElementById('st-mode-seg');
|
|
3847
|
+
if (!seg) return;
|
|
3848
|
+
Array.prototype.forEach.call(seg.querySelectorAll('.seg-btn'), function (b) {
|
|
3849
|
+
var on = b.getAttribute('data-mode') === selectedMode;
|
|
3850
|
+
b.style.background = on ? 'var(--surface-3, #302b27)' : 'transparent';
|
|
3851
|
+
b.style.color = on
|
|
3852
|
+
? MODE_INFO[selectedMode]
|
|
3853
|
+
? MODE_INFO[selectedMode].color
|
|
3854
|
+
: 'var(--text)'
|
|
3855
|
+
: 'var(--text-muted)';
|
|
3856
|
+
b.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
3857
|
+
});
|
|
3858
|
+
var d = document.getElementById('st-mode-desc');
|
|
3859
|
+
if (d && MODE_INFO[selectedMode]) {
|
|
3860
|
+
if (_modePendingRestart && selectedMode === 'protection') {
|
|
3861
|
+
// Preserve the pending-restart note across loadSettings() re-renders
|
|
3862
|
+
// so the OS-response caveat does not vanish on the next refresh.
|
|
3863
|
+
d.innerHTML =
|
|
3864
|
+
'This mode ' +
|
|
3865
|
+
esc(MODE_INFO[selectedMode].desc) +
|
|
3866
|
+
' <span style="color:var(--warn)">Pending restart: automatic OS responses arm after ' +
|
|
3867
|
+
'you restart Guard (<code>pga guard restart</code>).</span>';
|
|
3868
|
+
} else {
|
|
3869
|
+
d.textContent = 'This mode ' + MODE_INFO[selectedMode].desc;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
function selectMode(m) {
|
|
3874
|
+
if (!MODE_INFO[m]) return;
|
|
3875
|
+
// A fresh selection clears any prior pending-restart note; it re-arms
|
|
3876
|
+
// only after the next protection-mode save.
|
|
3877
|
+
if (m !== selectedMode) _modePendingRestart = false;
|
|
3878
|
+
selectedMode = m;
|
|
3879
|
+
renderModeSeg();
|
|
3880
|
+
}
|
|
3881
|
+
/* Optional alert channels. The dashboard NEVER sets channel secrets over
|
|
3882
|
+
HTTP (that was removed as an exfil surface). For each channel that Guard
|
|
3883
|
+
actually supports (NotificationConfig in types.ts: email / slack /
|
|
3884
|
+
telegram / line / webhook) it shows what the channel is, whether it is
|
|
3885
|
+
configured, and the exact JSON to paste into config.json. Placeholders
|
|
3886
|
+
only — the user fills in their own secret in the file, not here. */
|
|
3887
|
+
var NOTIFY_CHANNELS = [
|
|
3888
|
+
{
|
|
3889
|
+
key: 'slack',
|
|
3890
|
+
name: 'Slack',
|
|
3891
|
+
what: 'Post each alert into a Slack channel using an Incoming Webhook URL from Slack.',
|
|
3892
|
+
snippet: '"slack": { "webhookUrl": "https://hooks.slack.com/services/XXX/YYY/ZZZ" }',
|
|
3893
|
+
},
|
|
3894
|
+
{
|
|
3895
|
+
key: 'telegram',
|
|
3896
|
+
name: 'Telegram',
|
|
3897
|
+
what: 'Message you (or a group) through a Telegram bot. Needs the bot token and chat ID.',
|
|
3898
|
+
snippet: '"telegram": { "botToken": "YOUR_BOT_TOKEN", "chatId": "YOUR_CHAT_ID" }',
|
|
3899
|
+
},
|
|
3900
|
+
{
|
|
3901
|
+
key: 'email',
|
|
3902
|
+
name: 'Email',
|
|
3903
|
+
what: 'Email each alert through your own SMTP server.',
|
|
3904
|
+
snippet:
|
|
3905
|
+
'"email": { "host": "smtp.example.com", "port": 587, "secure": false, "auth": { "user": "YOU", "pass": "YOUR_SMTP_PASSWORD" }, "from": "guard@example.com", "to": ["you@example.com"] }',
|
|
3906
|
+
},
|
|
3907
|
+
{
|
|
3908
|
+
key: 'line',
|
|
3909
|
+
name: 'LINE',
|
|
3910
|
+
what: 'Send each alert to LINE using a LINE Notify access token.',
|
|
3911
|
+
snippet: '"line": { "accessToken": "YOUR_LINE_NOTIFY_TOKEN" }',
|
|
3912
|
+
},
|
|
3913
|
+
{
|
|
3914
|
+
key: 'webhook',
|
|
3915
|
+
name: 'Webhook',
|
|
3916
|
+
what: 'POST each alert as JSON to any URL you own (wire it into your own tooling).',
|
|
3917
|
+
snippet: '"webhook": { "url": "https://example.com/guard-hook", "secret": "OPTIONAL_SHARED_SECRET" }',
|
|
3918
|
+
},
|
|
3919
|
+
];
|
|
3920
|
+
var notifySnippetSeq = 0;
|
|
3921
|
+
function renderNotifyChannels(n) {
|
|
3922
|
+
n = n || {};
|
|
3923
|
+
var el = document.getElementById('st-notify-list');
|
|
3924
|
+
if (!el) return;
|
|
3925
|
+
// If the user already configured a channel, open the optional section so
|
|
3926
|
+
// they can see/manage it; otherwise keep it collapsed (zero-config default).
|
|
3927
|
+
var anyOn = NOTIFY_CHANNELS.some(function (c) {
|
|
3928
|
+
return !!n[c.key];
|
|
3929
|
+
});
|
|
3930
|
+
var details = document.getElementById('notify-details');
|
|
3931
|
+
if (details && anyOn) details.setAttribute('open', '');
|
|
3932
|
+
el.innerHTML = NOTIFY_CHANNELS.map(function (c) {
|
|
3933
|
+
var on = !!n[c.key];
|
|
3934
|
+
var stateHtml = on
|
|
3935
|
+
? '<span class="nc-state" style="color:var(--ok)">configured</span>'
|
|
3936
|
+
: '<span class="nc-state" style="color:var(--text-muted)">not set up</span>';
|
|
3937
|
+
var cmdId = 'nc-cmd-' + ++notifySnippetSeq;
|
|
3938
|
+
return (
|
|
3939
|
+
'<div class="nc">' +
|
|
3940
|
+
'<div class="nc-head"><span class="nc-name">' +
|
|
3941
|
+
esc(c.name) +
|
|
3942
|
+
'</span>' +
|
|
3943
|
+
stateHtml +
|
|
3944
|
+
'</div>' +
|
|
3945
|
+
'<div class="nc-what">' +
|
|
3946
|
+
esc(c.what) +
|
|
3947
|
+
'</div>' +
|
|
3948
|
+
'<div class="cmd-box">' +
|
|
3949
|
+
'<code id="' +
|
|
3950
|
+
cmdId +
|
|
3951
|
+
'">' +
|
|
3952
|
+
esc(c.snippet) +
|
|
3953
|
+
'</code>' +
|
|
3954
|
+
'<button class="cmd-copy" type="button" data-copy-target="' +
|
|
3955
|
+
cmdId +
|
|
3956
|
+
'">Copy</button>' +
|
|
3957
|
+
'</div>' +
|
|
3958
|
+
'</div>'
|
|
3959
|
+
);
|
|
3960
|
+
}).join('');
|
|
3961
|
+
el.innerHTML +=
|
|
3962
|
+
'<div style="font-size:12px;color:var(--text-muted);margin-top:2px">' +
|
|
3963
|
+
'Add the line inside the <code>"notifications"</code> object in ' +
|
|
3964
|
+
'<code>~/.panguard-guard/config.json</code>, then restart Guard. ' +
|
|
3965
|
+
'Without any channel, alerts still appear in this dashboard.</div>';
|
|
3966
|
+
}
|
|
3967
|
+
/* Read-only enforcement-policy (Protection-mode blast radius). Shows which
|
|
3968
|
+
automatic destructive responses are armed, so enabling Protection is not
|
|
3969
|
+
a black box. Configured in config.json, not here. */
|
|
3970
|
+
function renderEnforcement(p) {
|
|
3971
|
+
var el = document.getElementById('st-enforce-list');
|
|
3972
|
+
if (!el) return;
|
|
3973
|
+
if (!p) {
|
|
3974
|
+
el.innerHTML =
|
|
3975
|
+
'<div style="color:var(--text-muted)">Default — detections are logged and you are ' +
|
|
3976
|
+
'notified; no automatic destructive action is taken.</div>';
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
var acts = [
|
|
3980
|
+
['blockIPs', 'Block attacker IPs'],
|
|
3981
|
+
['killProcesses', 'Kill malicious processes'],
|
|
3982
|
+
['isolateFiles', 'Isolate / quarantine files'],
|
|
3983
|
+
['disableAccounts', 'Disable accounts'],
|
|
3984
|
+
];
|
|
3985
|
+
el.innerHTML = acts
|
|
3986
|
+
.map(function (a) {
|
|
3987
|
+
var on = p[a[0]] && p[a[0]].enabled;
|
|
3988
|
+
return (
|
|
3989
|
+
'<div>' +
|
|
3990
|
+
(on
|
|
3991
|
+
? '<span style="color:var(--danger)">● armed</span>'
|
|
3992
|
+
: '<span style="color:var(--text-muted)">○ off</span>') +
|
|
3993
|
+
' — ' +
|
|
3994
|
+
esc(a[1]) +
|
|
3995
|
+
'</div>'
|
|
3996
|
+
);
|
|
3997
|
+
})
|
|
3998
|
+
.join('');
|
|
3999
|
+
}
|
|
2020
4000
|
function loadSettings() {
|
|
4001
|
+
// Reflect Layer C connected/off state when the tab is opened cold, from
|
|
4002
|
+
// the last live status (the layer state rides on /api/status, not /api/config).
|
|
4003
|
+
if (_lastStatus) renderLayerCSettings(_lastStatus.layers);
|
|
2021
4004
|
af('/api/config')
|
|
2022
4005
|
.then(function (r) {
|
|
2023
4006
|
return r.json();
|
|
2024
4007
|
})
|
|
2025
4008
|
.then(function (cfg) {
|
|
2026
4009
|
if (!cfg) return;
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
if (ml) {
|
|
2035
|
-
ml.textContent = isProt ? 'Protection' : 'Learning';
|
|
2036
|
-
ml.style.color = isProt ? 'var(--ok)' : 'var(--warn)';
|
|
2037
|
-
}
|
|
4010
|
+
// Preserve the real mode — including report-only — instead of
|
|
4011
|
+
// collapsing it to a binary on/off.
|
|
4012
|
+
selectedMode =
|
|
4013
|
+
cfg.mode === 'protection' || cfg.mode === 'report-only' || cfg.mode === 'learning'
|
|
4014
|
+
? cfg.mode
|
|
4015
|
+
: 'learning';
|
|
4016
|
+
renderModeSeg();
|
|
2038
4017
|
var ep = document.getElementById('st-tc-endpoint');
|
|
2039
4018
|
if (ep) ep.value = cfg.threatCloudEndpoint || '';
|
|
4019
|
+
// Show the REAL port: the one this dashboard is actually served on
|
|
4020
|
+
// (location.port) is the ground truth a user needs to reconnect, not
|
|
4021
|
+
// a hard-coded fallback that can be flat wrong.
|
|
2040
4022
|
var port = document.getElementById('st-port');
|
|
2041
|
-
if (port) port.textContent = cfg.dashboardPort ||
|
|
4023
|
+
if (port) port.textContent = cfg.dashboardPort || location.port || '3100';
|
|
4024
|
+
renderNotifyChannels(cfg.notifications || {});
|
|
4025
|
+
renderEnforcement(cfg.enforcementPolicy);
|
|
2042
4026
|
})
|
|
2043
4027
|
.catch(function () {
|
|
2044
4028
|
/* ignore */
|
|
2045
4029
|
});
|
|
2046
4030
|
}
|
|
2047
|
-
function toggleMode() {
|
|
2048
|
-
var t = document.getElementById('st-mode-toggle');
|
|
2049
|
-
t.classList.toggle('on');
|
|
2050
|
-
var l = document.getElementById('st-mode-label');
|
|
2051
|
-
var isProt = t.classList.contains('on');
|
|
2052
|
-
if (l) {
|
|
2053
|
-
l.textContent = isProt ? 'Protection' : 'Learning';
|
|
2054
|
-
l.style.color = isProt ? 'var(--ok)' : 'var(--warn)';
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
4031
|
function saveSettings() {
|
|
2058
|
-
var
|
|
4032
|
+
var priorMode = selectedMode;
|
|
2059
4033
|
var body = {
|
|
2060
|
-
mode:
|
|
4034
|
+
mode: selectedMode,
|
|
2061
4035
|
endpoint: document.getElementById('st-tc-endpoint').value || undefined,
|
|
2062
4036
|
};
|
|
2063
4037
|
af('/api/threat-cloud', {
|
|
@@ -2070,16 +4044,41 @@
|
|
|
2070
4044
|
})
|
|
2071
4045
|
.then(function (d) {
|
|
2072
4046
|
if (d.success) {
|
|
2073
|
-
|
|
4047
|
+
// The daemon now live-applies the change (detection re-arms at
|
|
4048
|
+
// once), so loadSettings() re-reads the NEW mode — the control no
|
|
4049
|
+
// longer snaps back. Protection mode's OS-level response actions
|
|
4050
|
+
// still need a restart, so surface that as a pending state rather
|
|
4051
|
+
// than implying nothing happened.
|
|
4052
|
+
if (body.mode === 'protection') {
|
|
4053
|
+
toast('Saved. Detection is live now; restart Guard to arm automatic responses.');
|
|
4054
|
+
showModePendingRestart();
|
|
4055
|
+
} else {
|
|
4056
|
+
toast('Saved — applied immediately.');
|
|
4057
|
+
}
|
|
2074
4058
|
loadSettings();
|
|
2075
4059
|
} else {
|
|
2076
4060
|
toast(d.error || 'Save failed');
|
|
4061
|
+
// Revert the optimistic selection only when the save was rejected.
|
|
4062
|
+
selectMode(priorMode);
|
|
2077
4063
|
}
|
|
2078
4064
|
})
|
|
2079
4065
|
.catch(function () {
|
|
2080
4066
|
toast('Network error');
|
|
2081
4067
|
});
|
|
2082
4068
|
}
|
|
4069
|
+
/* Keep the optimistic mode value and show a clear "pending restart" note
|
|
4070
|
+
for the part that genuinely needs a restart (OS-level responses) —
|
|
4071
|
+
never snap the control back to the old value. */
|
|
4072
|
+
function showModePendingRestart() {
|
|
4073
|
+
_modePendingRestart = true;
|
|
4074
|
+
var d = document.getElementById('st-mode-desc');
|
|
4075
|
+
if (d) {
|
|
4076
|
+
d.innerHTML =
|
|
4077
|
+
(MODE_INFO[selectedMode] ? 'This mode ' + esc(MODE_INFO[selectedMode].desc) : '') +
|
|
4078
|
+
' <span style="color:var(--warn)">Pending restart: automatic OS responses arm after ' +
|
|
4079
|
+
'you restart Guard (<code>pga guard restart</code>).</span>';
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
2083
4082
|
|
|
2084
4083
|
/* ============================================================ */
|
|
2085
4084
|
/* Event wiring (CSP-safe: no inline handlers) */
|
|
@@ -2088,12 +4087,165 @@
|
|
|
2088
4087
|
var el = document.getElementById(id);
|
|
2089
4088
|
if (el) el.addEventListener(ev, fn);
|
|
2090
4089
|
}
|
|
4090
|
+
function skillAction(act, idx) {
|
|
4091
|
+
var s = skillsData[idx];
|
|
4092
|
+
if (!s || !s.name) return;
|
|
4093
|
+
var name = s.name;
|
|
4094
|
+
if (
|
|
4095
|
+
act === 'quarantine' &&
|
|
4096
|
+
!window.confirm(
|
|
4097
|
+
'Quarantine "' +
|
|
4098
|
+
name +
|
|
4099
|
+
'"?\n\n' +
|
|
4100
|
+
'This moves the skill file out of your agent’s reach, so the agent can no longer use ' +
|
|
4101
|
+
'it. The file is NOT deleted — it moves to ~/.panguard/quarantine and you can restore ' +
|
|
4102
|
+
'it anytime from the Quarantined Files list below.'
|
|
4103
|
+
)
|
|
4104
|
+
) {
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
if (
|
|
4108
|
+
act === 'whitelist' &&
|
|
4109
|
+
!window.confirm(
|
|
4110
|
+
'Mark "' +
|
|
4111
|
+
name +
|
|
4112
|
+
'" as safe?\n\n' +
|
|
4113
|
+
'Guard will trust this skill and stop flagging it for behavior drift — this reduces ' +
|
|
4114
|
+
'monitoring for it. Only do this for skills you installed and trust. You can undo it ' +
|
|
4115
|
+
'later with Un-trust.'
|
|
4116
|
+
)
|
|
4117
|
+
) {
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
var EPMAP = {
|
|
4121
|
+
quarantine: '/api/skills/quarantine',
|
|
4122
|
+
whitelist: '/api/skills/whitelist',
|
|
4123
|
+
unwhitelist: '/api/skills/unwhitelist',
|
|
4124
|
+
};
|
|
4125
|
+
var ep = EPMAP[act];
|
|
4126
|
+
if (!ep) return;
|
|
4127
|
+
af(ep, {
|
|
4128
|
+
method: 'POST',
|
|
4129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4130
|
+
body: JSON.stringify({ name: name }),
|
|
4131
|
+
})
|
|
4132
|
+
.then(function (r) {
|
|
4133
|
+
return r.json().then(function (d) {
|
|
4134
|
+
return { ok: r.ok, d: d };
|
|
4135
|
+
});
|
|
4136
|
+
})
|
|
4137
|
+
.then(function (res) {
|
|
4138
|
+
if (res.ok && res.d && res.d.success) {
|
|
4139
|
+
var msg =
|
|
4140
|
+
act === 'quarantine'
|
|
4141
|
+
? 'Quarantined ' + name + ' — moved out of the agent’s reach (restorable below).'
|
|
4142
|
+
: act === 'unwhitelist'
|
|
4143
|
+
? 'Un-trusted ' + name + ' — Guard will evaluate it again like any unknown skill.'
|
|
4144
|
+
: 'Marked safe — Guard will stop flagging ' + name + '.';
|
|
4145
|
+
toast(msg);
|
|
4146
|
+
loadSkills();
|
|
4147
|
+
loadQuarantined();
|
|
4148
|
+
} else {
|
|
4149
|
+
toast((res.d && res.d.error) || 'Action failed');
|
|
4150
|
+
}
|
|
4151
|
+
})
|
|
4152
|
+
.catch(function () {
|
|
4153
|
+
toast('Action failed');
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
4156
|
+
/* Quarantined-files list + restore — honors the 'can be restored' promise
|
|
4157
|
+
the quarantine dialog makes. Reads /api/skills/quarantined; Restore POSTs
|
|
4158
|
+
/api/skills/restore. Hidden when nothing is quarantined. */
|
|
4159
|
+
function loadQuarantined() {
|
|
4160
|
+
var sec = document.getElementById('qz-section');
|
|
4161
|
+
var tb = document.getElementById('qz-tb');
|
|
4162
|
+
if (!sec || !tb) return;
|
|
4163
|
+
af('/api/skills/quarantined')
|
|
4164
|
+
.then(function (r) {
|
|
4165
|
+
return r.json();
|
|
4166
|
+
})
|
|
4167
|
+
.then(function (d) {
|
|
4168
|
+
var recs = (d && d.records) || [];
|
|
4169
|
+
if (!recs.length) {
|
|
4170
|
+
sec.style.display = 'none';
|
|
4171
|
+
tb.innerHTML = '';
|
|
4172
|
+
return;
|
|
4173
|
+
}
|
|
4174
|
+
sec.style.display = '';
|
|
4175
|
+
tb.innerHTML = '';
|
|
4176
|
+
recs.forEach(function (rec) {
|
|
4177
|
+
var when = rec.quarantinedAt ? new Date(rec.quarantinedAt).toLocaleString() : '--';
|
|
4178
|
+
var tr = document.createElement('tr');
|
|
4179
|
+
tr.innerHTML =
|
|
4180
|
+
'<td class="mono" style="font-size:12px">' +
|
|
4181
|
+
esc(rec.originalPath || '') +
|
|
4182
|
+
'</td>' +
|
|
4183
|
+
'<td style="font-size:12px">' +
|
|
4184
|
+
esc(rec.reason || '') +
|
|
4185
|
+
'</td>' +
|
|
4186
|
+
'<td style="font-size:12px">' +
|
|
4187
|
+
esc(when) +
|
|
4188
|
+
'</td>' +
|
|
4189
|
+
'<td style="white-space:nowrap">' +
|
|
4190
|
+
'<button type="button" class="act" data-restore-id="' +
|
|
4191
|
+
esc(rec.id || '') +
|
|
4192
|
+
'" style="padding:3px 9px;font-size:11px;border-radius:6px;cursor:pointer;' +
|
|
4193
|
+
'background:transparent;border:0.5px solid var(--emerald,#34d399);' +
|
|
4194
|
+
'color:var(--emerald,#34d399)">Restore</button></td>';
|
|
4195
|
+
tb.appendChild(tr);
|
|
4196
|
+
});
|
|
4197
|
+
})
|
|
4198
|
+
.catch(function () {
|
|
4199
|
+
sec.style.display = 'none';
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
function restoreQuarantined(id) {
|
|
4203
|
+
if (!id) return;
|
|
4204
|
+
af('/api/skills/restore', {
|
|
4205
|
+
method: 'POST',
|
|
4206
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4207
|
+
body: JSON.stringify({ id: id }),
|
|
4208
|
+
})
|
|
4209
|
+
.then(function (r) {
|
|
4210
|
+
return r.json().then(function (d) {
|
|
4211
|
+
return { ok: r.ok, d: d };
|
|
4212
|
+
});
|
|
4213
|
+
})
|
|
4214
|
+
.then(function (res) {
|
|
4215
|
+
if (res.ok && res.d && res.d.success) {
|
|
4216
|
+
toast('Restored');
|
|
4217
|
+
loadQuarantined();
|
|
4218
|
+
loadSkills();
|
|
4219
|
+
} else {
|
|
4220
|
+
toast((res.d && res.d.error) || 'Restore failed');
|
|
4221
|
+
}
|
|
4222
|
+
})
|
|
4223
|
+
.catch(function () {
|
|
4224
|
+
toast('Restore failed');
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
2091
4227
|
function wireEvents() {
|
|
2092
4228
|
document.querySelectorAll('.fb').forEach(function (b) {
|
|
2093
4229
|
b.addEventListener('click', function (e) {
|
|
2094
4230
|
filterThreats(b.getAttribute('data-sev'), e);
|
|
2095
4231
|
});
|
|
2096
4232
|
});
|
|
4233
|
+
var skTb = document.getElementById('sk-tb');
|
|
4234
|
+
if (skTb) {
|
|
4235
|
+
skTb.addEventListener('click', function (e) {
|
|
4236
|
+
var btn = e.target && e.target.closest ? e.target.closest('button.act') : null;
|
|
4237
|
+
if (!btn) return;
|
|
4238
|
+
skillAction(btn.getAttribute('data-act'), parseInt(btn.getAttribute('data-idx'), 10));
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
4241
|
+
var qzTb = document.getElementById('qz-tb');
|
|
4242
|
+
if (qzTb) {
|
|
4243
|
+
qzTb.addEventListener('click', function (e) {
|
|
4244
|
+
var btn =
|
|
4245
|
+
e.target && e.target.closest ? e.target.closest('button[data-restore-id]') : null;
|
|
4246
|
+
if (btn) restoreQuarantined(btn.getAttribute('data-restore-id'));
|
|
4247
|
+
});
|
|
4248
|
+
}
|
|
2097
4249
|
document.querySelectorAll('th[data-sort]').forEach(function (th) {
|
|
2098
4250
|
th.addEventListener('click', function () {
|
|
2099
4251
|
sortThreats(th.getAttribute('data-sort'));
|
|
@@ -2119,7 +4271,72 @@
|
|
|
2119
4271
|
toast('Refreshing skills…');
|
|
2120
4272
|
});
|
|
2121
4273
|
on('tc-toggle', 'click', toggleUpload);
|
|
2122
|
-
|
|
4274
|
+
// The toggle is a role=switch — make it keyboard-operable (Space/Enter).
|
|
4275
|
+
on('tc-toggle', 'keydown', function (e) {
|
|
4276
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
4277
|
+
e.preventDefault();
|
|
4278
|
+
toggleUpload();
|
|
4279
|
+
}
|
|
4280
|
+
});
|
|
4281
|
+
// Delegated copy-to-clipboard for every [data-copy-target] command box.
|
|
4282
|
+
document.addEventListener('click', function (e) {
|
|
4283
|
+
var btn = e.target && e.target.closest ? e.target.closest('[data-copy-target]') : null;
|
|
4284
|
+
if (!btn) return;
|
|
4285
|
+
var src = document.getElementById(btn.getAttribute('data-copy-target'));
|
|
4286
|
+
if (!src) return;
|
|
4287
|
+
copyText(src.textContent || '', btn);
|
|
4288
|
+
});
|
|
4289
|
+
var modeSeg = document.getElementById('st-mode-seg');
|
|
4290
|
+
if (modeSeg) {
|
|
4291
|
+
modeSeg.addEventListener('click', function (e) {
|
|
4292
|
+
var btn = e.target && e.target.closest ? e.target.closest('button.seg-btn') : null;
|
|
4293
|
+
if (btn) selectMode(btn.getAttribute('data-mode'));
|
|
4294
|
+
});
|
|
4295
|
+
}
|
|
4296
|
+
var postureDed = document.getElementById('posture-deductions');
|
|
4297
|
+
if (postureDed) {
|
|
4298
|
+
postureDed.addEventListener('click', function (e) {
|
|
4299
|
+
var btn = e.target && e.target.closest ? e.target.closest('button[data-goto]') : null;
|
|
4300
|
+
if (btn) nav(btn.getAttribute('data-goto'));
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
4303
|
+
on('conn-copy', 'click', function () {
|
|
4304
|
+
copyText('pga up', document.getElementById('conn-copy'));
|
|
4305
|
+
});
|
|
4306
|
+
// Layer C CTA on Overview deep-links to the Settings section.
|
|
4307
|
+
on('lc-cta-btn', 'click', function () {
|
|
4308
|
+
nav('settings');
|
|
4309
|
+
setTimeout(function () {
|
|
4310
|
+
var s = document.getElementById('p-settings-layerc');
|
|
4311
|
+
if (s) s.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
4312
|
+
}, 60);
|
|
4313
|
+
});
|
|
4314
|
+
// Welcome guide: "Got it"/X persist the dismissal; "Dismiss for now" returns
|
|
4315
|
+
// next launch; the sidebar link reopens it. Its [data-goto] button is not
|
|
4316
|
+
// covered by the posture-deductions handler, so delegate nav here.
|
|
4317
|
+
on('welcome-done', 'click', function () {
|
|
4318
|
+
hideWelcome(true);
|
|
4319
|
+
});
|
|
4320
|
+
on('welcome-x', 'click', function () {
|
|
4321
|
+
hideWelcome(true);
|
|
4322
|
+
});
|
|
4323
|
+
on('welcome-later', 'click', function () {
|
|
4324
|
+
hideWelcome(false);
|
|
4325
|
+
});
|
|
4326
|
+
on('welcome-reopen', 'click', reopenWelcome);
|
|
4327
|
+
on('welcome-reopen', 'keydown', function (e) {
|
|
4328
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
4329
|
+
e.preventDefault();
|
|
4330
|
+
reopenWelcome();
|
|
4331
|
+
}
|
|
4332
|
+
});
|
|
4333
|
+
var wg = document.getElementById('welcome-guide');
|
|
4334
|
+
if (wg) {
|
|
4335
|
+
wg.addEventListener('click', function (e) {
|
|
4336
|
+
var b = e.target && e.target.closest ? e.target.closest('button[data-goto]') : null;
|
|
4337
|
+
if (b) nav(b.getAttribute('data-goto'));
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
2123
4340
|
on('btn-save', 'click', saveSettings);
|
|
2124
4341
|
on('btn-reload', 'click', loadSettings);
|
|
2125
4342
|
}
|
|
@@ -2128,12 +4345,18 @@
|
|
|
2128
4345
|
/* Boot */
|
|
2129
4346
|
/* ============================================================ */
|
|
2130
4347
|
(function () {
|
|
4348
|
+
setConnState('connecting');
|
|
2131
4349
|
wireEvents();
|
|
2132
4350
|
connectWS();
|
|
2133
4351
|
loadStatus();
|
|
2134
4352
|
loadThreats();
|
|
2135
4353
|
// Mark default nav active
|
|
2136
4354
|
nav('overview');
|
|
4355
|
+
// Fallback: if neither the WS push nor the initial fetch yields a live
|
|
4356
|
+
// status within a few seconds, Guard is almost certainly not running.
|
|
4357
|
+
_connectTimer = setTimeout(function () {
|
|
4358
|
+
if (_connState !== 'live' && !_unauthShown) setConnState('down');
|
|
4359
|
+
}, 6000);
|
|
2137
4360
|
})();
|
|
2138
4361
|
</script>
|
|
2139
4362
|
</body>
|