torusguard 1.0.0 → 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.
- package/.torusguard/.manifest.json +89 -75
- package/.torusguard/references/csharp-security.md +41 -0
- package/.torusguard/references/go-security.md +41 -0
- package/.torusguard/references/java-security.md +40 -0
- package/.torusguard/references/polyglot-security-matrix.md +25 -0
- package/.torusguard/references/rust-security.md +40 -0
- package/.torusguard/rules/custom/.gitkeep +1 -0
- package/.torusguard/rules/custom/README.md +30 -0
- package/.torusguard/runs/report-latest.html +328 -0
- package/.torusguard/schemas/golden-recipe.schema.json +75 -0
- package/.torusguard/scripts/diff_guard.py +131 -4
- package/.torusguard/scripts/finding_scorer.py +33 -5
- package/.torusguard/scripts/html_reporter.py +628 -0
- package/.torusguard/scripts/manifest_builder.py +5 -5
- package/.torusguard/scripts/memory_engine.py +521 -99
- package/.torusguard/scripts/monorepo_detector.py +123 -10
- package/.torusguard/scripts/rules_sync.py +321 -0
- package/.torusguard/scripts/stack_detect.py +458 -26
- package/.torusguard/skills/torusguard/references/csharp-security.md +41 -0
- package/.torusguard/skills/torusguard/references/go-security.md +41 -0
- package/.torusguard/skills/torusguard/references/java-security.md +40 -0
- package/.torusguard/skills/torusguard/references/polyglot-security-matrix.md +25 -0
- package/.torusguard/skills/torusguard/references/rust-security.md +40 -0
- package/README.md +307 -410
- package/bin/torusguard.js +108 -4
- package/package.json +1 -1
- package/skills/torusguard/bootstrap.py +2 -2
- package/skills/torusguard/payload/.manifest.json +89 -75
- package/skills/torusguard/payload/references/csharp-security.md +41 -0
- package/skills/torusguard/payload/references/go-security.md +41 -0
- package/skills/torusguard/payload/references/java-security.md +40 -0
- package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -0
- package/skills/torusguard/payload/references/rust-security.md +40 -0
- package/skills/torusguard/payload/rules/custom/.gitkeep +1 -0
- package/skills/torusguard/payload/rules/custom/README.md +30 -0
- package/skills/torusguard/payload/schemas/golden-recipe.schema.json +75 -0
- package/skills/torusguard/payload/scripts/diff_guard.py +131 -4
- package/skills/torusguard/payload/scripts/finding_scorer.py +33 -5
- package/skills/torusguard/payload/scripts/html_reporter.py +628 -0
- package/skills/torusguard/payload/scripts/manifest_builder.py +5 -5
- package/skills/torusguard/payload/scripts/memory_engine.py +521 -99
- package/skills/torusguard/payload/scripts/monorepo_detector.py +123 -10
- package/skills/torusguard/payload/scripts/rules_sync.py +321 -0
- package/skills/torusguard/payload/scripts/stack_detect.py +458 -26
- package/skills/torusguard/payload/skills/torusguard/bootstrap.py +2 -2
- package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -0
- package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -0
- package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -0
- package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -0
- package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -0
- package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
- package/skills/torusguard/__pycache__/bootstrap.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 · +{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">● {html.escape(primary_lang)}</span>']
|
|
215
|
+
if primary_fw and primary_fw != "None":
|
|
216
|
+
chips.append(f'<span class="chip">⚙ {html.escape(primary_fw)}</span>')
|
|
217
|
+
if primary_orm and primary_orm != "None":
|
|
218
|
+
chips.append(f'<span class="chip">🗃 {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);">📦 {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 · {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">🛡</span> TorusGuard Security Posture</h1>
|
|
459
|
+
<div class="project-meta">Workspace: <strong>{html.escape(telemetry['project_name'])}</strong> · Stack: <strong>{html.escape(stack_label)}</strong> · {telemetry['generated_at']}</div>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="status-badge">✓ 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 · {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 · 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">◆ Polyglot Architecture & 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
|
+
· Framework: <strong style="color: var(--text-main);">{html.escape(primary_fw)}</strong>
|
|
502
|
+
· 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">► 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">✨ 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">👁 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 · Autonomous Security Engine for AI-Built Applications · 100% Local-First · 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 · 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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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):
|