torusguard 0.9.5 → 1.3.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.
Files changed (80) hide show
  1. package/.torusguard/.manifest.json +98 -79
  2. package/.torusguard/config/torusguard.json +1 -1
  3. package/.torusguard/references/csharp-security.md +41 -0
  4. package/.torusguard/references/go-security.md +41 -0
  5. package/.torusguard/references/java-security.md +40 -0
  6. package/.torusguard/references/polyglot-security-matrix.md +25 -0
  7. package/.torusguard/references/rust-security.md +40 -0
  8. package/.torusguard/rules/custom/.gitkeep +1 -0
  9. package/.torusguard/rules/custom/README.md +30 -0
  10. package/.torusguard/runs/report-latest.html +328 -0
  11. package/.torusguard/schemas/golden-recipe.schema.json +75 -0
  12. package/.torusguard/schemas/memory-context.schema.json +95 -0
  13. package/.torusguard/schemas/memory-event.schema.json +70 -0
  14. package/.torusguard/schemas/memory-pattern.schema.json +65 -0
  15. package/.torusguard/scripts/diff_guard.py +202 -15
  16. package/.torusguard/scripts/finding_scorer.py +199 -86
  17. package/.torusguard/scripts/html_reporter.py +628 -0
  18. package/.torusguard/scripts/manifest_builder.py +7 -5
  19. package/.torusguard/scripts/memory_engine.py +1334 -0
  20. package/.torusguard/scripts/monorepo_detector.py +123 -10
  21. package/.torusguard/scripts/rules_sync.py +321 -0
  22. package/.torusguard/scripts/run_manager.py +171 -89
  23. package/.torusguard/scripts/stack_detect.py +458 -26
  24. package/.torusguard/skills/torusguard/SKILL.md +4 -3
  25. package/.torusguard/skills/torusguard/bootstrap.py +344 -56
  26. package/.torusguard/skills/torusguard/references/csharp-security.md +41 -0
  27. package/.torusguard/skills/torusguard/references/go-security.md +41 -0
  28. package/.torusguard/skills/torusguard/references/java-security.md +40 -0
  29. package/.torusguard/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  30. package/.torusguard/skills/torusguard/references/rust-security.md +40 -0
  31. package/.torusguard/skills/torusguard-audit/SKILL.md +25 -25
  32. package/.torusguard/skills/torusguard-harden/SKILL.md +3 -3
  33. package/.torusguard/skills/torusguard-recheck/SKILL.md +3 -3
  34. package/.torusguard/workflows/memory.md +53 -0
  35. package/README.md +307 -401
  36. package/bin/torusguard.js +204 -6
  37. package/package.json +1 -1
  38. package/skills/torusguard/SKILL.md +4 -3
  39. package/skills/torusguard/bootstrap.py +68 -4
  40. package/skills/torusguard/payload/.manifest.json +98 -79
  41. package/skills/torusguard/payload/config/torusguard.json +1 -1
  42. package/skills/torusguard/payload/references/csharp-security.md +41 -0
  43. package/skills/torusguard/payload/references/go-security.md +41 -0
  44. package/skills/torusguard/payload/references/java-security.md +40 -0
  45. package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -0
  46. package/skills/torusguard/payload/references/rust-security.md +40 -0
  47. package/skills/torusguard/payload/rules/custom/.gitkeep +1 -0
  48. package/skills/torusguard/payload/rules/custom/README.md +30 -0
  49. package/skills/torusguard/payload/schemas/golden-recipe.schema.json +75 -0
  50. package/skills/torusguard/payload/schemas/memory-context.schema.json +95 -0
  51. package/skills/torusguard/payload/schemas/memory-event.schema.json +70 -0
  52. package/skills/torusguard/payload/schemas/memory-pattern.schema.json +65 -0
  53. package/skills/torusguard/payload/scripts/diff_guard.py +202 -15
  54. package/skills/torusguard/payload/scripts/finding_scorer.py +199 -86
  55. package/skills/torusguard/payload/scripts/html_reporter.py +628 -0
  56. package/skills/torusguard/payload/scripts/manifest_builder.py +7 -5
  57. package/skills/torusguard/payload/scripts/memory_engine.py +1334 -0
  58. package/skills/torusguard/payload/scripts/monorepo_detector.py +123 -10
  59. package/skills/torusguard/payload/scripts/rules_sync.py +321 -0
  60. package/skills/torusguard/payload/scripts/run_manager.py +171 -89
  61. package/skills/torusguard/payload/scripts/stack_detect.py +458 -26
  62. package/skills/torusguard/payload/skills/torusguard/SKILL.md +4 -3
  63. package/skills/torusguard/payload/skills/torusguard/bootstrap.py +344 -56
  64. package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -0
  65. package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -0
  66. package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -0
  67. package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  68. package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -0
  69. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +25 -25
  70. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +3 -3
  71. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +3 -3
  72. package/skills/torusguard/payload/workflows/memory.md +53 -0
  73. package/skills/torusguard-audit/SKILL.md +25 -25
  74. package/skills/torusguard-harden/SKILL.md +3 -3
  75. package/skills/torusguard-recheck/SKILL.md +3 -3
  76. package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  77. package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
  78. package/skills/torusguard/__pycache__/bootstrap.cpython-311.pyc +0 -0
  79. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  80. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
@@ -0,0 +1,628 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ TorusGuard Visual HTML Report Emitter (v1.2.0)
4
+ Compiles a self-contained, zero-external-CDN, interactive dark-mode dashboard.
5
+ Embeds SVG gauges, 7-stage pipeline radar, Golden Fix Recipes, and attack surface analytics.
6
+ Zero external runtime dependencies (100% Python 3.10+ standard library).
7
+ """
8
+
9
+ import sys
10
+ import json
11
+ import html
12
+ import datetime
13
+ import argparse
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ # Windows console UTF-8 support
18
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
19
+ try:
20
+ getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
21
+ except (AttributeError, OSError):
22
+ pass
23
+ if sys.stderr and hasattr(sys.stderr, "reconfigure"):
24
+ try:
25
+ getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
26
+ except (AttributeError, OSError):
27
+ pass
28
+
29
+
30
+ def find_project_root(start_dir: str | Path | None = None) -> Path:
31
+ """Detect project root directory."""
32
+ current = Path(start_dir or Path.cwd()).resolve()
33
+ markers = [".git", "package.json", "pyproject.toml", ".torusguard"]
34
+ for m in markers:
35
+ if (current / m).exists():
36
+ return current
37
+ for parent in current.parents:
38
+ for m in markers:
39
+ if (parent / m).exists():
40
+ return parent
41
+ return current
42
+
43
+
44
+ def load_report_telemetry(root_dir: Path) -> dict[str, Any]:
45
+ """Gather all project telemetry, rules catalog, and memory state."""
46
+ tg_dir = root_dir / ".torusguard"
47
+ memory_dir = tg_dir / "memory"
48
+ config_file = tg_dir / "config" / "torusguard.json"
49
+
50
+ cfg: dict[str, Any] = {}
51
+ if config_file.exists():
52
+ try:
53
+ cfg = json.loads(config_file.read_text(encoding="utf-8"))
54
+ except (json.JSONDecodeError, OSError):
55
+ cfg = {}
56
+
57
+ profile_file = memory_dir / "profile.json"
58
+ patterns_file = memory_dir / "patterns.json"
59
+ context_file = memory_dir / "context.json"
60
+
61
+ profile: dict[str, Any] = {}
62
+ patterns: list[dict[str, Any]] = []
63
+ context: dict[str, Any] = {}
64
+
65
+ if profile_file.exists():
66
+ try:
67
+ profile = json.loads(profile_file.read_text(encoding="utf-8"))
68
+ except (json.JSONDecodeError, OSError):
69
+ profile = {}
70
+
71
+ if patterns_file.exists():
72
+ try:
73
+ patterns = json.loads(patterns_file.read_text(encoding="utf-8"))
74
+ except (json.JSONDecodeError, OSError):
75
+ patterns = []
76
+
77
+ if context_file.exists():
78
+ try:
79
+ context = json.loads(context_file.read_text(encoding="utf-8"))
80
+ except (json.JSONDecodeError, OSError):
81
+ context = {}
82
+
83
+ stack_info = cfg.get("detected_stack", {})
84
+ if not stack_info:
85
+ try:
86
+ import stack_detect # type: ignore
87
+ stack_info = stack_detect.detect_stack(root_dir)
88
+ except Exception:
89
+ stack_info = {"language": "Universal", "framework": "None", "data_layer": "None"}
90
+
91
+ return {
92
+ "project_name": root_dir.name or "Project",
93
+ "root_dir": str(root_dir),
94
+ "generated_at": datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC"),
95
+ "config": cfg,
96
+ "profile": profile,
97
+ "patterns": patterns,
98
+ "context": context,
99
+ "detected_stack": stack_info
100
+ }
101
+
102
+
103
+ def compute_posture_score(telemetry: dict[str, Any]) -> int:
104
+ """Calculate an overall health score (0-100) based on verified fixes and regressions."""
105
+ profile = telemetry.get("profile", {})
106
+ patterns = telemetry.get("patterns", [])
107
+
108
+ base_score = 85
109
+ fix_rate = profile.get("fix_rate_percentage")
110
+ if fix_rate is not None:
111
+ base_score = int(fix_rate)
112
+
113
+ regressions = sum(1 for p in patterns if p.get("pattern_type") == "regression_watch")
114
+ recipes = sum(1 for p in patterns if p.get("pattern_type") == "golden_fix_recipe")
115
+
116
+ score = base_score - (regressions * 5) + (recipes * 3)
117
+ return min(max(score, 10), 100)
118
+
119
+
120
+ def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
121
+ """Build a complete, standalone, single-file HTML dashboard."""
122
+ score = compute_posture_score(telemetry)
123
+ profile = telemetry.get("profile", {})
124
+ patterns = telemetry.get("patterns", [])
125
+ cfg = telemetry.get("config", {})
126
+ detected = cfg.get("detected_stack", {})
127
+ stack = profile.get("stack", [])
128
+ if not stack and detected.get("language") and detected.get("language") != "Unknown":
129
+ stack = [detected["language"]]
130
+ if detected.get("framework") and detected["framework"] != "None":
131
+ stack.append(detected["framework"])
132
+ stack_label = ", ".join(stack) if stack else "Universal Polyglot Application"
133
+
134
+ recipes = [p for p in patterns if p.get("pattern_type") == "golden_fix_recipe"]
135
+ regressions = [p for p in patterns if p.get("pattern_type") == "regression_watch"]
136
+ false_positives = [p for p in patterns if p.get("pattern_type") == "false_positive_class"]
137
+ recurring = [p for p in patterns if p.get("pattern_type") in ("recurring_fix", "common_vulnerability")]
138
+
139
+ # SVG Circle Math for Gauge (radius 40, circ = 251.2)
140
+ circumference = 251.2
141
+ dash_offset = circumference - (circumference * (score / 100.0))
142
+ score_color = "#3fb950" if score >= 80 else ("#d29922" if score >= 60 else "#f85149")
143
+
144
+ # Generate recipe cards HTML
145
+ recipe_cards_html = ""
146
+ if recipes:
147
+ for r in recipes[:6]:
148
+ rule = html.escape(r.get("rule_id", "TG-SEC"))
149
+ desc = html.escape(r.get("description", "Verified fix"))
150
+ rdata = r.get("recipe_data", {})
151
+ diff = html.escape(rdata.get("diff_snippet", ""))
152
+ verified = rdata.get("verified_count", 1)
153
+ metrics = rdata.get("ponytail_metrics", {})
154
+ adds = metrics.get("additions", 0)
155
+ dels = metrics.get("deletions", 0)
156
+
157
+ recipe_cards_html += f"""
158
+ <div class="card recipe-card">
159
+ <div class="card-header">
160
+ <span class="badge badge-success">{rule}</span>
161
+ <span class="meta-label">Verified {verified}x &middot; +{adds}/-{dels} Ponytail</span>
162
+ </div>
163
+ <p class="recipe-desc">{desc}</p>
164
+ <pre class="diff-block"><code>{diff}</code></pre>
165
+ </div>
166
+ """
167
+ else:
168
+ recipe_cards_html = """
169
+ <div class="empty-state">
170
+ <p>No golden fix recipes captured yet. Run <code>/torusguard-apply</code> on verified remediations to distill reusable recipes.</p>
171
+ </div>
172
+ """
173
+
174
+ # Generate regression watch & suppression rows
175
+ regression_rows_html = ""
176
+ combined_watches: list[dict[str, str]] = []
177
+ for reg in regressions:
178
+ combined_watches.append({
179
+ "badge_class": "badge-danger",
180
+ "rule": reg.get("rule_id", "TG-REG"),
181
+ "desc": reg.get("description", "Active watch"),
182
+ "target": ", ".join(reg.get("affected_files", [])) or "Monitored files"
183
+ })
184
+ for fp in false_positives:
185
+ combined_watches.append({
186
+ "badge_class": "badge-warning",
187
+ "rule": fp.get("rule_id", "TG-FP"),
188
+ "desc": fp.get("description", "False positive suppression"),
189
+ "target": fp.get("reason") or "Suppressed rule pattern"
190
+ })
191
+
192
+ if combined_watches:
193
+ for item in combined_watches:
194
+ rule = html.escape(item["rule"])
195
+ desc = html.escape(item["desc"])
196
+ target = html.escape(item["target"])
197
+ bclass = item["badge_class"]
198
+ regression_rows_html += f"""
199
+ <tr class="reg-row">
200
+ <td><span class="badge {bclass}">{rule}</span></td>
201
+ <td>{desc}</td>
202
+ <td><code>{target}</code></td>
203
+ </tr>
204
+ """
205
+ else:
206
+ regression_rows_html = "<tr><td colspan='3' class='empty-cell'>Zero active security regressions or suppressions. Posture clean.</td></tr>"
207
+
208
+ stack_info = telemetry.get("detected_stack", {})
209
+ primary_lang = stack_info.get("language", "Universal")
210
+ primary_fw = stack_info.get("framework", "None")
211
+ primary_orm = stack_info.get("data_layer", "None")
212
+ sub_stacks = stack_info.get("sub_stacks", [])
213
+
214
+ chips = [f'<span class="chip chip-primary">&#9679; {html.escape(primary_lang)}</span>']
215
+ if primary_fw and primary_fw != "None":
216
+ chips.append(f'<span class="chip">&#9881; {html.escape(primary_fw)}</span>')
217
+ if primary_orm and primary_orm != "None":
218
+ chips.append(f'<span class="chip">&#128451; {html.escape(primary_orm)}</span>')
219
+ for pkg in sub_stacks[:8]:
220
+ pkg_p = html.escape(pkg.get("path", ""))
221
+ pkg_l = html.escape(pkg.get("language", ""))
222
+ pkg_f = html.escape(pkg.get("framework", ""))
223
+ chips.append(f'<span class="chip" style="border-color: rgba(63, 185, 80, 0.4); color: var(--accent-green);">&#128230; {pkg_p} ({pkg_l} / {pkg_f})</span>')
224
+ stack_chips_html = "\n".join(chips)
225
+
226
+ return f"""<!DOCTYPE html>
227
+ <html lang="en">
228
+ <head>
229
+ <meta charset="UTF-8">
230
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
231
+ <title>TorusGuard Security Posture &middot; {html.escape(telemetry['project_name'])}</title>
232
+ <style>
233
+ :root {{
234
+ --bg-dark: #0d1117;
235
+ --bg-surface: #161b22;
236
+ --bg-card: #21262d;
237
+ --border-subtle: #30363d;
238
+ --text-main: #f0f6fc;
239
+ --text-muted: #8b949e;
240
+ --accent-blue: #58a6ff;
241
+ --accent-green: #3fb950;
242
+ --accent-yellow: #d29922;
243
+ --accent-red: #f85149;
244
+ --font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji";
245
+ }}
246
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
247
+ body {{
248
+ background: var(--bg-dark);
249
+ color: var(--text-main);
250
+ font-family: var(--font-stack);
251
+ line-height: 1.5;
252
+ padding: 24px;
253
+ }}
254
+ .container {{ max-width: 1200px; margin: 0 auto; }}
255
+ header {{
256
+ display: flex;
257
+ justify-content: space-between;
258
+ align-items: center;
259
+ padding-bottom: 20px;
260
+ border-bottom: 1px solid var(--border-subtle);
261
+ margin-bottom: 24px;
262
+ }}
263
+ .logo-group h1 {{
264
+ font-size: 24px;
265
+ font-weight: 700;
266
+ display: flex;
267
+ align-items: center;
268
+ gap: 8px;
269
+ }}
270
+ .shield-icon {{ font-size: 26px; }}
271
+ .project-meta {{ color: var(--text-muted); font-size: 14px; margin-top: 4px; }}
272
+ .status-badge {{
273
+ background: rgba(63, 185, 80, 0.15);
274
+ border: 1px solid var(--accent-green);
275
+ color: var(--accent-green);
276
+ padding: 6px 14px;
277
+ border-radius: 20px;
278
+ font-size: 13px;
279
+ font-weight: 600;
280
+ }}
281
+ .grid-top {{
282
+ display: grid;
283
+ grid-template-columns: 280px 1fr 1fr 1fr;
284
+ gap: 16px;
285
+ margin-bottom: 24px;
286
+ }}
287
+ @media (max-width: 900px) {{
288
+ .grid-top {{ grid-template-columns: 1fr 1fr; }}
289
+ }}
290
+ @media (max-width: 600px) {{
291
+ .grid-top {{ grid-template-columns: 1fr; }}
292
+ }}
293
+ .card {{
294
+ background: var(--bg-surface);
295
+ border: 1px solid var(--border-subtle);
296
+ border-radius: 8px;
297
+ padding: 20px;
298
+ }}
299
+ .gauge-card {{
300
+ display: flex;
301
+ flex-direction: column;
302
+ align-items: center;
303
+ justify-content: center;
304
+ text-align: center;
305
+ }}
306
+ .gauge-svg {{ width: 120px; height: 120px; transform: rotate(-90deg); }}
307
+ .gauge-bg {{ fill: none; stroke: var(--bg-card); stroke-width: 8; }}
308
+ .gauge-fill {{
309
+ fill: none;
310
+ stroke: {score_color};
311
+ stroke-width: 8;
312
+ stroke-linecap: round;
313
+ stroke-dasharray: 251.2;
314
+ stroke-dashoffset: {dash_offset};
315
+ transition: stroke-dashoffset 0.8s ease;
316
+ }}
317
+ .gauge-value {{
318
+ position: absolute;
319
+ font-size: 32px;
320
+ font-weight: 700;
321
+ color: var(--text-main);
322
+ }}
323
+ .stat-label {{ font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }}
324
+ .stat-value {{ font-size: 28px; font-weight: 700; color: var(--text-main); }}
325
+ .stat-sub {{ font-size: 12px; color: var(--text-muted); margin-top: 6px; }}
326
+ .pipeline-section {{ margin-bottom: 24px; }}
327
+ .pipeline-tracker {{
328
+ display: flex;
329
+ justify-content: space-between;
330
+ position: relative;
331
+ margin-top: 16px;
332
+ }}
333
+ .pipeline-step {{
334
+ display: flex;
335
+ flex-direction: column;
336
+ align-items: center;
337
+ flex: 1;
338
+ text-align: center;
339
+ position: relative;
340
+ z-index: 2;
341
+ }}
342
+ .step-circle {{
343
+ width: 32px;
344
+ height: 32px;
345
+ border-radius: 50%;
346
+ background: var(--bg-card);
347
+ border: 2px solid var(--accent-blue);
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ font-size: 13px;
352
+ font-weight: 700;
353
+ color: var(--accent-blue);
354
+ margin-bottom: 8px;
355
+ }}
356
+ .step-circle.active {{ background: var(--accent-blue); color: var(--bg-dark); }}
357
+ .step-name {{ font-size: 12px; font-weight: 600; color: var(--text-main); }}
358
+ .step-cmd {{ font-size: 11px; color: var(--text-muted); font-family: monospace; margin-top: 2px; }}
359
+ .pipeline-line {{
360
+ position: absolute;
361
+ top: 16px;
362
+ left: 6%;
363
+ right: 6%;
364
+ height: 2px;
365
+ background: var(--border-subtle);
366
+ z-index: 1;
367
+ }}
368
+ .polyglot-chips {{
369
+ display: flex;
370
+ flex-wrap: wrap;
371
+ gap: 8px;
372
+ margin-top: 10px;
373
+ }}
374
+ .chip {{
375
+ background: var(--bg-card);
376
+ border: 1px solid var(--border-subtle);
377
+ border-radius: 16px;
378
+ padding: 4px 12px;
379
+ font-size: 12px;
380
+ font-weight: 500;
381
+ color: var(--text-main);
382
+ display: inline-flex;
383
+ align-items: center;
384
+ gap: 6px;
385
+ }}
386
+ .chip-primary {{
387
+ border-color: var(--accent-blue);
388
+ color: var(--accent-blue);
389
+ background: rgba(88, 166, 255, 0.1);
390
+ }}
391
+ .grid-bottom {{
392
+ display: grid;
393
+ grid-template-columns: 1fr 1fr;
394
+ gap: 24px;
395
+ margin-top: 24px;
396
+ }}
397
+ @media (max-width: 800px) {{
398
+ .grid-bottom {{ grid-template-columns: 1fr; }}
399
+ }}
400
+ .section-title {{
401
+ font-size: 18px;
402
+ font-weight: 600;
403
+ margin-bottom: 16px;
404
+ display: flex;
405
+ align-items: center;
406
+ gap: 8px;
407
+ }}
408
+ .badge {{
409
+ display: inline-block;
410
+ padding: 3px 8px;
411
+ border-radius: 12px;
412
+ font-size: 11px;
413
+ font-weight: 600;
414
+ font-family: monospace;
415
+ }}
416
+ .badge-success {{ background: rgba(63, 185, 80, 0.15); color: var(--accent-green); border: 1px solid rgba(63, 185, 80, 0.3); }}
417
+ .badge-danger {{ background: rgba(248, 81, 73, 0.15); color: var(--accent-red); border: 1px solid rgba(248, 81, 73, 0.3); }}
418
+ .recipe-card {{
419
+ background: var(--bg-card);
420
+ border: 1px solid var(--border-subtle);
421
+ border-radius: 6px;
422
+ padding: 12px;
423
+ margin-bottom: 12px;
424
+ }}
425
+ .card-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }}
426
+ .meta-label {{ font-size: 11px; color: var(--text-muted); }}
427
+ .recipe-desc {{ font-size: 13px; color: var(--text-main); margin-bottom: 8px; }}
428
+ .diff-block {{
429
+ background: var(--bg-dark);
430
+ border: 1px solid var(--border-subtle);
431
+ border-radius: 4px;
432
+ padding: 8px 12px;
433
+ font-family: monospace;
434
+ font-size: 12px;
435
+ overflow-x: auto;
436
+ color: #e6edf3;
437
+ }}
438
+ table {{ width: 100%; border-collapse: collapse; }}
439
+ th, td {{ padding: 10px 12px; text-align: left; font-size: 13px; }}
440
+ th {{ color: var(--text-muted); border-bottom: 1px solid var(--border-subtle); font-weight: 600; font-size: 12px; text-transform: uppercase; }}
441
+ td {{ border-bottom: 1px solid rgba(48, 54, 61, 0.5); }}
442
+ .empty-state {{ text-align: center; padding: 24px; color: var(--text-muted); font-size: 13px; }}
443
+ .empty-cell {{ text-align: center; color: var(--text-muted); padding: 16px; font-style: italic; }}
444
+ footer {{
445
+ margin-top: 40px;
446
+ text-align: center;
447
+ color: var(--text-muted);
448
+ font-size: 12px;
449
+ border-top: 1px solid var(--border-subtle);
450
+ padding-top: 20px;
451
+ }}
452
+ </style>
453
+ </head>
454
+ <body>
455
+ <div class="container">
456
+ <header>
457
+ <div class="logo-group">
458
+ <h1><span class="shield-icon">&#128737;</span> TorusGuard Security Posture</h1>
459
+ <div class="project-meta">Workspace: <strong>{html.escape(telemetry['project_name'])}</strong> &middot; Stack: <strong>{html.escape(stack_label)}</strong> &middot; {telemetry['generated_at']}</div>
460
+ </div>
461
+ <div class="status-badge">&check; Local Governance Active</div>
462
+ </header>
463
+
464
+ <!-- Top KPI Grid -->
465
+ <div class="grid-top">
466
+ <div class="card gauge-card">
467
+ <div style="position: relative; display: flex; align-items: center; justify-content: center;">
468
+ <svg class="gauge-svg" viewBox="0 0 100 100">
469
+ <circle class="gauge-bg" cx="50" cy="50" r="40" />
470
+ <circle class="gauge-fill" cx="50" cy="50" r="40" />
471
+ </svg>
472
+ <div class="gauge-value">{score}</div>
473
+ </div>
474
+ <div class="stat-label" style="margin-top: 12px;">Security Posture Score</div>
475
+ </div>
476
+
477
+ <div class="card">
478
+ <div class="stat-label">Verified Golden Recipes</div>
479
+ <div class="stat-value" style="color: var(--accent-green);">{len(recipes)}</div>
480
+ <div class="stat-sub">Distilled Ponytail fixes (<=35 add, <=25 del)</div>
481
+ </div>
482
+
483
+ <div class="card">
484
+ <div class="stat-label">Active Memory Patterns</div>
485
+ <div class="stat-value">{len(patterns)}</div>
486
+ <div class="stat-sub">{len(false_positives)} suppressed &middot; {len(recurring)} recurring</div>
487
+ </div>
488
+
489
+ <div class="card">
490
+ <div class="stat-label">Security Rules Catalog</div>
491
+ <div class="stat-value" style="color: var(--accent-blue);">71</div>
492
+ <div class="stat-sub">11 rule families &middot; Multi-agent verified</div>
493
+ </div>
494
+ </div>
495
+
496
+ <!-- Polyglot Architecture & Workspace Intelligence -->
497
+ <div class="card" style="margin-bottom: 24px;">
498
+ <div class="section-title">&#9670; Polyglot Architecture &amp; Workspace Intelligence</div>
499
+ <div style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px;">
500
+ Primary Language: <strong style="color: var(--text-main);">{html.escape(primary_lang)}</strong>
501
+ &middot; Framework: <strong style="color: var(--text-main);">{html.escape(primary_fw)}</strong>
502
+ &middot; Data Layer: <strong style="color: var(--text-main);">{html.escape(primary_orm)}</strong>
503
+ </div>
504
+ <div class="polyglot-chips">
505
+ {stack_chips_html}
506
+ </div>
507
+ </div>
508
+
509
+ <!-- 7-Stage Canonical Pipeline Tracker -->
510
+ <div class="card pipeline-section">
511
+ <div class="section-title">&#9658; Canonical 7-Stage Closed-Loop Governance Flow</div>
512
+ <div class="pipeline-tracker">
513
+ <div class="pipeline-line"></div>
514
+ <div class="pipeline-step">
515
+ <div class="step-circle active">1</div>
516
+ <div class="step-name">Scan</div>
517
+ <div class="step-cmd">/audit</div>
518
+ </div>
519
+ <div class="pipeline-step">
520
+ <div class="step-circle active">2</div>
521
+ <div class="step-name">Score</div>
522
+ <div class="step-cmd">0-100</div>
523
+ </div>
524
+ <div class="pipeline-step">
525
+ <div class="step-circle active">3</div>
526
+ <div class="step-name">Harden</div>
527
+ <div class="step-cmd">/harden</div>
528
+ </div>
529
+ <div class="pipeline-step">
530
+ <div class="step-circle active">4</div>
531
+ <div class="step-name">Authorize</div>
532
+ <div class="step-cmd">Human Gate</div>
533
+ </div>
534
+ <div class="pipeline-step">
535
+ <div class="step-circle active">5</div>
536
+ <div class="step-name">Apply</div>
537
+ <div class="step-cmd">.bak Snapshot</div>
538
+ </div>
539
+ <div class="pipeline-step">
540
+ <div class="step-circle active">6</div>
541
+ <div class="step-name">Recheck</div>
542
+ <div class="step-cmd">Differential</div>
543
+ </div>
544
+ <div class="pipeline-step">
545
+ <div class="step-circle active">7</div>
546
+ <div class="step-name">Report</div>
547
+ <div class="step-cmd">SARIF v2.1.0</div>
548
+ </div>
549
+ </div>
550
+ </div>
551
+
552
+ <!-- Bottom Details Grid -->
553
+ <div class="grid-bottom">
554
+ <!-- Golden Recipes Catalog -->
555
+ <div class="card">
556
+ <div class="section-title">&#10024; Active Golden Fix Recipes ({len(recipes)})</div>
557
+ <div class="recipes-list">
558
+ {recipe_cards_html}
559
+ </div>
560
+ </div>
561
+
562
+ <!-- Regression Watches & False Positives -->
563
+ <div class="card">
564
+ <div class="section-title">&#128065; Regression Watches & Suppressions</div>
565
+ <table>
566
+ <thead>
567
+ <tr>
568
+ <th>Rule ID</th>
569
+ <th>Description / Reason</th>
570
+ <th>Target Path</th>
571
+ </tr>
572
+ </thead>
573
+ <tbody>
574
+ {regression_rows_html}
575
+ </tbody>
576
+ </table>
577
+ </div>
578
+ </div>
579
+
580
+ <footer>
581
+ TorusGuard v1.2.0 &middot; Autonomous Security Engine for AI-Built Applications &middot; 100% Local-First &middot; Zero Cloud Telemetry Leakage
582
+ </footer>
583
+ </div>
584
+ </body>
585
+ </html>
586
+ """
587
+
588
+
589
+ def emit_html_report(
590
+ target_path: str | Path | None = None,
591
+ root_dir: str | Path | None = None
592
+ ) -> dict[str, Any]:
593
+ """Emit the standalone HTML report to disk."""
594
+ root = Path(root_dir or find_project_root()).resolve()
595
+ telemetry = load_report_telemetry(root)
596
+ html_content = generate_html_dashboard(telemetry)
597
+
598
+ out_file = Path(target_path) if target_path else (root / ".torusguard" / "runs" / "report-latest.html")
599
+ out_file.parent.mkdir(parents=True, exist_ok=True)
600
+ out_file.write_text(html_content, encoding="utf-8")
601
+
602
+ return {
603
+ "status": "success",
604
+ "report_path": str(out_file),
605
+ "file_size_bytes": len(html_content.encode("utf-8")),
606
+ "posture_score": compute_posture_score(telemetry)
607
+ }
608
+
609
+
610
+ def main():
611
+ parser = argparse.ArgumentParser(description="TorusGuard Visual HTML Report Emitter")
612
+ parser.add_argument("--out", help="Output HTML file path (default: .torusguard/runs/report-latest.html)")
613
+ parser.add_argument("--root", help="Project root override")
614
+ parser.add_argument("--json", action="store_true", help="Output raw JSON summary")
615
+
616
+ args = parser.parse_args()
617
+ root = Path(args.root).resolve() if args.root else None
618
+
619
+ res = emit_html_report(target_path=args.out, root_dir=root)
620
+ if args.json:
621
+ print(json.dumps(res, indent=2))
622
+ else:
623
+ print(f"[SUCCESS] Emitted TorusGuard visual dashboard: {res['report_path']}")
624
+ print(f"Posture Score: {res['posture_score']}/100 &middot; Size: {res['file_size_bytes']} bytes")
625
+
626
+
627
+ if __name__ == "__main__":
628
+ main()
@@ -13,12 +13,12 @@ from pathlib import Path
13
13
 
14
14
 
15
15
  def compute_file_sha256(filepath):
16
- """Compute SHA-256 digest of a file in binary mode."""
17
- hasher = hashlib.sha256()
16
+ """Compute SHA-256 digest of a file, normalizing line endings for cross-platform determinism."""
18
17
  with open(filepath, "rb") as fh:
19
- while chunk := fh.read(65536):
20
- hasher.update(chunk)
21
- return hasher.hexdigest()
18
+ content = fh.read()
19
+ # Normalize CRLF to LF so manifests match identically on Windows, Linux, and macOS
20
+ content = content.replace(b"\r\n", b"\n")
21
+ return hashlib.sha256(content).hexdigest()
22
22
 
23
23
 
24
24
  def scan_workspace_files(base_dir):
@@ -29,6 +29,8 @@ def scan_workspace_files(base_dir):
29
29
  for root, dirs, files in os.walk(base_dir):
30
30
  if "runs" in dirs:
31
31
  dirs.remove("runs")
32
+ if "memory" in dirs:
33
+ dirs.remove("memory")
32
34
  if "__pycache__" in dirs:
33
35
  dirs.remove("__pycache__")
34
36