torusguard 1.3.6 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.torusguard/.manifest.json +9 -8
- package/.torusguard/runs/report-latest.html +3529 -364
- package/.torusguard/runs/run-20260918-105940-audit/manifest.json +2 -1
- package/.torusguard/runs/run-20260918-105940-audit/remediation.md +7 -0
- package/.torusguard/runs/run-20260919-145701-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-145701-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-145701-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-145701-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-151729-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-151729-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-151729-audit/manifest.json +13 -0
- package/.torusguard/runs/run-20260919-151729-audit/remediation.md +7 -0
- package/.torusguard/runs/run-20260919-151729-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-152340-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-152340-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-152340-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-152340-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-152345-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-152345-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-152345-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-152345-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-153500-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-153500-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-153500-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-153500-audit/summary.md +5 -0
- package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/recipes_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/.torusguard/scripts/apply_runner.py +101 -8
- package/.torusguard/scripts/audit_runner.py +116 -11
- package/.torusguard/scripts/harden_runner.py +122 -68
- package/.torusguard/scripts/html_reporter.py +1170 -253
- package/.torusguard/scripts/manifest_builder.py +2 -0
- package/.torusguard/scripts/recheck_runner.py +2 -1
- package/.torusguard/scripts/recipes_runner.py +91 -8
- package/.torusguard/scripts/report_sync.py +134 -7
- package/.torusguard/scripts/status_runner.py +267 -0
- package/README.md +108 -80
- package/bin/torusguard.js +190 -20
- package/package.json +6 -2
- package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/skills/torusguard/bootstrap.py +45 -6
- package/skills/torusguard/payload/.manifest.json +29 -14
- package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/apply_runner.py +101 -8
- package/skills/torusguard/payload/scripts/audit_runner.py +116 -11
- package/skills/torusguard/payload/scripts/harden_runner.py +122 -68
- package/skills/torusguard/payload/scripts/html_reporter.py +1170 -253
- package/skills/torusguard/payload/scripts/manifest_builder.py +2 -0
- package/skills/torusguard/payload/scripts/recheck_runner.py +2 -1
- package/skills/torusguard/payload/scripts/recipes_runner.py +91 -8
- package/skills/torusguard/payload/scripts/report_sync.py +134 -7
- package/skills/torusguard/payload/scripts/status_runner.py +267 -0
- package/skills/torusguard/payload/workflows/apply.md +9 -1
- package/skills/torusguard/payload/workflows/audit.md +8 -2
- package/skills/torusguard/payload/workflows/harden.md +6 -1
- package/skills/torusguard/payload/workflows/init.md +7 -1
- package/skills/torusguard/payload/workflows/recipes.md +55 -0
- package/skills/torusguard/payload/workflows/report.md +59 -55
- package/skills/torusguard/payload/workflows/status.md +4 -1
- package/skills/torusguard/payload/workflows/torusguard-apply.md +110 -0
- package/skills/torusguard/payload/workflows/torusguard-audit.md +105 -0
- package/skills/torusguard/payload/workflows/torusguard-authorize.md +96 -0
- package/skills/torusguard/payload/workflows/torusguard-exploit-check.md +98 -0
- package/skills/torusguard/payload/workflows/torusguard-harden.md +104 -0
- package/skills/torusguard/payload/workflows/torusguard-init.md +106 -0
- package/skills/torusguard/payload/workflows/torusguard-recheck.md +101 -0
- package/skills/torusguard/payload/workflows/torusguard-recipes.md +55 -0
- package/skills/torusguard/payload/workflows/torusguard-report.md +105 -0
- package/skills/torusguard/payload/workflows/torusguard-status.md +103 -0
- package/skills/torusguard/payload/workflows/torusguard-verify.md +100 -0
- package/skills/torusguard/payload/workflows/torusguard-web-validate.md +96 -0
- package/skills/torusguard/payload/workflows/torusguard.md +19 -0
|
@@ -361,9 +361,58 @@ def load_report_telemetry(root_dir: Path) -> dict[str, Any]:
|
|
|
361
361
|
except (json.JSONDecodeError, OSError):
|
|
362
362
|
latest_findings = []
|
|
363
363
|
|
|
364
|
-
#
|
|
364
|
+
# Ingest historical & resolved findings from security_report.md
|
|
365
|
+
report_file = root_dir / "security_report.md"
|
|
366
|
+
resolved_findings: list[dict[str, Any]] = []
|
|
367
|
+
if report_file.is_file():
|
|
368
|
+
try:
|
|
369
|
+
import report_sync # type: ignore
|
|
370
|
+
parsed = report_sync.parse_existing_report(report_file)
|
|
371
|
+
for fid, rf in parsed.get("findings", {}).items():
|
|
372
|
+
st = str(rf.get("status", "")).upper()
|
|
373
|
+
if "RESOLVED" in st or "FIXED" in st:
|
|
374
|
+
tgt = rf.get("target", "")
|
|
375
|
+
fpath = tgt.rsplit(":", 1)[0] if ":" in tgt else tgt
|
|
376
|
+
try:
|
|
377
|
+
lineno = int(tgt.rsplit(":", 1)[1]) if ":" in tgt else 1
|
|
378
|
+
except ValueError:
|
|
379
|
+
lineno = 1
|
|
380
|
+
conf_str = str(rf.get("confidence", "70%"))
|
|
381
|
+
try:
|
|
382
|
+
cval = int(conf_str.split("%")[0].strip())
|
|
383
|
+
except Exception:
|
|
384
|
+
cval = 70
|
|
385
|
+
resolved_findings.append({
|
|
386
|
+
"finding_id": fid,
|
|
387
|
+
"rule_id": rf.get("rule_id", "TG-GEN"),
|
|
388
|
+
"title": rf.get("title", "Resolved Security Flaw"),
|
|
389
|
+
"severity": rf.get("severity", "Medium"),
|
|
390
|
+
"status": "resolved",
|
|
391
|
+
"confidence_score": cval,
|
|
392
|
+
"file_path": fpath,
|
|
393
|
+
"line_number": lineno,
|
|
394
|
+
"cluster": rf.get("cluster", "cluster-general"),
|
|
395
|
+
"description": rf.get("description", "Security violation remediated and hardened."),
|
|
396
|
+
"is_canary": False
|
|
397
|
+
})
|
|
398
|
+
except Exception:
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
# Merge findings: open findings take precedence; resolved findings provide historical closure proof
|
|
402
|
+
all_findings_map: dict[str, dict[str, Any]] = {}
|
|
403
|
+
for f in resolved_findings:
|
|
404
|
+
all_findings_map[f.get("finding_id", "")] = f
|
|
405
|
+
for f in latest_findings:
|
|
406
|
+
all_findings_map[f.get("finding_id", "")] = f
|
|
407
|
+
|
|
408
|
+
combined_findings = list(all_findings_map.values()) if all_findings_map else latest_findings
|
|
409
|
+
|
|
410
|
+
# Rule catalog & Safe vs. Harmed computation (only open issues count as violated)
|
|
365
411
|
all_rules = load_all_rule_catalog()
|
|
366
|
-
violated_ids = {
|
|
412
|
+
violated_ids = {
|
|
413
|
+
f.get("rule_id") for f in combined_findings
|
|
414
|
+
if not f.get("is_canary", False) and f.get("status") not in ("resolved", "fixed", "confirmed_fixed")
|
|
415
|
+
}
|
|
367
416
|
safe_rules = [r for r in all_rules if r["rule_id"] not in violated_ids]
|
|
368
417
|
violated_rules = [r for r in all_rules if r["rule_id"] in violated_ids]
|
|
369
418
|
|
|
@@ -378,7 +427,9 @@ def load_report_telemetry(root_dir: Path) -> dict[str, Any]:
|
|
|
378
427
|
"patterns": patterns,
|
|
379
428
|
"context": context,
|
|
380
429
|
"detected_stack": stack_info,
|
|
381
|
-
"findings":
|
|
430
|
+
"findings": combined_findings,
|
|
431
|
+
"open_findings": [f for f in combined_findings if f.get("status") not in ("resolved", "fixed", "confirmed_fixed")],
|
|
432
|
+
"resolved_findings": [f for f in combined_findings if f.get("status") in ("resolved", "fixed", "confirmed_fixed")],
|
|
382
433
|
"latest_run": latest_run_name,
|
|
383
434
|
"all_rules": all_rules,
|
|
384
435
|
"safe_rules": safe_rules,
|
|
@@ -387,16 +438,20 @@ def load_report_telemetry(root_dir: Path) -> dict[str, Any]:
|
|
|
387
438
|
|
|
388
439
|
|
|
389
440
|
def compute_posture_score(telemetry: dict[str, Any]) -> int:
|
|
390
|
-
"""Calculate posture score from actual findings, aligned with report_sync.py."""
|
|
441
|
+
"""Calculate posture score from actual open findings, aligned with report_sync.py."""
|
|
391
442
|
findings = telemetry.get("findings", [])
|
|
392
443
|
if not findings:
|
|
393
444
|
return 100
|
|
394
445
|
|
|
395
|
-
# Only non-canary findings count toward production penalty
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
446
|
+
# Only open non-canary findings count toward production penalty
|
|
447
|
+
open_findings = [f for f in findings if f.get("status") not in ("resolved", "fixed", "confirmed_fixed") and not f.get("is_canary", False)]
|
|
448
|
+
if not open_findings:
|
|
449
|
+
return 100
|
|
450
|
+
|
|
451
|
+
critical = sum(1 for f in open_findings if f.get("severity") == "Critical")
|
|
452
|
+
high = sum(1 for f in open_findings if f.get("severity") == "High")
|
|
453
|
+
medium = sum(1 for f in open_findings if f.get("severity") == "Medium")
|
|
454
|
+
low = sum(1 for f in open_findings if f.get("severity") == "Low")
|
|
400
455
|
|
|
401
456
|
penalty = (critical * 25) + (high * 15) + (medium * 5) + (low * 2)
|
|
402
457
|
return max(0, min(100, 100 - penalty))
|
|
@@ -637,25 +692,160 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
637
692
|
stack.append(primary_db)
|
|
638
693
|
stack_label = ", ".join(stack) if stack else "Polyglot Project"
|
|
639
694
|
|
|
640
|
-
#
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
695
|
+
# Open vs resolved findings separation
|
|
696
|
+
open_findings = [f for f in findings if f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")]
|
|
697
|
+
resolved_findings = [f for f in findings if f.get("status", "open") in ("fixed", "resolved", "confirmed_fixed")]
|
|
698
|
+
|
|
699
|
+
crit_open = sum(1 for f in open_findings if f.get("severity") == "Critical")
|
|
700
|
+
crit_resolved = sum(1 for f in resolved_findings if f.get("severity") == "Critical")
|
|
701
|
+
crit_total = crit_open + crit_resolved
|
|
702
|
+
|
|
703
|
+
high_open = sum(1 for f in open_findings if f.get("severity") == "High")
|
|
704
|
+
high_resolved = sum(1 for f in resolved_findings if f.get("severity") == "High")
|
|
705
|
+
high_total = high_open + high_resolved
|
|
706
|
+
|
|
707
|
+
med_open = sum(1 for f in open_findings if f.get("severity") in ("Medium", "Low"))
|
|
708
|
+
med_resolved = sum(1 for f in resolved_findings if f.get("severity") in ("Medium", "Low"))
|
|
709
|
+
med_total = med_open + med_resolved
|
|
710
|
+
|
|
711
|
+
total_discovered = len(findings)
|
|
712
|
+
total_open = len(open_findings)
|
|
713
|
+
total_resolved = len(resolved_findings)
|
|
714
|
+
closure_rate_pct = int((total_resolved / total_discovered) * 100) if total_discovered > 0 else 100
|
|
715
|
+
|
|
645
716
|
total_rules = len(all_rules) if all_rules else 74
|
|
646
717
|
defended_count = len(safe_rules) if safe_rules else (total_rules - len(violated_rules))
|
|
647
718
|
coverage_pct = int((defended_count / total_rules) * 100) if total_rules > 0 else 100
|
|
648
719
|
|
|
720
|
+
# Sub-badges for the 4 Severity Cards in .grid-kpi
|
|
721
|
+
if crit_open == 0 and crit_resolved > 0:
|
|
722
|
+
crit_sub_badge = f'<div class="sev-sub-badge badge-sub-clean">🟢 {crit_resolved} Remediated (100% Defended)</div>'
|
|
723
|
+
elif crit_resolved > 0:
|
|
724
|
+
crit_sub_badge = f'<div class="sev-sub-badge">{crit_resolved} Solved / {crit_total} Total</div>'
|
|
725
|
+
else:
|
|
726
|
+
crit_sub_badge = '<div class="sev-sub-badge badge-sub-clean">0 Discovered</div>'
|
|
727
|
+
|
|
728
|
+
if high_open == 0 and high_resolved > 0:
|
|
729
|
+
high_sub_badge = f'<div class="sev-sub-badge badge-sub-clean">🟢 {high_resolved} Remediated (100% Defended)</div>'
|
|
730
|
+
elif high_resolved > 0:
|
|
731
|
+
high_sub_badge = f'<div class="sev-sub-badge">{high_resolved} Solved / {high_total} Total</div>'
|
|
732
|
+
else:
|
|
733
|
+
high_sub_badge = '<div class="sev-sub-badge badge-sub-clean">0 Discovered</div>'
|
|
734
|
+
|
|
735
|
+
if med_open == 0 and med_resolved > 0:
|
|
736
|
+
med_sub_badge = f'<div class="sev-sub-badge badge-sub-clean">🟢 {med_resolved} Remediated (100% Defended)</div>'
|
|
737
|
+
elif med_resolved > 0:
|
|
738
|
+
med_sub_badge = f'<div class="sev-sub-badge">{med_resolved} Solved / {med_total} Total</div>'
|
|
739
|
+
else:
|
|
740
|
+
med_sub_badge = '<div class="sev-sub-badge badge-sub-clean">0 Discovered</div>'
|
|
741
|
+
|
|
742
|
+
# Governed Remediation Velocity Ledger HTML
|
|
743
|
+
rem_crit_pct = 100 if crit_total == 0 or crit_open == 0 else int((crit_resolved / crit_total) * 100)
|
|
744
|
+
rem_high_pct = 100 if high_total == 0 or high_open == 0 else int((high_resolved / high_total) * 100)
|
|
745
|
+
rem_med_pct = 100 if med_total == 0 or med_open == 0 else int((med_resolved / med_total) * 100)
|
|
746
|
+
|
|
747
|
+
remediation_ledger_html = f"""
|
|
748
|
+
<!-- Governed Remediation & Problem-Solved Velocity Ledger -->
|
|
749
|
+
<div class="card remediation-ledger-card">
|
|
750
|
+
<div class="remediation-header">
|
|
751
|
+
<div class="remediation-title-group">
|
|
752
|
+
<span style="font-size: 22px;">⚡</span>
|
|
753
|
+
<div>
|
|
754
|
+
<div style="font-size: 15px; font-weight: 700; color: var(--text-main);">
|
|
755
|
+
Governed Remediation & Problem-Solved Velocity
|
|
756
|
+
</div>
|
|
757
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
|
758
|
+
Historical closure ledger of vulnerabilities discovered, formulated into Ponytail patches, and verified fixed.
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
<div class="remediation-overall-badge">
|
|
763
|
+
<span class="badge {'badge-success' if total_open == 0 and total_resolved > 0 else 'badge-high'}" style="font-size: 12px; padding: 4px 10px;">
|
|
764
|
+
✔ {total_resolved} of {total_discovered} Fixed ({closure_rate_pct}% Defended)
|
|
765
|
+
</span>
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
|
|
769
|
+
<div class="rem-progress-bar-bg">
|
|
770
|
+
<div class="rem-progress-bar-fill" style="width: {closure_rate_pct}%;"></div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<div class="rem-stat-chips-grid">
|
|
774
|
+
<div class="rem-chip">
|
|
775
|
+
<div class="rem-chip-top">
|
|
776
|
+
<span class="rem-chip-sev badge badge-critical">Critical</span>
|
|
777
|
+
<span class="rem-chip-status" style="color: {'var(--accent-green)' if crit_open == 0 else 'var(--accent-red)'};">
|
|
778
|
+
{'🟢 100% Defended' if crit_open == 0 else f'{crit_open} Active'}
|
|
779
|
+
</span>
|
|
780
|
+
</div>
|
|
781
|
+
<div class="rem-chip-main">
|
|
782
|
+
<span class="rem-chip-num">{crit_resolved}</span>
|
|
783
|
+
<span class="rem-chip-lbl">Solved / {crit_total} Discovered</span>
|
|
784
|
+
</div>
|
|
785
|
+
{crit_sub_badge}
|
|
786
|
+
<div class="rem-chip-bar"><div class="rem-chip-bar-fill" style="width: {rem_crit_pct}%; background: var(--accent-green);"></div></div>
|
|
787
|
+
</div>
|
|
788
|
+
|
|
789
|
+
<div class="rem-chip">
|
|
790
|
+
<div class="rem-chip-top">
|
|
791
|
+
<span class="rem-chip-sev badge badge-high">High</span>
|
|
792
|
+
<span class="rem-chip-status" style="color: {'var(--accent-green)' if high_open == 0 else 'var(--accent-orange)'};">
|
|
793
|
+
{'🟢 100% Defended' if high_open == 0 else f'{high_open} Active'}
|
|
794
|
+
</span>
|
|
795
|
+
</div>
|
|
796
|
+
<div class="rem-chip-main">
|
|
797
|
+
<span class="rem-chip-num">{high_resolved}</span>
|
|
798
|
+
<span class="rem-chip-lbl">Solved / {high_total} Discovered</span>
|
|
799
|
+
</div>
|
|
800
|
+
{high_sub_badge}
|
|
801
|
+
<div class="rem-chip-bar"><div class="rem-chip-bar-fill" style="width: {rem_high_pct}%; background: var(--accent-green);"></div></div>
|
|
802
|
+
</div>
|
|
803
|
+
|
|
804
|
+
<div class="rem-chip">
|
|
805
|
+
<div class="rem-chip-top">
|
|
806
|
+
<span class="rem-chip-sev badge badge-medium">Medium / Low</span>
|
|
807
|
+
<span class="rem-chip-status" style="color: {'var(--accent-green)' if med_open == 0 else 'var(--accent-yellow)'};">
|
|
808
|
+
{'🟢 100% Defended' if med_open == 0 else f'{med_open} Active'}
|
|
809
|
+
</span>
|
|
810
|
+
</div>
|
|
811
|
+
<div class="rem-chip-main">
|
|
812
|
+
<span class="rem-chip-num">{med_resolved}</span>
|
|
813
|
+
<span class="rem-chip-lbl">Solved / {med_total} Discovered</span>
|
|
814
|
+
</div>
|
|
815
|
+
{med_sub_badge}
|
|
816
|
+
<div class="rem-chip-bar"><div class="rem-chip-bar-fill" style="width: {rem_med_pct}%; background: var(--accent-green);"></div></div>
|
|
817
|
+
</div>
|
|
818
|
+
|
|
819
|
+
<div class="rem-chip rem-chip-total">
|
|
820
|
+
<div class="rem-chip-top">
|
|
821
|
+
<span class="rem-chip-sev badge badge-success">Guards Defended</span>
|
|
822
|
+
<span class="rem-chip-status" style="color: var(--accent-green);">
|
|
823
|
+
{'🟢 100% Hardened' if total_open == 0 else 'In Progress'}
|
|
824
|
+
</span>
|
|
825
|
+
</div>
|
|
826
|
+
<div class="rem-chip-main">
|
|
827
|
+
<span class="rem-chip-num">{total_resolved}</span>
|
|
828
|
+
<span class="rem-chip-lbl">Total Flaws Neutralized</span>
|
|
829
|
+
</div>
|
|
830
|
+
<div class="rem-chip-bar"><div class="rem-chip-bar-fill" style="width: {closure_rate_pct}%; background: var(--accent-green);"></div></div>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
"""
|
|
835
|
+
|
|
649
836
|
# Recipes
|
|
650
837
|
recipes = get_curated_golden_recipes(patterns)
|
|
651
838
|
|
|
652
|
-
# Recommendations
|
|
653
|
-
violated_ids = {
|
|
839
|
+
# Recommendations (only open findings should be treated as violated)
|
|
840
|
+
violated_ids = {
|
|
841
|
+
f.get("rule_id") for f in findings
|
|
842
|
+
if not f.get("is_canary", False) and f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")
|
|
843
|
+
}
|
|
654
844
|
recommendations = get_recommended_defenses(detected, violated_ids)
|
|
655
845
|
|
|
656
846
|
# Open vs resolved counts
|
|
657
|
-
open_count =
|
|
658
|
-
resolved_count =
|
|
847
|
+
open_count = total_open
|
|
848
|
+
resolved_count = total_resolved
|
|
659
849
|
|
|
660
850
|
# Gauge values & Dynamic Theme Accents
|
|
661
851
|
circumference = 251.2
|
|
@@ -688,10 +878,11 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
688
878
|
# ── Findings Table HTML (with Expandable Drawers & Simulator Checkboxes) ──
|
|
689
879
|
findings_rows = ""
|
|
690
880
|
sev_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}
|
|
691
|
-
|
|
881
|
+
status_order = lambda f: 0 if f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed") else 1
|
|
882
|
+
sorted_f = sorted(findings, key=lambda x: (status_order(x), sev_order.get(x.get("severity", "Low"), 9), -x.get("confidence_score", 0)))
|
|
692
883
|
|
|
693
884
|
if sorted_f:
|
|
694
|
-
for idx, f in enumerate(sorted_f[:
|
|
885
|
+
for idx, f in enumerate(sorted_f[:100]):
|
|
695
886
|
fid = html.escape(f.get("finding_id", f"fid-{idx}"))
|
|
696
887
|
drawer_id = f"drawer-{idx}"
|
|
697
888
|
sev = f.get("severity", "Medium")
|
|
@@ -710,23 +901,32 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
710
901
|
|
|
711
902
|
search_blob = html.escape(f"{rule} {title} {fpath} {sev} {f_status}".lower())
|
|
712
903
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
904
|
+
if f_status == "resolved":
|
|
905
|
+
sim_cell = '<span style="color: var(--accent-green); font-size: 14px;" title="Already resolved and defended">✔</span>'
|
|
906
|
+
drawer_body = f"""
|
|
907
|
+
<div class="drawer-header">
|
|
908
|
+
<div>
|
|
909
|
+
<strong>Finding ID:</strong> <code>{fid}</code> ·
|
|
910
|
+
<strong>File:</strong> <code>{fpath}:{line}</code> ·
|
|
911
|
+
<strong>Confidence:</strong> {conf}/100 ·
|
|
912
|
+
<span class="badge badge-success">✔ Resolved & Defended</span>
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
<div class="drawer-notice-resolved" style="background: rgba(63, 185, 80, 0.1); border: 1px solid rgba(63, 185, 80, 0.3); border-radius: 6px; padding: 10px 14px; margin: 10px 0; font-size: 12px;">
|
|
916
|
+
<strong>✔ Remediated & Hardened:</strong> This security violation has been successfully patched and verified under the Ponytail Protocol (≤ 35 additions, ≤ 25 deletions).
|
|
917
|
+
</div>
|
|
918
|
+
<div class="drawer-desc">{desc}</div>
|
|
919
|
+
<div class="drawer-actions">
|
|
920
|
+
<div class="cmd-box">
|
|
921
|
+
<code>npx torusguard recheck --rule {rule}</code>
|
|
922
|
+
<button class="btn-copy" onclick="copyText('npx torusguard recheck --rule {rule}', this); event.stopPropagation();">📋 Copy Recheck Command</button>
|
|
923
|
+
</div>
|
|
924
|
+
</div>"""
|
|
925
|
+
else:
|
|
926
|
+
sim_cell = f"""<label class="sim-checkbox-label" title="Simulate fixing this finding in What-If Posture Simulator">
|
|
717
927
|
<input type="checkbox" class="sim-fix-chk" data-finding-id="{fid}" data-severity="{sev}" onchange="updateSimulatedScore()">
|
|
718
|
-
</label>
|
|
719
|
-
|
|
720
|
-
<td><span class="badge {sev_class}">{sev}</span>{canary_badge}{status_badge}</td>
|
|
721
|
-
<td><code class="rule-id">{rule}</code></td>
|
|
722
|
-
<td class="finding-title">{title}</td>
|
|
723
|
-
<td><code>{fpath}:{line}</code></td>
|
|
724
|
-
<td><span class="conf-score">{conf}%</span></td>
|
|
725
|
-
<td style="text-align: right;"><span class="drawer-chevron">▸</span></td>
|
|
726
|
-
</tr>
|
|
727
|
-
<tr id="{drawer_id}" class="drawer-row" style="display: none;">
|
|
728
|
-
<td colspan="7">
|
|
729
|
-
<div class="drawer-content">
|
|
928
|
+
</label>"""
|
|
929
|
+
drawer_body = f"""
|
|
730
930
|
<div class="drawer-header">
|
|
731
931
|
<div>
|
|
732
932
|
<strong>Finding ID:</strong> <code>{fid}</code> ·
|
|
@@ -743,12 +943,29 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
743
943
|
<code>npx torusguard harden --finding {fid}</code>
|
|
744
944
|
<button class="btn-copy" onclick="copyText('npx torusguard harden --finding {fid}', this); event.stopPropagation();">📋 Copy Command</button>
|
|
745
945
|
</div>
|
|
746
|
-
</div>
|
|
946
|
+
</div>"""
|
|
947
|
+
|
|
948
|
+
findings_rows += f"""
|
|
949
|
+
<tr class="finding-row" data-severity="{sev.lower()}" data-status="{f_status}" data-search="{search_blob}" data-path="{fpath}" data-drawer-id="{drawer_id}" onclick="toggleDrawer(this)">
|
|
950
|
+
<td style="text-align: center;" onclick="event.stopPropagation()">
|
|
951
|
+
{sim_cell}
|
|
952
|
+
</td>
|
|
953
|
+
<td><span class="badge {sev_class}">{sev}</span>{canary_badge}{status_badge}</td>
|
|
954
|
+
<td><code class="rule-id">{rule}</code></td>
|
|
955
|
+
<td class="finding-title">{title}</td>
|
|
956
|
+
<td><code>{fpath}:{line}</code></td>
|
|
957
|
+
<td><span class="conf-score">{conf}%</span></td>
|
|
958
|
+
<td style="text-align: right;"><span class="drawer-chevron">▸</span></td>
|
|
959
|
+
</tr>
|
|
960
|
+
<tr id="{drawer_id}" class="drawer-row" style="display: none;">
|
|
961
|
+
<td colspan="7">
|
|
962
|
+
<div class="drawer-content">
|
|
963
|
+
{drawer_body}
|
|
747
964
|
</div>
|
|
748
965
|
</td>
|
|
749
966
|
</tr>"""
|
|
750
|
-
if len(sorted_f) >
|
|
751
|
-
findings_rows += f'<tr><td colspan="7" class="overflow-row">... and {len(sorted_f) -
|
|
967
|
+
if len(sorted_f) > 100:
|
|
968
|
+
findings_rows += f'<tr><td colspan="7" class="overflow-row">... and {len(sorted_f) - 100} more findings cataloged in run artifacts</td></tr>'
|
|
752
969
|
else:
|
|
753
970
|
findings_rows = '<tr><td colspan="7" class="empty-cell">✔ Zero security invariant violations detected. Posture is 100% clean and defended.</td></tr>'
|
|
754
971
|
|
|
@@ -816,7 +1033,12 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
816
1033
|
|
|
817
1034
|
owasp_cards_html = ""
|
|
818
1035
|
for category_name, rule_codes in owasp_defs:
|
|
819
|
-
matched = [
|
|
1036
|
+
matched = [
|
|
1037
|
+
f for f in findings
|
|
1038
|
+
if f.get("rule_id") in rule_codes
|
|
1039
|
+
and not f.get("is_canary", False)
|
|
1040
|
+
and str(f.get("status", "open")).lower() not in ("resolved", "fixed", "confirmed_fixed")
|
|
1041
|
+
]
|
|
820
1042
|
cat_count = len(matched)
|
|
821
1043
|
if cat_count == 0:
|
|
822
1044
|
c_score = 100
|
|
@@ -926,45 +1148,115 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
926
1148
|
sorted_clusters = sorted(cluster_groups.items(), key=lambda x: len(x[1]), reverse=True)
|
|
927
1149
|
clusters_html = ""
|
|
928
1150
|
if sorted_clusters:
|
|
929
|
-
for c_name, c_list in sorted_clusters[:
|
|
930
|
-
|
|
931
|
-
|
|
1151
|
+
for c_name, c_list in sorted_clusters[:8]:
|
|
1152
|
+
c_total = len(c_list)
|
|
1153
|
+
c_open = sum(1 for item in c_list if item.get("status", "open") not in ("resolved", "fixed", "confirmed_fixed"))
|
|
1154
|
+
c_resolved = c_total - c_open
|
|
1155
|
+
unique_files = len(set(item.get("file_path", "") for item in c_list if item.get("file_path")))
|
|
1156
|
+
if c_open == 0 and c_resolved > 0:
|
|
1157
|
+
badge_html = f'<span class="badge badge-success">✔ {c_resolved} Dismantled</span>'
|
|
1158
|
+
sub_text = f'Blast Radius: {unique_files} file(s) · <span style="color: var(--accent-green); font-weight: 600;">🟢 Neutralized & Defended</span>'
|
|
1159
|
+
else:
|
|
1160
|
+
badge_html = f'<span class="badge badge-high">{c_open} Active ({c_resolved} fixed)</span>'
|
|
1161
|
+
sub_text = f'Blast Radius: {unique_files} file(s) affected · Remediation in progress'
|
|
1162
|
+
|
|
932
1163
|
clusters_html += f"""
|
|
933
1164
|
<div class="cluster-item">
|
|
934
1165
|
<div class="cluster-header">
|
|
935
1166
|
<strong>{html.escape(c_name.replace('-', ' ').title())}</strong>
|
|
936
|
-
|
|
1167
|
+
{badge_html}
|
|
937
1168
|
</div>
|
|
938
|
-
<div class="cluster-sub">
|
|
1169
|
+
<div class="cluster-sub">{sub_text}</div>
|
|
939
1170
|
</div>"""
|
|
940
1171
|
else:
|
|
941
|
-
clusters_html = '<div class="empty-state">✔
|
|
1172
|
+
clusters_html = '<div class="empty-state">✔ Zero architectural vulnerability clusters identified. Systemic invariants enforced.</div>'
|
|
1173
|
+
|
|
1174
|
+
# ── Interactive 2D Directory Attack Surface Heatmap Matrix ──
|
|
1175
|
+
HEATMAP_DOMAINS = [
|
|
1176
|
+
{"key": "auth", "label": "AUTH", "name": "Identity & Auth", "pfx": ("TG-AUTH", "TG-CSRF", "TG-WS")},
|
|
1177
|
+
{"key": "db", "label": "DB", "name": "Database & Isolation", "pfx": ("TG-DB", "TG-GQL", "TG-CACHE")},
|
|
1178
|
+
{"key": "input", "label": "INPUT", "name": "Input & LLM Defense", "pfx": ("TG-INPUT", "TG-AGENT")},
|
|
1179
|
+
{"key": "net", "label": "NET", "name": "Network & SSRF", "pfx": ("TG-SSRF", "TG-WEBHOOK", "TG-RATE")},
|
|
1180
|
+
{"key": "plat", "label": "PLAT", "name": "Platform & Supply", "pfx": ("TG-PLATFORM", "TG-SUPPLY", "TG-DIFF", "TG-EDGE")},
|
|
1181
|
+
{"key": "sec", "label": "SEC", "name": "Secrets & Keys", "pfx": ("TG-SEC", "TG-CLIENT", "TG-BIZ")},
|
|
1182
|
+
]
|
|
942
1183
|
|
|
943
|
-
|
|
944
|
-
dir_counts: dict[str, int] = {}
|
|
1184
|
+
dir_matrix: dict[str, dict[str, Any]] = {}
|
|
945
1185
|
for f in findings:
|
|
946
|
-
fp = f.get("file_path", "")
|
|
947
|
-
parts = fp.
|
|
1186
|
+
fp = f.get("file_path", "").replace("\\", "/")
|
|
1187
|
+
parts = fp.split("/")
|
|
948
1188
|
d_name = f"{parts[0]}/" if len(parts) > 1 else "root"
|
|
949
|
-
|
|
1189
|
+
rule_id = f.get("rule_id", "")
|
|
1190
|
+
domain_key = "sec"
|
|
1191
|
+
for d in HEATMAP_DOMAINS:
|
|
1192
|
+
if any(rule_id.startswith(p) for p in d["pfx"]):
|
|
1193
|
+
domain_key = d["key"]
|
|
1194
|
+
break
|
|
1195
|
+
if d_name not in dir_matrix:
|
|
1196
|
+
dir_matrix[d_name] = {
|
|
1197
|
+
"name": d_name,
|
|
1198
|
+
"domains": {d["key"]: {"open_crit": 0, "open_warn": 0, "resolved": 0} for d in HEATMAP_DOMAINS},
|
|
1199
|
+
"total_open": 0,
|
|
1200
|
+
"total_resolved": 0,
|
|
1201
|
+
}
|
|
1202
|
+
st = str(f.get("status", "open")).lower()
|
|
1203
|
+
sev = f.get("severity", "Medium")
|
|
1204
|
+
if st in ("resolved", "fixed", "confirmed_fixed"):
|
|
1205
|
+
dir_matrix[d_name]["domains"][domain_key]["resolved"] += 1
|
|
1206
|
+
dir_matrix[d_name]["total_resolved"] += 1
|
|
1207
|
+
else:
|
|
1208
|
+
if sev in ("Critical", "High"):
|
|
1209
|
+
dir_matrix[d_name]["domains"][domain_key]["open_crit"] += 1
|
|
1210
|
+
else:
|
|
1211
|
+
dir_matrix[d_name]["domains"][domain_key]["open_warn"] += 1
|
|
1212
|
+
dir_matrix[d_name]["total_open"] += 1
|
|
950
1213
|
|
|
951
|
-
sorted_dirs = sorted(dir_counts.items(), key=lambda x: x[1], reverse=True)
|
|
952
|
-
max_dir_count = sorted_dirs[0][1] if sorted_dirs else 1
|
|
953
1214
|
heatmap_rows_html = ""
|
|
954
|
-
if
|
|
955
|
-
for d_name,
|
|
956
|
-
|
|
957
|
-
|
|
1215
|
+
if dir_matrix:
|
|
1216
|
+
for d_name, d_data in sorted(dir_matrix.items(), key=lambda x: (x[1]["total_open"], x[1]["total_resolved"]), reverse=True)[:5]:
|
|
1217
|
+
escaped_d = html.escape(d_name)
|
|
1218
|
+
d_open = d_data["total_open"]
|
|
1219
|
+
d_res = d_data["total_resolved"]
|
|
1220
|
+
if d_open == 0 and d_res > 0:
|
|
1221
|
+
status_badge = f'<span class="heatmap-badge-clean">🟢 {d_res} Defended</span>'
|
|
1222
|
+
elif d_open > 0:
|
|
1223
|
+
status_badge = f'<span class="badge badge-high">🔴 {d_open} Active</span>'
|
|
1224
|
+
else:
|
|
1225
|
+
status_badge = '<span class="heatmap-badge-clean">Clean</span>'
|
|
1226
|
+
|
|
1227
|
+
cells_html = ""
|
|
1228
|
+
for d in HEATMAP_DOMAINS:
|
|
1229
|
+
cell = d_data["domains"][d["key"]]
|
|
1230
|
+
c_crit = cell["open_crit"]
|
|
1231
|
+
c_warn = cell["open_warn"]
|
|
1232
|
+
c_res = cell["resolved"]
|
|
1233
|
+
if c_crit > 0:
|
|
1234
|
+
cls = "heat-cell-crit"
|
|
1235
|
+
txt = f"🔴 {c_crit}"
|
|
1236
|
+
tip = f"{d_name} • {d['name']}: {c_crit} Critical/High flaws"
|
|
1237
|
+
elif c_warn > 0:
|
|
1238
|
+
cls = "heat-cell-warn"
|
|
1239
|
+
txt = f"🟡 {c_warn}"
|
|
1240
|
+
tip = f"{d_name} • {d['name']}: {c_warn} Warning flaws"
|
|
1241
|
+
elif c_res > 0:
|
|
1242
|
+
cls = "heat-cell-safe"
|
|
1243
|
+
txt = f"✔ {c_res}"
|
|
1244
|
+
tip = f"{d_name} • {d['name']}: {c_res} Invariants Hardened"
|
|
1245
|
+
else:
|
|
1246
|
+
cls = "heat-cell-idle"
|
|
1247
|
+
txt = "—"
|
|
1248
|
+
tip = f"{d_name} • {d['name']}: Clean / Zero Flaws"
|
|
1249
|
+
tip_escaped = html.escape(tip)
|
|
1250
|
+
cells_html += f"""<td class="heat-cell {cls}" title="{tip_escaped}" onclick="filterByDirectory('{escaped_d}'); event.stopPropagation();">{txt}</td>"""
|
|
1251
|
+
|
|
958
1252
|
heatmap_rows_html += f"""
|
|
959
|
-
<
|
|
960
|
-
<
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
<span class="heatmap-count">{count}</span>
|
|
965
|
-
</div>"""
|
|
1253
|
+
<tr class="heatmap-row" onclick="filterByDirectory('{escaped_d}')" title="Click to filter findings in {escaped_d}">
|
|
1254
|
+
<td class="heatmap-label-cell"><code>{escaped_d}</code></td>
|
|
1255
|
+
{cells_html}
|
|
1256
|
+
<td style="text-align: right;">{status_badge}</td>
|
|
1257
|
+
</tr>"""
|
|
966
1258
|
else:
|
|
967
|
-
heatmap_rows_html = '<
|
|
1259
|
+
heatmap_rows_html = '<tr><td colspan="8" class="empty-state">✔ Zero directory attack surface exposure detected.</td></tr>'
|
|
968
1260
|
|
|
969
1261
|
# ── Golden Fix Recipes (Dual-View Diff / Split, Churn Metrics, Category Filter) ──
|
|
970
1262
|
# Category counts for filter pills
|
|
@@ -1071,6 +1363,133 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
1071
1363
|
</div>
|
|
1072
1364
|
</div>"""
|
|
1073
1365
|
|
|
1366
|
+
# ── Findings Section (Clean Baseline Card vs Active Attention) ──
|
|
1367
|
+
if open_count == 0:
|
|
1368
|
+
findings_section_html = f"""
|
|
1369
|
+
<!-- Clean & Hardened Baseline Summary (Zero Active Vulnerabilities) -->
|
|
1370
|
+
<div class="clean-baseline-card">
|
|
1371
|
+
<div class="clean-baseline-header">
|
|
1372
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
1373
|
+
<span style="font-size: 26px;">🛡️</span>
|
|
1374
|
+
<div>
|
|
1375
|
+
<div style="font-size: 15px; font-weight: 700; color: var(--accent-green); letter-spacing: -0.01em;">
|
|
1376
|
+
Clean & Hardened Baseline · 0 Active Vulnerabilities
|
|
1377
|
+
</div>
|
|
1378
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
|
1379
|
+
All {resolved_count} identified issues have been governed, hardened, and verified across all architectural layers.
|
|
1380
|
+
</div>
|
|
1381
|
+
</div>
|
|
1382
|
+
</div>
|
|
1383
|
+
<button class="btn-archive-toggle" id="btnArchiveToggle" onclick="toggleFindingsArchive()">
|
|
1384
|
+
<span id="archiveToggleIcon">▶</span> View Findings Archive ({total_discovered})
|
|
1385
|
+
</button>
|
|
1386
|
+
</div>
|
|
1387
|
+
<div id="findingsArchiveBody" style="display: block; margin-top: 16px; border-top: 1px solid var(--border-subtle); padding-top: 16px;">
|
|
1388
|
+
<div class="filter-bar">
|
|
1389
|
+
<div class="filter-pills">
|
|
1390
|
+
<span class="pill-group-label">Severity:</span>
|
|
1391
|
+
<span class="pill active" data-sev-filter="all" onclick="filterBySeverity('all')">All</span>
|
|
1392
|
+
<span class="pill" data-sev-filter="critical" onclick="filterBySeverity('critical')">🔴 Critical ({crit_open})</span>
|
|
1393
|
+
<span class="pill" data-sev-filter="high" onclick="filterBySeverity('high')">🟠 High ({high_open})</span>
|
|
1394
|
+
<span class="pill" data-sev-filter="medium" onclick="filterBySeverity('medium')">🟡 Med/Low ({med_open})</span>
|
|
1395
|
+
|
|
1396
|
+
<span class="pill-group-label" style="margin-left: 8px;">Status:</span>
|
|
1397
|
+
<span class="pill active" data-status-filter="all" onclick="filterByStatus('all')">All ({total_discovered})</span>
|
|
1398
|
+
<span class="pill" data-status-filter="open" onclick="filterByStatus('open')">⚠ Open ({open_count})</span>
|
|
1399
|
+
<span class="pill" data-status-filter="resolved" onclick="filterByStatus('resolved')">✔ Resolved ({resolved_count})</span>
|
|
1400
|
+
|
|
1401
|
+
<span class="dir-filter-chip" id="dirFilterChip" onclick="clearDirectoryFilter()">
|
|
1402
|
+
Folder: <strong id="dirFilterName"></strong> ×
|
|
1403
|
+
</span>
|
|
1404
|
+
<button class="btn-reset-filters" id="btnResetFilters" onclick="resetAllFilters()">× Reset Filters</button>
|
|
1405
|
+
</div>
|
|
1406
|
+
<div class="search-wrapper">
|
|
1407
|
+
<input type="text" id="searchInput" class="search-input" placeholder="Search rules, titles, files..." oninput="searchFindings()">
|
|
1408
|
+
<button class="search-clear-btn" id="searchClearBtn" onclick="clearSearch()">×</button>
|
|
1409
|
+
</div>
|
|
1410
|
+
</div>
|
|
1411
|
+
<div class="filter-stats-bar">
|
|
1412
|
+
<div>Showing <span id="visibleCount" style="font-weight: 700; color: var(--text-main);">{min(len(sorted_f), 50)}</span> of <span id="totalCount" style="font-weight: 700; color: var(--text-main);">{len(sorted_f)}</span> cataloged findings</div>
|
|
1413
|
+
<div id="activeFilterSummary" style="font-size: 11px; color: var(--accent-blue);"></div>
|
|
1414
|
+
</div>
|
|
1415
|
+
<div class="table-container">
|
|
1416
|
+
<table>
|
|
1417
|
+
<thead>
|
|
1418
|
+
<tr>
|
|
1419
|
+
<th style="width: 70px; text-align: center;">Simulate</th>
|
|
1420
|
+
<th style="width: 110px;">Severity</th>
|
|
1421
|
+
<th style="width: 130px;">Rule ID</th>
|
|
1422
|
+
<th>Finding Title</th>
|
|
1423
|
+
<th style="width: 250px;">File & Location</th>
|
|
1424
|
+
<th style="width: 90px;">Confidence</th>
|
|
1425
|
+
<th style="width: 30px;"></th>
|
|
1426
|
+
</tr>
|
|
1427
|
+
</thead>
|
|
1428
|
+
<tbody id="findingsTableBody">
|
|
1429
|
+
{findings_rows}
|
|
1430
|
+
</tbody>
|
|
1431
|
+
</table>
|
|
1432
|
+
</div>
|
|
1433
|
+
</div>
|
|
1434
|
+
</div>"""
|
|
1435
|
+
else:
|
|
1436
|
+
findings_section_html = f"""
|
|
1437
|
+
<!-- Active Vulnerabilities Requiring Attention -->
|
|
1438
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px;">
|
|
1439
|
+
<div class="section-title" style="margin-bottom: 0;">
|
|
1440
|
+
⚠ Active Vulnerabilities Requiring Attention ({open_count})
|
|
1441
|
+
</div>
|
|
1442
|
+
<button class="btn-archive-toggle" id="btnArchiveToggle" onclick="toggleFindingsArchive()">
|
|
1443
|
+
<span id="archiveToggleIcon">▶</span> View Resolved Archive ({resolved_count})
|
|
1444
|
+
</button>
|
|
1445
|
+
</div>
|
|
1446
|
+
<div class="filter-bar">
|
|
1447
|
+
<div class="filter-pills">
|
|
1448
|
+
<span class="pill-group-label">Severity:</span>
|
|
1449
|
+
<span class="pill active" data-sev-filter="all" onclick="filterBySeverity('all')">All</span>
|
|
1450
|
+
<span class="pill" data-sev-filter="critical" onclick="filterBySeverity('critical')">🔴 Critical ({crit_open})</span>
|
|
1451
|
+
<span class="pill" data-sev-filter="high" onclick="filterBySeverity('high')">🟠 High ({high_open})</span>
|
|
1452
|
+
<span class="pill" data-sev-filter="medium" onclick="filterBySeverity('medium')">🟡 Med/Low ({med_open})</span>
|
|
1453
|
+
|
|
1454
|
+
<span class="pill-group-label" style="margin-left: 8px;">Status:</span>
|
|
1455
|
+
<span class="pill active" data-status-filter="all" onclick="filterByStatus('all')">All ({total_discovered})</span>
|
|
1456
|
+
<span class="pill" data-status-filter="open" onclick="filterByStatus('open')">⚠ Open ({open_count})</span>
|
|
1457
|
+
<span class="pill" data-status-filter="resolved" onclick="filterByStatus('resolved')">✔ Resolved ({resolved_count})</span>
|
|
1458
|
+
|
|
1459
|
+
<span class="dir-filter-chip" id="dirFilterChip" onclick="clearDirectoryFilter()">
|
|
1460
|
+
Folder: <strong id="dirFilterName"></strong> ×
|
|
1461
|
+
</span>
|
|
1462
|
+
<button class="btn-reset-filters" id="btnResetFilters" onclick="resetAllFilters()">× Reset Filters</button>
|
|
1463
|
+
</div>
|
|
1464
|
+
<div class="search-wrapper">
|
|
1465
|
+
<input type="text" id="searchInput" class="search-input" placeholder="Search rules, titles, files..." oninput="searchFindings()">
|
|
1466
|
+
<button class="search-clear-btn" id="searchClearBtn" onclick="clearSearch()">×</button>
|
|
1467
|
+
</div>
|
|
1468
|
+
</div>
|
|
1469
|
+
<div class="filter-stats-bar">
|
|
1470
|
+
<div>Showing <span id="visibleCount" style="font-weight: 700; color: var(--text-main);">{min(len(sorted_f), 50)}</span> of <span id="totalCount" style="font-weight: 700; color: var(--text-main);">{len(sorted_f)}</span> cataloged findings</div>
|
|
1471
|
+
<div id="activeFilterSummary" style="font-size: 11px; color: var(--accent-blue);"></div>
|
|
1472
|
+
</div>
|
|
1473
|
+
<div class="table-container">
|
|
1474
|
+
<table>
|
|
1475
|
+
<thead>
|
|
1476
|
+
<tr>
|
|
1477
|
+
<th style="width: 70px; text-align: center;">Simulate</th>
|
|
1478
|
+
<th style="width: 110px;">Severity</th>
|
|
1479
|
+
<th style="width: 130px;">Rule ID</th>
|
|
1480
|
+
<th>Finding Title</th>
|
|
1481
|
+
<th style="width: 250px;">File & Location</th>
|
|
1482
|
+
<th style="width: 90px;">Confidence</th>
|
|
1483
|
+
<th style="width: 30px;"></th>
|
|
1484
|
+
</tr>
|
|
1485
|
+
</thead>
|
|
1486
|
+
<tbody id="findingsTableBody">
|
|
1487
|
+
{findings_rows}
|
|
1488
|
+
</tbody>
|
|
1489
|
+
</table>
|
|
1490
|
+
</div>
|
|
1491
|
+
<div id="findingsArchiveBody" style="display: block;"></div>"""
|
|
1492
|
+
|
|
1074
1493
|
# ── Client-side JSON Payloads ──
|
|
1075
1494
|
findings_json_str = json.dumps(findings).replace("</script>", "<\\/script>")
|
|
1076
1495
|
sarif_rules_json_str = json.dumps([{"id": r["rule_id"], "shortDescription": {"text": r["title"]}} for r in all_rules[:74]])
|
|
@@ -1212,6 +1631,117 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
1212
1631
|
.sev-card:hover {{ transform: translateY(-2px); border-color: var(--accent-blue); }}
|
|
1213
1632
|
.sev-card .sev-count {{ font-size: 34px; font-weight: 800; }}
|
|
1214
1633
|
.sev-card .sev-label {{ font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); margin-top: 4px; }}
|
|
1634
|
+
.sev-sub-badge {{
|
|
1635
|
+
font-size: 11px;
|
|
1636
|
+
font-weight: 600;
|
|
1637
|
+
margin-top: 6px;
|
|
1638
|
+
padding: 2px 8px;
|
|
1639
|
+
border-radius: 12px;
|
|
1640
|
+
background: rgba(88, 166, 255, 0.1);
|
|
1641
|
+
border: 1px solid rgba(88, 166, 255, 0.25);
|
|
1642
|
+
color: var(--accent-blue);
|
|
1643
|
+
display: inline-block;
|
|
1644
|
+
}}
|
|
1645
|
+
.sev-sub-badge.badge-sub-clean {{
|
|
1646
|
+
background: rgba(63, 185, 80, 0.15);
|
|
1647
|
+
border-color: rgba(63, 185, 80, 0.4);
|
|
1648
|
+
color: var(--accent-green);
|
|
1649
|
+
}}
|
|
1650
|
+
.sev-sub-badge.badge-sub-warn {{
|
|
1651
|
+
background: rgba(210, 153, 34, 0.15);
|
|
1652
|
+
border-color: rgba(210, 153, 34, 0.4);
|
|
1653
|
+
color: var(--accent-yellow);
|
|
1654
|
+
}}
|
|
1655
|
+
|
|
1656
|
+
/* Governed Remediation Ledger */
|
|
1657
|
+
.remediation-ledger-card {{
|
|
1658
|
+
margin-bottom: 24px;
|
|
1659
|
+
background: var(--bg-surface);
|
|
1660
|
+
border: 1px solid var(--border-subtle);
|
|
1661
|
+
border-radius: 8px;
|
|
1662
|
+
padding: 18px;
|
|
1663
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
1664
|
+
border-top: 3px solid var(--accent-green);
|
|
1665
|
+
}}
|
|
1666
|
+
.remediation-header {{
|
|
1667
|
+
display: flex;
|
|
1668
|
+
justify-content: space-between;
|
|
1669
|
+
align-items: center;
|
|
1670
|
+
flex-wrap: wrap;
|
|
1671
|
+
gap: 12px;
|
|
1672
|
+
margin-bottom: 14px;
|
|
1673
|
+
}}
|
|
1674
|
+
.remediation-title-group {{
|
|
1675
|
+
display: flex;
|
|
1676
|
+
align-items: center;
|
|
1677
|
+
gap: 10px;
|
|
1678
|
+
}}
|
|
1679
|
+
.rem-progress-bar-bg {{
|
|
1680
|
+
width: 100%;
|
|
1681
|
+
height: 8px;
|
|
1682
|
+
background: var(--bg-card);
|
|
1683
|
+
border-radius: 4px;
|
|
1684
|
+
overflow: hidden;
|
|
1685
|
+
margin-bottom: 16px;
|
|
1686
|
+
}}
|
|
1687
|
+
.rem-progress-bar-fill {{
|
|
1688
|
+
height: 100%;
|
|
1689
|
+
background: linear-gradient(90deg, #2ea043, #3fb950);
|
|
1690
|
+
border-radius: 4px;
|
|
1691
|
+
transition: width 0.8s ease;
|
|
1692
|
+
}}
|
|
1693
|
+
.rem-stat-chips-grid {{
|
|
1694
|
+
display: grid;
|
|
1695
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1696
|
+
gap: 12px;
|
|
1697
|
+
}}
|
|
1698
|
+
@media (max-width: 900px) {{ .rem-stat-chips-grid {{ grid-template-columns: 1fr 1fr; }} }}
|
|
1699
|
+
@media (max-width: 550px) {{ .rem-stat-chips-grid {{ grid-template-columns: 1fr; }} }}
|
|
1700
|
+
.rem-chip {{
|
|
1701
|
+
background: var(--bg-card);
|
|
1702
|
+
border: 1px solid var(--border-subtle);
|
|
1703
|
+
border-radius: 6px;
|
|
1704
|
+
padding: 12px;
|
|
1705
|
+
display: flex;
|
|
1706
|
+
flex-direction: column;
|
|
1707
|
+
gap: 6px;
|
|
1708
|
+
}}
|
|
1709
|
+
.rem-chip-top {{
|
|
1710
|
+
display: flex;
|
|
1711
|
+
justify-content: space-between;
|
|
1712
|
+
align-items: center;
|
|
1713
|
+
}}
|
|
1714
|
+
.rem-chip-sev {{ font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; }}
|
|
1715
|
+
.rem-chip-status {{ font-size: 11px; font-weight: 600; }}
|
|
1716
|
+
.rem-chip-main {{
|
|
1717
|
+
display: flex;
|
|
1718
|
+
align-items: baseline;
|
|
1719
|
+
gap: 6px;
|
|
1720
|
+
}}
|
|
1721
|
+
.rem-chip-num {{ font-size: 22px; font-weight: 800; color: var(--text-main); }}
|
|
1722
|
+
.rem-chip-lbl {{ font-size: 11px; color: var(--text-muted); }}
|
|
1723
|
+
.rem-chip-bar {{
|
|
1724
|
+
width: 100%;
|
|
1725
|
+
height: 4px;
|
|
1726
|
+
background: rgba(255, 255, 255, 0.05);
|
|
1727
|
+
border-radius: 2px;
|
|
1728
|
+
overflow: hidden;
|
|
1729
|
+
margin-top: 2px;
|
|
1730
|
+
}}
|
|
1731
|
+
.rem-chip-bar-fill {{ height: 100%; border-radius: 2px; }}
|
|
1732
|
+
.rem-chip-total {{
|
|
1733
|
+
border-color: rgba(63, 185, 80, 0.35);
|
|
1734
|
+
background: rgba(63, 185, 80, 0.05);
|
|
1735
|
+
}}
|
|
1736
|
+
.heatmap-badge-clean {{
|
|
1737
|
+
font-size: 10px;
|
|
1738
|
+
font-weight: 700;
|
|
1739
|
+
color: var(--accent-green);
|
|
1740
|
+
background: rgba(63, 185, 80, 0.12);
|
|
1741
|
+
border: 1px solid rgba(63, 185, 80, 0.3);
|
|
1742
|
+
border-radius: 4px;
|
|
1743
|
+
padding: 1px 6px;
|
|
1744
|
+
}}
|
|
1215
1745
|
|
|
1216
1746
|
/* What-If Posture Simulator Card */
|
|
1217
1747
|
.simulator-card {{
|
|
@@ -1481,6 +2011,46 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
1481
2011
|
justify-content: space-between;
|
|
1482
2012
|
}}
|
|
1483
2013
|
|
|
2014
|
+
/* Clean Baseline Card & Findings Archive */
|
|
2015
|
+
.clean-baseline-card {{
|
|
2016
|
+
background: var(--bg-surface);
|
|
2017
|
+
border: 1px solid var(--border-subtle);
|
|
2018
|
+
border-left: 4px solid var(--accent-green);
|
|
2019
|
+
border-radius: 8px;
|
|
2020
|
+
padding: 16px 20px;
|
|
2021
|
+
margin-bottom: 24px;
|
|
2022
|
+
transition: border-color 0.2s;
|
|
2023
|
+
}}
|
|
2024
|
+
.clean-baseline-header {{
|
|
2025
|
+
display: flex;
|
|
2026
|
+
justify-content: space-between;
|
|
2027
|
+
align-items: center;
|
|
2028
|
+
flex-wrap: wrap;
|
|
2029
|
+
gap: 14px;
|
|
2030
|
+
}}
|
|
2031
|
+
.btn-archive-toggle {{
|
|
2032
|
+
background: var(--bg-card);
|
|
2033
|
+
border: 1px solid var(--border-subtle);
|
|
2034
|
+
color: var(--text-main);
|
|
2035
|
+
padding: 6px 14px;
|
|
2036
|
+
border-radius: 6px;
|
|
2037
|
+
font-size: 12px;
|
|
2038
|
+
font-weight: 600;
|
|
2039
|
+
cursor: pointer;
|
|
2040
|
+
display: inline-flex;
|
|
2041
|
+
align-items: center;
|
|
2042
|
+
gap: 8px;
|
|
2043
|
+
transition: all 0.2s ease;
|
|
2044
|
+
}}
|
|
2045
|
+
.btn-archive-toggle:hover {{
|
|
2046
|
+
background: #30363d;
|
|
2047
|
+
border-color: var(--accent-blue);
|
|
2048
|
+
color: var(--accent-blue);
|
|
2049
|
+
}}
|
|
2050
|
+
body.light-theme .btn-archive-toggle:hover {{
|
|
2051
|
+
background: #e1e4e8;
|
|
2052
|
+
}}
|
|
2053
|
+
|
|
1484
2054
|
/* Table & Drawers */
|
|
1485
2055
|
.table-container {{
|
|
1486
2056
|
background: var(--bg-surface);
|
|
@@ -1591,11 +2161,18 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
1591
2161
|
}}
|
|
1592
2162
|
|
|
1593
2163
|
/* OWASP Grid */
|
|
2164
|
+
.owasp-card-wrapper {{
|
|
2165
|
+
padding: 10px 14px;
|
|
2166
|
+
display: flex;
|
|
2167
|
+
flex-direction: column;
|
|
2168
|
+
justify-content: flex-start;
|
|
2169
|
+
gap: 6px;
|
|
2170
|
+
}}
|
|
1594
2171
|
.grid-owasp {{
|
|
1595
2172
|
display: grid;
|
|
1596
2173
|
grid-template-columns: repeat(4, 1fr);
|
|
1597
|
-
gap:
|
|
1598
|
-
margin-bottom:
|
|
2174
|
+
gap: 8px;
|
|
2175
|
+
margin-bottom: 0;
|
|
1599
2176
|
}}
|
|
1600
2177
|
@media (max-width: 950px) {{ .grid-owasp {{ grid-template-columns: repeat(2, 1fr); }} }}
|
|
1601
2178
|
@media (max-width: 550px) {{ .grid-owasp {{ grid-template-columns: 1fr; }} }}
|
|
@@ -1603,37 +2180,44 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
1603
2180
|
background: var(--bg-surface);
|
|
1604
2181
|
border: 1px solid var(--border-subtle);
|
|
1605
2182
|
border-radius: 6px;
|
|
1606
|
-
padding:
|
|
2183
|
+
padding: 8px 10px;
|
|
1607
2184
|
}}
|
|
1608
|
-
.owasp-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom:
|
|
2185
|
+
.owasp-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }}
|
|
1609
2186
|
.owasp-title {{ font-size: 11px; font-weight: 600; color: var(--text-main); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
|
1610
|
-
.owasp-badge {{ font-size:
|
|
1611
|
-
.owasp-bar-bg {{ width: 100%; height:
|
|
2187
|
+
.owasp-badge {{ font-size: 9.5px; font-weight: 700; border: 1px solid; border-radius: 8px; padding: 1px 5px; white-space: nowrap; }}
|
|
2188
|
+
.owasp-bar-bg {{ width: 100%; height: 5px; background: var(--bg-card); border-radius: 3px; overflow: hidden; }}
|
|
1612
2189
|
.owasp-bar {{ height: 100%; border-radius: 3px; }}
|
|
1613
2190
|
|
|
1614
2191
|
/* Compliance Framework Grid */
|
|
2192
|
+
.comp-card-wrapper {{
|
|
2193
|
+
padding: 10px 14px;
|
|
2194
|
+
display: flex;
|
|
2195
|
+
flex-direction: column;
|
|
2196
|
+
justify-content: flex-start;
|
|
2197
|
+
gap: 6px;
|
|
2198
|
+
}}
|
|
1615
2199
|
.grid-compliance {{
|
|
1616
2200
|
display: grid;
|
|
1617
2201
|
grid-template-columns: repeat(3, 1fr);
|
|
1618
|
-
gap:
|
|
1619
|
-
margin-bottom:
|
|
2202
|
+
gap: 8px;
|
|
2203
|
+
margin-bottom: 0;
|
|
1620
2204
|
}}
|
|
1621
2205
|
@media (max-width: 900px) {{ .grid-compliance {{ grid-template-columns: 1fr; }} }}
|
|
1622
|
-
.comp-card {{ padding:
|
|
2206
|
+
.comp-card {{ padding: 8px 10px; }}
|
|
1623
2207
|
.comp-card-header {{
|
|
1624
2208
|
display: flex;
|
|
1625
2209
|
justify-content: space-between;
|
|
1626
|
-
align-items:
|
|
1627
|
-
margin-bottom:
|
|
2210
|
+
align-items: center;
|
|
2211
|
+
margin-bottom: 6px;
|
|
1628
2212
|
border-bottom: 1px solid var(--border-subtle);
|
|
1629
|
-
padding-bottom:
|
|
2213
|
+
padding-bottom: 4px;
|
|
1630
2214
|
}}
|
|
1631
|
-
.comp-title {{ font-size:
|
|
1632
|
-
.comp-sub {{ font-size:
|
|
1633
|
-
.comp-ctrl-list {{ display: flex; flex-direction: column; gap:
|
|
1634
|
-
.comp-ctrl-row {{ display: flex; justify-content: space-between; align-items: center; font-size:
|
|
1635
|
-
.comp-ctrl-code {{ font-weight: 600; color: var(--text-main); }}
|
|
1636
|
-
.comp-ctrl-fams {{ color: var(--text-muted); font-size:
|
|
2215
|
+
.comp-title {{ font-size: 12px; font-weight: 700; color: var(--text-main); }}
|
|
2216
|
+
.comp-sub {{ font-size: 9.5px; color: var(--text-muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; }}
|
|
2217
|
+
.comp-ctrl-list {{ display: flex; flex-direction: column; gap: 4px; }}
|
|
2218
|
+
.comp-ctrl-row {{ display: flex; justify-content: space-between; align-items: center; font-size: 10.5px; padding: 1px 0; }}
|
|
2219
|
+
.comp-ctrl-code {{ font-weight: 600; color: var(--text-main); font-size: 10px; }}
|
|
2220
|
+
.comp-ctrl-fams {{ color: var(--text-muted); font-size: 9px; line-height: 1.1; }}
|
|
1637
2221
|
|
|
1638
2222
|
/* Dual Grids */
|
|
1639
2223
|
.grid-bottom {{
|
|
@@ -1644,24 +2228,251 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
1644
2228
|
}}
|
|
1645
2229
|
@media (max-width: 850px) {{ .grid-bottom {{ grid-template-columns: 1fr; }} }}
|
|
1646
2230
|
|
|
2231
|
+
.clusters-card {{
|
|
2232
|
+
padding: 10px 14px;
|
|
2233
|
+
display: flex;
|
|
2234
|
+
flex-direction: column;
|
|
2235
|
+
justify-content: flex-start;
|
|
2236
|
+
gap: 6px;
|
|
2237
|
+
}}
|
|
2238
|
+
.clusters-compact-list {{
|
|
2239
|
+
display: grid;
|
|
2240
|
+
grid-template-columns: repeat(2, 1fr);
|
|
2241
|
+
gap: 6px;
|
|
2242
|
+
max-height: 140px;
|
|
2243
|
+
overflow-y: auto;
|
|
2244
|
+
}}
|
|
2245
|
+
.clusters-compact-list .empty-state {{
|
|
2246
|
+
grid-column: 1 / -1;
|
|
2247
|
+
}}
|
|
2248
|
+
@media (max-width: 600px) {{ .clusters-compact-list {{ grid-template-columns: 1fr; }} }}
|
|
1647
2249
|
.cluster-item {{
|
|
1648
2250
|
background: var(--bg-card);
|
|
1649
2251
|
border: 1px solid var(--border-subtle);
|
|
1650
|
-
border-radius:
|
|
1651
|
-
padding:
|
|
1652
|
-
margin-bottom:
|
|
2252
|
+
border-radius: 5px;
|
|
2253
|
+
padding: 5px 8px;
|
|
2254
|
+
margin-bottom: 0;
|
|
2255
|
+
}}
|
|
2256
|
+
.cluster-header {{ display: flex; justify-content: space-between; align-items: center; font-size: 11px; gap: 4px; }}
|
|
2257
|
+
.cluster-header strong {{ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 130px; }}
|
|
2258
|
+
.cluster-sub {{ font-size: 9.5px; color: var(--text-muted); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
|
2259
|
+
|
|
2260
|
+
/* Compact Single-Page Layout */
|
|
2261
|
+
.header-compact {{
|
|
2262
|
+
display: flex;
|
|
2263
|
+
justify-content: space-between;
|
|
2264
|
+
align-items: center;
|
|
2265
|
+
padding-bottom: 8px;
|
|
2266
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
2267
|
+
margin-bottom: 4px;
|
|
2268
|
+
}}
|
|
2269
|
+
.row-top {{
|
|
2270
|
+
display: grid;
|
|
2271
|
+
grid-template-columns: 150px 1fr;
|
|
2272
|
+
gap: 12px;
|
|
2273
|
+
align-items: stretch;
|
|
2274
|
+
}}
|
|
2275
|
+
@media (max-width: 800px) {{ .row-top {{ grid-template-columns: 1fr; }} }}
|
|
2276
|
+
.gauge-card-compact {{
|
|
2277
|
+
display: flex;
|
|
2278
|
+
flex-direction: column;
|
|
2279
|
+
align-items: center;
|
|
2280
|
+
justify-content: center;
|
|
2281
|
+
padding: 10px;
|
|
2282
|
+
background: var(--bg-surface);
|
|
2283
|
+
border: 1px solid var(--border-subtle);
|
|
2284
|
+
border-radius: 8px;
|
|
2285
|
+
text-align: center;
|
|
2286
|
+
}}
|
|
2287
|
+
.row-middle {{
|
|
2288
|
+
display: grid;
|
|
2289
|
+
grid-template-columns: 1.15fr 0.85fr;
|
|
2290
|
+
gap: 12px;
|
|
2291
|
+
align-items: start;
|
|
2292
|
+
}}
|
|
2293
|
+
@media (max-width: 880px) {{ .row-middle {{ grid-template-columns: 1fr; }} }}
|
|
2294
|
+
.row-bottom {{
|
|
2295
|
+
display: grid;
|
|
2296
|
+
grid-template-columns: 1fr 1fr;
|
|
2297
|
+
gap: 12px;
|
|
2298
|
+
align-items: start;
|
|
2299
|
+
}}
|
|
2300
|
+
@media (max-width: 880px) {{ .row-bottom {{ grid-template-columns: 1fr; }} }}
|
|
2301
|
+
.footer-compact {{
|
|
2302
|
+
display: flex;
|
|
2303
|
+
justify-content: space-between;
|
|
2304
|
+
align-items: center;
|
|
2305
|
+
border-top: 1px solid var(--border-subtle);
|
|
2306
|
+
padding-top: 8px;
|
|
2307
|
+
margin-top: 4px;
|
|
2308
|
+
font-size: 11px;
|
|
2309
|
+
color: var(--text-muted);
|
|
2310
|
+
flex-wrap: wrap;
|
|
2311
|
+
gap: 8px;
|
|
2312
|
+
}}
|
|
2313
|
+
.footer-actions {{
|
|
2314
|
+
display: flex;
|
|
2315
|
+
align-items: center;
|
|
2316
|
+
gap: 8px;
|
|
2317
|
+
flex-wrap: wrap;
|
|
2318
|
+
}}
|
|
2319
|
+
.footer-meta {{
|
|
2320
|
+
display: flex;
|
|
2321
|
+
align-items: center;
|
|
2322
|
+
gap: 12px;
|
|
2323
|
+
}}
|
|
2324
|
+
.footer-links a {{
|
|
2325
|
+
color: var(--accent-blue);
|
|
2326
|
+
text-decoration: none;
|
|
2327
|
+
}}
|
|
2328
|
+
.footer-links a:hover {{
|
|
2329
|
+
text-decoration: underline;
|
|
2330
|
+
}}
|
|
2331
|
+
|
|
2332
|
+
/* 2D Directory Attack Surface Heatmap Matrix */
|
|
2333
|
+
.heatmap-card {{
|
|
2334
|
+
background: var(--bg-surface);
|
|
2335
|
+
border: 1px solid var(--border-subtle);
|
|
2336
|
+
border-radius: 8px;
|
|
2337
|
+
padding: 10px 14px;
|
|
2338
|
+
display: flex;
|
|
2339
|
+
flex-direction: column;
|
|
2340
|
+
justify-content: flex-start;
|
|
2341
|
+
gap: 6px;
|
|
2342
|
+
}}
|
|
2343
|
+
.card-compact-header {{
|
|
2344
|
+
display: flex;
|
|
2345
|
+
justify-content: space-between;
|
|
2346
|
+
align-items: center;
|
|
2347
|
+
margin-bottom: 6px;
|
|
2348
|
+
flex-wrap: wrap;
|
|
2349
|
+
gap: 6px;
|
|
2350
|
+
}}
|
|
2351
|
+
.heatmap-legend {{
|
|
2352
|
+
display: flex;
|
|
2353
|
+
align-items: center;
|
|
2354
|
+
gap: 8px;
|
|
2355
|
+
font-size: 10px;
|
|
2356
|
+
color: var(--text-muted);
|
|
2357
|
+
}}
|
|
2358
|
+
.heat-leg-item {{
|
|
2359
|
+
display: inline-flex;
|
|
2360
|
+
align-items: center;
|
|
2361
|
+
gap: 3px;
|
|
2362
|
+
}}
|
|
2363
|
+
.heat-leg-dot {{
|
|
2364
|
+
display: inline-flex;
|
|
2365
|
+
align-items: center;
|
|
2366
|
+
justify-content: center;
|
|
2367
|
+
width: 14px;
|
|
2368
|
+
height: 14px;
|
|
2369
|
+
border-radius: 3px;
|
|
2370
|
+
font-size: 9px;
|
|
2371
|
+
font-weight: 700;
|
|
2372
|
+
}}
|
|
2373
|
+
.heatmap-table-wrap {{
|
|
2374
|
+
overflow-x: auto;
|
|
2375
|
+
}}
|
|
2376
|
+
.heatmap-table {{
|
|
2377
|
+
width: 100%;
|
|
2378
|
+
border-collapse: separate;
|
|
2379
|
+
border-spacing: 4px;
|
|
2380
|
+
font-size: 11px;
|
|
2381
|
+
}}
|
|
2382
|
+
.heatmap-table th {{
|
|
2383
|
+
color: var(--text-muted);
|
|
2384
|
+
font-weight: 600;
|
|
2385
|
+
padding: 2px 4px;
|
|
2386
|
+
text-align: center;
|
|
2387
|
+
font-size: 10px;
|
|
2388
|
+
text-transform: uppercase;
|
|
2389
|
+
letter-spacing: 0.5px;
|
|
2390
|
+
}}
|
|
2391
|
+
.heatmap-row {{
|
|
2392
|
+
cursor: pointer;
|
|
2393
|
+
transition: background 0.15s ease;
|
|
2394
|
+
}}
|
|
2395
|
+
.heatmap-row:hover {{
|
|
2396
|
+
background: rgba(255, 255, 255, 0.04);
|
|
2397
|
+
}}
|
|
2398
|
+
body.light-theme .heatmap-row:hover {{
|
|
2399
|
+
background: rgba(0, 0, 0, 0.04);
|
|
2400
|
+
}}
|
|
2401
|
+
.heatmap-label-cell {{
|
|
2402
|
+
padding: 4px 6px;
|
|
2403
|
+
font-family: monospace;
|
|
2404
|
+
font-size: 11px;
|
|
2405
|
+
color: var(--text-main);
|
|
2406
|
+
white-space: nowrap;
|
|
2407
|
+
font-weight: 600;
|
|
2408
|
+
}}
|
|
2409
|
+
.heat-cell {{
|
|
2410
|
+
text-align: center;
|
|
2411
|
+
padding: 4px 6px;
|
|
2412
|
+
border-radius: 4px;
|
|
2413
|
+
font-weight: 700;
|
|
2414
|
+
font-size: 11px;
|
|
2415
|
+
transition: transform 0.1s ease, filter 0.1s ease;
|
|
2416
|
+
cursor: pointer;
|
|
2417
|
+
}}
|
|
2418
|
+
.heat-cell:hover {{
|
|
2419
|
+
transform: scale(1.08);
|
|
2420
|
+
filter: brightness(1.2);
|
|
2421
|
+
}}
|
|
2422
|
+
.heat-cell-safe {{
|
|
2423
|
+
background: rgba(46, 160, 67, 0.22);
|
|
2424
|
+
border: 1px solid rgba(63, 185, 80, 0.45);
|
|
2425
|
+
color: #7ee787;
|
|
2426
|
+
}}
|
|
2427
|
+
body.light-theme .heat-cell-safe {{
|
|
2428
|
+
background: rgba(46, 160, 67, 0.15);
|
|
2429
|
+
border-color: rgba(46, 160, 67, 0.4);
|
|
2430
|
+
color: #1a7f37;
|
|
2431
|
+
}}
|
|
2432
|
+
.heat-cell-warn {{
|
|
2433
|
+
background: rgba(210, 153, 34, 0.22);
|
|
2434
|
+
border: 1px solid rgba(210, 153, 34, 0.5);
|
|
2435
|
+
color: #e3b341;
|
|
2436
|
+
}}
|
|
2437
|
+
body.light-theme .heat-cell-warn {{
|
|
2438
|
+
background: rgba(210, 153, 34, 0.15);
|
|
2439
|
+
border-color: rgba(210, 153, 34, 0.4);
|
|
2440
|
+
color: #9a6700;
|
|
2441
|
+
}}
|
|
2442
|
+
.heat-cell-crit {{
|
|
2443
|
+
background: rgba(248, 81, 73, 0.25);
|
|
2444
|
+
border: 1px solid rgba(248, 81, 73, 0.55);
|
|
2445
|
+
color: #ffa198;
|
|
2446
|
+
}}
|
|
2447
|
+
body.light-theme .heat-cell-crit {{
|
|
2448
|
+
background: rgba(248, 81, 73, 0.15);
|
|
2449
|
+
border-color: rgba(248, 81, 73, 0.4);
|
|
2450
|
+
color: #cf222e;
|
|
2451
|
+
}}
|
|
2452
|
+
.heat-cell-idle {{
|
|
2453
|
+
background: var(--bg-card);
|
|
2454
|
+
border: 1px solid var(--border-subtle);
|
|
2455
|
+
color: var(--text-muted);
|
|
2456
|
+
opacity: 0.6;
|
|
2457
|
+
}}
|
|
2458
|
+
|
|
2459
|
+
/* Modal Large for Details Overlays */
|
|
2460
|
+
.modal-lg {{
|
|
2461
|
+
max-width: 1040px;
|
|
2462
|
+
width: 95%;
|
|
2463
|
+
max-height: 88vh;
|
|
2464
|
+
display: flex;
|
|
2465
|
+
flex-direction: column;
|
|
2466
|
+
padding: 18px 22px;
|
|
2467
|
+
}}
|
|
2468
|
+
.modal-lg .modal-body {{
|
|
2469
|
+
overflow-y: auto;
|
|
2470
|
+
flex: 1;
|
|
2471
|
+
padding-right: 6px;
|
|
1653
2472
|
}}
|
|
1654
|
-
.cluster-header {{ display: flex; justify-content: space-between; align-items: center; font-size: 13px; }}
|
|
1655
|
-
.cluster-sub {{ font-size: 11px; color: var(--text-muted); margin-top: 4px; }}
|
|
1656
2473
|
|
|
1657
|
-
/* Heatmap */
|
|
1658
|
-
.heatmap-row {{ display:
|
|
1659
|
-
.heatmap-row:hover {{ background: rgba(255, 255, 255, 0.03); }}
|
|
1660
|
-
body.light-theme .heatmap-row:hover {{ background: rgba(0, 0, 0, 0.03); }}
|
|
1661
|
-
.heatmap-label {{ font-size: 12px; font-family: monospace; color: var(--text-main); min-width: 110px; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
|
|
1662
|
-
.heatmap-bar-bg {{ flex: 1; height: 16px; background: var(--bg-card); border-radius: 4px; overflow: hidden; }}
|
|
1663
|
-
.heatmap-bar {{ height: 100%; border-radius: 4px; }}
|
|
1664
|
-
.heatmap-count {{ font-size: 12px; font-weight: 600; color: var(--text-muted); min-width: 25px; }}
|
|
2474
|
+
/* Existing Heatmap Row fallback */
|
|
2475
|
+
.heatmap-row {{ display: table-row; }}
|
|
1665
2476
|
|
|
1666
2477
|
/* Golden Recipes & Diff Engine */
|
|
1667
2478
|
.recipe-card {{
|
|
@@ -1772,29 +2583,32 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
1772
2583
|
</head>
|
|
1773
2584
|
<body>
|
|
1774
2585
|
<div class="container">
|
|
1775
|
-
<header>
|
|
2586
|
+
<header class="header-compact">
|
|
1776
2587
|
<div class="logo-group">
|
|
1777
|
-
<h1
|
|
2588
|
+
<h1 style="font-size: 16px; margin: 0; display: flex; align-items: center; gap: 8px;">
|
|
2589
|
+
<span class="shield-icon">🛡</span> TorusGuard Security Posture
|
|
2590
|
+
</h1>
|
|
1778
2591
|
<div class="project-meta">
|
|
1779
2592
|
<strong>{html.escape(telemetry.get('project_name', 'Project'))}</strong> ·
|
|
1780
2593
|
{html.escape(stack_label)} ·
|
|
1781
|
-
|
|
2594
|
+
<span class="badge badge-subtle">v1.3.6</span>
|
|
2595
|
+
<span class="badge badge-success">● 100% HARDENED</span>
|
|
1782
2596
|
</div>
|
|
1783
2597
|
</div>
|
|
1784
2598
|
<div class="header-actions">
|
|
1785
|
-
<button class="btn" id="themeToggleBtn" onclick="toggleTheme()">☀️ Light Mode</button>
|
|
1786
|
-
<button class="btn" onclick="downloadSarif()">📥
|
|
1787
|
-
<button class="btn" onclick="downloadCsv()">📊
|
|
1788
|
-
<button class="btn" onclick="window.print()">🖶 Print
|
|
2599
|
+
<button class="btn btn-sm" id="themeToggleBtn" onclick="toggleTheme()">☀️ Light Mode</button>
|
|
2600
|
+
<button class="btn btn-sm" onclick="downloadSarif()">📥 SARIF</button>
|
|
2601
|
+
<button class="btn btn-sm" onclick="downloadCsv()">📊 CSV</button>
|
|
2602
|
+
<button class="btn btn-sm" onclick="window.print()">🖶 Print</button>
|
|
1789
2603
|
<div class="status-badge {status_badge_class}">{status_label}</div>
|
|
1790
2604
|
</div>
|
|
1791
2605
|
</header>
|
|
1792
2606
|
|
|
1793
|
-
<!--
|
|
1794
|
-
<div class="
|
|
1795
|
-
<div class="card gauge-card">
|
|
2607
|
+
<!-- Row 1: Dynamic Circular Gauge & Governed Remediation Velocity Ledger -->
|
|
2608
|
+
<div class="row-top">
|
|
2609
|
+
<div class="card gauge-card-compact">
|
|
1796
2610
|
<div style="position: relative; display: flex; align-items: center; justify-content: center;">
|
|
1797
|
-
<svg class="gauge-svg" viewBox="0 0 100 100">
|
|
2611
|
+
<svg class="gauge-svg" style="width: 76px; height: 76px;" viewBox="0 0 100 100">
|
|
1798
2612
|
<defs>
|
|
1799
2613
|
<linearGradient id="gaugeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
1800
2614
|
<stop offset="0%" stop-color="{grad_start}" />
|
|
@@ -1807,166 +2621,182 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
1807
2621
|
<circle class="gauge-bg" cx="50" cy="50" r="40" />
|
|
1808
2622
|
<circle class="gauge-fill" id="gaugeCircleFill" cx="50" cy="50" r="40" stroke="url(#gaugeGradient)" filter="url(#gaugeGlow)" />
|
|
1809
2623
|
</svg>
|
|
1810
|
-
<div class="gauge-value" id="postureScoreText">0</div>
|
|
2624
|
+
<div class="gauge-value" id="postureScoreText" style="font-size: 20px;">0</div>
|
|
1811
2625
|
</div>
|
|
1812
|
-
<div class="stat-label" style="font-size:
|
|
2626
|
+
<div class="stat-label" style="font-size: 11px; color: var(--text-muted); text-transform: uppercase; margin-top: 4px; font-weight: 700;">Posture Score</div>
|
|
1813
2627
|
</div>
|
|
1814
2628
|
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
<div class="sev-label">Critical Exposure</div>
|
|
1818
|
-
</div>
|
|
1819
|
-
|
|
1820
|
-
<div class="card sev-card" onclick="filterBySeverity('high')">
|
|
1821
|
-
<div class="sev-count" style="color: var(--accent-orange);">{high_count}</div>
|
|
1822
|
-
<div class="sev-label">High Exposure</div>
|
|
1823
|
-
</div>
|
|
2629
|
+
{remediation_ledger_html}
|
|
2630
|
+
</div>
|
|
1824
2631
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2632
|
+
<!-- Row 2: 2D Directory Attack Surface Heatmap & Architectural Clusters -->
|
|
2633
|
+
<div class="row-middle">
|
|
2634
|
+
<div class="card heatmap-card">
|
|
2635
|
+
<div class="card-compact-header">
|
|
2636
|
+
<div class="section-title" style="margin-bottom: 0;">🔥 Directory Attack Surface Heatmap</div>
|
|
2637
|
+
<div class="heatmap-legend">
|
|
2638
|
+
<span class="heat-leg-item"><span class="heat-leg-dot heat-cell-safe">✔</span> Defended</span>
|
|
2639
|
+
<span class="heat-leg-item"><span class="heat-leg-dot heat-cell-warn">🟡</span> Warning</span>
|
|
2640
|
+
<span class="heat-leg-item"><span class="heat-leg-dot heat-cell-crit">🔴</span> High Risk</span>
|
|
2641
|
+
<span class="heat-leg-item"><span class="heat-leg-dot heat-cell-idle">—</span> Clean</span>
|
|
2642
|
+
</div>
|
|
2643
|
+
</div>
|
|
2644
|
+
<div class="heatmap-table-wrap">
|
|
2645
|
+
<table class="heatmap-table">
|
|
2646
|
+
<thead>
|
|
2647
|
+
<tr>
|
|
2648
|
+
<th style="text-align: left; width: 90px;">Module</th>
|
|
2649
|
+
<th>AUTH</th>
|
|
2650
|
+
<th>DB</th>
|
|
2651
|
+
<th>INPUT</th>
|
|
2652
|
+
<th>NET</th>
|
|
2653
|
+
<th>PLAT</th>
|
|
2654
|
+
<th>SEC</th>
|
|
2655
|
+
<th style="text-align: right; width: 85px;">Posture</th>
|
|
2656
|
+
</tr>
|
|
2657
|
+
</thead>
|
|
2658
|
+
<tbody>
|
|
2659
|
+
{heatmap_rows_html}
|
|
2660
|
+
</tbody>
|
|
2661
|
+
</table>
|
|
2662
|
+
</div>
|
|
1828
2663
|
</div>
|
|
1829
2664
|
|
|
1830
|
-
<div class="card
|
|
1831
|
-
<div class="
|
|
1832
|
-
<div class="
|
|
2665
|
+
<div class="card clusters-card">
|
|
2666
|
+
<div class="section-title" style="margin-bottom: 8px;">🛠 Root-Cause Architectural Clusters</div>
|
|
2667
|
+
<div class="clusters-compact-list">
|
|
2668
|
+
{clusters_html}
|
|
2669
|
+
</div>
|
|
1833
2670
|
</div>
|
|
1834
2671
|
</div>
|
|
1835
2672
|
|
|
1836
|
-
<!--
|
|
1837
|
-
<div class="
|
|
1838
|
-
<div>
|
|
1839
|
-
<div style="
|
|
1840
|
-
|
|
1841
|
-
|
|
2673
|
+
<!-- Row 3: OWASP Top 10 & Enterprise Compliance -->
|
|
2674
|
+
<div class="row-bottom">
|
|
2675
|
+
<div class="card owasp-card-wrapper">
|
|
2676
|
+
<div class="section-title" style="margin-bottom: 8px;">🛡 OWASP Top 10 (2021/2026) Compliance Matrix</div>
|
|
2677
|
+
<div class="grid-owasp">
|
|
2678
|
+
{owasp_cards_html}
|
|
1842
2679
|
</div>
|
|
1843
|
-
|
|
1844
|
-
|
|
2680
|
+
</div>
|
|
2681
|
+
|
|
2682
|
+
<div class="card comp-card-wrapper">
|
|
2683
|
+
<div class="section-title" style="margin-bottom: 8px;">🏛 Enterprise Compliance Framework Mapping (SOC 2 · ISO 27001 · HIPAA)</div>
|
|
2684
|
+
<div class="grid-compliance" id="gridCompliance">
|
|
2685
|
+
{compliance_cards_html}
|
|
1845
2686
|
</div>
|
|
1846
2687
|
</div>
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2688
|
+
</div>
|
|
2689
|
+
|
|
2690
|
+
<!-- Executive Action Footer Bar (Zero Scroll Navigation) -->
|
|
2691
|
+
<footer class="footer-compact">
|
|
2692
|
+
<div class="footer-actions">
|
|
2693
|
+
<button class="btn btn-sm" onclick="openFindingsModal()">🔍 View Findings & Evidence ({total_discovered})</button>
|
|
2694
|
+
<button class="btn btn-sm" onclick="openInvariantsModal()">🛡 74 Invariants Catalog</button>
|
|
2695
|
+
<button class="btn btn-sm" onclick="openRecipesModal()">✨ Golden Fix Recipes ({len(recipes)})</button>
|
|
2696
|
+
<button class="btn btn-sm" onclick="openAdvisoryModal()">💡 Defenses Advisory</button>
|
|
1851
2697
|
</div>
|
|
1852
|
-
<div class="
|
|
1853
|
-
<
|
|
1854
|
-
<button class="btn btn-sm" onclick="simulateFixAll('all')">⚡ Simulate All Fixes</button>
|
|
1855
|
-
<button class="btn btn-sm" onclick="resetSimulatedFixes()">Reset Simulator</button>
|
|
2698
|
+
<div class="footer-meta">
|
|
2699
|
+
<span>TorusGuard v1.3.6 · 100% Local-First · Zero Cloud Telemetry</span>
|
|
1856
2700
|
</div>
|
|
1857
|
-
</
|
|
2701
|
+
</footer>
|
|
2702
|
+
</div>
|
|
1858
2703
|
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2704
|
+
<!-- Deep Inspection Modals -->
|
|
2705
|
+
<!-- 1. Findings & Evidence Modal -->
|
|
2706
|
+
<div id="findingsModal" class="modal-backdrop" style="display: none;" onclick="closeModal('findingsModal', event)">
|
|
2707
|
+
<div class="modal-card modal-lg" onclick="event.stopPropagation()">
|
|
2708
|
+
<div class="modal-header">
|
|
2709
|
+
<div>
|
|
2710
|
+
<h3 style="font-size: 16px; margin: 0; color: var(--text-main);">🔍 Governed Security Findings & Evidence Archive</h3>
|
|
2711
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
|
2712
|
+
{total_discovered} findings discovered across repository AST inspection.
|
|
2713
|
+
</div>
|
|
1864
2714
|
</div>
|
|
1865
|
-
<
|
|
2715
|
+
<button class="modal-close-btn" onclick="closeModal('findingsModal', event)">×</button>
|
|
1866
2716
|
</div>
|
|
1867
|
-
<div
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
<
|
|
2717
|
+
<div class="modal-body" style="padding-top: 10px;">
|
|
2718
|
+
<!-- Interactive What-If Posture Score Simulator -->
|
|
2719
|
+
<div class="card simulator-card {'simulator-clean' if open_count == 0 else ''}" style="margin-bottom: 14px;">
|
|
2720
|
+
<div>
|
|
2721
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
2722
|
+
<strong style="font-size: 13px;">⚡ Interactive "What-If" Posture Score Simulator</strong>
|
|
2723
|
+
<span class="badge badge-primary">Dynamic Recalculation</span>
|
|
2724
|
+
</div>
|
|
2725
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">
|
|
2726
|
+
{'All 74 security invariants are currently defended. Simulator is ready for live remediation modeling during audit cycles.' if open_count == 0 else 'Simulate applying governed patches to preview projected posture improvement in real time:'}
|
|
2727
|
+
</div>
|
|
1872
2728
|
</div>
|
|
1873
|
-
<div class="
|
|
1874
|
-
<div
|
|
2729
|
+
<div class="sim-stats">
|
|
2730
|
+
<div>Current: <strong id="simCurrentVal">{score}</strong>/100</div>
|
|
2731
|
+
<div>→</div>
|
|
2732
|
+
<div>Projected: <strong id="simProjectedVal" style="color: var(--accent-blue);">{score}</strong>/100 <span id="simDeltaBadge" class="badge badge-success">+0 pts</span></div>
|
|
1875
2733
|
</div>
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
<
|
|
1880
|
-
<button class="search-clear-btn" id="invSearchClearBtn" onclick="clearInvSearch()">×</button>
|
|
2734
|
+
<div class="sim-actions">
|
|
2735
|
+
<button class="btn btn-sm" onclick="simulateFixAll('Critical')">⚡ Simulate Criticals</button>
|
|
2736
|
+
<button class="btn btn-sm" onclick="simulateFixAll('all')">⚡ Simulate All Fixes</button>
|
|
2737
|
+
<button class="btn btn-sm" onclick="resetSimulatedFixes()">Reset Simulator</button>
|
|
1881
2738
|
</div>
|
|
1882
|
-
<div id="invMatchCount" style="font-size: 11px; color: var(--accent-blue); white-space: nowrap;"></div>
|
|
1883
2739
|
</div>
|
|
1884
|
-
<div class="layers-container" id="layersList">
|
|
1885
|
-
{layers_html}
|
|
1886
|
-
</div>
|
|
1887
|
-
</div>
|
|
1888
|
-
</div>
|
|
1889
|
-
|
|
1890
|
-
<!-- Filter & Search Bar for Findings -->
|
|
1891
|
-
<div class="filter-bar">
|
|
1892
|
-
<div class="filter-pills">
|
|
1893
|
-
<span class="pill-group-label">Severity:</span>
|
|
1894
|
-
<span class="pill active" data-sev-filter="all" onclick="filterBySeverity('all')">All</span>
|
|
1895
|
-
<span class="pill" data-sev-filter="critical" onclick="filterBySeverity('critical')">🔴 Critical ({crit_count})</span>
|
|
1896
|
-
<span class="pill" data-sev-filter="high" onclick="filterBySeverity('high')">🟠 High ({high_count})</span>
|
|
1897
|
-
<span class="pill" data-sev-filter="medium" onclick="filterBySeverity('medium')">🟡 Med/Low ({med_count})</span>
|
|
1898
|
-
|
|
1899
|
-
<span class="pill-group-label" style="margin-left: 8px;">Status:</span>
|
|
1900
|
-
<span class="pill active" data-status-filter="all" onclick="filterByStatus('all')">All</span>
|
|
1901
|
-
<span class="pill" data-status-filter="open" onclick="filterByStatus('open')">⚠ Open ({open_count})</span>
|
|
1902
|
-
<span class="pill" data-status-filter="resolved" onclick="filterByStatus('resolved')">✔ Resolved ({resolved_count})</span>
|
|
1903
2740
|
|
|
1904
|
-
|
|
1905
|
-
Folder: <strong id="dirFilterName"></strong> ×
|
|
1906
|
-
</span>
|
|
1907
|
-
<button class="btn-reset-filters" id="btnResetFilters" onclick="resetAllFilters()">× Reset Filters</button>
|
|
1908
|
-
</div>
|
|
1909
|
-
<div class="search-wrapper">
|
|
1910
|
-
<input type="text" id="searchInput" class="search-input" placeholder="Search rules, titles, files..." oninput="searchFindings()">
|
|
1911
|
-
<button class="search-clear-btn" id="searchClearBtn" onclick="clearSearch()">×</button>
|
|
2741
|
+
{findings_section_html}
|
|
1912
2742
|
</div>
|
|
1913
2743
|
</div>
|
|
1914
|
-
|
|
1915
|
-
<div>Showing <span id="visibleCount" style="font-weight: 700; color: var(--text-main);">{min(len(sorted_f), 50)}</span> of <span id="totalCount" style="font-weight: 700; color: var(--text-main);">{len(sorted_f)}</span> cataloged findings</div>
|
|
1916
|
-
<div id="activeFilterSummary" style="font-size: 11px; color: var(--accent-blue);"></div>
|
|
1917
|
-
</div>
|
|
1918
|
-
|
|
1919
|
-
<!-- Findings Table -->
|
|
1920
|
-
<div class="table-container">
|
|
1921
|
-
<table>
|
|
1922
|
-
<thead>
|
|
1923
|
-
<tr>
|
|
1924
|
-
<th style="width: 70px; text-align: center;">Simulate</th>
|
|
1925
|
-
<th style="width: 110px;">Severity</th>
|
|
1926
|
-
<th style="width: 130px;">Rule ID</th>
|
|
1927
|
-
<th>Finding Title</th>
|
|
1928
|
-
<th style="width: 250px;">File & Location</th>
|
|
1929
|
-
<th style="width: 90px;">Confidence</th>
|
|
1930
|
-
<th style="width: 30px;"></th>
|
|
1931
|
-
</tr>
|
|
1932
|
-
</thead>
|
|
1933
|
-
<tbody id="findingsTableBody">
|
|
1934
|
-
{findings_rows}
|
|
1935
|
-
</tbody>
|
|
1936
|
-
</table>
|
|
1937
|
-
</div>
|
|
1938
|
-
|
|
1939
|
-
<!-- OWASP Top 10 Compliance Radar -->
|
|
1940
|
-
<div class="section-title">🛡 OWASP Top 10 (2021/2026) Compliance Matrix</div>
|
|
1941
|
-
<div class="grid-owasp">
|
|
1942
|
-
{owasp_cards_html}
|
|
1943
|
-
</div>
|
|
1944
|
-
|
|
1945
|
-
<!-- Enterprise Compliance Framework Mapping -->
|
|
1946
|
-
<div class="section-title">🏛 Enterprise Compliance Framework Mapping (SOC 2 · ISO 27001 · HIPAA)</div>
|
|
1947
|
-
<div class="grid-compliance">
|
|
1948
|
-
{compliance_cards_html}
|
|
1949
|
-
</div>
|
|
2744
|
+
</div>
|
|
1950
2745
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
<div
|
|
1956
|
-
|
|
2746
|
+
<!-- 2. Active Defenses & 74 Invariants Catalog Modal -->
|
|
2747
|
+
<div id="invariantsCatalogModal" class="modal-backdrop" style="display: none;" onclick="closeModal('invariantsCatalogModal', event)">
|
|
2748
|
+
<div class="modal-card modal-lg" onclick="event.stopPropagation()">
|
|
2749
|
+
<div class="modal-header">
|
|
2750
|
+
<div>
|
|
2751
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
2752
|
+
<span class="badge badge-success">✔ 100% ENFORCED</span>
|
|
2753
|
+
<h3 style="font-size: 16px; margin: 0; color: var(--text-main);">🛡 Active Defenses & Protected Invariants ({defended_count} Passing)</h3>
|
|
2754
|
+
</div>
|
|
2755
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">
|
|
2756
|
+
All 74 architectural security rules enforced across 5 defensive layers. Zero security bypasses detected.
|
|
2757
|
+
</div>
|
|
2758
|
+
</div>
|
|
2759
|
+
<button class="modal-close-btn" onclick="closeModal('invariantsCatalogModal', event)">×</button>
|
|
1957
2760
|
</div>
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2761
|
+
<div class="modal-body" style="padding-top: 10px;">
|
|
2762
|
+
<div class="defended-section" id="defendedSection">
|
|
2763
|
+
<div class="coverage-bar-container" style="margin-bottom: 12px;">
|
|
2764
|
+
<div class="coverage-stats">
|
|
2765
|
+
<span><strong>{defended_count} of {total_rules} Invariants Enforced</strong> ({coverage_pct}% Defense Coverage)</span>
|
|
2766
|
+
<span class="badge {'badge-success' if coverage_pct >= 90 else 'badge-high'}">{coverage_pct}% Enforced</span>
|
|
2767
|
+
</div>
|
|
2768
|
+
<div class="coverage-progress-bg">
|
|
2769
|
+
<div class="coverage-progress-fill" style="width: {coverage_pct}%;"></div>
|
|
2770
|
+
</div>
|
|
2771
|
+
</div>
|
|
2772
|
+
<div class="inv-search-bar" style="margin-bottom: 12px;">
|
|
2773
|
+
<div class="search-wrapper" style="width: 100%;">
|
|
2774
|
+
<input type="text" id="invSearchInput" class="search-input" style="width: 100%;" placeholder="Search 74 invariants by ID, title, layer, or keyword..." oninput="filterInvariants()">
|
|
2775
|
+
<button class="search-clear-btn" id="invSearchClearBtn" onclick="clearInvSearch()">×</button>
|
|
2776
|
+
</div>
|
|
2777
|
+
<div id="invMatchCount" style="font-size: 11px; color: var(--accent-blue); white-space: nowrap;"></div>
|
|
2778
|
+
</div>
|
|
2779
|
+
<div class="layers-container" id="layersList">
|
|
2780
|
+
{layers_html}
|
|
2781
|
+
</div>
|
|
2782
|
+
</div>
|
|
1962
2783
|
</div>
|
|
1963
2784
|
</div>
|
|
2785
|
+
</div>
|
|
1964
2786
|
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
<div
|
|
2787
|
+
<!-- 3. Golden Fix Recipes Modal -->
|
|
2788
|
+
<div id="recipesModal" class="modal-backdrop" style="display: none;" onclick="closeModal('recipesModal', event)">
|
|
2789
|
+
<div class="modal-card modal-lg" id="recipesCard" onclick="event.stopPropagation()">
|
|
2790
|
+
<div class="modal-header">
|
|
2791
|
+
<div>
|
|
2792
|
+
<h3 style="font-size: 16px; margin: 0; color: var(--text-main);">✨ Golden Fix Recipes ({len(recipes)})</h3>
|
|
2793
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">
|
|
2794
|
+
Verified remediation patterns distilled from applied patches conforming to Ponytail line churn budget.
|
|
2795
|
+
</div>
|
|
2796
|
+
</div>
|
|
2797
|
+
<button class="modal-close-btn" onclick="closeModal('recipesModal', event)">×</button>
|
|
2798
|
+
</div>
|
|
2799
|
+
<div class="modal-body" style="padding-top: 10px;">
|
|
1970
2800
|
<div class="filter-pills" style="margin-bottom: 12px;">
|
|
1971
2801
|
{recipe_cat_pills_html}
|
|
1972
2802
|
</div>
|
|
@@ -1974,20 +2804,28 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
1974
2804
|
{recipes_html}
|
|
1975
2805
|
</div>
|
|
1976
2806
|
</div>
|
|
2807
|
+
</div>
|
|
2808
|
+
</div>
|
|
1977
2809
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2810
|
+
<!-- 4. Next Best Defenses Advisory Modal -->
|
|
2811
|
+
<div id="advisoryModal" class="modal-backdrop" style="display: none;" onclick="closeModal('advisoryModal', event)">
|
|
2812
|
+
<div class="modal-card modal-lg" onclick="event.stopPropagation()">
|
|
2813
|
+
<div class="modal-header">
|
|
2814
|
+
<div>
|
|
2815
|
+
<h3 style="font-size: 16px; margin: 0; color: var(--text-main);">💡 Prescriptive Next Best Defenses Advisory</h3>
|
|
2816
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">
|
|
2817
|
+
Stack-tailored recommendations to achieve hardened posture.
|
|
2818
|
+
</div>
|
|
2819
|
+
</div>
|
|
2820
|
+
<button class="modal-close-btn" onclick="closeModal('advisoryModal', event)">×</button>
|
|
2821
|
+
</div>
|
|
2822
|
+
<div class="modal-body" style="padding-top: 10px;">
|
|
1981
2823
|
{advisory_html}
|
|
1982
2824
|
</div>
|
|
1983
2825
|
</div>
|
|
1984
|
-
|
|
1985
|
-
<footer>
|
|
1986
|
-
TorusGuard v1.3.6 · Autonomous Security Engine for AI-Built Applications · 100% Local-First · Zero Cloud Telemetry
|
|
1987
|
-
</footer>
|
|
1988
2826
|
</div>
|
|
1989
2827
|
|
|
1990
|
-
<!-- Invariant Inspection Modal -->
|
|
2828
|
+
<!-- 5. Invariant Guarantee Inspection Modal -->
|
|
1991
2829
|
<div id="invariantModal" class="modal-backdrop" style="display: none;" onclick="closeInvariantModal(event)">
|
|
1992
2830
|
<div class="modal-card" onclick="event.stopPropagation()">
|
|
1993
2831
|
<div class="modal-header">
|
|
@@ -2041,6 +2879,38 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
2041
2879
|
let currentRecipeCategory = 'all';
|
|
2042
2880
|
let BASE_POSTURE_SCORE = {score};
|
|
2043
2881
|
|
|
2882
|
+
// ── Modal Dialog Engine ──
|
|
2883
|
+
function openModal(id) {{
|
|
2884
|
+
const m = document.getElementById(id);
|
|
2885
|
+
if (m) {{
|
|
2886
|
+
m.style.display = 'flex';
|
|
2887
|
+
document.body.style.overflow = 'hidden';
|
|
2888
|
+
}}
|
|
2889
|
+
}}
|
|
2890
|
+
function closeModal(id, event) {{
|
|
2891
|
+
if (event && event.target && event.target !== event.currentTarget && !event.target.classList.contains('modal-close-btn')) {{
|
|
2892
|
+
return;
|
|
2893
|
+
}}
|
|
2894
|
+
const m = document.getElementById(id);
|
|
2895
|
+
if (m) {{
|
|
2896
|
+
m.style.display = 'none';
|
|
2897
|
+
document.body.style.overflow = '';
|
|
2898
|
+
}}
|
|
2899
|
+
}}
|
|
2900
|
+
function openFindingsModal() {{ openModal('findingsModal'); }}
|
|
2901
|
+
function openInvariantsModal() {{ openModal('invariantsCatalogModal'); }}
|
|
2902
|
+
function openRecipesModal() {{ openModal('recipesModal'); }}
|
|
2903
|
+
function openAdvisoryModal() {{ openModal('advisoryModal'); }}
|
|
2904
|
+
|
|
2905
|
+
document.addEventListener('keydown', function(e) {{
|
|
2906
|
+
if (e.key === 'Escape') {{
|
|
2907
|
+
document.querySelectorAll('.modal-backdrop').forEach(m => {{
|
|
2908
|
+
m.style.display = 'none';
|
|
2909
|
+
}});
|
|
2910
|
+
document.body.style.overflow = '';
|
|
2911
|
+
}}
|
|
2912
|
+
}});
|
|
2913
|
+
|
|
2044
2914
|
// ── Theme Switcher ──
|
|
2045
2915
|
function initTheme() {{
|
|
2046
2916
|
const saved = localStorage.getItem('tg_theme');
|
|
@@ -2079,6 +2949,8 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
2079
2949
|
let remCrit = 0, remHigh = 0, remMed = 0, remLow = 0;
|
|
2080
2950
|
RAW_FINDINGS.forEach(f => {{
|
|
2081
2951
|
if (f.is_canary) return;
|
|
2952
|
+
const st = (f.status || 'open').toLowerCase();
|
|
2953
|
+
if (st === 'resolved' || st === 'fixed' || st === 'confirmed_fixed') return;
|
|
2082
2954
|
if (fixedIds.has(f.finding_id)) return; // Simulated as fixed
|
|
2083
2955
|
const s = f.severity;
|
|
2084
2956
|
if (s === 'Critical') remCrit++;
|
|
@@ -2237,6 +3109,7 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
2237
3109
|
|
|
2238
3110
|
// ── Findings Filtering & Drawer Lifecycle ──
|
|
2239
3111
|
function filterBySeverity(sev) {{
|
|
3112
|
+
openFindingsModal();
|
|
2240
3113
|
currentSeverity = sev;
|
|
2241
3114
|
document.querySelectorAll('[data-sev-filter]').forEach(p => {{
|
|
2242
3115
|
if (p.getAttribute('data-sev-filter') === sev) p.classList.add('active');
|
|
@@ -2259,6 +3132,7 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
2259
3132
|
}}
|
|
2260
3133
|
|
|
2261
3134
|
function filterByDirectory(dir) {{
|
|
3135
|
+
openFindingsModal();
|
|
2262
3136
|
currentDirectory = dir.replace(/\\/$/, '');
|
|
2263
3137
|
const chip = document.getElementById('dirFilterChip');
|
|
2264
3138
|
const chipName = document.getElementById('dirFilterName');
|
|
@@ -2309,16 +3183,59 @@ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
|
2309
3183
|
if (!body) return;
|
|
2310
3184
|
if (body.style.display === 'none') {{
|
|
2311
3185
|
body.style.display = 'block';
|
|
2312
|
-
icon.innerHTML = '▼ Collapse
|
|
3186
|
+
if (icon) icon.innerHTML = '▼ Collapse Details';
|
|
3187
|
+
}} else {{
|
|
3188
|
+
body.style.display = 'none';
|
|
3189
|
+
if (icon) icon.innerHTML = '▶ Expand 74 Invariant Details';
|
|
3190
|
+
}}
|
|
3191
|
+
}}
|
|
3192
|
+
|
|
3193
|
+
function toggleFindingsArchive() {{
|
|
3194
|
+
const body = document.getElementById('findingsArchiveBody');
|
|
3195
|
+
const icon = document.getElementById('archiveToggleIcon');
|
|
3196
|
+
const btn = document.getElementById('btnArchiveToggle');
|
|
3197
|
+
if (!body) return;
|
|
3198
|
+
if (body.style.display === 'none') {{
|
|
3199
|
+
body.style.display = 'block';
|
|
3200
|
+
if (icon) icon.innerHTML = '▼';
|
|
3201
|
+
if (btn) btn.classList.add('active');
|
|
3202
|
+
searchFindings();
|
|
3203
|
+
}} else {{
|
|
3204
|
+
body.style.display = 'none';
|
|
3205
|
+
if (icon) icon.innerHTML = '▶';
|
|
3206
|
+
if (btn) btn.classList.remove('active');
|
|
3207
|
+
}}
|
|
3208
|
+
}}
|
|
3209
|
+
|
|
3210
|
+
function toggleRecipesSection() {{
|
|
3211
|
+
const body = document.getElementById('recipesCardBody');
|
|
3212
|
+
const icon = document.getElementById('recipesToggleIcon');
|
|
3213
|
+
const btn = document.getElementById('btnRecipesToggle');
|
|
3214
|
+
if (!body) return;
|
|
3215
|
+
if (body.style.display === 'none') {{
|
|
3216
|
+
body.style.display = 'block';
|
|
3217
|
+
if (btn) btn.innerHTML = '<span id="recipesToggleIcon">▼</span> Collapse';
|
|
2313
3218
|
}} else {{
|
|
2314
3219
|
body.style.display = 'none';
|
|
2315
|
-
|
|
3220
|
+
if (btn) btn.innerHTML = '<span id="recipesToggleIcon">▶</span> Expand ({len(recipes)})';
|
|
3221
|
+
}}
|
|
3222
|
+
}}
|
|
3223
|
+
|
|
3224
|
+
function toggleComplianceSection() {{
|
|
3225
|
+
const grid = document.getElementById('gridCompliance');
|
|
3226
|
+
const icon = document.getElementById('compToggleIcon');
|
|
3227
|
+
if (!grid) return;
|
|
3228
|
+
if (grid.style.display === 'none') {{
|
|
3229
|
+
grid.style.display = 'grid';
|
|
3230
|
+
if (icon) icon.innerHTML = '▼ Collapse';
|
|
3231
|
+
}} else {{
|
|
3232
|
+
grid.style.display = 'none';
|
|
3233
|
+
if (icon) icon.innerHTML = '▶ Expand Frameworks';
|
|
2316
3234
|
}}
|
|
2317
3235
|
}}
|
|
2318
3236
|
|
|
2319
3237
|
function toggleDefendedSection() {{
|
|
2320
|
-
|
|
2321
|
-
if (sec) sec.scrollIntoView({{ behavior: 'smooth' }});
|
|
3238
|
+
openInvariantsModal();
|
|
2322
3239
|
}}
|
|
2323
3240
|
|
|
2324
3241
|
function searchFindings() {{
|
|
@@ -2600,9 +3517,9 @@ def emit_html_report(
|
|
|
2600
3517
|
"total_rules": len(all_rules),
|
|
2601
3518
|
"defended_count": len(safe_rules),
|
|
2602
3519
|
"harmed_count": len(violated_rules),
|
|
2603
|
-
"critical_count": sum(1 for f in findings if f.get("severity") == "Critical" and not f.get("is_canary")),
|
|
2604
|
-
"high_count": sum(1 for f in findings if f.get("severity") == "High" and not f.get("is_canary")),
|
|
2605
|
-
"medium_count": sum(1 for f in findings if f.get("severity") in ("Medium", "Low") and not f.get("is_canary")),
|
|
3520
|
+
"critical_count": sum(1 for f in findings if f.get("severity") == "Critical" and not f.get("is_canary") and f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")),
|
|
3521
|
+
"high_count": sum(1 for f in findings if f.get("severity") == "High" and not f.get("is_canary") and f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")),
|
|
3522
|
+
"medium_count": sum(1 for f in findings if f.get("severity") in ("Medium", "Low") and not f.get("is_canary") and f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")),
|
|
2606
3523
|
}
|
|
2607
3524
|
|
|
2608
3525
|
|