torusguard 1.3.5 → 1.3.6

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.
Files changed (152) hide show
  1. package/.torusguard/.manifest.json +35 -35
  2. package/.torusguard/runs/report-latest.html +2685 -303
  3. package/.torusguard/runs/results-latest.sarif +20 -0
  4. package/.torusguard/runs/run-20260918-104221-audit/findings.json +1 -0
  5. package/.torusguard/runs/run-20260918-104221-audit/findings.md +8 -0
  6. package/.torusguard/runs/run-20260918-104221-audit/manifest.json +12 -0
  7. package/.torusguard/runs/run-20260918-104221-audit/summary.md +5 -0
  8. package/.torusguard/runs/run-20260918-104236-audit/findings.json +2314 -0
  9. package/.torusguard/runs/run-20260918-104236-audit/findings.md +960 -0
  10. package/.torusguard/runs/run-20260918-104236-audit/manifest.json +12 -0
  11. package/.torusguard/runs/run-20260918-104236-audit/summary.md +5 -0
  12. package/.torusguard/runs/run-20260918-105723-audit/findings.json +2382 -0
  13. package/.torusguard/runs/run-20260918-105723-audit/findings.md +960 -0
  14. package/.torusguard/runs/run-20260918-105723-audit/manifest.json +12 -0
  15. package/.torusguard/runs/run-20260918-105723-audit/summary.md +5 -0
  16. package/.torusguard/runs/run-20260918-105731-audit/findings.json +1 -0
  17. package/.torusguard/runs/run-20260918-105731-audit/findings.md +8 -0
  18. package/.torusguard/runs/run-20260918-105731-audit/manifest.json +12 -0
  19. package/.torusguard/runs/run-20260918-105731-audit/summary.md +5 -0
  20. package/.torusguard/runs/run-20260918-105940-audit/findings.json +1 -0
  21. package/.torusguard/runs/run-20260918-105940-audit/findings.md +8 -0
  22. package/.torusguard/runs/run-20260918-105940-audit/manifest.json +12 -0
  23. package/.torusguard/runs/run-20260918-105940-audit/summary.md +5 -0
  24. package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  25. package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  26. package/.torusguard/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
  27. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  28. package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  29. package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  30. package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  31. package/.torusguard/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
  32. package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
  33. package/.torusguard/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
  34. package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  35. package/.torusguard/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
  36. package/.torusguard/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
  37. package/.torusguard/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
  38. package/.torusguard/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  39. package/.torusguard/scripts/apply_runner.py +11 -5
  40. package/.torusguard/scripts/audit_runner.py +222 -15
  41. package/.torusguard/scripts/harden_runner.py +3 -3
  42. package/.torusguard/scripts/html_reporter.py +2668 -648
  43. package/.torusguard/scripts/recheck_runner.py +3 -3
  44. package/.torusguard/scripts/recipes_runner.py +3 -3
  45. package/.torusguard/scripts/report_sync.py +370 -74
  46. package/.torusguard/scripts/sarif_exporter.py +48 -8
  47. package/.torusguard/scripts/stack_detect.py +44 -2
  48. package/.torusguard/scripts/term_ui.py +1 -1
  49. package/.torusguard/skills/torusguard/SKILL.md +75 -72
  50. package/.torusguard/skills/torusguard-apply/SKILL.md +84 -80
  51. package/.torusguard/skills/torusguard-audit/SKILL.md +87 -83
  52. package/.torusguard/skills/torusguard-authorize/SKILL.md +7 -2
  53. package/.torusguard/skills/torusguard-exploit-check/SKILL.md +7 -2
  54. package/.torusguard/skills/torusguard-full/SKILL.md +7 -2
  55. package/.torusguard/skills/torusguard-harden/SKILL.md +98 -94
  56. package/.torusguard/skills/torusguard-init/SKILL.md +68 -64
  57. package/.torusguard/skills/torusguard-recheck/SKILL.md +81 -77
  58. package/.torusguard/skills/torusguard-report/SKILL.md +93 -72
  59. package/.torusguard/skills/torusguard-status/SKILL.md +62 -58
  60. package/.torusguard/skills/torusguard-verify/SKILL.md +7 -2
  61. package/.torusguard/skills/torusguard-web-validate/SKILL.md +7 -2
  62. package/.torusguard/workflows/apply.md +1 -1
  63. package/.torusguard/workflows/audit.md +1 -1
  64. package/.torusguard/workflows/authorize.md +1 -1
  65. package/.torusguard/workflows/exploit-check.md +1 -1
  66. package/.torusguard/workflows/harden.md +1 -1
  67. package/.torusguard/workflows/init.md +1 -1
  68. package/.torusguard/workflows/memory.md +1 -1
  69. package/.torusguard/workflows/recheck.md +1 -1
  70. package/.torusguard/workflows/report.md +1 -1
  71. package/.torusguard/workflows/status.md +1 -1
  72. package/.torusguard/workflows/verify.md +1 -1
  73. package/.torusguard/workflows/web-validate.md +1 -1
  74. package/README.md +14 -8
  75. package/bin/torusguard.js +56 -20
  76. package/package.json +1 -1
  77. package/skills/torusguard/SKILL.md +1 -1
  78. package/skills/torusguard/bootstrap.py +7 -2
  79. package/skills/torusguard/payload/.manifest.json +35 -35
  80. package/skills/torusguard/payload/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  81. package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
  82. package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  83. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  84. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
  85. package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  86. package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  87. package/skills/torusguard/payload/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  88. package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
  89. package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  90. package/skills/torusguard/payload/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  91. package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
  92. package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
  93. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
  94. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
  95. package/skills/torusguard/payload/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
  96. package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
  97. package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  98. package/skills/torusguard/payload/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
  99. package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-311.pyc +0 -0
  100. package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
  101. package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
  102. package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
  103. package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
  104. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
  105. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  106. package/skills/torusguard/payload/scripts/apply_runner.py +11 -5
  107. package/skills/torusguard/payload/scripts/audit_runner.py +222 -15
  108. package/skills/torusguard/payload/scripts/harden_runner.py +3 -3
  109. package/skills/torusguard/payload/scripts/html_reporter.py +2668 -648
  110. package/skills/torusguard/payload/scripts/recheck_runner.py +3 -3
  111. package/skills/torusguard/payload/scripts/recipes_runner.py +3 -3
  112. package/skills/torusguard/payload/scripts/report_sync.py +370 -74
  113. package/skills/torusguard/payload/scripts/sarif_exporter.py +48 -8
  114. package/skills/torusguard/payload/scripts/stack_detect.py +44 -2
  115. package/skills/torusguard/payload/scripts/term_ui.py +1 -1
  116. package/skills/torusguard/payload/skills/torusguard/SKILL.md +1 -1
  117. package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +1 -1
  118. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +1 -1
  119. package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +1 -1
  120. package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +1 -1
  121. package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +1 -1
  122. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +1 -1
  123. package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +1 -1
  124. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +1 -1
  125. package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +40 -23
  126. package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +1 -1
  127. package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +1 -1
  128. package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +1 -1
  129. package/skills/torusguard/payload/workflows/apply.md +1 -1
  130. package/skills/torusguard/payload/workflows/audit.md +1 -1
  131. package/skills/torusguard/payload/workflows/authorize.md +1 -1
  132. package/skills/torusguard/payload/workflows/exploit-check.md +1 -1
  133. package/skills/torusguard/payload/workflows/harden.md +1 -1
  134. package/skills/torusguard/payload/workflows/init.md +1 -1
  135. package/skills/torusguard/payload/workflows/memory.md +1 -1
  136. package/skills/torusguard/payload/workflows/recheck.md +1 -1
  137. package/skills/torusguard/payload/workflows/report.md +1 -1
  138. package/skills/torusguard/payload/workflows/status.md +1 -1
  139. package/skills/torusguard/payload/workflows/verify.md +1 -1
  140. package/skills/torusguard/payload/workflows/web-validate.md +1 -1
  141. package/skills/torusguard-apply/SKILL.md +1 -1
  142. package/skills/torusguard-audit/SKILL.md +1 -1
  143. package/skills/torusguard-authorize/SKILL.md +1 -1
  144. package/skills/torusguard-exploit-check/SKILL.md +1 -1
  145. package/skills/torusguard-full/SKILL.md +1 -1
  146. package/skills/torusguard-harden/SKILL.md +1 -1
  147. package/skills/torusguard-init/SKILL.md +1 -1
  148. package/skills/torusguard-recheck/SKILL.md +1 -1
  149. package/skills/torusguard-report/SKILL.md +40 -23
  150. package/skills/torusguard-status/SKILL.md +1 -1
  151. package/skills/torusguard-verify/SKILL.md +1 -1
  152. package/skills/torusguard-web-validate/SKILL.md +1 -1
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- TorusGuard Targeted Differential Recheck Engine (v1.3.5)
3
+ TorusGuard Targeted Differential Recheck Engine (v1.3.6)
4
4
  Executes scoped differential re-audits verifying only impacted files and adjacent boundaries.
5
5
  Evaluates formal status transitions (Confirmed Fixed vs Regressed vs Unresolved), updates run manifest,
6
6
  and records verification telemetry in the persistent security memory subsystem.
@@ -57,7 +57,7 @@ def box_line(content: str, width: int = 67, border: str = "│", border_color: s
57
57
  return term_ui.format_box_line(content, width=width, border=border, border_color=border_color)
58
58
  return f" {border_color}{border}{RESET} {content}"
59
59
 
60
- def box_header(title: str, subtitle: str = "", version: str = "v1.3.5", border_color: str = CYAN) -> str:
60
+ def box_header(title: str, subtitle: str = "", version: str = "v1.3.6", border_color: str = CYAN) -> str:
61
61
  if term_ui:
62
62
  return term_ui.card_header(title, subtitle, version, border_color)
63
63
  return f"=== {title} ({version}) ==="
@@ -106,7 +106,7 @@ def execute_recheck(target_root: Path, run_id_arg: Optional[str] = None) -> Dict
106
106
 
107
107
  # Header Card
108
108
  print()
109
- print(box_header("🛡️ TORUSGUARD DIFFERENTIAL RECHECK ENGINE", "Targeted Differential AST Scan & Regression Verification", "v1.3.5"))
109
+ print(box_header("🛡️ TORUSGUARD DIFFERENTIAL RECHECK ENGINE", "Targeted Differential AST Scan & Regression Verification", "v1.3.6"))
110
110
  print()
111
111
 
112
112
  print(border_top("Recheck Verification Scope"))
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- TorusGuard Golden Fix Recipe Explorer (v1.3.5)
3
+ TorusGuard Golden Fix Recipe Explorer (v1.3.6)
4
4
  Inspect, list, and export reusable, verified AST remediation code snippets
5
5
  distilled from passing security patches and persistent memory patterns.
6
6
  Standardized 75-column terminal UI formatting.
@@ -47,7 +47,7 @@ def box_line(content: str, width: int = 67, border: str = "│", border_color: s
47
47
  return term_ui.format_box_line(content, width=width, border=border, border_color=border_color)
48
48
  return f" {border_color}{border}{RESET} {content}"
49
49
 
50
- def box_header(title: str, subtitle: str = "", version: str = "v1.3.5", border_color: str = CYAN) -> str:
50
+ def box_header(title: str, subtitle: str = "", version: str = "v1.3.6", border_color: str = CYAN) -> str:
51
51
  if term_ui:
52
52
  return term_ui.card_header(title, subtitle, version, border_color)
53
53
  return f"=== {title} ({version}) ==="
@@ -80,7 +80,7 @@ def list_recipes(target_root: Path, detail_id: Optional[str] = None, json_output
80
80
 
81
81
  # Header Card
82
82
  print()
83
- print(box_header("🛡️ TORUSGUARD GOLDEN FIX RECIPES", "Distilled Ponytail Fixes (<= 35 add, <= 25 del) in Persistent Memory", "v1.3.5"))
83
+ print(box_header("🛡️ TORUSGUARD GOLDEN FIX RECIPES", "Distilled Ponytail Fixes (<= 35 add, <= 25 del) in Persistent Memory", "v1.3.6"))
84
84
  print()
85
85
 
86
86
  if not recipes:
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- TorusGuard Living Security Report Sync Engine (v1.3.5)
3
+ TorusGuard Living Security Report Sync Engine (v1.3.6)
4
4
  Maintains a living, ground-truth `security_report.md` ledger at the root of the workspace.
5
5
  Tracks finding lifecycles from Discovery -> Candidate -> Applied -> Resolved -> Regressed.
6
6
  Pure Python 3.10+ standard library (zero external dependencies).
@@ -10,6 +10,7 @@ import sys
10
10
  import os
11
11
  import re
12
12
  import json
13
+ import time
13
14
  import datetime
14
15
  from pathlib import Path
15
16
  from typing import Dict, List, Any, Optional, Tuple
@@ -42,9 +43,106 @@ def get_report_path(target_root: Path) -> Path:
42
43
  """Return absolute path to security_report.md at the workspace root."""
43
44
  return target_root / "security_report.md"
44
45
 
46
+
47
+ def atomic_write_text(file_path: Path, content: str, max_retries: int = 3, backoff_base: float = 0.05) -> bool:
48
+ """
49
+ Atomically write text to disk using a temporary file and atomic replace.
50
+ Handles Windows file-locking (e.g. browser read locks on open HTML reports)
51
+ via exponential backoff retries.
52
+ """
53
+ file_path = Path(file_path).resolve()
54
+ file_path.parent.mkdir(parents=True, exist_ok=True)
55
+
56
+ pid = os.getpid()
57
+ ts = int(time.time() * 1000)
58
+ tmp_path = file_path.with_name(f".tmp.{pid}.{ts}.{file_path.name}")
59
+
60
+ try:
61
+ with open(tmp_path, "w", encoding="utf-8") as f:
62
+ f.write(content)
63
+ f.flush()
64
+ os.fsync(f.fileno())
65
+ except Exception as e:
66
+ if tmp_path.exists():
67
+ try:
68
+ tmp_path.unlink()
69
+ except Exception:
70
+ pass
71
+ raise e
72
+
73
+ for attempt in range(max_retries):
74
+ try:
75
+ os.replace(tmp_path, file_path)
76
+ return True
77
+ except (PermissionError, OSError):
78
+ if attempt == max_retries - 1:
79
+ try:
80
+ file_path.write_text(content, encoding="utf-8")
81
+ if tmp_path.exists():
82
+ tmp_path.unlink()
83
+ return True
84
+ except Exception:
85
+ if tmp_path.exists():
86
+ try:
87
+ tmp_path.unlink()
88
+ except Exception:
89
+ pass
90
+ raise
91
+ time.sleep(backoff_base * (2 ** attempt))
92
+
93
+ return False
94
+
95
+
96
+ def reconcile_from_run_artifacts(target_root: Path) -> List[Dict[str, Any]]:
97
+ """
98
+ Authoritative fallback: if security_report.md is missing or damaged,
99
+ heal state from the latest run's findings.json in .torusguard/runs/.
100
+ """
101
+ runs_dir = target_root / ".torusguard" / "runs"
102
+ if not runs_dir.is_dir():
103
+ return []
104
+
105
+ run_folders = sorted(
106
+ [d for d in runs_dir.iterdir() if d.is_dir() and (d / "findings.json").is_file()],
107
+ key=lambda p: p.stat().st_mtime,
108
+ reverse=True
109
+ )
110
+ if not run_folders:
111
+ return []
112
+
113
+ latest_file = run_folders[0] / "findings.json"
114
+ try:
115
+ data = json.loads(latest_file.read_text(encoding="utf-8"))
116
+ if isinstance(data, list):
117
+ return data
118
+ except Exception:
119
+ pass
120
+ return []
121
+
122
+
45
123
  def parse_existing_report(report_path: Path) -> Dict[str, Any]:
46
- """Parse existing security_report.md into structured data if it exists."""
124
+ """Parse existing security_report.md into structured data if it exists.
125
+ Supports both the legacy format and the new v2 summary-first format.
126
+ Heals from latest run findings if report is absent or damaged.
127
+ """
47
128
  if not report_path.is_file():
129
+ recovered = reconcile_from_run_artifacts(report_path.parent)
130
+ if recovered:
131
+ f_map = {}
132
+ for r in recovered:
133
+ fid = r.get("finding_id", "")
134
+ if fid:
135
+ f_map[fid] = {
136
+ "finding_id": fid,
137
+ "rule_id": r.get("rule_id", "TG-GEN"),
138
+ "title": r.get("title", "Security Finding"),
139
+ "status": "🔴 OPEN",
140
+ "severity": r.get("severity", "Medium"),
141
+ "confidence": f"{r.get('confidence_score', 70)}% ({r.get('confidence_band', 'High Confidence')})",
142
+ "target": f"{r.get('file_path', '')}:{r.get('line_number', 1)}",
143
+ "history": []
144
+ }
145
+ return {"findings": f_map, "metadata": {}}
48
146
  return {"findings": {}, "metadata": {}}
49
147
 
50
148
  try:
@@ -53,8 +151,9 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
53
151
  return {"findings": {}, "metadata": {}}
54
152
 
55
153
  findings = {}
56
- # Parse individual finding cards: ### [RULE-ID] Title
57
- card_pattern = re.compile(
154
+
155
+ # ── Format A (legacy): ### [RULE-ID] Title ──
156
+ card_pattern_legacy = re.compile(
58
157
  r'###\s+\[(TG-[A-Z]+-\d+)\]\s+(.*?)\n'
59
158
  r'- \*\*Finding ID:\*\*\s+`([^`]+)`\n'
60
159
  r'- \*\*Status:\*\*\s+([^\n]+)\n'
@@ -63,7 +162,7 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
63
162
  re.MULTILINE
64
163
  )
65
164
 
66
- for m in card_pattern.finditer(content):
165
+ for m in card_pattern_legacy.finditer(content):
67
166
  rule_id = m.group(1).strip()
68
167
  title = m.group(2).strip()
69
168
  finding_id = m.group(3).strip()
@@ -83,7 +182,49 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
83
182
  "history": []
84
183
  }
85
184
 
86
- # Extract history tables if present
185
+ # ── Format B (new v2): ### TG-RULE-001 · Title `status` `confidence` ──
186
+ card_pattern_v2 = re.compile(
187
+ r'###\s+(TG-[A-Z]+-\d+)\s+·\s+(.*?)\s+`([^`]+)`\s+`([^`]+)`\n'
188
+ r'- \*\*Finding ID:\*\*\s+`([^`]+)`(?:[^\n]*\*\*Severity:\*\*\s+`([^`]+)`)?\n'
189
+ r'(?:📁|- \*\*Target:\*\*)\s*`([^`\n]+)`',
190
+ re.MULTILINE
191
+ )
192
+
193
+ for m in card_pattern_v2.finditer(content):
194
+ rule_id = m.group(1).strip()
195
+ title = m.group(2).strip()
196
+ status = m.group(3).strip()
197
+ confidence = m.group(4).strip()
198
+ finding_id = m.group(5).strip()
199
+ sev_explicit = m.group(6).strip() if m.group(6) else None
200
+ target = m.group(7).strip()
201
+
202
+ if finding_id not in findings: # Don't overwrite legacy matches
203
+ if sev_explicit:
204
+ severity = sev_explicit
205
+ else:
206
+ # Infer severity from section header or status
207
+ severity = "High" # Default
208
+ match_pos = m.start()
209
+ preceding = content[:match_pos]
210
+ if "Critical" in preceding.split("##")[-1]:
211
+ severity = "Critical"
212
+ elif "Medium" in preceding.split("##")[-1]:
213
+ severity = "Medium"
214
+
215
+ findings[finding_id] = {
216
+ "finding_id": finding_id,
217
+ "rule_id": rule_id,
218
+ "title": title,
219
+ "status": status,
220
+ "severity": severity,
221
+ "confidence": confidence,
222
+ "target": target,
223
+ "history": []
224
+ }
225
+
226
+ # Extract history from inline blockquote format (new) or table format (legacy)
227
+ # Legacy: #### Lifecycle History for `finding_id`
87
228
  history_pattern = re.compile(r'#### Lifecycle History for `([^`]+)`\n([\s\S]*?)(?=\n---|\n###|\Z)')
88
229
  for hm in history_pattern.finditer(content):
89
230
  fid = hm.group(1).strip()
@@ -100,8 +241,40 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
100
241
  "details": cols[3]
101
242
  })
102
243
 
244
+ # New format: > Last: Action — timestamp · `run_id`
245
+ inline_history = re.compile(
246
+ r'###\s+TG-[A-Z]+-\d+.*?\n.*?Finding ID:\*\*\s+`([^`]+)`.*?'
247
+ r'>\s+Last:\s+(.*?)\s+—\s+(.*?)\s+·\s+`([^`]+)`',
248
+ re.DOTALL
249
+ )
250
+ for hm in inline_history.finditer(content):
251
+ fid = hm.group(1).strip()
252
+ if fid in findings and not findings[fid]["history"]:
253
+ findings[fid]["history"].append({
254
+ "timestamp": hm.group(3).strip(),
255
+ "action": hm.group(2).strip(),
256
+ "run_id": hm.group(4).strip(),
257
+ "details": "Restored from inline history"
258
+ })
259
+
260
+
103
261
  return {"findings": findings, "raw_content": content}
104
262
 
263
+
264
+ def parse_metadata_block(content: str) -> Dict[str, Any]:
265
+ """Parse the YAML metadata block between TORUSGUARD_METADATA_START/END markers."""
266
+ metadata = {}
267
+ meta_match = re.search(
268
+ r'<!-- TORUSGUARD_METADATA_START\s*\n(.*?)\nTORUSGUARD_METADATA_END -->',
269
+ content, re.DOTALL
270
+ )
271
+ if meta_match:
272
+ for line in meta_match.group(1).strip().splitlines():
273
+ if ':' in line:
274
+ key, _, val = line.partition(':')
275
+ metadata[key.strip()] = val.strip()
276
+ return metadata
277
+
105
278
  def compute_health_score(findings: List[Dict[str, Any]]) -> Tuple[int, str]:
106
279
  """
107
280
  Compute security health score (0-100) based on remaining open/regressed findings.
@@ -127,7 +300,8 @@ def compute_health_score(findings: List[Dict[str, Any]]) -> Tuple[int, str]:
127
300
  return score, status
128
301
 
129
302
  def render_report(findings: List[Dict[str, Any]], target_root: Path) -> str:
130
- """Render the full living security_report.md document."""
303
+ """Render the living security_report.md with summary-first layout,
304
+ AI-parseable metadata, severity grouping, and concise finding cards."""
131
305
  score, status_text = compute_health_score(findings)
132
306
  now_str = format_timestamp()
133
307
 
@@ -146,36 +320,11 @@ def render_report(findings: List[Dict[str, Any]], target_root: Path) -> str:
146
320
  med_count = sum(1 for f in findings if f["severity"].lower() == "medium")
147
321
  low_count = sum(1 for f in findings if f["severity"].lower() == "low")
148
322
 
149
- lines = [
150
- "# 🛡️ TorusGuard Living Security Report",
151
- "",
152
- "> **Notice:** This document is the living, single-source-of-truth ledger for security findings,",
153
- "> candidate patches, and verified closures across this repository. Updated automatically by",
154
- "> TorusGuard CLI commands (`audit`, `harden`, `apply`, `recheck`) and AI chat workflows.",
155
- "",
156
- "---",
157
- "",
158
- "## 📊 Security Posture Overview",
159
- "",
160
- f"- **Health Score:** `{score}/100` — **{status_text}**",
161
- f"- **Last Updated:** `{now_str}`",
162
- f"- **Total Tracked Findings:** `{total}`",
163
- f"- **Status Breakdown:** `🔴 {open_count} Open` · `🟠 {verified_count} Verified` · `🟡 {candidate_count} Candidate` · `🔵 {applied_count} Applied` · `🟢 {resolved_count} Resolved` · `❌ {regressed_count} Regressed` · `⚪ {fp_count} Suppressed`",
164
- "",
165
- "| Severity | Total | Open / Regressed | Resolved | Candidate / Applied |",
166
- "| :--- | :---: | :---: | :---: | :---: |",
167
- f"| **Critical** | {crit_count} | {sum(1 for f in findings if f['severity'].lower() == 'critical' and ('OPEN' in f['status'] or 'REGRESSED' in f['status']))} | {sum(1 for f in findings if f['severity'].lower() == 'critical' and 'RESOLVED' in f['status'])} | {sum(1 for f in findings if f['severity'].lower() == 'critical' and ('CANDIDATE' in f['status'] or 'APPLIED' in f['status']))} |",
168
- f"| **High** | {high_count} | {sum(1 for f in findings if f['severity'].lower() == 'high' and ('OPEN' in f['status'] or 'REGRESSED' in f['status']))} | {sum(1 for f in findings if f['severity'].lower() == 'high' and 'RESOLVED' in f['status'])} | {sum(1 for f in findings if f['severity'].lower() == 'high' and ('CANDIDATE' in f['status'] or 'APPLIED' in f['status']))} |",
169
- f"| **Medium** | {med_count} | {sum(1 for f in findings if f['severity'].lower() == 'medium' and ('OPEN' in f['status'] or 'REGRESSED' in f['status']))} | {sum(1 for f in findings if f['severity'].lower() == 'medium' and 'RESOLVED' in f['status'])} | {sum(1 for f in findings if f['severity'].lower() == 'medium' and ('CANDIDATE' in f['status'] or 'APPLIED' in f['status']))} |",
170
- f"| **Low** | {low_count} | {sum(1 for f in findings if f['severity'].lower() == 'low' and ('OPEN' in f['status'] or 'REGRESSED' in f['status']))} | {sum(1 for f in findings if f['severity'].lower() == 'low' and 'RESOLVED' in f['status'])} | {sum(1 for f in findings if f['severity'].lower() == 'low' and ('CANDIDATE' in f['status'] or 'APPLIED' in f['status']))} |",
171
- "",
172
- "---",
173
- "",
174
- "## 🔍 Tracked Findings Detail",
175
- ""
176
- ]
323
+ crit_open = sum(1 for f in findings if f["severity"].lower() == "critical" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
324
+ high_open = sum(1 for f in findings if f["severity"].lower() == "high" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
325
+ med_open = sum(1 for f in findings if f["severity"].lower() == "medium" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
177
326
 
178
- # Sort findings: Open/Regressed first, then by severity (Critical -> High -> Medium -> Low), then rule_id
327
+ # Sort findings by status priority and severity
179
328
  sev_rank = {"critical": 0, "high": 1, "medium": 2, "low": 3}
180
329
  def sort_key(f):
181
330
  is_active = 0 if ("OPEN" in f["status"] or "REGRESSED" in f["status"]) else 1
@@ -184,50 +333,143 @@ def render_report(findings: List[Dict[str, Any]], target_root: Path) -> str:
184
333
 
185
334
  sorted_findings = sorted(findings, key=sort_key)
186
335
 
187
- for f in sorted_findings:
188
- lines.append(f"### [{f['rule_id']}] {f['title']}")
189
- lines.append(f"- **Finding ID:** `{f['finding_id']}`")
190
- lines.append(f"- **Status:** {f['status']}")
191
- lines.append(f"- **Severity:** {f['severity']} | **Confidence:** {f.get('confidence', '70% (High Confidence)')}")
192
- lines.append(f"- **Target:** `{f['target']}`")
193
- if f.get("cluster"):
194
- lines.append(f"- **Root-Cause Cluster:** `{f['cluster']}`")
336
+ # Group by severity for active findings
337
+ active = [f for f in sorted_findings if "OPEN" in f["status"] or "REGRESSED" in f["status"]]
338
+ resolved = [f for f in sorted_findings if "RESOLVED" in f["status"]]
339
+ in_progress = [f for f in sorted_findings if any(s in f["status"] for s in ("CANDIDATE", "APPLIED", "VERIFIED"))]
340
+ suppressed = [f for f in sorted_findings if "FALSE POSITIVE" in f["status"]]
341
+
342
+ critical_findings = [f for f in active if f["severity"].lower() == "critical"]
343
+ high_findings = [f for f in active if f["severity"].lower() == "high"]
344
+ medium_findings = [f for f in active if f["severity"].lower() in ("medium", "low")]
345
+
346
+ # ── Build the report ──
347
+ lines = [
348
+ "# 🛡️ TorusGuard Security Report",
349
+ "",
350
+ "<!-- TORUSGUARD_METADATA_START",
351
+ f"health_score: {score}",
352
+ f"status: {status_text}",
353
+ f"total_findings: {total}",
354
+ f"critical: {crit_count}",
355
+ f"critical_open: {crit_open}",
356
+ f"high: {high_count}",
357
+ f"high_open: {high_open}",
358
+ f"medium: {med_count}",
359
+ f"medium_open: {med_open}",
360
+ f"low: {low_count}",
361
+ f"open: {open_count}",
362
+ f"resolved: {resolved_count}",
363
+ f"regressed: {regressed_count}",
364
+ f"suppressed: {fp_count}",
365
+ f"last_scan: {now_str}",
366
+ "TORUSGUARD_METADATA_END -->",
367
+ "",
368
+ "## Executive Summary",
369
+ "",
370
+ "| Metric | Value |",
371
+ "| :--- | :--- |",
372
+ f"| **Health Score** | `{score}/100` — {status_text} |",
373
+ f"| **Critical (Open)** | {crit_open} |",
374
+ f"| **High (Open)** | {high_open} |",
375
+ f"| **Medium (Open)** | {med_open} |",
376
+ f"| **Resolved** | {resolved_count} |",
377
+ f"| **In Progress** | {candidate_count + applied_count} |",
378
+ f"| **Last Scan** | {now_str} |",
379
+ "",
380
+ ]
381
+
382
+ # Action required callout
383
+ if crit_open > 0:
384
+ lines.append(f"> ⚠️ **Action Required:** {crit_open} critical finding{'s' if crit_open != 1 else ''} require immediate attention. Run `npx torusguard harden` to generate patches.")
385
+ lines.append("")
386
+
387
+ lines.append("---")
388
+ lines.append("")
389
+
390
+ # ── Critical Findings Section ──
391
+ if critical_findings:
392
+ lines.append(f"## 🔴 Critical Findings ({len(critical_findings)})")
195
393
  lines.append("")
196
- lines.append(f"**What is wrong:** \n{f.get('what_is_wrong', f.get('description', 'Security violation detected by TorusGuard scanner.'))}")
394
+ for f in critical_findings:
395
+ lines.extend(_render_finding_card(f))
396
+
397
+ # ── High Findings Section ──
398
+ if high_findings:
399
+ lines.append(f"## 🟠 High Findings ({len(high_findings)})")
400
+ lines.append("")
401
+ for f in high_findings:
402
+ lines.extend(_render_finding_card(f))
403
+
404
+ # ── Medium/Low Findings Section ──
405
+ if medium_findings:
406
+ lines.append(f"## 🟡 Medium / Low Findings ({len(medium_findings)})")
197
407
  lines.append("")
198
- lines.append(f"**Why it matters:** \n{f.get('why_it_matters', 'Violates TorusGuard strict production safety and isolation invariant.')}")
408
+ for f in medium_findings:
409
+ lines.extend(_render_finding_card(f))
410
+
411
+ # ── In Progress Section ──
412
+ if in_progress:
413
+ lines.append(f"## 🔵 In Progress ({len(in_progress)})")
414
+ lines.append("")
415
+ for f in in_progress:
416
+ lines.extend(_render_finding_card(f))
417
+
418
+ # ── Resolved Section ──
419
+ if resolved:
420
+ lines.append(f"## ✅ Resolved ({len(resolved)})")
199
421
  lines.append("")
200
- if f.get("what_should_change"):
201
- lines.append(f"**Governed Remediation:** \n{f['what_should_change']}")
202
- lines.append("")
203
- if f.get("snippet"):
204
- lines.append("```")
205
- lines.append(f["snippet"].strip())
206
- lines.append("```")
207
- lines.append("")
208
- if f.get("proposed_diff"):
209
- lines.append("**Proposed Minimal Patch Preview (Ponytail Bounded):**")
210
- lines.append("```diff")
211
- lines.append(f["proposed_diff"].strip())
212
- lines.append("```")
213
- lines.append("")
214
-
215
- # History table
216
- history = f.get("history", [])
217
- if history:
218
- lines.append(f"#### Lifecycle History for `{f['finding_id']}`")
219
- lines.append("| Timestamp | Action | Run ID | Details |")
220
- lines.append("| :--- | :--- | :--- | :--- |")
221
- for h in history:
222
- lines.append(f"| {h['timestamp']} | {h['action']} | `{h['run_id']}` | {h['details']} |")
223
- lines.append("")
422
+ for f in resolved:
423
+ lines.extend(_render_finding_card(f))
224
424
 
425
+ # ── Suppressed Section ──
426
+ if suppressed:
427
+ lines.append(f"## ⚪ Suppressed ({len(suppressed)})")
428
+ lines.append("")
429
+ for f in suppressed:
430
+ lines.append(f"- **{f['rule_id']}** · {f['title']} — `{f['target']}` · False Positive")
431
+ lines.append("")
225
432
  lines.append("---")
226
433
  lines.append("")
227
434
 
435
+ # Footer
436
+ lines.append("---")
437
+ lines.append(f"*Generated by TorusGuard v1.3.6 · {now_str} · 100% Local-First*")
438
+ lines.append("")
439
+
228
440
  return "\n".join(lines)
229
441
 
230
442
 
443
+ def _render_finding_card(f: Dict[str, Any]) -> List[str]:
444
+ """Render a concise finding card (4-6 lines max)."""
445
+ status_full = f["status"]
446
+ conf = f.get('confidence', '70% (High Confidence)')
447
+ sev = f.get('severity', 'High')
448
+ card = [
449
+ f"### {f['rule_id']} · {f['title']} `{status_full}` `{conf}`",
450
+ f"- **Finding ID:** `{f['finding_id']}` · **Severity:** `{sev}`",
451
+ f"📁 `{f['target']}`",
452
+ f"**Problem:** {f.get('what_is_wrong', f.get('description', 'Security violation detected.'))}",
453
+ ]
454
+ if f.get("what_should_change"):
455
+ card.append(f"**Fix:** {f['what_should_change']}")
456
+ if f.get("proposed_diff"):
457
+ card.append("")
458
+ card.append("```diff")
459
+ card.append(f["proposed_diff"].strip())
460
+ card.append("```")
461
+ card.append("")
462
+ # Include lifecycle history inline (compact)
463
+ history = f.get("history", [])
464
+ if history and len(history) > 0:
465
+ last_event = history[-1]
466
+ card.append(f"> Last: {last_event.get('action', '?')} — {last_event.get('timestamp', '?')} · `{last_event.get('run_id', '?')}`")
467
+ card.append("")
468
+ card.append("---")
469
+ card.append("")
470
+ return card
471
+
472
+
231
473
  def record_audit_findings(target_root: Path, findings: List[Dict[str, Any]], run_id: str):
232
474
  """
233
475
  Called at the conclusion of `npx torusguard audit`.
@@ -298,7 +540,8 @@ def record_audit_findings(target_root: Path, findings: List[Dict[str, Any]], run
298
540
 
299
541
  # Render and save
300
542
  rendered = render_report(list(existing_map.values()), target_root)
301
- report_file.write_text(rendered, encoding="utf-8")
543
+ atomic_write_text(report_file, rendered)
544
+ sync_html_if_exists(target_root)
302
545
  return report_file
303
546
 
304
547
 
@@ -340,7 +583,8 @@ def record_harden_bundles(target_root: Path, bundles: List[Dict[str, Any]], run_
340
583
  })
341
584
 
342
585
  rendered = render_report(list(existing_map.values()), target_root)
343
- report_file.write_text(rendered, encoding="utf-8")
586
+ atomic_write_text(report_file, rendered)
587
+ sync_html_if_exists(target_root)
344
588
  return report_file
345
589
 
346
590
 
@@ -378,7 +622,8 @@ def record_applied_patches(target_root: Path, applied_bundles: List[Dict[str, An
378
622
  })
379
623
 
380
624
  rendered = render_report(list(existing_map.values()), target_root)
381
- report_file.write_text(rendered, encoding="utf-8")
625
+ atomic_write_text(report_file, rendered)
626
+ sync_html_if_exists(target_root)
382
627
  return report_file
383
628
 
384
629
 
@@ -419,5 +664,56 @@ def record_recheck_results(target_root: Path, recheck_results: Dict[str, Any], r
419
664
  })
420
665
 
421
666
  rendered = render_report(list(existing_map.values()), target_root)
422
- report_file.write_text(rendered, encoding="utf-8")
667
+ atomic_write_text(report_file, rendered)
668
+ sync_html_if_exists(target_root)
669
+ return report_file
670
+
671
+
672
+ def record_rollback_results(target_root: Path, restored_files: List[str], run_id: str):
673
+ """
674
+ Called at the conclusion of `npx torusguard rollback`.
675
+ Reverts findings for restored files back to OPEN 🔴.
676
+ """
677
+ report_file = get_report_path(target_root)
678
+ existing = parse_existing_report(report_file)
679
+ existing_map = existing["findings"]
680
+ now_str = format_timestamp()
681
+
682
+ norm_restored = {rf.replace("\\", "/") for rf in restored_files}
683
+
684
+ for item in existing_map.values():
685
+ target_path = item.get("target", "").split(":")[0].replace("\\", "/")
686
+ if any(rf in target_path or target_path in rf for rf in norm_restored):
687
+ item["status"] = "🔴 OPEN"
688
+ item["history"].append({
689
+ "timestamp": now_str,
690
+ "action": "Rollback Restored",
691
+ "run_id": run_id,
692
+ "details": f"File {target_path} reverted from pre-apply snapshot. Status restored to OPEN."
693
+ })
694
+
695
+ rendered = render_report(list(existing_map.values()), target_root)
696
+ atomic_write_text(report_file, rendered)
697
+ sync_html_if_exists(target_root)
423
698
  return report_file
699
+
700
+
701
+ def sync_html_if_exists(target_root: Path):
702
+ """
703
+ If the user has already generated report.html (at root or in .torusguard/runs),
704
+ refresh it so the HTML dashboard stays in lockstep with security_report.md.
705
+ """
706
+ root_html = target_root / "report.html"
707
+ runs_html = target_root / ".torusguard" / "runs" / "report-latest.html"
708
+ if root_html.is_file() or runs_html.is_file():
709
+ try:
710
+ s_dir = Path(__file__).resolve().parent
711
+ if str(s_dir) not in sys.path:
712
+ sys.path.insert(0, str(s_dir))
713
+ import html_reporter
714
+ if runs_html.is_file():
715
+ html_reporter.emit_html_report(target_path=runs_html, root_dir=target_root)
716
+ if root_html.is_file():
717
+ html_reporter.emit_html_report(target_path=root_html, root_dir=target_root)
718
+ except Exception:
719
+ pass