averecion-lite 1.5.2 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -443,46 +443,6 @@ body {
443
443
  color: var(--text-muted);
444
444
  }
445
445
 
446
- .safety-grid {
447
- display: grid;
448
- gap: 0.75rem;
449
- }
450
-
451
- .safety-item {
452
- display: flex;
453
- align-items: center;
454
- gap: 0.75rem;
455
- padding: 1rem;
456
- background: var(--bg-card);
457
- border-radius: 12px;
458
- border: 1px solid var(--border);
459
- }
460
-
461
- .safety-item.pass {
462
- border-color: rgba(34, 197, 94, 0.3);
463
- }
464
-
465
- .safety-item.fail {
466
- border-color: rgba(239, 68, 68, 0.3);
467
- }
468
-
469
- .check-icon {
470
- width: 24px;
471
- height: 24px;
472
- border-radius: 50%;
473
- display: flex;
474
- align-items: center;
475
- justify-content: center;
476
- font-size: 0.8rem;
477
- }
478
-
479
- .safety-item.pass .check-icon {
480
- background: var(--success);
481
- }
482
-
483
- .safety-item.fail .check-icon {
484
- background: var(--danger);
485
- }
486
446
 
487
447
  .activity-list {
488
448
  background: var(--bg-card);
@@ -606,75 +566,251 @@ body {
606
566
  cursor: help;
607
567
  }
608
568
 
609
- .protection-section {
569
+ .protection-safety-section {
610
570
  margin-bottom: 1.5rem;
611
571
  }
612
572
 
613
- .protection-score {
573
+ .protection-safety-card {
614
574
  background: var(--bg-card);
615
575
  border-radius: 16px;
616
- padding: 1.5rem;
576
+ padding: 1.25rem;
617
577
  border: 1px solid var(--border);
618
578
  }
619
579
 
620
- .score-header {
580
+ .ps-header {
621
581
  display: flex;
622
582
  align-items: center;
623
- gap: 1rem;
583
+ gap: 0.75rem;
624
584
  margin-bottom: 1rem;
585
+ padding-bottom: 0.75rem;
586
+ border-bottom: 1px solid var(--border);
625
587
  }
626
588
 
627
- .score-value {
628
- font-size: 2rem;
589
+ .ps-score {
590
+ font-size: 1.75rem;
629
591
  font-weight: 700;
630
592
  color: var(--success);
631
593
  }
632
594
 
633
- .score-label {
595
+ .ps-label {
634
596
  color: var(--text-secondary);
635
- font-size: 1rem;
597
+ font-size: 0.95rem;
636
598
  }
637
599
 
638
- .score-bars {
639
- display: grid;
640
- grid-template-columns: repeat(4, 1fr);
641
- gap: 0.75rem;
600
+ .ps-checks {
601
+ display: flex;
602
+ flex-direction: column;
603
+ gap: 0.5rem;
642
604
  }
643
605
 
644
- .score-bar {
606
+ .ps-check {
645
607
  display: flex;
646
- flex-direction: column;
647
608
  align-items: center;
648
- gap: 0.5rem;
649
- padding: 1rem 0.5rem;
609
+ gap: 0.75rem;
610
+ padding: 0.6rem 0.75rem;
650
611
  background: var(--bg-dark);
651
- border-radius: 12px;
652
- border: 2px solid var(--border);
612
+ border-radius: 10px;
613
+ border: 1px solid var(--border);
653
614
  cursor: help;
654
- transition: border-color 0.2s, transform 0.2s;
615
+ transition: border-color 0.2s;
655
616
  }
656
617
 
657
- .score-bar.active {
658
- border-color: var(--success);
618
+ .ps-check.active {
619
+ border-color: rgba(34, 197, 94, 0.3);
659
620
  }
660
621
 
661
- .score-bar.inactive {
662
- border-color: var(--danger);
663
- opacity: 0.6;
622
+ .ps-check.inactive {
623
+ border-color: rgba(239, 68, 68, 0.3);
624
+ opacity: 0.7;
664
625
  }
665
626
 
666
- .score-bar:hover {
667
- transform: translateY(-2px);
627
+ .ps-check-icon {
628
+ font-size: 1.1rem;
668
629
  }
669
630
 
670
- .bar-icon {
671
- font-size: 1.5rem;
631
+ .ps-check-label {
632
+ flex: 1;
633
+ font-size: 0.85rem;
634
+ color: var(--text-secondary);
672
635
  }
673
636
 
674
- .bar-label {
675
- font-size: 0.75rem;
637
+ .ps-check-status {
638
+ font-size: 0.8rem;
639
+ font-weight: 700;
640
+ color: var(--success);
641
+ width: 20px;
642
+ height: 20px;
643
+ border-radius: 50%;
644
+ background: rgba(34, 197, 94, 0.2);
645
+ display: flex;
646
+ align-items: center;
647
+ justify-content: center;
648
+ }
649
+
650
+ .ps-check.inactive .ps-check-status {
651
+ color: var(--danger);
652
+ background: rgba(239, 68, 68, 0.2);
653
+ }
654
+
655
+ .header-scan-btn {
656
+ background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
657
+ border: none;
658
+ color: #fff;
659
+ padding: 0.5rem 1rem;
660
+ border-radius: 8px;
661
+ font-size: 0.85rem;
662
+ font-weight: 600;
663
+ cursor: pointer;
664
+ transition: all 0.2s;
665
+ box-shadow: 0 2px 10px rgba(124, 58, 237, 0.3);
666
+ }
667
+
668
+ .header-scan-btn:hover {
669
+ transform: translateY(-1px);
670
+ box-shadow: 0 4px 15px rgba(124, 58, 237, 0.4);
671
+ }
672
+
673
+ .section-accordion {
674
+ display: flex;
675
+ align-items: center;
676
+ justify-content: space-between;
677
+ width: 100%;
678
+ padding: 0.85rem 1rem;
679
+ background: var(--bg-card);
680
+ border: 1px solid var(--border);
681
+ border-radius: 12px;
676
682
  color: var(--text-secondary);
683
+ font-size: 0.9rem;
684
+ font-weight: 500;
685
+ cursor: pointer;
686
+ transition: all 0.2s;
687
+ }
688
+
689
+ .section-accordion:hover {
690
+ border-color: rgba(255, 255, 255, 0.15);
691
+ color: var(--text-primary);
692
+ }
693
+
694
+ .accordion-arrow {
695
+ font-size: 0.7rem;
696
+ transition: transform 0.2s;
697
+ }
698
+
699
+ .section-accordion.expanded .accordion-arrow {
700
+ transform: rotate(90deg);
701
+ }
702
+
703
+ .security-arch-grid.collapsed {
704
+ display: none;
705
+ }
706
+
707
+ .modal-overlay {
708
+ position: fixed;
709
+ top: 0;
710
+ left: 0;
711
+ right: 0;
712
+ bottom: 0;
713
+ background: rgba(0, 0, 0, 0.7);
714
+ backdrop-filter: blur(4px);
715
+ z-index: 2000;
716
+ display: flex;
717
+ align-items: center;
718
+ justify-content: center;
719
+ padding: 2rem;
720
+ animation: fadeIn 0.2s ease;
721
+ }
722
+
723
+ .modal-overlay.hidden {
724
+ display: none;
725
+ }
726
+
727
+ .modal-content {
728
+ background: var(--bg-dark);
729
+ border: 1px solid var(--border);
730
+ border-radius: 20px;
731
+ width: 100%;
732
+ max-width: 700px;
733
+ max-height: 85vh;
734
+ display: flex;
735
+ flex-direction: column;
736
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
737
+ }
738
+
739
+ .modal-header {
740
+ display: flex;
741
+ align-items: center;
742
+ justify-content: space-between;
743
+ padding: 1.25rem 1.5rem;
744
+ border-bottom: 1px solid var(--border);
745
+ }
746
+
747
+ .modal-header h2 {
748
+ font-size: 1.2rem;
749
+ color: var(--text-primary);
750
+ margin: 0;
751
+ }
752
+
753
+ .modal-close {
754
+ background: none;
755
+ border: none;
756
+ color: var(--text-muted);
757
+ font-size: 1.75rem;
758
+ cursor: pointer;
759
+ padding: 0;
760
+ line-height: 1;
761
+ transition: color 0.2s;
762
+ }
763
+
764
+ .modal-close:hover {
765
+ color: var(--text-primary);
766
+ }
767
+
768
+ .modal-body {
769
+ padding: 1.5rem;
770
+ overflow-y: auto;
771
+ flex: 1;
772
+ }
773
+
774
+ .modal-scan-area {
677
775
  text-align: center;
776
+ padding: 1rem 0;
777
+ }
778
+
779
+ .modal-scan-desc {
780
+ color: var(--text-secondary);
781
+ font-size: 0.9rem;
782
+ margin-bottom: 1.25rem;
783
+ line-height: 1.5;
784
+ }
785
+
786
+ .modal-footer {
787
+ display: flex;
788
+ gap: 0.75rem;
789
+ padding: 1rem 1.5rem;
790
+ border-top: 1px solid var(--border);
791
+ }
792
+
793
+ .modal-footer.hidden {
794
+ display: none;
795
+ }
796
+
797
+ .modal-export-btn {
798
+ flex: 1;
799
+ padding: 0.65rem 1rem;
800
+ background: var(--bg-card);
801
+ border: 1px solid var(--border);
802
+ border-radius: 10px;
803
+ color: var(--text-secondary);
804
+ font-size: 0.85rem;
805
+ font-weight: 500;
806
+ cursor: pointer;
807
+ transition: all 0.2s;
808
+ }
809
+
810
+ .modal-export-btn:hover {
811
+ border-color: rgba(124, 58, 237, 0.5);
812
+ color: var(--text-primary);
813
+ background: var(--bg-card-hover);
678
814
  }
679
815
 
680
816
  .activity-item.has-context {
@@ -936,8 +1072,14 @@ body {
936
1072
  align-items: flex-start;
937
1073
  }
938
1074
 
939
- .score-bars {
940
- grid-template-columns: repeat(2, 1fr);
1075
+ .header-actions {
1076
+ flex-wrap: wrap;
1077
+ justify-content: center;
1078
+ }
1079
+
1080
+ .modal-content {
1081
+ max-height: 90vh;
1082
+ margin: 1rem;
941
1083
  }
942
1084
 
943
1085
  .hero h1 {
@@ -1157,18 +1299,14 @@ body {
1157
1299
  }
1158
1300
 
1159
1301
  .security-arch-section {
1160
- margin-top: 2rem;
1302
+ margin-bottom: 1.5rem;
1161
1303
  }
1162
1304
 
1163
1305
  .security-arch-grid {
1164
1306
  display: grid;
1165
- grid-template-columns: repeat(2, 1fr);
1166
- gap: 0.75rem;
1167
- margin-top: 1rem;
1168
- }
1169
-
1170
- .dash-col-right .security-arch-grid {
1171
1307
  grid-template-columns: 1fr;
1308
+ gap: 0.5rem;
1309
+ margin-top: 0.5rem;
1172
1310
  }
1173
1311
 
1174
1312
  .arch-item {
@@ -1201,13 +1339,6 @@ body {
1201
1339
  line-height: 1.5;
1202
1340
  }
1203
1341
 
1204
- .threat-section {
1205
- margin-top: 2rem;
1206
- }
1207
-
1208
- .threat-header {
1209
- margin-top: 1rem;
1210
- }
1211
1342
 
1212
1343
  .threat-scan-btn {
1213
1344
  background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
@@ -1298,48 +1429,13 @@ body {
1298
1429
  .threat-summary-badge.low { background: rgba(96, 165, 250, 0.2); color: #60a5fa; }
1299
1430
  .threat-summary-badge.passed { background: rgba(34, 197, 94, 0.2); color: var(--success); }
1300
1431
 
1301
- .threat-findings-toggle {
1302
- display: flex;
1303
- align-items: center;
1304
- gap: 0.5rem;
1305
- margin-top: 0.75rem;
1306
- padding: 0.5rem 0.75rem;
1307
- background: var(--bg-card);
1308
- border: 1px solid var(--border);
1309
- border-radius: 8px;
1310
- cursor: pointer;
1311
- color: var(--text-secondary);
1312
- font-size: 0.8rem;
1313
- transition: all 0.2s;
1314
- width: 100%;
1315
- text-align: left;
1316
- }
1317
-
1318
- .threat-findings-toggle:hover {
1319
- border-color: var(--primary, #7c3aed);
1320
- color: var(--text-primary);
1321
- }
1322
-
1323
- .threat-findings-toggle .toggle-arrow {
1324
- transition: transform 0.2s;
1325
- font-size: 0.7rem;
1326
- }
1327
-
1328
- .threat-findings-toggle.expanded .toggle-arrow {
1329
- transform: rotate(90deg);
1330
- }
1331
-
1332
1432
  .threat-findings {
1333
- margin-top: 0.5rem;
1433
+ margin-top: 1rem;
1334
1434
  display: flex;
1335
1435
  flex-direction: column;
1336
1436
  gap: 0.5rem;
1337
1437
  }
1338
1438
 
1339
- .threat-findings.collapsed {
1340
- display: none;
1341
- }
1342
-
1343
1439
  .threat-finding {
1344
1440
  background: var(--bg-card);
1345
1441
  border: 1px solid var(--border);
package/dashboard/dash.js CHANGED
@@ -235,13 +235,6 @@
235
235
  globalStatus.className = "status-badge protected";
236
236
  }
237
237
 
238
- const checkLocal = document.getElementById("check-local");
239
- const checkSecret = document.getElementById("check-secret");
240
- const checkInjection = document.getElementById("check-injection");
241
-
242
- if (checkLocal) checkLocal.className = "safety-item " + (metrics.instance.dashboardLocalOnly ? "pass" : "fail");
243
- if (checkSecret) checkSecret.className = "safety-item " + (metrics.instance.secretsEnvOnly ? "pass" : "fail");
244
- if (checkInjection) checkInjection.className = "safety-item pass";
245
238
 
246
239
  const activityList = document.getElementById("activity-list");
247
240
  if (metrics.lastActions && metrics.lastActions.length > 0) {
@@ -397,42 +390,35 @@
397
390
 
398
391
  function updateProtectionScore(metrics) {
399
392
  let score = 0;
400
- const bars = {
393
+ const checks = {
401
394
  local: document.getElementById("bar-local"),
402
395
  secret: document.getElementById("bar-secret"),
403
396
  injection: document.getElementById("bar-injection"),
404
397
  approval: document.getElementById("bar-approval")
405
398
  };
399
+ const statusEls = {
400
+ local: document.getElementById("check-local"),
401
+ secret: document.getElementById("check-secret"),
402
+ injection: document.getElementById("check-injection")
403
+ };
406
404
 
407
- if (metrics.instance.dashboardLocalOnly) {
408
- score++;
409
- bars.local?.classList.add("active");
410
- bars.local?.classList.remove("inactive");
411
- } else {
412
- bars.local?.classList.remove("active");
413
- bars.local?.classList.add("inactive");
414
- }
415
-
416
- if (metrics.instance.secretsEnvOnly) {
417
- score++;
418
- bars.secret?.classList.add("active");
419
- bars.secret?.classList.remove("inactive");
420
- } else {
421
- bars.secret?.classList.remove("active");
422
- bars.secret?.classList.add("inactive");
405
+ function setCheck(key, active) {
406
+ if (active) {
407
+ score++;
408
+ checks[key]?.classList.add("active");
409
+ checks[key]?.classList.remove("inactive");
410
+ if (statusEls[key]) { statusEls[key].textContent = ""; }
411
+ } else {
412
+ checks[key]?.classList.remove("active");
413
+ checks[key]?.classList.add("inactive");
414
+ if (statusEls[key]) { statusEls[key].textContent = "✗"; }
415
+ }
423
416
  }
424
417
 
425
- score++;
426
- bars.injection?.classList.add("active");
427
-
428
- if (localStorage.getItem("protection_level") !== "relaxed") {
429
- score++;
430
- bars.approval?.classList.add("active");
431
- bars.approval?.classList.remove("inactive");
432
- } else {
433
- bars.approval?.classList.remove("active");
434
- bars.approval?.classList.add("inactive");
435
- }
418
+ setCheck("local", metrics.instance.dashboardLocalOnly);
419
+ setCheck("secret", metrics.instance.secretsEnvOnly);
420
+ setCheck("injection", true);
421
+ setCheck("approval", localStorage.getItem("protection_level") !== "relaxed");
436
422
 
437
423
  const scoreEl = document.getElementById("score-value");
438
424
  if (scoreEl) {
@@ -911,7 +897,8 @@
911
897
  initProtectionToggle();
912
898
  initNotificationToggle();
913
899
  initResetButton();
914
- initThreatScan();
900
+ initThreatModal();
901
+ initSecurityAccordion();
915
902
 
916
903
  function initNotificationToggle() {
917
904
  const btn = document.getElementById("btn-notifications");
@@ -949,15 +936,41 @@
949
936
  });
950
937
  }
951
938
 
952
- function initThreatScan() {
953
- const btn = document.getElementById("btn-scan");
939
+ let lastScanData = null;
940
+
941
+ function initThreatModal() {
942
+ const modal = document.getElementById("threat-modal");
943
+ const headerBtn = document.getElementById("btn-scan-header");
944
+ const closeBtn = document.getElementById("btn-modal-close");
945
+ const scanBtn = document.getElementById("btn-scan");
946
+ const exportJsonBtn = document.getElementById("btn-export-json");
947
+ const exportCopyBtn = document.getElementById("btn-export-copy");
948
+ if (!modal || !headerBtn) return;
949
+
950
+ headerBtn.addEventListener("click", () => {
951
+ modal.classList.remove("hidden");
952
+ });
953
+
954
+ closeBtn?.addEventListener("click", () => {
955
+ modal.classList.add("hidden");
956
+ });
957
+
958
+ modal.addEventListener("click", (e) => {
959
+ if (e.target === modal) modal.classList.add("hidden");
960
+ });
961
+
962
+ document.addEventListener("keydown", (e) => {
963
+ if (e.key === "Escape" && !modal.classList.contains("hidden")) {
964
+ modal.classList.add("hidden");
965
+ }
966
+ });
967
+
954
968
  const scoreDisplay = document.getElementById("threat-score-display");
955
969
  const findingsEl = document.getElementById("threat-findings");
956
- if (!btn || !scoreDisplay || !findingsEl) return;
957
970
 
958
- btn.addEventListener("click", async () => {
959
- btn.disabled = true;
960
- btn.textContent = "🔍 Scanning...";
971
+ scanBtn?.addEventListener("click", async () => {
972
+ scanBtn.disabled = true;
973
+ scanBtn.textContent = "🔍 Scanning...";
961
974
 
962
975
  try {
963
976
  const headers = {};
@@ -965,13 +978,36 @@
965
978
  const res = await fetch("/api/threat-assessment", { headers });
966
979
  if (!res.ok) throw new Error("Scan failed");
967
980
  const data = await res.json();
981
+ lastScanData = data;
968
982
  renderThreatResults(data, scoreDisplay, findingsEl);
969
- btn.textContent = "🔍 Rescan";
983
+ scanBtn.textContent = "🔍 Rescan";
984
+ document.getElementById("modal-export-footer")?.classList.remove("hidden");
970
985
  } catch (err) {
971
- btn.textContent = "⚠️ Scan Failed — Retry";
986
+ scanBtn.textContent = "⚠️ Scan Failed — Retry";
972
987
  console.error("Threat scan error:", err);
973
988
  }
974
- btn.disabled = false;
989
+ scanBtn.disabled = false;
990
+ });
991
+
992
+ exportJsonBtn?.addEventListener("click", () => {
993
+ if (!lastScanData) return;
994
+ const blob = new Blob([JSON.stringify(lastScanData, null, 2)], { type: "application/json" });
995
+ const url = URL.createObjectURL(blob);
996
+ const a = document.createElement("a");
997
+ a.href = url;
998
+ a.download = `clawguard-scan-${new Date().toISOString().slice(0, 10)}.json`;
999
+ a.click();
1000
+ URL.revokeObjectURL(url);
1001
+ });
1002
+
1003
+ exportCopyBtn?.addEventListener("click", () => {
1004
+ if (!lastScanData) return;
1005
+ const text = `Clawguard Security Scan - ${new Date().toLocaleDateString()}\nGrade: ${lastScanData.grade} | Score: ${lastScanData.score}/100\n\n` +
1006
+ lastScanData.findings.map(f => `[${f.passed ? "PASS" : f.severity.toUpperCase()}] ${f.title}: ${f.description}${!f.passed ? "\n → " + f.recommendation : ""}`).join("\n");
1007
+ navigator.clipboard.writeText(text).then(() => {
1008
+ exportCopyBtn.textContent = "✓ Copied!";
1009
+ setTimeout(() => { exportCopyBtn.textContent = "📋 Copy to Clipboard"; }, 2000);
1010
+ });
975
1011
  });
976
1012
  }
977
1013
 
@@ -997,30 +1033,6 @@
997
1033
  const failed = data.findings.filter(f => !f.passed);
998
1034
  const passed = data.findings.filter(f => f.passed);
999
1035
  const sorted = [...failed, ...passed];
1000
- const failCount = failed.length;
1001
-
1002
- let existingToggle = document.querySelector(".threat-findings-toggle");
1003
- if (!existingToggle) {
1004
- existingToggle = document.createElement("button");
1005
- existingToggle.className = "threat-findings-toggle";
1006
- existingToggle.setAttribute("data-testid", "btn-findings-toggle");
1007
- findingsEl.parentNode.insertBefore(existingToggle, findingsEl);
1008
- existingToggle.addEventListener("click", () => {
1009
- const isCollapsed = findingsEl.classList.contains("collapsed");
1010
- if (isCollapsed) {
1011
- findingsEl.classList.remove("collapsed");
1012
- existingToggle.classList.add("expanded");
1013
- } else {
1014
- findingsEl.classList.add("collapsed");
1015
- existingToggle.classList.remove("expanded");
1016
- }
1017
- existingToggle.querySelector(".toggle-label").textContent =
1018
- findingsEl.classList.contains("collapsed") ? `Show ${data.findings.length} findings (${failCount} issues)` : "Hide findings";
1019
- });
1020
- }
1021
- existingToggle.innerHTML = `<span class="toggle-arrow">▶</span> <span class="toggle-label">Show ${data.findings.length} findings (${failCount} issues)</span>`;
1022
- findingsEl.classList.add("collapsed");
1023
- existingToggle.classList.remove("expanded");
1024
1036
 
1025
1037
  findingsEl.innerHTML = sorted.map(f => {
1026
1038
  const sevIcon = f.passed ? "✓" : (f.severity === "critical" ? "!!" : f.severity === "high" ? "!" : f.severity === "medium" ? "~" : "·");
@@ -1038,6 +1050,23 @@
1038
1050
  }).join("");
1039
1051
  }
1040
1052
 
1053
+ function initSecurityAccordion() {
1054
+ const btn = document.getElementById("btn-toggle-security");
1055
+ const content = document.getElementById("security-arch-content");
1056
+ if (!btn || !content) return;
1057
+
1058
+ btn.addEventListener("click", () => {
1059
+ const isCollapsed = content.classList.contains("collapsed");
1060
+ if (isCollapsed) {
1061
+ content.classList.remove("collapsed");
1062
+ btn.classList.add("expanded");
1063
+ } else {
1064
+ content.classList.add("collapsed");
1065
+ btn.classList.remove("expanded");
1066
+ }
1067
+ });
1068
+ }
1069
+
1041
1070
  function initResetButton() {
1042
1071
  const btn = document.getElementById("btn-reset");
1043
1072
  if (!btn) return;
@@ -106,6 +106,9 @@
106
106
  </div>
107
107
  </div>
108
108
  <div class="header-actions">
109
+ <button id="btn-scan-header" class="header-scan-btn" data-testid="btn-scan-header" title="Run a full security scan">
110
+ 🔍 Security Scan
111
+ </button>
109
112
  <button id="btn-notifications" class="notification-toggle" data-testid="btn-notifications" title="Enable browser notifications">
110
113
  🔔 Notifications
111
114
  </button>
@@ -200,96 +203,66 @@
200
203
  </div>
201
204
 
202
205
  <div class="dash-col-right">
203
- <section class="protection-section">
204
- <div class="protection-score" data-testid="protection-score">
205
- <div class="score-header">
206
- <span class="score-value" id="score-value">4/4</span>
207
- <span class="score-label">Protection Score</span>
206
+ <section class="protection-safety-section">
207
+ <div class="protection-safety-card" data-testid="protection-score">
208
+ <div class="ps-header">
209
+ <span class="ps-score" id="score-value">4/4</span>
210
+ <span class="ps-label">Protection Active</span>
208
211
  </div>
209
- <div class="score-bars">
210
- <div class="score-bar active" id="bar-local" data-tooltip="Dashboard only accessible from your computer">
211
- <span class="bar-icon">🏠</span>
212
- <span class="bar-label">Local Only</span>
212
+ <div class="ps-checks">
213
+ <div class="ps-check active" id="bar-local" data-tooltip="Dashboard only accessible from your computer">
214
+ <span class="ps-check-icon">🏠</span>
215
+ <span class="ps-check-label">Local Only</span>
216
+ <span class="ps-check-status" id="check-local" data-testid="check-local">✓</span>
213
217
  </div>
214
- <div class="score-bar active" id="bar-secret" data-tooltip="Your dashboard is protected by a secret key">
215
- <span class="bar-icon">🔑</span>
216
- <span class="bar-label">Secret Key</span>
218
+ <div class="ps-check active" id="bar-secret" data-tooltip="Your dashboard is protected by a secret key">
219
+ <span class="ps-check-icon">🔑</span>
220
+ <span class="ps-check-label">Secret Key</span>
221
+ <span class="ps-check-status" id="check-secret" data-testid="check-secret">✓</span>
217
222
  </div>
218
- <div class="score-bar active" id="bar-injection" data-tooltip="Detecting prompt injection attempts in real-time">
219
- <span class="bar-icon">🛡️</span>
220
- <span class="bar-label">Injection Detection</span>
223
+ <div class="ps-check active" id="bar-injection" data-tooltip="Detecting prompt injection attempts in real-time">
224
+ <span class="ps-check-icon">🛡️</span>
225
+ <span class="ps-check-label">Injection Detection</span>
226
+ <span class="ps-check-status" id="check-injection" data-testid="check-injection">✓</span>
221
227
  </div>
222
- <div class="score-bar active" id="bar-approval" data-tooltip="Real-time visibility into all agent activity">
223
- <span class="bar-icon">👁️</span>
224
- <span class="bar-label">Live Monitoring</span>
228
+ <div class="ps-check active" id="bar-approval" data-tooltip="Real-time visibility into all agent activity">
229
+ <span class="ps-check-icon">👁️</span>
230
+ <span class="ps-check-label">Live Monitoring</span>
231
+ <span class="ps-check-status">✓</span>
225
232
  </div>
226
233
  </div>
227
234
  </div>
228
235
  </section>
229
236
 
230
- <section class="safety-section">
231
- <h2>Safety Checks</h2>
232
- <div class="safety-grid">
233
- <div class="safety-item pass" id="check-local" data-testid="check-local">
234
- <span class="check-icon">✓</span>
235
- <span>Dashboard is local-only</span>
236
- <span class="legend-help" data-tooltip="Only you can see this dashboard - no remote access">?</span>
237
- </div>
238
- <div class="safety-item pass" id="check-secret" data-testid="check-secret">
239
- <span class="check-icon">✓</span>
240
- <span>Secret key protected</span>
241
- <span class="legend-help" data-tooltip="Your dashboard is locked with a secret key">?</span>
242
- </div>
243
- <div class="safety-item pass" id="check-injection" data-testid="check-injection">
244
- <span class="check-icon">✓</span>
245
- <span>Prompt injection detection active</span>
246
- <span class="legend-help" data-tooltip="Detecting hidden attack instructions in real-time">?</span>
247
- </div>
248
- </div>
249
- </section>
250
-
251
237
  <section class="security-arch-section">
252
- <h2>Dashboard Security <span class="legend-help" data-tooltip="Why your bot can never access or tamper with this dashboard">?</span></h2>
253
- <div class="security-arch-grid">
238
+ <button class="section-accordion" id="btn-toggle-security" data-testid="btn-toggle-security">
239
+ <span>🔒 How Your Dashboard Stays Safe</span>
240
+ <span class="accordion-arrow">▶</span>
241
+ </button>
242
+ <div class="security-arch-grid collapsed" id="security-arch-content">
254
243
  <div class="arch-item">
255
244
  <div class="arch-icon">🔒</div>
256
245
  <div class="arch-title">Local-Only Binding</div>
257
- <div class="arch-desc">Dashboard listens on localhost only. No external network can reach it — even if your server is public.</div>
246
+ <div class="arch-desc">Dashboard listens on localhost only. No external network can reach it.</div>
258
247
  </div>
259
248
  <div class="arch-item">
260
249
  <div class="arch-icon">🔑</div>
261
250
  <div class="arch-title">Secret Key Auth</div>
262
- <div class="arch-desc">Every request requires a secret key your bot never sees. Even local processes can't access data without it.</div>
251
+ <div class="arch-desc">Every request requires a secret key your bot never sees.</div>
263
252
  </div>
264
253
  <div class="arch-item">
265
254
  <div class="arch-icon">👁️</div>
266
255
  <div class="arch-title">Passive Monitoring</div>
267
- <div class="arch-desc">Clawguard only reads log files — it never writes to the bot, sends commands, or modifies any configuration.</div>
256
+ <div class="arch-desc">Clawguard only reads log files — never writes to the bot.</div>
268
257
  </div>
269
258
  <div class="arch-item">
270
259
  <div class="arch-icon">🚫</div>
271
260
  <div class="arch-title">No Write-Back Path</div>
272
- <div class="arch-desc">There is no API, socket, or channel from the dashboard back to your bot. The connection is strictly one-way.</div>
261
+ <div class="arch-desc">No API, socket, or channel from dashboard back to your bot.</div>
273
262
  </div>
274
263
  </div>
275
264
  </section>
276
265
 
277
- <section class="threat-section" id="threat-section">
278
- <h2>Threat Assessment <span class="legend-help" data-tooltip="Holistic security scan of your server">?</span></h2>
279
- <div class="threat-header" id="threat-header">
280
- <button class="threat-scan-btn" id="btn-scan" data-testid="btn-scan">🔍 Run Security Scan</button>
281
- <div class="threat-score-display hidden" id="threat-score-display">
282
- <div class="threat-grade" id="threat-grade">-</div>
283
- <div class="threat-score-info">
284
- <span class="threat-score-value" id="threat-score-value">-/100</span>
285
- <span class="threat-score-label">Security Score</span>
286
- </div>
287
- <div class="threat-summary" id="threat-summary"></div>
288
- </div>
289
- </div>
290
- <div class="threat-findings collapsed" id="threat-findings" data-testid="threat-findings"></div>
291
- </section>
292
-
293
266
  <section class="control-section">
294
267
  <h2>Protection Level</h2>
295
268
  <div class="protection-toggle" data-testid="protection-toggle">
@@ -361,6 +334,34 @@
361
334
  </div>
362
335
  </div>
363
336
 
337
+ <div class="modal-overlay hidden" id="threat-modal" data-testid="threat-modal">
338
+ <div class="modal-content">
339
+ <div class="modal-header">
340
+ <h2>🔍 Threat Assessment</h2>
341
+ <button class="modal-close" id="btn-modal-close" data-testid="btn-modal-close">&times;</button>
342
+ </div>
343
+ <div class="modal-body">
344
+ <div class="modal-scan-area" id="modal-scan-area">
345
+ <p class="modal-scan-desc">Run a full security scan of your server environment. Checks processes, files, secrets, network, system, and bot-specific risks.</p>
346
+ <button class="threat-scan-btn" id="btn-scan" data-testid="btn-scan">🔍 Run Security Scan</button>
347
+ </div>
348
+ <div class="threat-score-display hidden" id="threat-score-display">
349
+ <div class="threat-grade" id="threat-grade">-</div>
350
+ <div class="threat-score-info">
351
+ <span class="threat-score-value" id="threat-score-value">-/100</span>
352
+ <span class="threat-score-label">Security Score</span>
353
+ </div>
354
+ <div class="threat-summary" id="threat-summary"></div>
355
+ </div>
356
+ <div class="threat-findings" id="threat-findings" data-testid="threat-findings"></div>
357
+ </div>
358
+ <div class="modal-footer hidden" id="modal-export-footer">
359
+ <button class="modal-export-btn" id="btn-export-json" data-testid="btn-export-json">📥 Download JSON</button>
360
+ <button class="modal-export-btn" id="btn-export-copy" data-testid="btn-export-copy">📋 Copy to Clipboard</button>
361
+ </div>
362
+ </div>
363
+ </div>
364
+
364
365
  <div class="tooltip" id="tooltip"></div>
365
366
 
366
367
  <script src="/static/dash.js"></script>
@@ -17,6 +17,7 @@ export declare class LogWatcher extends EventEmitter {
17
17
  private readNewLines;
18
18
  private processLine;
19
19
  private parseLogLine;
20
+ private parsePlainTextLine;
20
21
  private extractToolArgs;
21
22
  private getContentFingerprint;
22
23
  private isDuplicate;
@@ -1 +1 @@
1
- {"version":3,"file":"log-watcher.d.ts","sourceRoot":"","sources":["../log-watcher.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAmDtC,qBAAa,UAAW,SAAQ,YAAY;IAC1C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,qBAAqB,CAA+B;;IAY5D,KAAK,IAAI,IAAI;IAUb,IAAI,IAAI,IAAI;IAeZ,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,SAAS;IAuCjB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,YAAY;IA+BpB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAmMpB,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,eAAe;IAmCvB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,gBAAgB;CAWzB;AAID,wBAAgB,eAAe,IAAI,UAAU,CAM5C;AAED,wBAAgB,cAAc,IAAI,IAAI,CAKrC;AAED,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD"}
1
+ {"version":3,"file":"log-watcher.d.ts","sourceRoot":"","sources":["../log-watcher.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAsDtC,qBAAa,UAAW,SAAQ,YAAY;IAC1C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,qBAAqB,CAA+B;;IAY5D,KAAK,IAAI,IAAI;IAUb,IAAI,IAAI,IAAI;IAeZ,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,SAAS;IAuCjB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,YAAY;IA+BpB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAmMpB,OAAO,CAAC,kBAAkB;IA+D1B,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,eAAe;IAmCvB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,gBAAgB;CAWzB;AAID,wBAAgB,eAAe,IAAI,UAAU,CAM5C;AAED,wBAAgB,cAAc,IAAI,IAAI,CAKrC;AAED,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD"}
@@ -29,9 +29,13 @@ exports.stopLogWatcher = stopLogWatcher;
29
29
  exports.getLogWatcher = getLogWatcher;
30
30
  const fs = __importStar(require("fs"));
31
31
  const path = __importStar(require("path"));
32
+ const os = __importStar(require("os"));
32
33
  const events_1 = require("events");
33
34
  const storage_1 = require("./storage");
34
- const CLAWDBOT_LOG_DIR = "/tmp/clawdbot";
35
+ const CLAWDBOT_LOG_DIRS = [
36
+ path.join(os.homedir(), ".clawdbot", "logs"),
37
+ "/tmp/clawdbot",
38
+ ];
35
39
  const DANGEROUS_PATTERNS = [
36
40
  { pattern: /rm\s+-rf?\s+\//, reason: "Dangerous rm command (root delete)" },
37
41
  { pattern: /rm\s+-rf?\s+~/, reason: "Dangerous rm command (home delete)" },
@@ -80,7 +84,7 @@ class LogWatcher extends events_1.EventEmitter {
80
84
  start() {
81
85
  const logFile = this.getCurrentLogFile();
82
86
  if (!logFile) {
83
- console.log("[LogWatcher] ClawdBot log directory not found, will poll for it...");
87
+ console.log(`[LogWatcher] No log files found in ${CLAWDBOT_LOG_DIRS.join(", ")} polling every 5s...`);
84
88
  this.pollInterval = setInterval(() => this.checkForLogFile(), 5000);
85
89
  return;
86
90
  }
@@ -101,11 +105,27 @@ class LogWatcher extends events_1.EventEmitter {
101
105
  }
102
106
  }
103
107
  getCurrentLogFile() {
104
- if (!fs.existsSync(CLAWDBOT_LOG_DIR))
105
- return null;
106
- const today = new Date().toISOString().split("T")[0];
107
- const logFile = path.join(CLAWDBOT_LOG_DIR, `clawdbot-${today}.log`);
108
- return fs.existsSync(logFile) ? logFile : null;
108
+ for (const dir of CLAWDBOT_LOG_DIRS) {
109
+ if (!fs.existsSync(dir))
110
+ continue;
111
+ const commandsLog = path.join(dir, "commands.log");
112
+ if (fs.existsSync(commandsLog))
113
+ return commandsLog;
114
+ const today = new Date().toISOString().split("T")[0];
115
+ const dateLog = path.join(dir, `clawdbot-${today}.log`);
116
+ if (fs.existsSync(dateLog))
117
+ return dateLog;
118
+ try {
119
+ const files = fs.readdirSync(dir)
120
+ .filter(f => f.endsWith(".log"))
121
+ .map(f => ({ name: f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
122
+ .sort((a, b) => b.mtime - a.mtime);
123
+ if (files.length > 0)
124
+ return path.join(dir, files[0].name);
125
+ }
126
+ catch { }
127
+ }
128
+ return null;
109
129
  }
110
130
  checkForLogFile() {
111
131
  const logFile = this.getCurrentLogFile();
@@ -373,8 +393,69 @@ class LogWatcher extends events_1.EventEmitter {
373
393
  return { subsystem, message, timestamp, logLevel, toolEvent };
374
394
  }
375
395
  catch {
396
+ return this.parsePlainTextLine(line);
397
+ }
398
+ }
399
+ parsePlainTextLine(line) {
400
+ const trimmed = line.trim();
401
+ if (!trimmed || trimmed.length < 5)
376
402
  return null;
403
+ const timestamp = new Date();
404
+ let toolEvent;
405
+ const timestampMatch = trimmed.match(/^\[?(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[^\]]*)\]?\s*(.*)/);
406
+ const content = timestampMatch ? timestampMatch[2] : trimmed;
407
+ if (timestampMatch) {
408
+ try {
409
+ const d = new Date(timestampMatch[1]);
410
+ if (!isNaN(d.getTime()))
411
+ Object.assign(timestamp, { _parsed: d });
412
+ }
413
+ catch { }
414
+ }
415
+ const cmdMatch = content.match(/(?:command|cmd|exec|run|shell)\s*[=:]\s*(.+)/i);
416
+ if (cmdMatch) {
417
+ toolEvent = {
418
+ runId: `cmd-${Date.now()}`,
419
+ tool: "exec",
420
+ toolCallId: `cmd-${Date.now()}`,
421
+ phase: "start",
422
+ timestamp,
423
+ args: { command: cmdMatch[1].trim() },
424
+ };
425
+ }
426
+ const msgMatch = content.match(/(?:message|msg|body|text|input)\s*[=:]\s*(.+)/i);
427
+ if (!toolEvent && msgMatch) {
428
+ toolEvent = {
429
+ runId: `msg-${Date.now()}`,
430
+ tool: "whatsapp-inbound",
431
+ toolCallId: `msg-${Date.now()}`,
432
+ phase: "start",
433
+ timestamp,
434
+ args: { body: msgMatch[1].trim() },
435
+ };
436
+ }
437
+ const replyMatch = content.match(/(?:reply|response|answer|out)\s*[=:]\s*(.+)/i);
438
+ if (!toolEvent && replyMatch) {
439
+ toolEvent = {
440
+ runId: `reply-${Date.now()}`,
441
+ tool: "whatsapp-reply",
442
+ toolCallId: `reply-${Date.now()}`,
443
+ phase: "start",
444
+ timestamp,
445
+ args: { text: replyMatch[1].trim() },
446
+ };
447
+ }
448
+ if (!toolEvent && content.length > 3) {
449
+ toolEvent = {
450
+ runId: `log-${Date.now()}`,
451
+ tool: "agent-log",
452
+ toolCallId: `log-${Date.now()}`,
453
+ phase: "start",
454
+ timestamp,
455
+ args: { message: content },
456
+ };
377
457
  }
458
+ return { message: content, timestamp, logLevel: "INFO", toolEvent };
378
459
  }
379
460
  extractToolArgs(json) {
380
461
  const args = {};
@@ -1 +1 @@
1
- {"version":3,"file":"threat-assessment.d.ts","sourceRoot":"","sources":["../../src/threat-assessment.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,KAAK,CAAC;IACzE,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,OAAO,EAAE;QACP,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAieD,wBAAgB,mBAAmB,IAAI,gBAAgB,CAgDtD"}
1
+ {"version":3,"file":"threat-assessment.d.ts","sourceRoot":"","sources":["../../src/threat-assessment.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,KAAK,CAAC;IACzE,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,OAAO,EAAE;QACP,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAqeD,wBAAgB,mBAAmB,IAAI,gBAAgB,CAgDtD"}
@@ -29,7 +29,10 @@ const path = __importStar(require("path"));
29
29
  const os = __importStar(require("os"));
30
30
  const child_process_1 = require("child_process");
31
31
  const CLAWGUARD_DIR = path.join(os.homedir(), ".clawguard");
32
- const CLAWDBOT_LOG_DIR = "/tmp/clawdbot";
32
+ const CLAWDBOT_LOG_DIRS = [
33
+ path.join(os.homedir(), ".clawdbot", "logs"),
34
+ "/tmp/clawdbot",
35
+ ];
33
36
  const SEVERITY_PENALTIES = {
34
37
  critical: 25,
35
38
  high: 15,
@@ -445,14 +448,16 @@ function checkBotSecurity() {
445
448
  let credentialLeak = false;
446
449
  let leakDetail = "";
447
450
  try {
448
- if (fs.existsSync(CLAWDBOT_LOG_DIR)) {
449
- const logFiles = fs.readdirSync(CLAWDBOT_LOG_DIR)
451
+ for (const logDir of CLAWDBOT_LOG_DIRS) {
452
+ if (!fs.existsSync(logDir))
453
+ continue;
454
+ const logFiles = fs.readdirSync(logDir)
450
455
  .filter(f => f.endsWith(".log"))
451
456
  .sort()
452
457
  .slice(-3);
453
458
  for (const file of logFiles) {
454
459
  try {
455
- const filePath = path.join(CLAWDBOT_LOG_DIR, file);
460
+ const filePath = path.join(logDir, file);
456
461
  const stat = fs.statSync(filePath);
457
462
  const readSize = Math.min(stat.size, 50000);
458
463
  const fd = fs.openSync(filePath, "r");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "averecion-lite",
3
- "version": "1.5.2",
3
+ "version": "1.6.1",
4
4
  "description": "Real-time AI agent monitoring - watches logs, detects dangerous commands and prompt injection attempts",
5
5
  "author": "Averecion <hello@averecion.com>",
6
6
  "homepage": "https://github.com/averecion/clawguard#readme",