torusguard 1.3.5 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.torusguard/.manifest.json +37 -36
- package/.torusguard/runs/report-latest.html +5862 -315
- package/.torusguard/runs/results-latest.sarif +20 -0
- package/.torusguard/runs/run-20260918-104221-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260918-104221-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260918-104221-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-104221-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-104236-audit/findings.json +2314 -0
- package/.torusguard/runs/run-20260918-104236-audit/findings.md +960 -0
- package/.torusguard/runs/run-20260918-104236-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-104236-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-105723-audit/findings.json +2382 -0
- package/.torusguard/runs/run-20260918-105723-audit/findings.md +960 -0
- package/.torusguard/runs/run-20260918-105723-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-105723-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-105731-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260918-105731-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260918-105731-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-105731-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-105940-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260918-105940-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260918-105940-audit/manifest.json +13 -0
- package/.torusguard/runs/run-20260918-105940-audit/remediation.md +7 -0
- package/.torusguard/runs/run-20260918-105940-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-145701-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-145701-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-145701-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-145701-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-151729-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-151729-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-151729-audit/manifest.json +13 -0
- package/.torusguard/runs/run-20260919-151729-audit/remediation.md +7 -0
- package/.torusguard/runs/run-20260919-151729-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-152340-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-152340-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-152340-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-152340-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-152345-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-152345-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-152345-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-152345-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-153500-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-153500-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-153500-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-153500-audit/summary.md +5 -0
- package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/recipes_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
- package/.torusguard/scripts/apply_runner.py +108 -9
- package/.torusguard/scripts/audit_runner.py +336 -24
- package/.torusguard/scripts/harden_runner.py +124 -70
- package/.torusguard/scripts/html_reporter.py +3585 -648
- package/.torusguard/scripts/manifest_builder.py +2 -0
- package/.torusguard/scripts/recheck_runner.py +5 -4
- package/.torusguard/scripts/recipes_runner.py +91 -8
- package/.torusguard/scripts/report_sync.py +496 -73
- package/.torusguard/scripts/sarif_exporter.py +48 -8
- package/.torusguard/scripts/stack_detect.py +44 -2
- package/.torusguard/scripts/status_runner.py +267 -0
- package/.torusguard/scripts/term_ui.py +1 -1
- package/.torusguard/skills/torusguard/SKILL.md +75 -72
- package/.torusguard/skills/torusguard-apply/SKILL.md +84 -80
- package/.torusguard/skills/torusguard-audit/SKILL.md +87 -83
- package/.torusguard/skills/torusguard-authorize/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-exploit-check/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-full/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-harden/SKILL.md +98 -94
- package/.torusguard/skills/torusguard-init/SKILL.md +68 -64
- package/.torusguard/skills/torusguard-recheck/SKILL.md +81 -77
- package/.torusguard/skills/torusguard-report/SKILL.md +93 -72
- package/.torusguard/skills/torusguard-status/SKILL.md +62 -58
- package/.torusguard/skills/torusguard-verify/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-web-validate/SKILL.md +7 -2
- package/.torusguard/workflows/apply.md +1 -1
- package/.torusguard/workflows/audit.md +1 -1
- package/.torusguard/workflows/authorize.md +1 -1
- package/.torusguard/workflows/exploit-check.md +1 -1
- package/.torusguard/workflows/harden.md +1 -1
- package/.torusguard/workflows/init.md +1 -1
- package/.torusguard/workflows/memory.md +1 -1
- package/.torusguard/workflows/recheck.md +1 -1
- package/.torusguard/workflows/report.md +1 -1
- package/.torusguard/workflows/status.md +1 -1
- package/.torusguard/workflows/verify.md +1 -1
- package/.torusguard/workflows/web-validate.md +1 -1
- package/README.md +115 -81
- package/bin/torusguard.js +244 -38
- package/package.json +6 -2
- package/skills/torusguard/SKILL.md +1 -1
- package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/skills/torusguard/bootstrap.py +52 -8
- package/skills/torusguard/payload/.manifest.json +51 -36
- package/skills/torusguard/payload/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/apply_runner.py +108 -9
- package/skills/torusguard/payload/scripts/audit_runner.py +336 -24
- package/skills/torusguard/payload/scripts/harden_runner.py +124 -70
- package/skills/torusguard/payload/scripts/html_reporter.py +3585 -648
- package/skills/torusguard/payload/scripts/manifest_builder.py +2 -0
- package/skills/torusguard/payload/scripts/recheck_runner.py +5 -4
- package/skills/torusguard/payload/scripts/recipes_runner.py +91 -8
- package/skills/torusguard/payload/scripts/report_sync.py +496 -73
- package/skills/torusguard/payload/scripts/sarif_exporter.py +48 -8
- package/skills/torusguard/payload/scripts/stack_detect.py +44 -2
- package/skills/torusguard/payload/scripts/status_runner.py +267 -0
- package/skills/torusguard/payload/scripts/term_ui.py +1 -1
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +40 -23
- package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +1 -1
- package/skills/torusguard/payload/workflows/apply.md +9 -1
- package/skills/torusguard/payload/workflows/audit.md +8 -2
- package/skills/torusguard/payload/workflows/authorize.md +1 -1
- package/skills/torusguard/payload/workflows/exploit-check.md +1 -1
- package/skills/torusguard/payload/workflows/harden.md +6 -1
- package/skills/torusguard/payload/workflows/init.md +7 -1
- package/skills/torusguard/payload/workflows/memory.md +1 -1
- package/skills/torusguard/payload/workflows/recheck.md +1 -1
- package/skills/torusguard/payload/workflows/recipes.md +55 -0
- package/skills/torusguard/payload/workflows/report.md +59 -55
- package/skills/torusguard/payload/workflows/status.md +4 -1
- package/skills/torusguard/payload/workflows/torusguard-apply.md +110 -0
- package/skills/torusguard/payload/workflows/torusguard-audit.md +105 -0
- package/skills/torusguard/payload/workflows/torusguard-authorize.md +96 -0
- package/skills/torusguard/payload/workflows/torusguard-exploit-check.md +98 -0
- package/skills/torusguard/payload/workflows/torusguard-harden.md +104 -0
- package/skills/torusguard/payload/workflows/torusguard-init.md +106 -0
- package/skills/torusguard/payload/workflows/torusguard-recheck.md +101 -0
- package/skills/torusguard/payload/workflows/torusguard-recipes.md +55 -0
- package/skills/torusguard/payload/workflows/torusguard-report.md +105 -0
- package/skills/torusguard/payload/workflows/torusguard-status.md +103 -0
- package/skills/torusguard/payload/workflows/torusguard-verify.md +100 -0
- package/skills/torusguard/payload/workflows/torusguard-web-validate.md +96 -0
- package/skills/torusguard/payload/workflows/torusguard.md +19 -0
- package/skills/torusguard/payload/workflows/verify.md +1 -1
- package/skills/torusguard/payload/workflows/web-validate.md +1 -1
- package/skills/torusguard-apply/SKILL.md +1 -1
- package/skills/torusguard-audit/SKILL.md +1 -1
- package/skills/torusguard-authorize/SKILL.md +1 -1
- package/skills/torusguard-exploit-check/SKILL.md +1 -1
- package/skills/torusguard-full/SKILL.md +1 -1
- package/skills/torusguard-harden/SKILL.md +1 -1
- package/skills/torusguard-init/SKILL.md +1 -1
- package/skills/torusguard-recheck/SKILL.md +1 -1
- package/skills/torusguard-report/SKILL.md +40 -23
- package/skills/torusguard-status/SKILL.md +1 -1
- package/skills/torusguard-verify/SKILL.md +1 -1
- package/skills/torusguard-web-validate/SKILL.md +1 -1
|
@@ -1,648 +1,3585 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
TorusGuard Visual HTML Report Emitter (
|
|
4
|
-
Compiles a self-contained, zero-external-CDN, interactive
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
""
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"""
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
</
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
""
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TorusGuard Visual HTML Report Emitter (v2.3.0)
|
|
4
|
+
Compiles a self-contained, zero-external-CDN, interactive dashboard
|
|
5
|
+
focused on ACTUAL SECURITY POSTURE & GOVERNED REMEDIATION:
|
|
6
|
+
1. Safe Defenses vs. Harmed Exposure (5-Layer Architectural Invariant Matrix)
|
|
7
|
+
2. Severity Breakdown (Dynamic Circular SVG Gauge with Glow & Count-Up, KPI Cards)
|
|
8
|
+
3. Interactive Findings Drawers (Vulnerable Snippets, Invariant Rules, Fix Commands)
|
|
9
|
+
4. Interactive "What-If" Posture Score Simulator (Live Dynamic Gauge Recalculation)
|
|
10
|
+
5. Interactive Directory Attack Surface Heatmap (Click-to-Filter)
|
|
11
|
+
6. OWASP Top 10 (2021/2026) Compliance Radar
|
|
12
|
+
7. Enterprise Compliance Framework Mapping (SOC 2 Type II, ISO/IEC 27001:2022, HIPAA)
|
|
13
|
+
8. Root-Cause Architectural Clusters & Blast Radius
|
|
14
|
+
9. Golden Recipe Explorer (Distilled Verified Fixes, Diffs & Before/After Views)
|
|
15
|
+
10. Prescriptive "Next Best Defenses" Advisory (Stack-Aware Gaps)
|
|
16
|
+
11. In-Browser Artifact Exporters (Zero-Network SARIF v2.1.0 & CSV Downloaders)
|
|
17
|
+
12. Zero-CDN Executive Dark / Light Mode Switcher with LocalStorage Persistence
|
|
18
|
+
13. High-Contrast Print & PDF Export Styling (@media print)
|
|
19
|
+
|
|
20
|
+
Pure Python 3.10+ standard library (100% zero external dependencies, zero CDNs).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
import json
|
|
25
|
+
import html
|
|
26
|
+
import datetime
|
|
27
|
+
import argparse
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, TypedDict
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ComplianceControl(TypedDict):
|
|
33
|
+
code: str
|
|
34
|
+
families: str
|
|
35
|
+
rules: list[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ComplianceFramework(TypedDict):
|
|
39
|
+
name: str
|
|
40
|
+
subtitle: str
|
|
41
|
+
controls: list[ComplianceControl]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Windows console UTF-8 support
|
|
45
|
+
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
46
|
+
try:
|
|
47
|
+
getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
|
|
48
|
+
except (AttributeError, OSError):
|
|
49
|
+
pass
|
|
50
|
+
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
|
|
51
|
+
try:
|
|
52
|
+
getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
|
|
53
|
+
except (AttributeError, OSError):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
FAMILY_METADATA: dict[str, dict[str, str]] = {
|
|
58
|
+
"TG-AGENT": {
|
|
59
|
+
"layer_id": "layer-app",
|
|
60
|
+
"layer_name": "Application & LLM Defense",
|
|
61
|
+
"domain": "AI Agent & LLM Injection Defense",
|
|
62
|
+
"guarantee": "Structural user prompt isolation, inert delimiters, and tool call schema validation. Prevents direct and indirect prompt injection attacks.",
|
|
63
|
+
"owasp": "OWASP LLM01: Prompt Injection",
|
|
64
|
+
"compliance": "ISO 27001 A.8.28 (Secure Coding)"
|
|
65
|
+
},
|
|
66
|
+
"TG-INPUT": {
|
|
67
|
+
"layer_id": "layer-app",
|
|
68
|
+
"layer_name": "Application & LLM Defense",
|
|
69
|
+
"domain": "Input Sanitization & Traversal",
|
|
70
|
+
"guarantee": "Strict path normalization, command argument escaping, and safe DOM assignment. Prevents path traversal, OS command injection, and Reflected/DOM XSS.",
|
|
71
|
+
"owasp": "OWASP A03:2021 - Injection",
|
|
72
|
+
"compliance": "SOC 2 CC6.6 / ISO 27001 A.8.28"
|
|
73
|
+
},
|
|
74
|
+
"TG-AUTH": {
|
|
75
|
+
"layer_id": "layer-auth",
|
|
76
|
+
"layer_name": "Identity, Auth & Real-Time",
|
|
77
|
+
"domain": "Authentication & Session Integrity",
|
|
78
|
+
"guarantee": "Timing-safe secret compares, strong argon2id/bcrypt password hashing, explicit JWT algorithm pinning, and secure cookie session attributes.",
|
|
79
|
+
"owasp": "OWASP A07:2021 - Identification & Authentication Failures",
|
|
80
|
+
"compliance": "SOC 2 CC6.1 / ISO 27001 A.8.24 / HIPAA § 164.312(a)(1)"
|
|
81
|
+
},
|
|
82
|
+
"TG-CSRF": {
|
|
83
|
+
"layer_id": "layer-auth",
|
|
84
|
+
"layer_name": "Identity, Auth & Real-Time",
|
|
85
|
+
"domain": "Cross-Site Request Forgery",
|
|
86
|
+
"guarantee": "Cryptographic anti-CSRF token verification on state-changing requests and SameSite=Lax/Strict cookie isolation.",
|
|
87
|
+
"owasp": "OWASP A01:2021 - Broken Access Control",
|
|
88
|
+
"compliance": "SOC 2 CC6.1 / ISO 27001 A.8.28"
|
|
89
|
+
},
|
|
90
|
+
"TG-WS": {
|
|
91
|
+
"layer_id": "layer-auth",
|
|
92
|
+
"layer_name": "Identity, Auth & Real-Time",
|
|
93
|
+
"domain": "WebSocket & Real-Time Security",
|
|
94
|
+
"guarantee": "Handshake origin validation, connection ticket verification, and inbound frame size bounds to eliminate CSWSH and socket exhaustion.",
|
|
95
|
+
"owasp": "OWASP A01:2021 - Broken Access Control / A05: Misconfiguration",
|
|
96
|
+
"compliance": "SOC 2 CC6.6 / ISO 27001 A.8.20"
|
|
97
|
+
},
|
|
98
|
+
"TG-BIZ": {
|
|
99
|
+
"layer_id": "layer-auth",
|
|
100
|
+
"layer_name": "Identity, Auth & Real-Time",
|
|
101
|
+
"domain": "Business Logic & Workflow Limits",
|
|
102
|
+
"guarantee": "Strict positive financial amount constraints, atomic database transaction locks, and coupon stacking bounds to prevent race conditions.",
|
|
103
|
+
"owasp": "OWASP A04:2021 - Insecure Design",
|
|
104
|
+
"compliance": "HIPAA § 164.312(c)(1) Data Integrity / SOC 2 CC7.1"
|
|
105
|
+
},
|
|
106
|
+
"TG-DB": {
|
|
107
|
+
"layer_id": "layer-db",
|
|
108
|
+
"layer_name": "Database, GraphQL & Cache",
|
|
109
|
+
"domain": "Database Isolation & Injection",
|
|
110
|
+
"guarantee": "Zero unparameterized SQL concatenation. Enforces prepared statements with bound parameters and mandatory tenant partition scoping.",
|
|
111
|
+
"owasp": "OWASP A03:2021 - Injection / A01: Broken Access Control",
|
|
112
|
+
"compliance": "SOC 2 CC6.1 / ISO 27001 A.8.28 / HIPAA § 164.312(a)(1)"
|
|
113
|
+
},
|
|
114
|
+
"TG-GQL": {
|
|
115
|
+
"layer_id": "layer-db",
|
|
116
|
+
"layer_name": "Database, GraphQL & Cache",
|
|
117
|
+
"domain": "GraphQL Safety & Introspection",
|
|
118
|
+
"guarantee": "Query depth limiting (max depth <= 7), circular reference blocking, field complexity cost analysis, and production schema introspection suppression.",
|
|
119
|
+
"owasp": "OWASP A05:2021 - Security Misconfiguration",
|
|
120
|
+
"compliance": "SOC 2 CC6.6 / ISO 27001 A.8.28"
|
|
121
|
+
},
|
|
122
|
+
"TG-CACHE": {
|
|
123
|
+
"layer_id": "layer-db",
|
|
124
|
+
"layer_name": "Database, GraphQL & Cache",
|
|
125
|
+
"domain": "Cache Poisoning & Cache Timing",
|
|
126
|
+
"guarantee": "Mandatory Cache-Control: no-store on authenticated sensitive responses and unkeyed HTTP header sanitization to prevent web cache poisoning.",
|
|
127
|
+
"owasp": "OWASP A05:2021 - Security Misconfiguration",
|
|
128
|
+
"compliance": "SOC 2 CC6.7 / ISO 27001 A.8.20"
|
|
129
|
+
},
|
|
130
|
+
"TG-SSRF": {
|
|
131
|
+
"layer_id": "layer-net",
|
|
132
|
+
"layer_name": "Network, SSRF & Webhooks",
|
|
133
|
+
"domain": "Server-Side Request Forgery",
|
|
134
|
+
"guarantee": "Outbound URL hostname whitelisting, RFC1918 private IP range blocking (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.1) and cloud metadata blocking (169.254.169.254).",
|
|
135
|
+
"owasp": "OWASP A10:2021 - Server-Side Request Forgery (SSRF)",
|
|
136
|
+
"compliance": "SOC 2 CC6.6 / ISO 27001 A.8.20"
|
|
137
|
+
},
|
|
138
|
+
"TG-WEBHOOK": {
|
|
139
|
+
"layer_id": "layer-net",
|
|
140
|
+
"layer_name": "Network, SSRF & Webhooks",
|
|
141
|
+
"domain": "Inbound Webhook Verification",
|
|
142
|
+
"guarantee": "Mandatory cryptographic HMAC-SHA256 signature verification over raw request buffers and replay attack protection with <= 300s timestamp limits.",
|
|
143
|
+
"owasp": "OWASP A08:2021 - Software and Data Integrity Failures",
|
|
144
|
+
"compliance": "SOC 2 CC6.6 / ISO 27001 A.8.20 / HIPAA § 164.312(c)(1)"
|
|
145
|
+
},
|
|
146
|
+
"TG-RATE": {
|
|
147
|
+
"layer_id": "layer-net",
|
|
148
|
+
"layer_name": "Network, SSRF & Webhooks",
|
|
149
|
+
"domain": "Rate Limiting & Resource Protection",
|
|
150
|
+
"guarantee": "Sliding-window IP rate limiting on auth and write endpoints, bounded JSON payload parsers (<= 1MB), and slowloris connection timeouts.",
|
|
151
|
+
"owasp": "OWASP A04:2021 - Insecure Design / A07: Auth Failures",
|
|
152
|
+
"compliance": "SOC 2 CC6.6 / ISO 27001 A.8.20"
|
|
153
|
+
},
|
|
154
|
+
"TG-PLATFORM": {
|
|
155
|
+
"layer_id": "layer-platform",
|
|
156
|
+
"layer_name": "Platform, Supply Chain & Secrets",
|
|
157
|
+
"domain": "Server Hardening & Security Headers",
|
|
158
|
+
"guarantee": "Mandatory production security headers (CSP, HSTS max-age>=31536000, X-Content-Type-Options: nosniff, X-Frame-Options: DENY) and debug mode suppression.",
|
|
159
|
+
"owasp": "OWASP A05:2021 - Security Misconfiguration",
|
|
160
|
+
"compliance": "SOC 2 CC6.7 / ISO 27001 A.8.20 / HIPAA § 164.312(e)(1)"
|
|
161
|
+
},
|
|
162
|
+
"TG-SUPPLY": {
|
|
163
|
+
"layer_id": "layer-platform",
|
|
164
|
+
"layer_name": "Platform, Supply Chain & Secrets",
|
|
165
|
+
"domain": "Supply Chain & Dependency Health",
|
|
166
|
+
"guarantee": "Lockfile cryptographic integrity checks, known high/critical CVE build gates, verified registry origins, and build script execution bounds.",
|
|
167
|
+
"owasp": "OWASP A06:2021 - Vulnerable and Outdated Components",
|
|
168
|
+
"compliance": "SOC 2 CC7.1 / ISO 27001 A.8.28"
|
|
169
|
+
},
|
|
170
|
+
"TG-CLIENT": {
|
|
171
|
+
"layer_id": "layer-platform",
|
|
172
|
+
"layer_name": "Platform, Supply Chain & Secrets",
|
|
173
|
+
"domain": "Client Bundle & Frontend Secrets",
|
|
174
|
+
"guarantee": "Zero server role keys, database master credentials, or private API secrets leaked into frontend bundles or public environment variables.",
|
|
175
|
+
"owasp": "OWASP A01:2021 - Broken Access Control / A05: Misconfiguration",
|
|
176
|
+
"compliance": "SOC 2 CC6.1 / ISO 27001 A.8.24 / HIPAA § 164.312(e)(1)"
|
|
177
|
+
},
|
|
178
|
+
"TG-EDGE": {
|
|
179
|
+
"layer_id": "layer-platform",
|
|
180
|
+
"layer_name": "Platform, Supply Chain & Secrets",
|
|
181
|
+
"domain": "Edge Computing & Serverless Limits",
|
|
182
|
+
"guarantee": "Subrequest fan-out limits (<= 50 per invocation), serverless wall-clock execution timeouts (<= 30s), and memory bounds for edge functions.",
|
|
183
|
+
"owasp": "OWASP A04:2021 - Insecure Design",
|
|
184
|
+
"compliance": "SOC 2 CC6.6 / ISO 27001 A.8.20"
|
|
185
|
+
},
|
|
186
|
+
"TG-DIFF": {
|
|
187
|
+
"layer_id": "layer-platform",
|
|
188
|
+
"layer_name": "Platform, Supply Chain & Secrets",
|
|
189
|
+
"domain": "Polyglot Bypass & Churn Bounds",
|
|
190
|
+
"guarantee": "Strict Ponytail Protocol enforcement (<= 35 additions, <= 25 deletions per patch) and zero security suppression pragmas (nosec pragmas, InsecureSkipVerify).",
|
|
191
|
+
"owasp": "OWASP A08:2021 - Software Integrity",
|
|
192
|
+
"compliance": "SOC 2 CC7.1 / ISO 27001 A.8.28 / HIPAA § 164.312(c)(1)"
|
|
193
|
+
},
|
|
194
|
+
"TG-SEC": {
|
|
195
|
+
"layer_id": "layer-platform",
|
|
196
|
+
"layer_name": "Platform, Supply Chain & Secrets",
|
|
197
|
+
"domain": "Core Secrets & API Tokens",
|
|
198
|
+
"guarantee": "Zero hardcoded API keys, private certificates, AWS/GCP/Stripe tokens, or JWT secrets in tracked source code. Mandatory externalized environment configuration.",
|
|
199
|
+
"owasp": "OWASP A02:2021 - Cryptographic Failures",
|
|
200
|
+
"compliance": "SOC 2 CC6.7 / ISO 27001 A.8.24 / HIPAA § 164.312(e)(1)"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
ARCHITECTURAL_LAYERS: list[dict[str, Any]] = [
|
|
205
|
+
{
|
|
206
|
+
"id": "layer-app",
|
|
207
|
+
"name": "Application & LLM Defense",
|
|
208
|
+
"icon": "🛡️",
|
|
209
|
+
"families": ["TG-AGENT", "TG-INPUT"],
|
|
210
|
+
"description": "User prompt isolation, inert delimiters, traversal defense, and input escaping."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"id": "layer-auth",
|
|
214
|
+
"name": "Identity, Auth & Real-Time",
|
|
215
|
+
"icon": "🔑",
|
|
216
|
+
"families": ["TG-AUTH", "TG-CSRF", "TG-WS", "TG-BIZ"],
|
|
217
|
+
"description": "Constant-time comparisons, password hashing, algorithm pinning, SameSite cookies, and business flow bounds."
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"id": "layer-db",
|
|
221
|
+
"name": "Database, GraphQL & Cache",
|
|
222
|
+
"icon": "💾",
|
|
223
|
+
"families": ["TG-DB", "TG-GQL", "TG-CACHE"],
|
|
224
|
+
"description": "Parameterized SQL queries, multi-tenant row partitioning, GraphQL query depth limits, and cache timing guards."
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"id": "layer-net",
|
|
228
|
+
"name": "Network, SSRF & Webhooks",
|
|
229
|
+
"icon": "🌐",
|
|
230
|
+
"families": ["TG-SSRF", "TG-WEBHOOK", "TG-RATE"],
|
|
231
|
+
"description": "Private IP / metadata blocking, cryptographic HMAC webhook authentication, and sliding-window rate limiters."
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"id": "layer-platform",
|
|
235
|
+
"name": "Platform, Supply Chain & Secrets",
|
|
236
|
+
"icon": "🏗️",
|
|
237
|
+
"families": ["TG-PLATFORM", "TG-SUPPLY", "TG-CLIENT", "TG-EDGE", "TG-DIFF", "TG-SEC"],
|
|
238
|
+
"description": "Security headers, dependency lockfile integrity, zero client bundle secrets, serverless limits, and Ponytail bounds."
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def find_project_root(start_dir: str | Path | None = None) -> Path:
|
|
244
|
+
"""Detect project root directory."""
|
|
245
|
+
current = Path(start_dir or Path.cwd()).resolve()
|
|
246
|
+
markers = [".git", "package.json", "pyproject.toml", ".torusguard"]
|
|
247
|
+
for m in markers:
|
|
248
|
+
if (current / m).exists():
|
|
249
|
+
return current
|
|
250
|
+
for parent in current.parents:
|
|
251
|
+
for m in markers:
|
|
252
|
+
if (parent / m).exists():
|
|
253
|
+
return parent
|
|
254
|
+
return current
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def load_all_rule_catalog() -> list[dict[str, Any]]:
|
|
258
|
+
"""Load complete 74-rule catalog from audit_runner."""
|
|
259
|
+
try:
|
|
260
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
261
|
+
if str(scripts_dir) not in sys.path:
|
|
262
|
+
sys.path.insert(0, str(scripts_dir))
|
|
263
|
+
import audit_runner # type: ignore
|
|
264
|
+
catalog = []
|
|
265
|
+
for r in getattr(audit_runner, "RULE_PATTERNS", []):
|
|
266
|
+
rid = r.get("rule_id", "TG-GEN")
|
|
267
|
+
fam = rid.rsplit("-", 1)[0]
|
|
268
|
+
meta = FAMILY_METADATA.get(fam, {})
|
|
269
|
+
catalog.append({
|
|
270
|
+
"rule_id": rid,
|
|
271
|
+
"title": r.get("title", "Security Invariant"),
|
|
272
|
+
"severity": r.get("severity", "Medium"),
|
|
273
|
+
"category": r.get("category", "general"),
|
|
274
|
+
"cluster": r.get("cluster", "cluster-general"),
|
|
275
|
+
"layer_id": meta.get("layer_id", "layer-platform"),
|
|
276
|
+
"layer_name": meta.get("layer_name", "Platform & Security"),
|
|
277
|
+
"guarantee": meta.get("guarantee", "Enforces security guardrails without bypasses."),
|
|
278
|
+
"owasp": meta.get("owasp", "OWASP Security Standard"),
|
|
279
|
+
"compliance": meta.get("compliance", "Enterprise Security Baseline")
|
|
280
|
+
})
|
|
281
|
+
if catalog:
|
|
282
|
+
return catalog
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
# Fallback built-in catalog if audit_runner cannot be loaded
|
|
287
|
+
return [
|
|
288
|
+
{"rule_id": "TG-SEC-001", "title": "Hardcoded Secret or API Key", "severity": "Critical", "category": "secrets", "layer_id": "layer-platform", "layer_name": "Platform, Supply Chain & Secrets", "guarantee": "Zero hardcoded secrets in source.", "owasp": "OWASP A02:2021", "compliance": "SOC 2 CC6.7"},
|
|
289
|
+
{"rule_id": "TG-SEC-002", "title": "Public Client Environment Secret", "severity": "Critical", "category": "secrets", "layer_id": "layer-platform", "layer_name": "Platform, Supply Chain & Secrets", "guarantee": "Zero private keys in frontend bundles.", "owasp": "OWASP A01:2021", "compliance": "SOC 2 CC6.1"},
|
|
290
|
+
{"rule_id": "TG-AUTH-001", "title": "Weak Password Storage", "severity": "Critical", "category": "auth", "layer_id": "layer-auth", "layer_name": "Identity, Auth & Real-Time", "guarantee": "Enforces strong password hashing.", "owasp": "OWASP A07:2021", "compliance": "SOC 2 CC6.1"},
|
|
291
|
+
{"rule_id": "TG-DB-001", "title": "Raw SQL Concatenation", "severity": "Critical", "category": "database", "layer_id": "layer-db", "layer_name": "Database, GraphQL & Cache", "guarantee": "Enforces parameterized queries.", "owasp": "OWASP A03:2021", "compliance": "SOC 2 CC6.1"},
|
|
292
|
+
{"rule_id": "TG-INPUT-001", "title": "Missing Server Input Validation", "severity": "High", "category": "input", "layer_id": "layer-app", "layer_name": "Application & LLM Defense", "guarantee": "Enforces boundary schema validation.", "owasp": "OWASP A03:2021", "compliance": "ISO 27001 A.8.28"},
|
|
293
|
+
{"rule_id": "TG-RATE-001", "title": "Unlimited Auth Endpoint", "severity": "High", "category": "rate", "layer_id": "layer-net", "layer_name": "Network, SSRF & Webhooks", "guarantee": "Enforces auth rate limiting.", "owasp": "OWASP A04:2021", "compliance": "SOC 2 CC6.6"},
|
|
294
|
+
{"rule_id": "TG-SSRF-001", "title": "Unvalidated Outbound HTTP Request", "severity": "High", "category": "ssrf", "layer_id": "layer-net", "layer_name": "Network, SSRF & Webhooks", "guarantee": "Blocks private VPC and metadata IPs.", "owasp": "OWASP A10:2021", "compliance": "SOC 2 CC6.6"},
|
|
295
|
+
{"rule_id": "TG-PLATFORM-001", "title": "Missing Security Headers", "severity": "Medium", "category": "platform", "layer_id": "layer-platform", "layer_name": "Platform, Supply Chain & Secrets", "guarantee": "Enforces HTTP hardening headers.", "owasp": "OWASP A05:2021", "compliance": "SOC 2 CC6.7"}
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def load_report_telemetry(root_dir: Path) -> dict[str, Any]:
|
|
300
|
+
"""Gather all project telemetry, findings, rules catalog, and memory state."""
|
|
301
|
+
tg_dir = root_dir / ".torusguard"
|
|
302
|
+
memory_dir = tg_dir / "memory"
|
|
303
|
+
config_file = tg_dir / "config" / "torusguard.json"
|
|
304
|
+
|
|
305
|
+
cfg: dict[str, Any] = {}
|
|
306
|
+
if config_file.exists():
|
|
307
|
+
try:
|
|
308
|
+
cfg = json.loads(config_file.read_text(encoding="utf-8"))
|
|
309
|
+
except (json.JSONDecodeError, OSError):
|
|
310
|
+
cfg = {}
|
|
311
|
+
|
|
312
|
+
profile_file = memory_dir / "profile.json"
|
|
313
|
+
patterns_file = memory_dir / "patterns.json"
|
|
314
|
+
context_file = memory_dir / "context.json"
|
|
315
|
+
|
|
316
|
+
profile: dict[str, Any] = {}
|
|
317
|
+
patterns: list[dict[str, Any]] = []
|
|
318
|
+
context: dict[str, Any] = {}
|
|
319
|
+
|
|
320
|
+
if profile_file.exists():
|
|
321
|
+
try:
|
|
322
|
+
profile = json.loads(profile_file.read_text(encoding="utf-8"))
|
|
323
|
+
except (json.JSONDecodeError, OSError):
|
|
324
|
+
profile = {}
|
|
325
|
+
|
|
326
|
+
if patterns_file.exists():
|
|
327
|
+
try:
|
|
328
|
+
patterns = json.loads(patterns_file.read_text(encoding="utf-8"))
|
|
329
|
+
except (json.JSONDecodeError, OSError):
|
|
330
|
+
patterns = []
|
|
331
|
+
|
|
332
|
+
if context_file.exists():
|
|
333
|
+
try:
|
|
334
|
+
context = json.loads(context_file.read_text(encoding="utf-8"))
|
|
335
|
+
except (json.JSONDecodeError, OSError):
|
|
336
|
+
context = {}
|
|
337
|
+
|
|
338
|
+
stack_info = cfg.get("detected_stack", {})
|
|
339
|
+
if not stack_info:
|
|
340
|
+
try:
|
|
341
|
+
import stack_detect # type: ignore
|
|
342
|
+
stack_info = stack_detect.detect_stack(root_dir)
|
|
343
|
+
except Exception:
|
|
344
|
+
stack_info = {"language": "Universal", "framework": "None", "data_layer": "None"}
|
|
345
|
+
|
|
346
|
+
# Load actual findings from latest audit run
|
|
347
|
+
runs_dir = tg_dir / "runs"
|
|
348
|
+
latest_findings: list[dict[str, Any]] = []
|
|
349
|
+
latest_run_name = "none"
|
|
350
|
+
if runs_dir.is_dir():
|
|
351
|
+
run_folders = sorted(
|
|
352
|
+
[d for d in runs_dir.iterdir() if d.is_dir() and (d / "findings.json").is_file()],
|
|
353
|
+
key=lambda p: p.stat().st_mtime,
|
|
354
|
+
reverse=True
|
|
355
|
+
)
|
|
356
|
+
if run_folders:
|
|
357
|
+
findings_file = run_folders[0] / "findings.json"
|
|
358
|
+
latest_run_name = run_folders[0].name
|
|
359
|
+
try:
|
|
360
|
+
latest_findings = json.loads(findings_file.read_text(encoding="utf-8"))
|
|
361
|
+
except (json.JSONDecodeError, OSError):
|
|
362
|
+
latest_findings = []
|
|
363
|
+
|
|
364
|
+
# Ingest historical & resolved findings from security_report.md
|
|
365
|
+
report_file = root_dir / "security_report.md"
|
|
366
|
+
resolved_findings: list[dict[str, Any]] = []
|
|
367
|
+
if report_file.is_file():
|
|
368
|
+
try:
|
|
369
|
+
import report_sync # type: ignore
|
|
370
|
+
parsed = report_sync.parse_existing_report(report_file)
|
|
371
|
+
for fid, rf in parsed.get("findings", {}).items():
|
|
372
|
+
st = str(rf.get("status", "")).upper()
|
|
373
|
+
if "RESOLVED" in st or "FIXED" in st:
|
|
374
|
+
tgt = rf.get("target", "")
|
|
375
|
+
fpath = tgt.rsplit(":", 1)[0] if ":" in tgt else tgt
|
|
376
|
+
try:
|
|
377
|
+
lineno = int(tgt.rsplit(":", 1)[1]) if ":" in tgt else 1
|
|
378
|
+
except ValueError:
|
|
379
|
+
lineno = 1
|
|
380
|
+
conf_str = str(rf.get("confidence", "70%"))
|
|
381
|
+
try:
|
|
382
|
+
cval = int(conf_str.split("%")[0].strip())
|
|
383
|
+
except Exception:
|
|
384
|
+
cval = 70
|
|
385
|
+
resolved_findings.append({
|
|
386
|
+
"finding_id": fid,
|
|
387
|
+
"rule_id": rf.get("rule_id", "TG-GEN"),
|
|
388
|
+
"title": rf.get("title", "Resolved Security Flaw"),
|
|
389
|
+
"severity": rf.get("severity", "Medium"),
|
|
390
|
+
"status": "resolved",
|
|
391
|
+
"confidence_score": cval,
|
|
392
|
+
"file_path": fpath,
|
|
393
|
+
"line_number": lineno,
|
|
394
|
+
"cluster": rf.get("cluster", "cluster-general"),
|
|
395
|
+
"description": rf.get("description", "Security violation remediated and hardened."),
|
|
396
|
+
"is_canary": False
|
|
397
|
+
})
|
|
398
|
+
except Exception:
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
# Merge findings: open findings take precedence; resolved findings provide historical closure proof
|
|
402
|
+
all_findings_map: dict[str, dict[str, Any]] = {}
|
|
403
|
+
for f in resolved_findings:
|
|
404
|
+
all_findings_map[f.get("finding_id", "")] = f
|
|
405
|
+
for f in latest_findings:
|
|
406
|
+
all_findings_map[f.get("finding_id", "")] = f
|
|
407
|
+
|
|
408
|
+
combined_findings = list(all_findings_map.values()) if all_findings_map else latest_findings
|
|
409
|
+
|
|
410
|
+
# Rule catalog & Safe vs. Harmed computation (only open issues count as violated)
|
|
411
|
+
all_rules = load_all_rule_catalog()
|
|
412
|
+
violated_ids = {
|
|
413
|
+
f.get("rule_id") for f in combined_findings
|
|
414
|
+
if not f.get("is_canary", False) and f.get("status") not in ("resolved", "fixed", "confirmed_fixed")
|
|
415
|
+
}
|
|
416
|
+
safe_rules = [r for r in all_rules if r["rule_id"] not in violated_ids]
|
|
417
|
+
violated_rules = [r for r in all_rules if r["rule_id"] in violated_ids]
|
|
418
|
+
|
|
419
|
+
ist_tz = datetime.timezone(datetime.timedelta(hours=5, minutes=30), name="IST")
|
|
420
|
+
now_ist = datetime.datetime.now(ist_tz)
|
|
421
|
+
return {
|
|
422
|
+
"project_name": root_dir.name or "Project",
|
|
423
|
+
"root_dir": str(root_dir),
|
|
424
|
+
"generated_at": now_ist.strftime("%Y-%m-%d %H:%M:%S IST"),
|
|
425
|
+
"config": cfg,
|
|
426
|
+
"profile": profile,
|
|
427
|
+
"patterns": patterns,
|
|
428
|
+
"context": context,
|
|
429
|
+
"detected_stack": stack_info,
|
|
430
|
+
"findings": combined_findings,
|
|
431
|
+
"open_findings": [f for f in combined_findings if f.get("status") not in ("resolved", "fixed", "confirmed_fixed")],
|
|
432
|
+
"resolved_findings": [f for f in combined_findings if f.get("status") in ("resolved", "fixed", "confirmed_fixed")],
|
|
433
|
+
"latest_run": latest_run_name,
|
|
434
|
+
"all_rules": all_rules,
|
|
435
|
+
"safe_rules": safe_rules,
|
|
436
|
+
"violated_rules": violated_rules,
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def compute_posture_score(telemetry: dict[str, Any]) -> int:
|
|
441
|
+
"""Calculate posture score from actual open findings, aligned with report_sync.py."""
|
|
442
|
+
findings = telemetry.get("findings", [])
|
|
443
|
+
if not findings:
|
|
444
|
+
return 100
|
|
445
|
+
|
|
446
|
+
# Only open non-canary findings count toward production penalty
|
|
447
|
+
open_findings = [f for f in findings if f.get("status") not in ("resolved", "fixed", "confirmed_fixed") and not f.get("is_canary", False)]
|
|
448
|
+
if not open_findings:
|
|
449
|
+
return 100
|
|
450
|
+
|
|
451
|
+
critical = sum(1 for f in open_findings if f.get("severity") == "Critical")
|
|
452
|
+
high = sum(1 for f in open_findings if f.get("severity") == "High")
|
|
453
|
+
medium = sum(1 for f in open_findings if f.get("severity") == "Medium")
|
|
454
|
+
low = sum(1 for f in open_findings if f.get("severity") == "Low")
|
|
455
|
+
|
|
456
|
+
penalty = (critical * 25) + (high * 15) + (medium * 5) + (low * 2)
|
|
457
|
+
return max(0, min(100, 100 - penalty))
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def get_recommended_defenses(detected_stack: dict[str, Any], violated_rule_ids: set[str]) -> list[dict[str, Any]]:
|
|
461
|
+
"""Generate prescriptive next best defense recommendations tailored to the stack."""
|
|
462
|
+
recs = []
|
|
463
|
+
lang = (detected_stack.get("language") or "Universal")
|
|
464
|
+
fw = (detected_stack.get("framework") or "Universal")
|
|
465
|
+
stack_tag = f"{lang}" + (f" / {fw}" if fw != "None" else "")
|
|
466
|
+
|
|
467
|
+
# Platform headers recommendation
|
|
468
|
+
if "TG-PLATFORM-002" not in violated_rule_ids and "TG-PLATFORM-001" not in violated_rule_ids:
|
|
469
|
+
recs.append({
|
|
470
|
+
"rule_id": "TG-PLATFORM-002",
|
|
471
|
+
"priority": "P0 Critical",
|
|
472
|
+
"title": "Enforce HTTP Security Headers (CSP, HSTS, X-Frame-Options)",
|
|
473
|
+
"mitigates": "OWASP A05: Security Misconfiguration",
|
|
474
|
+
"stack_tag": stack_tag,
|
|
475
|
+
"rationale": "Protects against clickjacking, MIME-confusion, and cross-site scripting attacks.",
|
|
476
|
+
"invariant_guarantee": "Zero unhardened HTTP endpoints. Requires HSTS max-age >= 31536000 and CSP nonce isolation.",
|
|
477
|
+
"command": "npx torusguard harden --rule TG-PLATFORM-002"
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
# Rate limiting on auth
|
|
481
|
+
recs.append({
|
|
482
|
+
"rule_id": "TG-RATE-001",
|
|
483
|
+
"priority": "P0 Critical",
|
|
484
|
+
"title": "Auth Endpoint Rate Limiting & Resource Throttling",
|
|
485
|
+
"mitigates": "OWASP A07: Identification and Authentication Failures",
|
|
486
|
+
"stack_tag": stack_tag,
|
|
487
|
+
"rationale": "Prevents brute-force credential stuffing, password spraying, and resource exhaustion.",
|
|
488
|
+
"invariant_guarantee": "Max 5 failed attempts per IP window on auth routes with exponential backoff.",
|
|
489
|
+
"command": "npx torusguard harden --rule TG-RATE-001"
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
# Timing-safe auth compare
|
|
493
|
+
recs.append({
|
|
494
|
+
"rule_id": "TG-AUTH-002",
|
|
495
|
+
"priority": "P1 Recommended",
|
|
496
|
+
"title": "Timing-Safe Secret & Signature Verification",
|
|
497
|
+
"mitigates": "OWASP A02: Cryptographic Failures",
|
|
498
|
+
"stack_tag": stack_tag,
|
|
499
|
+
"rationale": "Eliminates side-channel timing attacks on API keys, HMAC signatures, and tokens.",
|
|
500
|
+
"invariant_guarantee": "Constant-time evaluation across all cryptographic comparison call-sites.",
|
|
501
|
+
"command": "npx torusguard harden --rule TG-AUTH-002"
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
# Tenant database isolation
|
|
505
|
+
if "TG-DB-004" not in violated_rule_ids:
|
|
506
|
+
recs.append({
|
|
507
|
+
"rule_id": "TG-DB-004",
|
|
508
|
+
"priority": "P0 Critical",
|
|
509
|
+
"title": "Enforce Multi-Tenant Database Query Partition Scoping",
|
|
510
|
+
"mitigates": "OWASP A01: Broken Access Control",
|
|
511
|
+
"stack_tag": stack_tag,
|
|
512
|
+
"rationale": "Guarantees cross-tenant data isolation and eliminates BOLA/IDOR vulnerabilities.",
|
|
513
|
+
"invariant_guarantee": "All database queries MUST be explicitly scoped by authenticated tenant ID.",
|
|
514
|
+
"command": "npx torusguard harden --rule TG-DB-004"
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
# SSRF Private IP protection
|
|
518
|
+
recs.append({
|
|
519
|
+
"rule_id": "TG-SSRF-002",
|
|
520
|
+
"priority": "P1 Recommended",
|
|
521
|
+
"title": "Block Outbound Requests to Cloud Metadata (169.254.169.254)",
|
|
522
|
+
"mitigates": "OWASP A10: Server-Side Request Forgery",
|
|
523
|
+
"stack_tag": stack_tag,
|
|
524
|
+
"rationale": "Stops attackers from querying internal cloud metadata instances or private VPC ranges.",
|
|
525
|
+
"invariant_guarantee": "Strict hostname resolution with CIDR blocking for 169.254.169.254 and RFC1918 subnets.",
|
|
526
|
+
"command": "npx torusguard harden --rule TG-SSRF-002"
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
# AI Agent structural defense
|
|
530
|
+
if "TG-AGENT-001" not in violated_rule_ids:
|
|
531
|
+
recs.append({
|
|
532
|
+
"rule_id": "TG-AGENT-001",
|
|
533
|
+
"priority": "P2 Hardening",
|
|
534
|
+
"title": "Structural AI Prompt Sandboxing & Inert Delimiters",
|
|
535
|
+
"mitigates": "OWASP LLM01: Prompt Injection",
|
|
536
|
+
"stack_tag": "AI / LLM Stack",
|
|
537
|
+
"rationale": "Enforces user prompt boundary isolation to stop direct and indirect jailbreaks.",
|
|
538
|
+
"invariant_guarantee": "User input MUST be wrapped in inert XML tags and never directly concatenated into system prompt.",
|
|
539
|
+
"command": "npx torusguard harden --rule TG-AGENT-001"
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
return recs[:5]
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def format_diff_html(diff_text: str) -> str:
|
|
546
|
+
"""Format unified diff with colorized line styling."""
|
|
547
|
+
if not diff_text:
|
|
548
|
+
return '<span class="diff-line-ctx">// No diff representation recorded</span>'
|
|
549
|
+
formatted_lines = []
|
|
550
|
+
for line in diff_text.splitlines():
|
|
551
|
+
esc = html.escape(line)
|
|
552
|
+
if line.startswith("+++") or line.startswith("---"):
|
|
553
|
+
formatted_lines.append(f'<span class="diff-line-header">{esc}</span>')
|
|
554
|
+
elif line.startswith("@@"):
|
|
555
|
+
formatted_lines.append(f'<span class="diff-line-hunk">{esc}</span>')
|
|
556
|
+
elif line.startswith("+"):
|
|
557
|
+
formatted_lines.append(f'<span class="diff-line-add">{esc}</span>')
|
|
558
|
+
elif line.startswith("-"):
|
|
559
|
+
formatted_lines.append(f'<span class="diff-line-del">{esc}</span>')
|
|
560
|
+
else:
|
|
561
|
+
formatted_lines.append(f'<span class="diff-line-ctx">{esc}</span>')
|
|
562
|
+
return "\n".join(formatted_lines)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def get_curated_golden_recipes(patterns: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
566
|
+
"""Return memory recipes unpacked with rich diffs, Before/After snippets, and Ponytail metrics."""
|
|
567
|
+
memory_recipes = [p for p in patterns if p.get("pattern_type") == "golden_fix_recipe"]
|
|
568
|
+
unpacked: list[dict[str, Any]] = []
|
|
569
|
+
|
|
570
|
+
for idx, r in enumerate(memory_recipes):
|
|
571
|
+
rd = r.get("recipe_data") or {}
|
|
572
|
+
rule_id = rd.get("rule_id") or r.get("rule_id") or "TG-GEN"
|
|
573
|
+
desc = rd.get("description") or r.get("description") or "Verified fix pattern"
|
|
574
|
+
diff = rd.get("diff_snippet") or ""
|
|
575
|
+
before = rd.get("before_snippet") or ""
|
|
576
|
+
after = rd.get("after_snippet") or ""
|
|
577
|
+
metrics = rd.get("ponytail_metrics") or {"additions": 1, "deletions": 1}
|
|
578
|
+
ftype = rd.get("file_type") or r.get("file_type") or ".ts"
|
|
579
|
+
verified = rd.get("verified_count") or 1
|
|
580
|
+
rec_id = rd.get("recipe_id") or r.get("recipe_id") or f"recipe-{rule_id}-{idx}"
|
|
581
|
+
|
|
582
|
+
# Determine human category
|
|
583
|
+
if "SEC" in rule_id:
|
|
584
|
+
cat = "Secrets"
|
|
585
|
+
elif "CLIENT" in rule_id:
|
|
586
|
+
cat = "Client"
|
|
587
|
+
elif "DB" in rule_id or "GQL" in rule_id:
|
|
588
|
+
cat = "Database"
|
|
589
|
+
elif "INPUT" in rule_id or "AGENT" in rule_id:
|
|
590
|
+
cat = "Input"
|
|
591
|
+
elif "PLATFORM" in rule_id or "SUPPLY" in rule_id or "EDGE" in rule_id or "DIFF" in rule_id:
|
|
592
|
+
cat = "Platform"
|
|
593
|
+
elif "RATE" in rule_id:
|
|
594
|
+
cat = "Rate Limiting"
|
|
595
|
+
elif "SSRF" in rule_id or "WEBHOOK" in rule_id:
|
|
596
|
+
cat = "SSRF"
|
|
597
|
+
else:
|
|
598
|
+
cat = "General"
|
|
599
|
+
|
|
600
|
+
unpacked.append({
|
|
601
|
+
"recipe_id": rec_id,
|
|
602
|
+
"rule_id": rule_id,
|
|
603
|
+
"title": desc,
|
|
604
|
+
"description": desc,
|
|
605
|
+
"diff_snippet": diff,
|
|
606
|
+
"before_snippet": before,
|
|
607
|
+
"after_snippet": after,
|
|
608
|
+
"solution_pattern": after or diff,
|
|
609
|
+
"code_pattern": diff,
|
|
610
|
+
"ponytail_metrics": metrics,
|
|
611
|
+
"file_type": ftype,
|
|
612
|
+
"verified_count": verified,
|
|
613
|
+
"category": cat
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
if unpacked:
|
|
617
|
+
return unpacked
|
|
618
|
+
|
|
619
|
+
# Fallback built-in reference Golden Recipes if patterns is empty
|
|
620
|
+
return [
|
|
621
|
+
{
|
|
622
|
+
"recipe_id": "recipe-TG-DB-001-default",
|
|
623
|
+
"rule_id": "TG-DB-001",
|
|
624
|
+
"title": "Parameterized SQL Query Pattern",
|
|
625
|
+
"description": "Replaces vulnerable raw string interpolation with parameterized bound parameters.",
|
|
626
|
+
"diff_snippet": "--- a/db/queries.py\n+++ b/db/queries.py\n@@ -10,3 +10,3 @@\n-db.execute(f'SELECT * FROM users WHERE id = {user_id}')\n+db.execute('SELECT * FROM users WHERE id = :id', {'id': user_id})",
|
|
627
|
+
"before_snippet": "db.execute(f'SELECT * FROM users WHERE id = {user_id}')",
|
|
628
|
+
"after_snippet": "db.execute('SELECT * FROM users WHERE id = :id', {'id': user_id})",
|
|
629
|
+
"solution_pattern": "db.execute('SELECT * FROM users WHERE id = :id', {'id': user_id})",
|
|
630
|
+
"code_pattern": "- db.execute(f'SELECT * FROM users WHERE id = {user_id}')\n+ db.execute('SELECT * FROM users WHERE id = :id', {'id': user_id})",
|
|
631
|
+
"ponytail_metrics": {"additions": 1, "deletions": 1},
|
|
632
|
+
"file_type": ".py",
|
|
633
|
+
"verified_count": 1,
|
|
634
|
+
"category": "Database"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
"recipe_id": "recipe-TG-AUTH-002-default",
|
|
638
|
+
"rule_id": "TG-AUTH-002",
|
|
639
|
+
"title": "Timing-Safe Cryptographic Comparison",
|
|
640
|
+
"description": "Replaces standard equality (===) with timing-safe constant-time evaluation.",
|
|
641
|
+
"diff_snippet": "--- a/auth/verify.ts\n+++ b/auth/verify.ts\n@@ -22,3 +22,3 @@\n-if (token === expectedSecret) {\n+if (crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expectedSecret))) {",
|
|
642
|
+
"before_snippet": "if (token === expectedSecret) { ... }",
|
|
643
|
+
"after_snippet": "if (crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expectedSecret))) { ... }",
|
|
644
|
+
"solution_pattern": "if (crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expectedSecret))) { ... }",
|
|
645
|
+
"code_pattern": "- if (token === expectedSecret) { ... }\n+ if (crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expectedSecret))) { ... }",
|
|
646
|
+
"ponytail_metrics": {"additions": 1, "deletions": 1},
|
|
647
|
+
"file_type": ".ts",
|
|
648
|
+
"verified_count": 1,
|
|
649
|
+
"category": "Auth"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
"recipe_id": "recipe-TG-SSRF-002-default",
|
|
653
|
+
"rule_id": "TG-SSRF-002",
|
|
654
|
+
"title": "Outbound Private IP Range Filtering",
|
|
655
|
+
"description": "Validates outbound target IP against private VPC ranges and cloud metadata IPs.",
|
|
656
|
+
"diff_snippet": "--- a/net/proxy.py\n+++ b/net/proxy.py\n@@ -15,4 +15,6 @@\n-response = requests.get(target_url)\n+ip = socket.gethostbyname(urlparse(target_url).hostname)\n+if ipaddress.ip_address(ip).is_private: raise SecurityError('SSRF blocked')\n+response = requests.get(target_url)",
|
|
657
|
+
"before_snippet": "response = requests.get(target_url)",
|
|
658
|
+
"after_snippet": "ip = socket.gethostbyname(urlparse(target_url).hostname)\nif ipaddress.ip_address(ip).is_private: raise SecurityError('SSRF blocked')\nresponse = requests.get(target_url)",
|
|
659
|
+
"solution_pattern": "ip = socket.gethostbyname(urlparse(target_url).hostname)\nif ipaddress.ip_address(ip).is_private: raise SecurityError('SSRF blocked')\nresponse = requests.get(target_url)",
|
|
660
|
+
"code_pattern": "- response = requests.get(target_url)\n+ ip = socket.gethostbyname(urlparse(target_url).hostname)\n+ if ipaddress.ip_address(ip).is_private: raise SecurityError('SSRF blocked')",
|
|
661
|
+
"ponytail_metrics": {"additions": 3, "deletions": 1},
|
|
662
|
+
"file_type": ".py",
|
|
663
|
+
"verified_count": 1,
|
|
664
|
+
"category": "SSRF"
|
|
665
|
+
}
|
|
666
|
+
]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def generate_html_dashboard(telemetry: dict[str, Any]) -> str:
|
|
670
|
+
"""Build a complete, standalone, single-file interactive HTML security dashboard."""
|
|
671
|
+
score = compute_posture_score(telemetry)
|
|
672
|
+
findings = telemetry.get("findings", [])
|
|
673
|
+
patterns = telemetry.get("patterns", [])
|
|
674
|
+
cfg = telemetry.get("config", {})
|
|
675
|
+
detected = cfg.get("detected_stack", {})
|
|
676
|
+
profile = telemetry.get("profile", {})
|
|
677
|
+
all_rules = telemetry.get("all_rules", [])
|
|
678
|
+
safe_rules = telemetry.get("safe_rules", [])
|
|
679
|
+
violated_rules = telemetry.get("violated_rules", [])
|
|
680
|
+
|
|
681
|
+
# Polyglot stack information
|
|
682
|
+
primary_lang = detected.get("language") or "Universal"
|
|
683
|
+
primary_fw = detected.get("framework") or "None"
|
|
684
|
+
primary_db = detected.get("data_layer") or "None"
|
|
685
|
+
|
|
686
|
+
stack = profile.get("stack", [])
|
|
687
|
+
if not stack and primary_lang != "Unknown":
|
|
688
|
+
stack = [primary_lang]
|
|
689
|
+
if primary_fw != "None":
|
|
690
|
+
stack.append(primary_fw)
|
|
691
|
+
if primary_db != "None":
|
|
692
|
+
stack.append(primary_db)
|
|
693
|
+
stack_label = ", ".join(stack) if stack else "Polyglot Project"
|
|
694
|
+
|
|
695
|
+
# Open vs resolved findings separation
|
|
696
|
+
open_findings = [f for f in findings if f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")]
|
|
697
|
+
resolved_findings = [f for f in findings if f.get("status", "open") in ("fixed", "resolved", "confirmed_fixed")]
|
|
698
|
+
|
|
699
|
+
crit_open = sum(1 for f in open_findings if f.get("severity") == "Critical")
|
|
700
|
+
crit_resolved = sum(1 for f in resolved_findings if f.get("severity") == "Critical")
|
|
701
|
+
crit_total = crit_open + crit_resolved
|
|
702
|
+
|
|
703
|
+
high_open = sum(1 for f in open_findings if f.get("severity") == "High")
|
|
704
|
+
high_resolved = sum(1 for f in resolved_findings if f.get("severity") == "High")
|
|
705
|
+
high_total = high_open + high_resolved
|
|
706
|
+
|
|
707
|
+
med_open = sum(1 for f in open_findings if f.get("severity") in ("Medium", "Low"))
|
|
708
|
+
med_resolved = sum(1 for f in resolved_findings if f.get("severity") in ("Medium", "Low"))
|
|
709
|
+
med_total = med_open + med_resolved
|
|
710
|
+
|
|
711
|
+
total_discovered = len(findings)
|
|
712
|
+
total_open = len(open_findings)
|
|
713
|
+
total_resolved = len(resolved_findings)
|
|
714
|
+
closure_rate_pct = int((total_resolved / total_discovered) * 100) if total_discovered > 0 else 100
|
|
715
|
+
|
|
716
|
+
total_rules = len(all_rules) if all_rules else 74
|
|
717
|
+
defended_count = len(safe_rules) if safe_rules else (total_rules - len(violated_rules))
|
|
718
|
+
coverage_pct = int((defended_count / total_rules) * 100) if total_rules > 0 else 100
|
|
719
|
+
|
|
720
|
+
# Sub-badges for the 4 Severity Cards in .grid-kpi
|
|
721
|
+
if crit_open == 0 and crit_resolved > 0:
|
|
722
|
+
crit_sub_badge = f'<div class="sev-sub-badge badge-sub-clean">🟢 {crit_resolved} Remediated (100% Defended)</div>'
|
|
723
|
+
elif crit_resolved > 0:
|
|
724
|
+
crit_sub_badge = f'<div class="sev-sub-badge">{crit_resolved} Solved / {crit_total} Total</div>'
|
|
725
|
+
else:
|
|
726
|
+
crit_sub_badge = '<div class="sev-sub-badge badge-sub-clean">0 Discovered</div>'
|
|
727
|
+
|
|
728
|
+
if high_open == 0 and high_resolved > 0:
|
|
729
|
+
high_sub_badge = f'<div class="sev-sub-badge badge-sub-clean">🟢 {high_resolved} Remediated (100% Defended)</div>'
|
|
730
|
+
elif high_resolved > 0:
|
|
731
|
+
high_sub_badge = f'<div class="sev-sub-badge">{high_resolved} Solved / {high_total} Total</div>'
|
|
732
|
+
else:
|
|
733
|
+
high_sub_badge = '<div class="sev-sub-badge badge-sub-clean">0 Discovered</div>'
|
|
734
|
+
|
|
735
|
+
if med_open == 0 and med_resolved > 0:
|
|
736
|
+
med_sub_badge = f'<div class="sev-sub-badge badge-sub-clean">🟢 {med_resolved} Remediated (100% Defended)</div>'
|
|
737
|
+
elif med_resolved > 0:
|
|
738
|
+
med_sub_badge = f'<div class="sev-sub-badge">{med_resolved} Solved / {med_total} Total</div>'
|
|
739
|
+
else:
|
|
740
|
+
med_sub_badge = '<div class="sev-sub-badge badge-sub-clean">0 Discovered</div>'
|
|
741
|
+
|
|
742
|
+
# Governed Remediation Velocity Ledger HTML
|
|
743
|
+
rem_crit_pct = 100 if crit_total == 0 or crit_open == 0 else int((crit_resolved / crit_total) * 100)
|
|
744
|
+
rem_high_pct = 100 if high_total == 0 or high_open == 0 else int((high_resolved / high_total) * 100)
|
|
745
|
+
rem_med_pct = 100 if med_total == 0 or med_open == 0 else int((med_resolved / med_total) * 100)
|
|
746
|
+
|
|
747
|
+
remediation_ledger_html = f"""
|
|
748
|
+
<!-- Governed Remediation & Problem-Solved Velocity Ledger -->
|
|
749
|
+
<div class="card remediation-ledger-card">
|
|
750
|
+
<div class="remediation-header">
|
|
751
|
+
<div class="remediation-title-group">
|
|
752
|
+
<span style="font-size: 22px;">⚡</span>
|
|
753
|
+
<div>
|
|
754
|
+
<div style="font-size: 15px; font-weight: 700; color: var(--text-main);">
|
|
755
|
+
Governed Remediation & Problem-Solved Velocity
|
|
756
|
+
</div>
|
|
757
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
|
758
|
+
Historical closure ledger of vulnerabilities discovered, formulated into Ponytail patches, and verified fixed.
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
<div class="remediation-overall-badge">
|
|
763
|
+
<span class="badge {'badge-success' if total_open == 0 and total_resolved > 0 else 'badge-high'}" style="font-size: 12px; padding: 4px 10px;">
|
|
764
|
+
✔ {total_resolved} of {total_discovered} Fixed ({closure_rate_pct}% Defended)
|
|
765
|
+
</span>
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
|
|
769
|
+
<div class="rem-progress-bar-bg">
|
|
770
|
+
<div class="rem-progress-bar-fill" style="width: {closure_rate_pct}%;"></div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<div class="rem-stat-chips-grid">
|
|
774
|
+
<div class="rem-chip">
|
|
775
|
+
<div class="rem-chip-top">
|
|
776
|
+
<span class="rem-chip-sev badge badge-critical">Critical</span>
|
|
777
|
+
<span class="rem-chip-status" style="color: {'var(--accent-green)' if crit_open == 0 else 'var(--accent-red)'};">
|
|
778
|
+
{'🟢 100% Defended' if crit_open == 0 else f'{crit_open} Active'}
|
|
779
|
+
</span>
|
|
780
|
+
</div>
|
|
781
|
+
<div class="rem-chip-main">
|
|
782
|
+
<span class="rem-chip-num">{crit_resolved}</span>
|
|
783
|
+
<span class="rem-chip-lbl">Solved / {crit_total} Discovered</span>
|
|
784
|
+
</div>
|
|
785
|
+
{crit_sub_badge}
|
|
786
|
+
<div class="rem-chip-bar"><div class="rem-chip-bar-fill" style="width: {rem_crit_pct}%; background: var(--accent-green);"></div></div>
|
|
787
|
+
</div>
|
|
788
|
+
|
|
789
|
+
<div class="rem-chip">
|
|
790
|
+
<div class="rem-chip-top">
|
|
791
|
+
<span class="rem-chip-sev badge badge-high">High</span>
|
|
792
|
+
<span class="rem-chip-status" style="color: {'var(--accent-green)' if high_open == 0 else 'var(--accent-orange)'};">
|
|
793
|
+
{'🟢 100% Defended' if high_open == 0 else f'{high_open} Active'}
|
|
794
|
+
</span>
|
|
795
|
+
</div>
|
|
796
|
+
<div class="rem-chip-main">
|
|
797
|
+
<span class="rem-chip-num">{high_resolved}</span>
|
|
798
|
+
<span class="rem-chip-lbl">Solved / {high_total} Discovered</span>
|
|
799
|
+
</div>
|
|
800
|
+
{high_sub_badge}
|
|
801
|
+
<div class="rem-chip-bar"><div class="rem-chip-bar-fill" style="width: {rem_high_pct}%; background: var(--accent-green);"></div></div>
|
|
802
|
+
</div>
|
|
803
|
+
|
|
804
|
+
<div class="rem-chip">
|
|
805
|
+
<div class="rem-chip-top">
|
|
806
|
+
<span class="rem-chip-sev badge badge-medium">Medium / Low</span>
|
|
807
|
+
<span class="rem-chip-status" style="color: {'var(--accent-green)' if med_open == 0 else 'var(--accent-yellow)'};">
|
|
808
|
+
{'🟢 100% Defended' if med_open == 0 else f'{med_open} Active'}
|
|
809
|
+
</span>
|
|
810
|
+
</div>
|
|
811
|
+
<div class="rem-chip-main">
|
|
812
|
+
<span class="rem-chip-num">{med_resolved}</span>
|
|
813
|
+
<span class="rem-chip-lbl">Solved / {med_total} Discovered</span>
|
|
814
|
+
</div>
|
|
815
|
+
{med_sub_badge}
|
|
816
|
+
<div class="rem-chip-bar"><div class="rem-chip-bar-fill" style="width: {rem_med_pct}%; background: var(--accent-green);"></div></div>
|
|
817
|
+
</div>
|
|
818
|
+
|
|
819
|
+
<div class="rem-chip rem-chip-total">
|
|
820
|
+
<div class="rem-chip-top">
|
|
821
|
+
<span class="rem-chip-sev badge badge-success">Guards Defended</span>
|
|
822
|
+
<span class="rem-chip-status" style="color: var(--accent-green);">
|
|
823
|
+
{'🟢 100% Hardened' if total_open == 0 else 'In Progress'}
|
|
824
|
+
</span>
|
|
825
|
+
</div>
|
|
826
|
+
<div class="rem-chip-main">
|
|
827
|
+
<span class="rem-chip-num">{total_resolved}</span>
|
|
828
|
+
<span class="rem-chip-lbl">Total Flaws Neutralized</span>
|
|
829
|
+
</div>
|
|
830
|
+
<div class="rem-chip-bar"><div class="rem-chip-bar-fill" style="width: {closure_rate_pct}%; background: var(--accent-green);"></div></div>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
"""
|
|
835
|
+
|
|
836
|
+
# Recipes
|
|
837
|
+
recipes = get_curated_golden_recipes(patterns)
|
|
838
|
+
|
|
839
|
+
# Recommendations (only open findings should be treated as violated)
|
|
840
|
+
violated_ids = {
|
|
841
|
+
f.get("rule_id") for f in findings
|
|
842
|
+
if not f.get("is_canary", False) and f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")
|
|
843
|
+
}
|
|
844
|
+
recommendations = get_recommended_defenses(detected, violated_ids)
|
|
845
|
+
|
|
846
|
+
# Open vs resolved counts
|
|
847
|
+
open_count = total_open
|
|
848
|
+
resolved_count = total_resolved
|
|
849
|
+
|
|
850
|
+
# Gauge values & Dynamic Theme Accents
|
|
851
|
+
circumference = 251.2
|
|
852
|
+
dash_offset = circumference - (circumference * (score / 100.0))
|
|
853
|
+
if score >= 90:
|
|
854
|
+
grad_start = "#3fb950"
|
|
855
|
+
grad_end = "#2ea043"
|
|
856
|
+
glow_color = "#3fb950"
|
|
857
|
+
status_label = "🟢 HARDENED & SECURE"
|
|
858
|
+
status_badge_class = "status-clean"
|
|
859
|
+
elif score >= 75:
|
|
860
|
+
grad_start = "#58a6ff"
|
|
861
|
+
grad_end = "#1f6feb"
|
|
862
|
+
glow_color = "#58a6ff"
|
|
863
|
+
status_label = "🔵 ELEVATED RESILIENCE"
|
|
864
|
+
status_badge_class = "status-elevated"
|
|
865
|
+
elif score >= 50:
|
|
866
|
+
grad_start = "#d29922"
|
|
867
|
+
grad_end = "#bb8009"
|
|
868
|
+
glow_color = "#d29922"
|
|
869
|
+
status_label = "🟡 WARNINGS DETECTED"
|
|
870
|
+
status_badge_class = "status-warn"
|
|
871
|
+
else:
|
|
872
|
+
grad_start = "#f85149"
|
|
873
|
+
grad_end = "#da3633"
|
|
874
|
+
glow_color = "#f85149"
|
|
875
|
+
status_label = "🔴 CRITICAL RISK"
|
|
876
|
+
status_badge_class = "status-critical"
|
|
877
|
+
|
|
878
|
+
# ── Findings Table HTML (with Expandable Drawers & Simulator Checkboxes) ──
|
|
879
|
+
findings_rows = ""
|
|
880
|
+
sev_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}
|
|
881
|
+
status_order = lambda f: 0 if f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed") else 1
|
|
882
|
+
sorted_f = sorted(findings, key=lambda x: (status_order(x), sev_order.get(x.get("severity", "Low"), 9), -x.get("confidence_score", 0)))
|
|
883
|
+
|
|
884
|
+
if sorted_f:
|
|
885
|
+
for idx, f in enumerate(sorted_f[:100]):
|
|
886
|
+
fid = html.escape(f.get("finding_id", f"fid-{idx}"))
|
|
887
|
+
drawer_id = f"drawer-{idx}"
|
|
888
|
+
sev = f.get("severity", "Medium")
|
|
889
|
+
sev_class = "badge-critical" if sev == "Critical" else ("badge-high" if sev == "High" else "badge-medium")
|
|
890
|
+
rule = html.escape(f.get("rule_id", "TG-GEN"))
|
|
891
|
+
title = html.escape(f.get("title", "Finding"))
|
|
892
|
+
fpath = html.escape(f.get("file_path", "unknown"))
|
|
893
|
+
line = f.get("line_number", 0)
|
|
894
|
+
conf = f.get("confidence_score", 70)
|
|
895
|
+
desc = html.escape(f.get("description", "Security invariant check failed."))
|
|
896
|
+
snippet = html.escape(f.get("snippet", f.get("matched_text", "// No preview snippet available")))
|
|
897
|
+
is_canary = f.get("is_canary", False)
|
|
898
|
+
canary_badge = ' <span class="badge badge-canary">Canary Test</span>' if is_canary else ''
|
|
899
|
+
f_status = "resolved" if f.get("status") in ("fixed", "resolved", "confirmed_fixed") else "open"
|
|
900
|
+
status_badge = ' <span class="badge badge-success">✔ Resolved</span>' if f_status == "resolved" else ""
|
|
901
|
+
|
|
902
|
+
search_blob = html.escape(f"{rule} {title} {fpath} {sev} {f_status}".lower())
|
|
903
|
+
|
|
904
|
+
if f_status == "resolved":
|
|
905
|
+
sim_cell = '<span style="color: var(--accent-green); font-size: 14px;" title="Already resolved and defended">✔</span>'
|
|
906
|
+
drawer_body = f"""
|
|
907
|
+
<div class="drawer-header">
|
|
908
|
+
<div>
|
|
909
|
+
<strong>Finding ID:</strong> <code>{fid}</code> ·
|
|
910
|
+
<strong>File:</strong> <code>{fpath}:{line}</code> ·
|
|
911
|
+
<strong>Confidence:</strong> {conf}/100 ·
|
|
912
|
+
<span class="badge badge-success">✔ Resolved & Defended</span>
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
<div class="drawer-notice-resolved" style="background: rgba(63, 185, 80, 0.1); border: 1px solid rgba(63, 185, 80, 0.3); border-radius: 6px; padding: 10px 14px; margin: 10px 0; font-size: 12px;">
|
|
916
|
+
<strong>✔ Remediated & Hardened:</strong> This security violation has been successfully patched and verified under the Ponytail Protocol (≤ 35 additions, ≤ 25 deletions).
|
|
917
|
+
</div>
|
|
918
|
+
<div class="drawer-desc">{desc}</div>
|
|
919
|
+
<div class="drawer-actions">
|
|
920
|
+
<div class="cmd-box">
|
|
921
|
+
<code>npx torusguard recheck --rule {rule}</code>
|
|
922
|
+
<button class="btn-copy" onclick="copyText('npx torusguard recheck --rule {rule}', this); event.stopPropagation();">📋 Copy Recheck Command</button>
|
|
923
|
+
</div>
|
|
924
|
+
</div>"""
|
|
925
|
+
else:
|
|
926
|
+
sim_cell = f"""<label class="sim-checkbox-label" title="Simulate fixing this finding in What-If Posture Simulator">
|
|
927
|
+
<input type="checkbox" class="sim-fix-chk" data-finding-id="{fid}" data-severity="{sev}" onchange="updateSimulatedScore()">
|
|
928
|
+
</label>"""
|
|
929
|
+
drawer_body = f"""
|
|
930
|
+
<div class="drawer-header">
|
|
931
|
+
<div>
|
|
932
|
+
<strong>Finding ID:</strong> <code>{fid}</code> ·
|
|
933
|
+
<strong>File:</strong> <code>{fpath}:{line}</code> ·
|
|
934
|
+
<strong>Confidence:</strong> {conf}/100
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
<div class="drawer-desc">{desc}</div>
|
|
938
|
+
<div class="code-preview">
|
|
939
|
+
<pre><code>{snippet}</code></pre>
|
|
940
|
+
</div>
|
|
941
|
+
<div class="drawer-actions">
|
|
942
|
+
<div class="cmd-box">
|
|
943
|
+
<code>npx torusguard harden --finding {fid}</code>
|
|
944
|
+
<button class="btn-copy" onclick="copyText('npx torusguard harden --finding {fid}', this); event.stopPropagation();">📋 Copy Command</button>
|
|
945
|
+
</div>
|
|
946
|
+
</div>"""
|
|
947
|
+
|
|
948
|
+
findings_rows += f"""
|
|
949
|
+
<tr class="finding-row" data-severity="{sev.lower()}" data-status="{f_status}" data-search="{search_blob}" data-path="{fpath}" data-drawer-id="{drawer_id}" onclick="toggleDrawer(this)">
|
|
950
|
+
<td style="text-align: center;" onclick="event.stopPropagation()">
|
|
951
|
+
{sim_cell}
|
|
952
|
+
</td>
|
|
953
|
+
<td><span class="badge {sev_class}">{sev}</span>{canary_badge}{status_badge}</td>
|
|
954
|
+
<td><code class="rule-id">{rule}</code></td>
|
|
955
|
+
<td class="finding-title">{title}</td>
|
|
956
|
+
<td><code>{fpath}:{line}</code></td>
|
|
957
|
+
<td><span class="conf-score">{conf}%</span></td>
|
|
958
|
+
<td style="text-align: right;"><span class="drawer-chevron">▸</span></td>
|
|
959
|
+
</tr>
|
|
960
|
+
<tr id="{drawer_id}" class="drawer-row" style="display: none;">
|
|
961
|
+
<td colspan="7">
|
|
962
|
+
<div class="drawer-content">
|
|
963
|
+
{drawer_body}
|
|
964
|
+
</div>
|
|
965
|
+
</td>
|
|
966
|
+
</tr>"""
|
|
967
|
+
if len(sorted_f) > 100:
|
|
968
|
+
findings_rows += f'<tr><td colspan="7" class="overflow-row">... and {len(sorted_f) - 100} more findings cataloged in run artifacts</td></tr>'
|
|
969
|
+
else:
|
|
970
|
+
findings_rows = '<tr><td colspan="7" class="empty-cell">✔ Zero security invariant violations detected. Posture is 100% clean and defended.</td></tr>'
|
|
971
|
+
|
|
972
|
+
# ── 5-Layer Architectural Invariant Matrix ──
|
|
973
|
+
# Group all rules by architectural layer
|
|
974
|
+
layers_html = ""
|
|
975
|
+
for layer in ARCHITECTURAL_LAYERS:
|
|
976
|
+
lid = layer["id"]
|
|
977
|
+
lname = layer["name"]
|
|
978
|
+
licon = layer["icon"]
|
|
979
|
+
ldesc = layer["description"]
|
|
980
|
+
lfams = layer["families"]
|
|
981
|
+
|
|
982
|
+
layer_rules = [r for r in all_rules if any(r.get("rule_id", "").startswith(fam) for fam in lfams)]
|
|
983
|
+
l_total = len(layer_rules)
|
|
984
|
+
l_defended = sum(1 for r in layer_rules if r["rule_id"] not in violated_ids)
|
|
985
|
+
l_status_class = "status-clean" if l_defended == l_total else ("status-warn" if l_defended >= l_total * 0.75 else "status-critical")
|
|
986
|
+
|
|
987
|
+
chips_html = ""
|
|
988
|
+
for r in layer_rules:
|
|
989
|
+
rid = r["rule_id"]
|
|
990
|
+
rtitle = r["title"]
|
|
991
|
+
rsev = r.get("severity", "Medium")
|
|
992
|
+
is_defended = rid not in violated_ids
|
|
993
|
+
chip_cls = "inv-chip-safe" if is_defended else "inv-chip-violated"
|
|
994
|
+
dot_color = "var(--accent-green)" if is_defended else "var(--accent-red)"
|
|
995
|
+
status_text = "Defended" if is_defended else "Violated"
|
|
996
|
+
search_data = f"{rid} {rtitle} {rsev} {status_text} {lname}".lower()
|
|
997
|
+
|
|
998
|
+
chips_html += f"""
|
|
999
|
+
<div class="inv-chip {chip_cls}" data-rule="{rid}" data-layer="{lid}" data-search="{html.escape(search_data)}" onclick="inspectInvariant('{rid}')" title="Click to inspect security invariant guarantee for {rid}">
|
|
1000
|
+
<span class="inv-dot" style="background: {dot_color};"></span>
|
|
1001
|
+
<code class="inv-code">{rid}</code>
|
|
1002
|
+
<span class="inv-title">{html.escape(rtitle)}</span>
|
|
1003
|
+
</div>"""
|
|
1004
|
+
|
|
1005
|
+
layers_html += f"""
|
|
1006
|
+
<div class="layer-card" data-layer-id="{lid}">
|
|
1007
|
+
<div class="layer-header">
|
|
1008
|
+
<div class="layer-title-group">
|
|
1009
|
+
<span class="layer-icon">{licon}</span>
|
|
1010
|
+
<div>
|
|
1011
|
+
<div class="layer-name">{lname}</div>
|
|
1012
|
+
<div class="layer-desc">{ldesc}</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
</div>
|
|
1015
|
+
<div class="layer-badge {l_status_class}">{l_defended}/{l_total} Enforced</div>
|
|
1016
|
+
</div>
|
|
1017
|
+
<div class="layer-chips-grid">
|
|
1018
|
+
{chips_html}
|
|
1019
|
+
</div>
|
|
1020
|
+
</div>"""
|
|
1021
|
+
|
|
1022
|
+
# ── OWASP Top 10 Compliance Radar ──
|
|
1023
|
+
owasp_defs = [
|
|
1024
|
+
("A01: Broken Access Control", ["TG-DB-001", "TG-DB-004", "TG-AUTH-006", "TG-WS-002", "TG-BIZ-001", "TG-BIZ-002"]),
|
|
1025
|
+
("A02: Cryptographic Failures", ["TG-SEC-001", "TG-SEC-002", "TG-SEC-003", "TG-SEC-004", "TG-SEC-005", "TG-AUTH-002"]),
|
|
1026
|
+
("A03: Injection Defense", ["TG-INPUT-001", "TG-INPUT-002", "TG-INPUT-003", "TG-INPUT-004", "TG-DB-002", "TG-DB-003", "TG-GQL-001"]),
|
|
1027
|
+
("A05: Security Configuration", ["TG-PLATFORM-001", "TG-PLATFORM-002", "TG-PLATFORM-003", "TG-PLATFORM-004", "TG-CLIENT-001", "TG-CLIENT-002"]),
|
|
1028
|
+
("A06: Supply Chain Health", ["TG-SUPPLY-001", "TG-SUPPLY-002", "TG-SUPPLY-003", "TG-SUPPLY-004", "TG-SUPPLY-005"]),
|
|
1029
|
+
("A07: Identity & Auth", ["TG-AUTH-001", "TG-AUTH-003", "TG-AUTH-004", "TG-AUTH-005", "TG-AUTH-007", "TG-CSRF-001", "TG-CSRF-002"]),
|
|
1030
|
+
("A08: Software Integrity", ["TG-WEBHOOK-001", "TG-WEBHOOK-002", "TG-WEBHOOK-003", "TG-DIFF-001", "TG-DIFF-002"]),
|
|
1031
|
+
("A10: SSRF Defense", ["TG-SSRF-001", "TG-SSRF-002", "TG-SSRF-003", "TG-SSRF-004"])
|
|
1032
|
+
]
|
|
1033
|
+
|
|
1034
|
+
owasp_cards_html = ""
|
|
1035
|
+
for category_name, rule_codes in owasp_defs:
|
|
1036
|
+
matched = [
|
|
1037
|
+
f for f in findings
|
|
1038
|
+
if f.get("rule_id") in rule_codes
|
|
1039
|
+
and not f.get("is_canary", False)
|
|
1040
|
+
and str(f.get("status", "open")).lower() not in ("resolved", "fixed", "confirmed_fixed")
|
|
1041
|
+
]
|
|
1042
|
+
cat_count = len(matched)
|
|
1043
|
+
if cat_count == 0:
|
|
1044
|
+
c_score = 100
|
|
1045
|
+
c_color = "var(--accent-green)"
|
|
1046
|
+
c_badge = "100% Compliant"
|
|
1047
|
+
elif cat_count == 1:
|
|
1048
|
+
c_score = 80
|
|
1049
|
+
c_color = "var(--accent-yellow)"
|
|
1050
|
+
c_badge = "1 Issue"
|
|
1051
|
+
else:
|
|
1052
|
+
c_score = max(20, 100 - (cat_count * 20))
|
|
1053
|
+
c_color = "var(--accent-red)"
|
|
1054
|
+
c_badge = f"{cat_count} Issues"
|
|
1055
|
+
|
|
1056
|
+
owasp_cards_html += f"""
|
|
1057
|
+
<div class="owasp-card">
|
|
1058
|
+
<div class="owasp-top">
|
|
1059
|
+
<span class="owasp-title">{category_name}</span>
|
|
1060
|
+
<span class="owasp-badge" style="color: {c_color}; border-color: {c_color};">{c_badge}</span>
|
|
1061
|
+
</div>
|
|
1062
|
+
<div class="owasp-bar-bg">
|
|
1063
|
+
<div class="owasp-bar" style="width: {c_score}%; background: {c_color};"></div>
|
|
1064
|
+
</div>
|
|
1065
|
+
</div>"""
|
|
1066
|
+
|
|
1067
|
+
# ── Enterprise Compliance Framework Matrix ──
|
|
1068
|
+
compliance_frameworks: list[ComplianceFramework] = [
|
|
1069
|
+
{
|
|
1070
|
+
"name": "SOC 2 Type II",
|
|
1071
|
+
"subtitle": "AICPA Trust Services Criteria (Security & Boundary)",
|
|
1072
|
+
"controls": [
|
|
1073
|
+
{"code": "CC6.1 Logical Access", "families": "TG-AUTH, TG-CLIENT", "rules": ["TG-AUTH-001", "TG-AUTH-002", "TG-AUTH-003", "TG-AUTH-004", "TG-CLIENT-001"]},
|
|
1074
|
+
{"code": "CC6.6 Network Boundary", "families": "TG-SSRF, TG-RATE, TG-WS", "rules": ["TG-SSRF-001", "TG-SSRF-002", "TG-RATE-001", "TG-WS-001"]},
|
|
1075
|
+
{"code": "CC6.7 Transmission Security", "families": "TG-PLATFORM, TG-SEC", "rules": ["TG-PLATFORM-001", "TG-PLATFORM-002", "TG-SEC-001", "TG-SEC-002"]},
|
|
1076
|
+
{"code": "CC7.1 Vulnerability & Churn", "families": "TG-DIFF, TG-SUPPLY", "rules": ["TG-DIFF-001", "TG-DIFF-002", "TG-SUPPLY-001", "TG-SUPPLY-002"]}
|
|
1077
|
+
]
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
"name": "ISO/IEC 27001:2022",
|
|
1081
|
+
"subtitle": "Annex A Controls for Technological & Application Security",
|
|
1082
|
+
"controls": [
|
|
1083
|
+
{"code": "A.8.20 Network Security", "families": "TG-SSRF, TG-WEBHOOK, TG-RATE", "rules": ["TG-SSRF-001", "TG-WEBHOOK-001", "TG-RATE-001"]},
|
|
1084
|
+
{"code": "A.8.24 Cryptography Usage", "families": "TG-SEC, TG-AUTH", "rules": ["TG-SEC-001", "TG-SEC-003", "TG-AUTH-002"]},
|
|
1085
|
+
{"code": "A.8.28 Secure Coding", "families": "TG-INPUT, TG-DB, TG-GQL, TG-AGENT", "rules": ["TG-INPUT-001", "TG-DB-001", "TG-GQL-001", "TG-AGENT-001"]}
|
|
1086
|
+
]
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
"name": "HIPAA Security Rule",
|
|
1090
|
+
"subtitle": "45 CFR Part 164 Technical Safeguards for ePHI",
|
|
1091
|
+
"controls": [
|
|
1092
|
+
{"code": "§ 164.312(a)(1) Access Control", "families": "TG-AUTH, TG-DB (Tenant Isolation)", "rules": ["TG-AUTH-001", "TG-DB-004", "TG-AUTH-006"]},
|
|
1093
|
+
{"code": "§ 164.312(c)(1) ePHI Integrity", "families": "TG-WEBHOOK, TG-BIZ, TG-DIFF", "rules": ["TG-WEBHOOK-001", "TG-BIZ-001", "TG-DIFF-001"]},
|
|
1094
|
+
{"code": "§ 164.312(e)(1) Transmission Security", "families": "TG-PLATFORM, TG-SEC, TG-CLIENT", "rules": ["TG-PLATFORM-001", "TG-SEC-001", "TG-CLIENT-001"]}
|
|
1095
|
+
]
|
|
1096
|
+
}
|
|
1097
|
+
]
|
|
1098
|
+
|
|
1099
|
+
compliance_cards_html = ""
|
|
1100
|
+
for fw in compliance_frameworks:
|
|
1101
|
+
fw_name = fw["name"]
|
|
1102
|
+
fw_sub = fw["subtitle"]
|
|
1103
|
+
controls = fw["controls"]
|
|
1104
|
+
all_fw_rules = [rid for c in controls for rid in c["rules"]]
|
|
1105
|
+
fw_violated = [rid for rid in all_fw_rules if rid in violated_ids]
|
|
1106
|
+
fw_pct = int(((len(all_fw_rules) - len(fw_violated)) / len(all_fw_rules)) * 100) if all_fw_rules else 100
|
|
1107
|
+
badge_cls = "badge-success" if fw_pct == 100 else ("badge-medium" if fw_pct >= 80 else "badge-critical")
|
|
1108
|
+
|
|
1109
|
+
ctrl_rows_html = ""
|
|
1110
|
+
for ctrl in controls:
|
|
1111
|
+
c_code = ctrl["code"]
|
|
1112
|
+
c_fams = ctrl["families"]
|
|
1113
|
+
c_rules = ctrl["rules"]
|
|
1114
|
+
c_viol = [r for r in c_rules if r in violated_ids]
|
|
1115
|
+
c_status = '<span class="badge badge-success">✔ Passing</span>' if not c_viol else f'<span class="badge badge-critical">{len(c_viol)} Violated</span>'
|
|
1116
|
+
|
|
1117
|
+
ctrl_rows_html += f"""
|
|
1118
|
+
<div class="comp-ctrl-row">
|
|
1119
|
+
<div>
|
|
1120
|
+
<div class="comp-ctrl-code">{c_code}</div>
|
|
1121
|
+
<div class="comp-ctrl-fams">{c_fams}</div>
|
|
1122
|
+
</div>
|
|
1123
|
+
<div>{c_status}</div>
|
|
1124
|
+
</div>"""
|
|
1125
|
+
|
|
1126
|
+
compliance_cards_html += f"""
|
|
1127
|
+
<div class="card comp-card">
|
|
1128
|
+
<div class="comp-card-header">
|
|
1129
|
+
<div>
|
|
1130
|
+
<div class="comp-title">{fw_name}</div>
|
|
1131
|
+
<div class="comp-sub">{fw_sub}</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
<span class="badge {badge_cls}">{fw_pct}% Met</span>
|
|
1134
|
+
</div>
|
|
1135
|
+
<div class="comp-ctrl-list">
|
|
1136
|
+
{ctrl_rows_html}
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>"""
|
|
1139
|
+
|
|
1140
|
+
# ── Root-Cause Architectural Clusters ──
|
|
1141
|
+
cluster_groups: dict[str, list[dict[str, Any]]] = {}
|
|
1142
|
+
for f in findings:
|
|
1143
|
+
c_key = f.get("cluster") or f.get("category") or "General Invariants"
|
|
1144
|
+
if c_key not in cluster_groups:
|
|
1145
|
+
cluster_groups[c_key] = []
|
|
1146
|
+
cluster_groups[c_key].append(f)
|
|
1147
|
+
|
|
1148
|
+
sorted_clusters = sorted(cluster_groups.items(), key=lambda x: len(x[1]), reverse=True)
|
|
1149
|
+
clusters_html = ""
|
|
1150
|
+
if sorted_clusters:
|
|
1151
|
+
for c_name, c_list in sorted_clusters[:8]:
|
|
1152
|
+
c_total = len(c_list)
|
|
1153
|
+
c_open = sum(1 for item in c_list if item.get("status", "open") not in ("resolved", "fixed", "confirmed_fixed"))
|
|
1154
|
+
c_resolved = c_total - c_open
|
|
1155
|
+
unique_files = len(set(item.get("file_path", "") for item in c_list if item.get("file_path")))
|
|
1156
|
+
if c_open == 0 and c_resolved > 0:
|
|
1157
|
+
badge_html = f'<span class="badge badge-success">✔ {c_resolved} Dismantled</span>'
|
|
1158
|
+
sub_text = f'Blast Radius: {unique_files} file(s) · <span style="color: var(--accent-green); font-weight: 600;">🟢 Neutralized & Defended</span>'
|
|
1159
|
+
else:
|
|
1160
|
+
badge_html = f'<span class="badge badge-high">{c_open} Active ({c_resolved} fixed)</span>'
|
|
1161
|
+
sub_text = f'Blast Radius: {unique_files} file(s) affected · Remediation in progress'
|
|
1162
|
+
|
|
1163
|
+
clusters_html += f"""
|
|
1164
|
+
<div class="cluster-item">
|
|
1165
|
+
<div class="cluster-header">
|
|
1166
|
+
<strong>{html.escape(c_name.replace('-', ' ').title())}</strong>
|
|
1167
|
+
{badge_html}
|
|
1168
|
+
</div>
|
|
1169
|
+
<div class="cluster-sub">{sub_text}</div>
|
|
1170
|
+
</div>"""
|
|
1171
|
+
else:
|
|
1172
|
+
clusters_html = '<div class="empty-state">✔ Zero architectural vulnerability clusters identified. Systemic invariants enforced.</div>'
|
|
1173
|
+
|
|
1174
|
+
# ── Interactive 2D Directory Attack Surface Heatmap Matrix ──
|
|
1175
|
+
HEATMAP_DOMAINS = [
|
|
1176
|
+
{"key": "auth", "label": "AUTH", "name": "Identity & Auth", "pfx": ("TG-AUTH", "TG-CSRF", "TG-WS")},
|
|
1177
|
+
{"key": "db", "label": "DB", "name": "Database & Isolation", "pfx": ("TG-DB", "TG-GQL", "TG-CACHE")},
|
|
1178
|
+
{"key": "input", "label": "INPUT", "name": "Input & LLM Defense", "pfx": ("TG-INPUT", "TG-AGENT")},
|
|
1179
|
+
{"key": "net", "label": "NET", "name": "Network & SSRF", "pfx": ("TG-SSRF", "TG-WEBHOOK", "TG-RATE")},
|
|
1180
|
+
{"key": "plat", "label": "PLAT", "name": "Platform & Supply", "pfx": ("TG-PLATFORM", "TG-SUPPLY", "TG-DIFF", "TG-EDGE")},
|
|
1181
|
+
{"key": "sec", "label": "SEC", "name": "Secrets & Keys", "pfx": ("TG-SEC", "TG-CLIENT", "TG-BIZ")},
|
|
1182
|
+
]
|
|
1183
|
+
|
|
1184
|
+
dir_matrix: dict[str, dict[str, Any]] = {}
|
|
1185
|
+
for f in findings:
|
|
1186
|
+
fp = f.get("file_path", "").replace("\\", "/")
|
|
1187
|
+
parts = fp.split("/")
|
|
1188
|
+
d_name = f"{parts[0]}/" if len(parts) > 1 else "root"
|
|
1189
|
+
rule_id = f.get("rule_id", "")
|
|
1190
|
+
domain_key = "sec"
|
|
1191
|
+
for d in HEATMAP_DOMAINS:
|
|
1192
|
+
if any(rule_id.startswith(p) for p in d["pfx"]):
|
|
1193
|
+
domain_key = d["key"]
|
|
1194
|
+
break
|
|
1195
|
+
if d_name not in dir_matrix:
|
|
1196
|
+
dir_matrix[d_name] = {
|
|
1197
|
+
"name": d_name,
|
|
1198
|
+
"domains": {d["key"]: {"open_crit": 0, "open_warn": 0, "resolved": 0} for d in HEATMAP_DOMAINS},
|
|
1199
|
+
"total_open": 0,
|
|
1200
|
+
"total_resolved": 0,
|
|
1201
|
+
}
|
|
1202
|
+
st = str(f.get("status", "open")).lower()
|
|
1203
|
+
sev = f.get("severity", "Medium")
|
|
1204
|
+
if st in ("resolved", "fixed", "confirmed_fixed"):
|
|
1205
|
+
dir_matrix[d_name]["domains"][domain_key]["resolved"] += 1
|
|
1206
|
+
dir_matrix[d_name]["total_resolved"] += 1
|
|
1207
|
+
else:
|
|
1208
|
+
if sev in ("Critical", "High"):
|
|
1209
|
+
dir_matrix[d_name]["domains"][domain_key]["open_crit"] += 1
|
|
1210
|
+
else:
|
|
1211
|
+
dir_matrix[d_name]["domains"][domain_key]["open_warn"] += 1
|
|
1212
|
+
dir_matrix[d_name]["total_open"] += 1
|
|
1213
|
+
|
|
1214
|
+
heatmap_rows_html = ""
|
|
1215
|
+
if dir_matrix:
|
|
1216
|
+
for d_name, d_data in sorted(dir_matrix.items(), key=lambda x: (x[1]["total_open"], x[1]["total_resolved"]), reverse=True)[:5]:
|
|
1217
|
+
escaped_d = html.escape(d_name)
|
|
1218
|
+
d_open = d_data["total_open"]
|
|
1219
|
+
d_res = d_data["total_resolved"]
|
|
1220
|
+
if d_open == 0 and d_res > 0:
|
|
1221
|
+
status_badge = f'<span class="heatmap-badge-clean">🟢 {d_res} Defended</span>'
|
|
1222
|
+
elif d_open > 0:
|
|
1223
|
+
status_badge = f'<span class="badge badge-high">🔴 {d_open} Active</span>'
|
|
1224
|
+
else:
|
|
1225
|
+
status_badge = '<span class="heatmap-badge-clean">Clean</span>'
|
|
1226
|
+
|
|
1227
|
+
cells_html = ""
|
|
1228
|
+
for d in HEATMAP_DOMAINS:
|
|
1229
|
+
cell = d_data["domains"][d["key"]]
|
|
1230
|
+
c_crit = cell["open_crit"]
|
|
1231
|
+
c_warn = cell["open_warn"]
|
|
1232
|
+
c_res = cell["resolved"]
|
|
1233
|
+
if c_crit > 0:
|
|
1234
|
+
cls = "heat-cell-crit"
|
|
1235
|
+
txt = f"🔴 {c_crit}"
|
|
1236
|
+
tip = f"{d_name} • {d['name']}: {c_crit} Critical/High flaws"
|
|
1237
|
+
elif c_warn > 0:
|
|
1238
|
+
cls = "heat-cell-warn"
|
|
1239
|
+
txt = f"🟡 {c_warn}"
|
|
1240
|
+
tip = f"{d_name} • {d['name']}: {c_warn} Warning flaws"
|
|
1241
|
+
elif c_res > 0:
|
|
1242
|
+
cls = "heat-cell-safe"
|
|
1243
|
+
txt = f"✔ {c_res}"
|
|
1244
|
+
tip = f"{d_name} • {d['name']}: {c_res} Invariants Hardened"
|
|
1245
|
+
else:
|
|
1246
|
+
cls = "heat-cell-idle"
|
|
1247
|
+
txt = "—"
|
|
1248
|
+
tip = f"{d_name} • {d['name']}: Clean / Zero Flaws"
|
|
1249
|
+
tip_escaped = html.escape(tip)
|
|
1250
|
+
cells_html += f"""<td class="heat-cell {cls}" title="{tip_escaped}" onclick="filterByDirectory('{escaped_d}'); event.stopPropagation();">{txt}</td>"""
|
|
1251
|
+
|
|
1252
|
+
heatmap_rows_html += f"""
|
|
1253
|
+
<tr class="heatmap-row" onclick="filterByDirectory('{escaped_d}')" title="Click to filter findings in {escaped_d}">
|
|
1254
|
+
<td class="heatmap-label-cell"><code>{escaped_d}</code></td>
|
|
1255
|
+
{cells_html}
|
|
1256
|
+
<td style="text-align: right;">{status_badge}</td>
|
|
1257
|
+
</tr>"""
|
|
1258
|
+
else:
|
|
1259
|
+
heatmap_rows_html = '<tr><td colspan="8" class="empty-state">✔ Zero directory attack surface exposure detected.</td></tr>'
|
|
1260
|
+
|
|
1261
|
+
# ── Golden Fix Recipes (Dual-View Diff / Split, Churn Metrics, Category Filter) ──
|
|
1262
|
+
# Category counts for filter pills
|
|
1263
|
+
recipe_cats: dict[str, int] = {}
|
|
1264
|
+
for r in recipes:
|
|
1265
|
+
c = r.get("category", "General")
|
|
1266
|
+
recipe_cats[c] = recipe_cats.get(c, 0) + 1
|
|
1267
|
+
|
|
1268
|
+
recipe_cat_pills_html = f'<span class="pill active" data-recipe-cat-filter="all" onclick="filterRecipeCategory(\'all\')">All ({len(recipes)})</span>'
|
|
1269
|
+
for c_name, c_cnt in sorted(recipe_cats.items()):
|
|
1270
|
+
c_slug = c_name.lower().replace(" ", "-")
|
|
1271
|
+
recipe_cat_pills_html += f'<span class="pill" data-recipe-cat-filter="{c_slug}" onclick="filterRecipeCategory(\'{c_slug}\')">{c_name} ({c_cnt})</span>'
|
|
1272
|
+
|
|
1273
|
+
recipes_html = ""
|
|
1274
|
+
for r in recipes:
|
|
1275
|
+
rid = html.escape(r.get("rule_id", "TG-FIX"))
|
|
1276
|
+
rtitle = html.escape(r.get("title", "Verified fix pattern"))
|
|
1277
|
+
rcat = html.escape(r.get("category", "General").lower().replace(" ", "-"))
|
|
1278
|
+
rec_key = html.escape(r.get("recipe_id", f"recipe-{rid}").replace(" ", "-"))
|
|
1279
|
+
metrics = r.get("ponytail_metrics") or {"additions": 1, "deletions": 1}
|
|
1280
|
+
adds = metrics.get("additions", 1)
|
|
1281
|
+
dels = metrics.get("deletions", 1)
|
|
1282
|
+
ftype = html.escape(r.get("file_type", ".ts"))
|
|
1283
|
+
vcount = r.get("verified_count", 1)
|
|
1284
|
+
|
|
1285
|
+
diff_str = r.get("diff_snippet") or ""
|
|
1286
|
+
before_str = r.get("before_snippet") or "// Vulnerable snippet"
|
|
1287
|
+
after_str = r.get("after_snippet") or r.get("solution_pattern") or "// Hardened pattern"
|
|
1288
|
+
|
|
1289
|
+
formatted_diff = format_diff_html(diff_str)
|
|
1290
|
+
escaped_before = html.escape(before_str)
|
|
1291
|
+
escaped_after = html.escape(after_str)
|
|
1292
|
+
copy_payload = html.escape((after_str or diff_str).replace("\n", " ").replace('"', '"'))
|
|
1293
|
+
|
|
1294
|
+
recipes_html += f"""
|
|
1295
|
+
<div class="recipe-card" data-category="{rcat}">
|
|
1296
|
+
<div class="recipe-header">
|
|
1297
|
+
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
|
1298
|
+
<span class="badge badge-success">{rid}</span>
|
|
1299
|
+
<strong>{rtitle}</strong>
|
|
1300
|
+
</div>
|
|
1301
|
+
<div class="recipe-meta-badges">
|
|
1302
|
+
<span class="chip chip-filetype">{ftype}</span>
|
|
1303
|
+
<span class="ponytail-badge" title="Ponytail line churn budget (≤ 35 additions, ≤ 25 deletions)">⚡ +{adds} / -{dels} lines</span>
|
|
1304
|
+
<span class="verified-badge">✔ Verified {vcount}x</span>
|
|
1305
|
+
</div>
|
|
1306
|
+
</div>
|
|
1307
|
+
<div class="recipe-view-switcher">
|
|
1308
|
+
<button class="recipe-tab-btn active" id="btn-view-diff-{rec_key}" onclick="setRecipeView('{rec_key}', 'diff')">Unified Diff</button>
|
|
1309
|
+
<button class="recipe-tab-btn" id="btn-view-split-{rec_key}" onclick="setRecipeView('{rec_key}', 'split')">Before / After</button>
|
|
1310
|
+
</div>
|
|
1311
|
+
<div id="recipe-view-diff-{rec_key}" class="code-preview recipe-diff-container">
|
|
1312
|
+
<pre><code>{formatted_diff}</code></pre>
|
|
1313
|
+
</div>
|
|
1314
|
+
<div id="recipe-view-split-{rec_key}" class="recipe-split-container" style="display: none;">
|
|
1315
|
+
<div class="split-pane">
|
|
1316
|
+
<div class="split-label split-del">✘ Before (Vulnerable)</div>
|
|
1317
|
+
<div class="code-preview"><pre><code>{escaped_before}</code></pre></div>
|
|
1318
|
+
</div>
|
|
1319
|
+
<div class="split-pane">
|
|
1320
|
+
<div class="split-label split-add">✔ After (Hardened)</div>
|
|
1321
|
+
<div class="code-preview"><pre><code>{escaped_after}</code></pre></div>
|
|
1322
|
+
</div>
|
|
1323
|
+
</div>
|
|
1324
|
+
<div class="recipe-actions">
|
|
1325
|
+
<div class="cmd-box">
|
|
1326
|
+
<code>npx torusguard harden --rule {rid}</code>
|
|
1327
|
+
<button class="btn-copy" onclick="copyText('npx torusguard harden --rule {rid}', this); event.stopPropagation();">📋 Copy Command</button>
|
|
1328
|
+
</div>
|
|
1329
|
+
<button class="btn btn-sm" onclick="copyText('{copy_payload}', this); event.stopPropagation();">📋 Copy Pattern</button>
|
|
1330
|
+
</div>
|
|
1331
|
+
</div>"""
|
|
1332
|
+
|
|
1333
|
+
# ── Prescriptive "Next Best Defenses" Advisory ──
|
|
1334
|
+
advisory_html = ""
|
|
1335
|
+
for adv in recommendations:
|
|
1336
|
+
arid = html.escape(adv["rule_id"])
|
|
1337
|
+
aprio = html.escape(adv.get("priority", "P1 Recommended"))
|
|
1338
|
+
atitle = html.escape(adv["title"])
|
|
1339
|
+
amitig = html.escape(adv.get("mitigates", "OWASP Invariant"))
|
|
1340
|
+
astack = html.escape(adv.get("stack_tag", "Polyglot"))
|
|
1341
|
+
arationale = html.escape(adv["rationale"])
|
|
1342
|
+
aguarantee = html.escape(adv.get("invariant_guarantee", "Guarantees zero architectural bypasses."))
|
|
1343
|
+
acmd = html.escape(adv["command"])
|
|
1344
|
+
|
|
1345
|
+
prio_cls = "badge-critical" if "P0" in aprio else ("badge-high" if "P1" in aprio else "badge-canary")
|
|
1346
|
+
|
|
1347
|
+
advisory_html += f"""
|
|
1348
|
+
<div class="advisory-item">
|
|
1349
|
+
<div class="advisory-top">
|
|
1350
|
+
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
|
1351
|
+
<span class="badge {prio_cls}">{aprio}</span>
|
|
1352
|
+
<span class="badge badge-primary">{arid}</span>
|
|
1353
|
+
<strong>{atitle}</strong>
|
|
1354
|
+
</div>
|
|
1355
|
+
<span class="chip chip-filetype">{astack}</span>
|
|
1356
|
+
</div>
|
|
1357
|
+
<div class="advisory-mitigates"><strong>Mitigates:</strong> {amitig}</div>
|
|
1358
|
+
<div class="advisory-desc">{arationale}</div>
|
|
1359
|
+
<div class="advisory-guarantee"><strong>Invariant Guarantee:</strong> {aguarantee}</div>
|
|
1360
|
+
<div class="cmd-box" style="margin-top: 8px;">
|
|
1361
|
+
<code>{acmd}</code>
|
|
1362
|
+
<button class="btn-copy" onclick="copyText('{acmd}', this); event.stopPropagation();">📋 Copy</button>
|
|
1363
|
+
</div>
|
|
1364
|
+
</div>"""
|
|
1365
|
+
|
|
1366
|
+
# ── Findings Section (Clean Baseline Card vs Active Attention) ──
|
|
1367
|
+
if open_count == 0:
|
|
1368
|
+
findings_section_html = f"""
|
|
1369
|
+
<!-- Clean & Hardened Baseline Summary (Zero Active Vulnerabilities) -->
|
|
1370
|
+
<div class="clean-baseline-card">
|
|
1371
|
+
<div class="clean-baseline-header">
|
|
1372
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
1373
|
+
<span style="font-size: 26px;">🛡️</span>
|
|
1374
|
+
<div>
|
|
1375
|
+
<div style="font-size: 15px; font-weight: 700; color: var(--accent-green); letter-spacing: -0.01em;">
|
|
1376
|
+
Clean & Hardened Baseline · 0 Active Vulnerabilities
|
|
1377
|
+
</div>
|
|
1378
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
|
1379
|
+
All {resolved_count} identified issues have been governed, hardened, and verified across all architectural layers.
|
|
1380
|
+
</div>
|
|
1381
|
+
</div>
|
|
1382
|
+
</div>
|
|
1383
|
+
<button class="btn-archive-toggle" id="btnArchiveToggle" onclick="toggleFindingsArchive()">
|
|
1384
|
+
<span id="archiveToggleIcon">▶</span> View Findings Archive ({total_discovered})
|
|
1385
|
+
</button>
|
|
1386
|
+
</div>
|
|
1387
|
+
<div id="findingsArchiveBody" style="display: block; margin-top: 16px; border-top: 1px solid var(--border-subtle); padding-top: 16px;">
|
|
1388
|
+
<div class="filter-bar">
|
|
1389
|
+
<div class="filter-pills">
|
|
1390
|
+
<span class="pill-group-label">Severity:</span>
|
|
1391
|
+
<span class="pill active" data-sev-filter="all" onclick="filterBySeverity('all')">All</span>
|
|
1392
|
+
<span class="pill" data-sev-filter="critical" onclick="filterBySeverity('critical')">🔴 Critical ({crit_open})</span>
|
|
1393
|
+
<span class="pill" data-sev-filter="high" onclick="filterBySeverity('high')">🟠 High ({high_open})</span>
|
|
1394
|
+
<span class="pill" data-sev-filter="medium" onclick="filterBySeverity('medium')">🟡 Med/Low ({med_open})</span>
|
|
1395
|
+
|
|
1396
|
+
<span class="pill-group-label" style="margin-left: 8px;">Status:</span>
|
|
1397
|
+
<span class="pill active" data-status-filter="all" onclick="filterByStatus('all')">All ({total_discovered})</span>
|
|
1398
|
+
<span class="pill" data-status-filter="open" onclick="filterByStatus('open')">⚠ Open ({open_count})</span>
|
|
1399
|
+
<span class="pill" data-status-filter="resolved" onclick="filterByStatus('resolved')">✔ Resolved ({resolved_count})</span>
|
|
1400
|
+
|
|
1401
|
+
<span class="dir-filter-chip" id="dirFilterChip" onclick="clearDirectoryFilter()">
|
|
1402
|
+
Folder: <strong id="dirFilterName"></strong> ×
|
|
1403
|
+
</span>
|
|
1404
|
+
<button class="btn-reset-filters" id="btnResetFilters" onclick="resetAllFilters()">× Reset Filters</button>
|
|
1405
|
+
</div>
|
|
1406
|
+
<div class="search-wrapper">
|
|
1407
|
+
<input type="text" id="searchInput" class="search-input" placeholder="Search rules, titles, files..." oninput="searchFindings()">
|
|
1408
|
+
<button class="search-clear-btn" id="searchClearBtn" onclick="clearSearch()">×</button>
|
|
1409
|
+
</div>
|
|
1410
|
+
</div>
|
|
1411
|
+
<div class="filter-stats-bar">
|
|
1412
|
+
<div>Showing <span id="visibleCount" style="font-weight: 700; color: var(--text-main);">{min(len(sorted_f), 50)}</span> of <span id="totalCount" style="font-weight: 700; color: var(--text-main);">{len(sorted_f)}</span> cataloged findings</div>
|
|
1413
|
+
<div id="activeFilterSummary" style="font-size: 11px; color: var(--accent-blue);"></div>
|
|
1414
|
+
</div>
|
|
1415
|
+
<div class="table-container">
|
|
1416
|
+
<table>
|
|
1417
|
+
<thead>
|
|
1418
|
+
<tr>
|
|
1419
|
+
<th style="width: 70px; text-align: center;">Simulate</th>
|
|
1420
|
+
<th style="width: 110px;">Severity</th>
|
|
1421
|
+
<th style="width: 130px;">Rule ID</th>
|
|
1422
|
+
<th>Finding Title</th>
|
|
1423
|
+
<th style="width: 250px;">File & Location</th>
|
|
1424
|
+
<th style="width: 90px;">Confidence</th>
|
|
1425
|
+
<th style="width: 30px;"></th>
|
|
1426
|
+
</tr>
|
|
1427
|
+
</thead>
|
|
1428
|
+
<tbody id="findingsTableBody">
|
|
1429
|
+
{findings_rows}
|
|
1430
|
+
</tbody>
|
|
1431
|
+
</table>
|
|
1432
|
+
</div>
|
|
1433
|
+
</div>
|
|
1434
|
+
</div>"""
|
|
1435
|
+
else:
|
|
1436
|
+
findings_section_html = f"""
|
|
1437
|
+
<!-- Active Vulnerabilities Requiring Attention -->
|
|
1438
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px;">
|
|
1439
|
+
<div class="section-title" style="margin-bottom: 0;">
|
|
1440
|
+
⚠ Active Vulnerabilities Requiring Attention ({open_count})
|
|
1441
|
+
</div>
|
|
1442
|
+
<button class="btn-archive-toggle" id="btnArchiveToggle" onclick="toggleFindingsArchive()">
|
|
1443
|
+
<span id="archiveToggleIcon">▶</span> View Resolved Archive ({resolved_count})
|
|
1444
|
+
</button>
|
|
1445
|
+
</div>
|
|
1446
|
+
<div class="filter-bar">
|
|
1447
|
+
<div class="filter-pills">
|
|
1448
|
+
<span class="pill-group-label">Severity:</span>
|
|
1449
|
+
<span class="pill active" data-sev-filter="all" onclick="filterBySeverity('all')">All</span>
|
|
1450
|
+
<span class="pill" data-sev-filter="critical" onclick="filterBySeverity('critical')">🔴 Critical ({crit_open})</span>
|
|
1451
|
+
<span class="pill" data-sev-filter="high" onclick="filterBySeverity('high')">🟠 High ({high_open})</span>
|
|
1452
|
+
<span class="pill" data-sev-filter="medium" onclick="filterBySeverity('medium')">🟡 Med/Low ({med_open})</span>
|
|
1453
|
+
|
|
1454
|
+
<span class="pill-group-label" style="margin-left: 8px;">Status:</span>
|
|
1455
|
+
<span class="pill active" data-status-filter="all" onclick="filterByStatus('all')">All ({total_discovered})</span>
|
|
1456
|
+
<span class="pill" data-status-filter="open" onclick="filterByStatus('open')">⚠ Open ({open_count})</span>
|
|
1457
|
+
<span class="pill" data-status-filter="resolved" onclick="filterByStatus('resolved')">✔ Resolved ({resolved_count})</span>
|
|
1458
|
+
|
|
1459
|
+
<span class="dir-filter-chip" id="dirFilterChip" onclick="clearDirectoryFilter()">
|
|
1460
|
+
Folder: <strong id="dirFilterName"></strong> ×
|
|
1461
|
+
</span>
|
|
1462
|
+
<button class="btn-reset-filters" id="btnResetFilters" onclick="resetAllFilters()">× Reset Filters</button>
|
|
1463
|
+
</div>
|
|
1464
|
+
<div class="search-wrapper">
|
|
1465
|
+
<input type="text" id="searchInput" class="search-input" placeholder="Search rules, titles, files..." oninput="searchFindings()">
|
|
1466
|
+
<button class="search-clear-btn" id="searchClearBtn" onclick="clearSearch()">×</button>
|
|
1467
|
+
</div>
|
|
1468
|
+
</div>
|
|
1469
|
+
<div class="filter-stats-bar">
|
|
1470
|
+
<div>Showing <span id="visibleCount" style="font-weight: 700; color: var(--text-main);">{min(len(sorted_f), 50)}</span> of <span id="totalCount" style="font-weight: 700; color: var(--text-main);">{len(sorted_f)}</span> cataloged findings</div>
|
|
1471
|
+
<div id="activeFilterSummary" style="font-size: 11px; color: var(--accent-blue);"></div>
|
|
1472
|
+
</div>
|
|
1473
|
+
<div class="table-container">
|
|
1474
|
+
<table>
|
|
1475
|
+
<thead>
|
|
1476
|
+
<tr>
|
|
1477
|
+
<th style="width: 70px; text-align: center;">Simulate</th>
|
|
1478
|
+
<th style="width: 110px;">Severity</th>
|
|
1479
|
+
<th style="width: 130px;">Rule ID</th>
|
|
1480
|
+
<th>Finding Title</th>
|
|
1481
|
+
<th style="width: 250px;">File & Location</th>
|
|
1482
|
+
<th style="width: 90px;">Confidence</th>
|
|
1483
|
+
<th style="width: 30px;"></th>
|
|
1484
|
+
</tr>
|
|
1485
|
+
</thead>
|
|
1486
|
+
<tbody id="findingsTableBody">
|
|
1487
|
+
{findings_rows}
|
|
1488
|
+
</tbody>
|
|
1489
|
+
</table>
|
|
1490
|
+
</div>
|
|
1491
|
+
<div id="findingsArchiveBody" style="display: block;"></div>"""
|
|
1492
|
+
|
|
1493
|
+
# ── Client-side JSON Payloads ──
|
|
1494
|
+
findings_json_str = json.dumps(findings).replace("</script>", "<\\/script>")
|
|
1495
|
+
sarif_rules_json_str = json.dumps([{"id": r["rule_id"], "shortDescription": {"text": r["title"]}} for r in all_rules[:74]])
|
|
1496
|
+
rule_catalog_json_str = json.dumps({r["rule_id"]: r for r in all_rules}).replace("</script>", "<\\/script>")
|
|
1497
|
+
violated_ids_json_str = json.dumps(list(violated_ids))
|
|
1498
|
+
|
|
1499
|
+
return f"""<!DOCTYPE html>
|
|
1500
|
+
<html lang="en">
|
|
1501
|
+
<head>
|
|
1502
|
+
<meta charset="UTF-8">
|
|
1503
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1504
|
+
<title>TorusGuard Security Report · {html.escape(telemetry.get('project_name', 'Project'))}</title>
|
|
1505
|
+
<style>
|
|
1506
|
+
:root {{
|
|
1507
|
+
--bg-dark: #0d1117;
|
|
1508
|
+
--bg-surface: #161b22;
|
|
1509
|
+
--bg-card: #21262d;
|
|
1510
|
+
--border-subtle: #30363d;
|
|
1511
|
+
--text-main: #f0f6fc;
|
|
1512
|
+
--text-muted: #8b949e;
|
|
1513
|
+
--accent-blue: #58a6ff;
|
|
1514
|
+
--accent-green: #3fb950;
|
|
1515
|
+
--accent-yellow: #d29922;
|
|
1516
|
+
--accent-red: #f85149;
|
|
1517
|
+
--accent-orange: #db6d28;
|
|
1518
|
+
--accent-purple: #bc8cff;
|
|
1519
|
+
--font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
1520
|
+
}}
|
|
1521
|
+
body.light-theme {{
|
|
1522
|
+
--bg-dark: #f6f8fa;
|
|
1523
|
+
--bg-surface: #ffffff;
|
|
1524
|
+
--bg-card: #f0f3f6;
|
|
1525
|
+
--border-subtle: #d0d7de;
|
|
1526
|
+
--text-main: #1f2328;
|
|
1527
|
+
--text-muted: #656d76;
|
|
1528
|
+
}}
|
|
1529
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
1530
|
+
body {{
|
|
1531
|
+
background: var(--bg-dark);
|
|
1532
|
+
color: var(--text-main);
|
|
1533
|
+
font-family: var(--font-stack);
|
|
1534
|
+
line-height: 1.5;
|
|
1535
|
+
padding: 24px;
|
|
1536
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
1537
|
+
}}
|
|
1538
|
+
.container {{ max-width: 1240px; margin: 0 auto; }}
|
|
1539
|
+
|
|
1540
|
+
/* Header */
|
|
1541
|
+
header {{
|
|
1542
|
+
display: flex;
|
|
1543
|
+
justify-content: space-between;
|
|
1544
|
+
align-items: center;
|
|
1545
|
+
padding-bottom: 20px;
|
|
1546
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
1547
|
+
margin-bottom: 24px;
|
|
1548
|
+
flex-wrap: wrap;
|
|
1549
|
+
gap: 12px;
|
|
1550
|
+
}}
|
|
1551
|
+
.logo-group h1 {{
|
|
1552
|
+
font-size: 22px;
|
|
1553
|
+
font-weight: 700;
|
|
1554
|
+
display: flex;
|
|
1555
|
+
align-items: center;
|
|
1556
|
+
gap: 8px;
|
|
1557
|
+
}}
|
|
1558
|
+
.shield-icon {{ font-size: 24px; }}
|
|
1559
|
+
.project-meta {{ color: var(--text-muted); font-size: 13px; margin-top: 4px; }}
|
|
1560
|
+
.header-actions {{ display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }}
|
|
1561
|
+
.status-badge {{
|
|
1562
|
+
padding: 6px 14px;
|
|
1563
|
+
border-radius: 20px;
|
|
1564
|
+
font-size: 12px;
|
|
1565
|
+
font-weight: 600;
|
|
1566
|
+
}}
|
|
1567
|
+
.status-clean {{ background: rgba(63, 185, 80, 0.15); border: 1px solid var(--accent-green); color: var(--accent-green); }}
|
|
1568
|
+
.status-elevated {{ background: rgba(88, 166, 255, 0.15); border: 1px solid var(--accent-blue); color: var(--accent-blue); }}
|
|
1569
|
+
.status-warn {{ background: rgba(210, 153, 34, 0.15); border: 1px solid var(--accent-yellow); color: var(--accent-yellow); }}
|
|
1570
|
+
.status-critical {{ background: rgba(248, 81, 73, 0.15); border: 1px solid var(--accent-red); color: var(--accent-red); }}
|
|
1571
|
+
|
|
1572
|
+
.btn {{
|
|
1573
|
+
background: var(--bg-card);
|
|
1574
|
+
border: 1px solid var(--border-subtle);
|
|
1575
|
+
color: var(--text-main);
|
|
1576
|
+
padding: 6px 12px;
|
|
1577
|
+
border-radius: 6px;
|
|
1578
|
+
font-size: 12px;
|
|
1579
|
+
font-weight: 500;
|
|
1580
|
+
cursor: pointer;
|
|
1581
|
+
display: inline-flex;
|
|
1582
|
+
align-items: center;
|
|
1583
|
+
gap: 6px;
|
|
1584
|
+
transition: background 0.2s, border-color 0.2s;
|
|
1585
|
+
}}
|
|
1586
|
+
.btn:hover {{ background: #30363d; border-color: var(--text-muted); }}
|
|
1587
|
+
body.light-theme .btn:hover {{ background: #e1e4e8; }}
|
|
1588
|
+
.btn-sm {{ padding: 3px 8px; font-size: 11px; }}
|
|
1589
|
+
|
|
1590
|
+
/* KPI Grid */
|
|
1591
|
+
.grid-kpi {{
|
|
1592
|
+
display: grid;
|
|
1593
|
+
grid-template-columns: 220px repeat(4, 1fr);
|
|
1594
|
+
gap: 14px;
|
|
1595
|
+
margin-bottom: 24px;
|
|
1596
|
+
}}
|
|
1597
|
+
@media (max-width: 950px) {{ .grid-kpi {{ grid-template-columns: 1fr 1fr; }} }}
|
|
1598
|
+
@media (max-width: 600px) {{ .grid-kpi {{ grid-template-columns: 1fr; }} }}
|
|
1599
|
+
|
|
1600
|
+
.card {{
|
|
1601
|
+
background: var(--bg-surface);
|
|
1602
|
+
border: 1px solid var(--border-subtle);
|
|
1603
|
+
border-radius: 8px;
|
|
1604
|
+
padding: 18px;
|
|
1605
|
+
}}
|
|
1606
|
+
.gauge-card {{
|
|
1607
|
+
display: flex;
|
|
1608
|
+
flex-direction: column;
|
|
1609
|
+
align-items: center;
|
|
1610
|
+
justify-content: center;
|
|
1611
|
+
text-align: center;
|
|
1612
|
+
}}
|
|
1613
|
+
.gauge-svg {{ width: 110px; height: 110px; transform: rotate(-90deg); filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.3)); }}
|
|
1614
|
+
.gauge-bg {{ fill: none; stroke: var(--bg-card); stroke-width: 8; }}
|
|
1615
|
+
.gauge-fill {{
|
|
1616
|
+
fill: none;
|
|
1617
|
+
stroke-width: 8;
|
|
1618
|
+
stroke-linecap: round;
|
|
1619
|
+
stroke-dasharray: 251.2;
|
|
1620
|
+
stroke-dashoffset: 251.2;
|
|
1621
|
+
transition: stroke-dashoffset 0.9s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1622
|
+
}}
|
|
1623
|
+
.gauge-value {{
|
|
1624
|
+
position: absolute;
|
|
1625
|
+
font-size: 30px;
|
|
1626
|
+
font-weight: 700;
|
|
1627
|
+
color: var(--text-main);
|
|
1628
|
+
letter-spacing: -0.5px;
|
|
1629
|
+
}}
|
|
1630
|
+
.sev-card {{ text-align: center; cursor: pointer; transition: transform 0.15s, border-color 0.2s; }}
|
|
1631
|
+
.sev-card:hover {{ transform: translateY(-2px); border-color: var(--accent-blue); }}
|
|
1632
|
+
.sev-card .sev-count {{ font-size: 34px; font-weight: 800; }}
|
|
1633
|
+
.sev-card .sev-label {{ font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); margin-top: 4px; }}
|
|
1634
|
+
.sev-sub-badge {{
|
|
1635
|
+
font-size: 11px;
|
|
1636
|
+
font-weight: 600;
|
|
1637
|
+
margin-top: 6px;
|
|
1638
|
+
padding: 2px 8px;
|
|
1639
|
+
border-radius: 12px;
|
|
1640
|
+
background: rgba(88, 166, 255, 0.1);
|
|
1641
|
+
border: 1px solid rgba(88, 166, 255, 0.25);
|
|
1642
|
+
color: var(--accent-blue);
|
|
1643
|
+
display: inline-block;
|
|
1644
|
+
}}
|
|
1645
|
+
.sev-sub-badge.badge-sub-clean {{
|
|
1646
|
+
background: rgba(63, 185, 80, 0.15);
|
|
1647
|
+
border-color: rgba(63, 185, 80, 0.4);
|
|
1648
|
+
color: var(--accent-green);
|
|
1649
|
+
}}
|
|
1650
|
+
.sev-sub-badge.badge-sub-warn {{
|
|
1651
|
+
background: rgba(210, 153, 34, 0.15);
|
|
1652
|
+
border-color: rgba(210, 153, 34, 0.4);
|
|
1653
|
+
color: var(--accent-yellow);
|
|
1654
|
+
}}
|
|
1655
|
+
|
|
1656
|
+
/* Governed Remediation Ledger */
|
|
1657
|
+
.remediation-ledger-card {{
|
|
1658
|
+
margin-bottom: 24px;
|
|
1659
|
+
background: var(--bg-surface);
|
|
1660
|
+
border: 1px solid var(--border-subtle);
|
|
1661
|
+
border-radius: 8px;
|
|
1662
|
+
padding: 18px;
|
|
1663
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
1664
|
+
border-top: 3px solid var(--accent-green);
|
|
1665
|
+
}}
|
|
1666
|
+
.remediation-header {{
|
|
1667
|
+
display: flex;
|
|
1668
|
+
justify-content: space-between;
|
|
1669
|
+
align-items: center;
|
|
1670
|
+
flex-wrap: wrap;
|
|
1671
|
+
gap: 12px;
|
|
1672
|
+
margin-bottom: 14px;
|
|
1673
|
+
}}
|
|
1674
|
+
.remediation-title-group {{
|
|
1675
|
+
display: flex;
|
|
1676
|
+
align-items: center;
|
|
1677
|
+
gap: 10px;
|
|
1678
|
+
}}
|
|
1679
|
+
.rem-progress-bar-bg {{
|
|
1680
|
+
width: 100%;
|
|
1681
|
+
height: 8px;
|
|
1682
|
+
background: var(--bg-card);
|
|
1683
|
+
border-radius: 4px;
|
|
1684
|
+
overflow: hidden;
|
|
1685
|
+
margin-bottom: 16px;
|
|
1686
|
+
}}
|
|
1687
|
+
.rem-progress-bar-fill {{
|
|
1688
|
+
height: 100%;
|
|
1689
|
+
background: linear-gradient(90deg, #2ea043, #3fb950);
|
|
1690
|
+
border-radius: 4px;
|
|
1691
|
+
transition: width 0.8s ease;
|
|
1692
|
+
}}
|
|
1693
|
+
.rem-stat-chips-grid {{
|
|
1694
|
+
display: grid;
|
|
1695
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1696
|
+
gap: 12px;
|
|
1697
|
+
}}
|
|
1698
|
+
@media (max-width: 900px) {{ .rem-stat-chips-grid {{ grid-template-columns: 1fr 1fr; }} }}
|
|
1699
|
+
@media (max-width: 550px) {{ .rem-stat-chips-grid {{ grid-template-columns: 1fr; }} }}
|
|
1700
|
+
.rem-chip {{
|
|
1701
|
+
background: var(--bg-card);
|
|
1702
|
+
border: 1px solid var(--border-subtle);
|
|
1703
|
+
border-radius: 6px;
|
|
1704
|
+
padding: 12px;
|
|
1705
|
+
display: flex;
|
|
1706
|
+
flex-direction: column;
|
|
1707
|
+
gap: 6px;
|
|
1708
|
+
}}
|
|
1709
|
+
.rem-chip-top {{
|
|
1710
|
+
display: flex;
|
|
1711
|
+
justify-content: space-between;
|
|
1712
|
+
align-items: center;
|
|
1713
|
+
}}
|
|
1714
|
+
.rem-chip-sev {{ font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; }}
|
|
1715
|
+
.rem-chip-status {{ font-size: 11px; font-weight: 600; }}
|
|
1716
|
+
.rem-chip-main {{
|
|
1717
|
+
display: flex;
|
|
1718
|
+
align-items: baseline;
|
|
1719
|
+
gap: 6px;
|
|
1720
|
+
}}
|
|
1721
|
+
.rem-chip-num {{ font-size: 22px; font-weight: 800; color: var(--text-main); }}
|
|
1722
|
+
.rem-chip-lbl {{ font-size: 11px; color: var(--text-muted); }}
|
|
1723
|
+
.rem-chip-bar {{
|
|
1724
|
+
width: 100%;
|
|
1725
|
+
height: 4px;
|
|
1726
|
+
background: rgba(255, 255, 255, 0.05);
|
|
1727
|
+
border-radius: 2px;
|
|
1728
|
+
overflow: hidden;
|
|
1729
|
+
margin-top: 2px;
|
|
1730
|
+
}}
|
|
1731
|
+
.rem-chip-bar-fill {{ height: 100%; border-radius: 2px; }}
|
|
1732
|
+
.rem-chip-total {{
|
|
1733
|
+
border-color: rgba(63, 185, 80, 0.35);
|
|
1734
|
+
background: rgba(63, 185, 80, 0.05);
|
|
1735
|
+
}}
|
|
1736
|
+
.heatmap-badge-clean {{
|
|
1737
|
+
font-size: 10px;
|
|
1738
|
+
font-weight: 700;
|
|
1739
|
+
color: var(--accent-green);
|
|
1740
|
+
background: rgba(63, 185, 80, 0.12);
|
|
1741
|
+
border: 1px solid rgba(63, 185, 80, 0.3);
|
|
1742
|
+
border-radius: 4px;
|
|
1743
|
+
padding: 1px 6px;
|
|
1744
|
+
}}
|
|
1745
|
+
|
|
1746
|
+
/* What-If Posture Simulator Card */
|
|
1747
|
+
.simulator-card {{
|
|
1748
|
+
margin-bottom: 24px;
|
|
1749
|
+
background: var(--bg-surface);
|
|
1750
|
+
border: 1px solid var(--border-subtle);
|
|
1751
|
+
border-left: 4px solid var(--accent-blue);
|
|
1752
|
+
display: flex;
|
|
1753
|
+
justify-content: space-between;
|
|
1754
|
+
align-items: center;
|
|
1755
|
+
flex-wrap: wrap;
|
|
1756
|
+
gap: 12px;
|
|
1757
|
+
padding: 14px 18px;
|
|
1758
|
+
}}
|
|
1759
|
+
.simulator-clean {{ border-left-color: var(--accent-green); }}
|
|
1760
|
+
.sim-stats {{ display: flex; align-items: center; gap: 10px; font-size: 13px; }}
|
|
1761
|
+
.sim-actions {{ display: flex; gap: 6px; flex-wrap: wrap; }}
|
|
1762
|
+
.sim-checkbox-label {{
|
|
1763
|
+
display: inline-flex;
|
|
1764
|
+
align-items: center;
|
|
1765
|
+
gap: 4px;
|
|
1766
|
+
cursor: pointer;
|
|
1767
|
+
font-size: 11px;
|
|
1768
|
+
color: var(--text-muted);
|
|
1769
|
+
}}
|
|
1770
|
+
.sim-fix-chk {{ cursor: pointer; accent-color: var(--accent-blue); }}
|
|
1771
|
+
|
|
1772
|
+
/* Invariant Matrix */
|
|
1773
|
+
.defended-section {{
|
|
1774
|
+
background: var(--bg-surface);
|
|
1775
|
+
border: 1px solid var(--border-subtle);
|
|
1776
|
+
border-radius: 8px;
|
|
1777
|
+
padding: 18px;
|
|
1778
|
+
margin-bottom: 24px;
|
|
1779
|
+
}}
|
|
1780
|
+
.defended-header {{
|
|
1781
|
+
display: flex;
|
|
1782
|
+
justify-content: space-between;
|
|
1783
|
+
align-items: center;
|
|
1784
|
+
margin-bottom: 14px;
|
|
1785
|
+
cursor: pointer;
|
|
1786
|
+
}}
|
|
1787
|
+
.coverage-bar-container {{
|
|
1788
|
+
background: var(--bg-card);
|
|
1789
|
+
border: 1px solid var(--border-subtle);
|
|
1790
|
+
border-radius: 6px;
|
|
1791
|
+
padding: 10px 14px;
|
|
1792
|
+
margin-bottom: 16px;
|
|
1793
|
+
}}
|
|
1794
|
+
.coverage-stats {{
|
|
1795
|
+
display: flex;
|
|
1796
|
+
justify-content: space-between;
|
|
1797
|
+
align-items: center;
|
|
1798
|
+
font-size: 12px;
|
|
1799
|
+
color: var(--text-muted);
|
|
1800
|
+
margin-bottom: 6px;
|
|
1801
|
+
}}
|
|
1802
|
+
.coverage-progress-bg {{
|
|
1803
|
+
height: 8px;
|
|
1804
|
+
background: rgba(255, 255, 255, 0.05);
|
|
1805
|
+
border-radius: 4px;
|
|
1806
|
+
overflow: hidden;
|
|
1807
|
+
}}
|
|
1808
|
+
.coverage-progress-fill {{
|
|
1809
|
+
height: 100%;
|
|
1810
|
+
background: var(--accent-green);
|
|
1811
|
+
border-radius: 4px;
|
|
1812
|
+
transition: width 0.6s ease;
|
|
1813
|
+
}}
|
|
1814
|
+
.inv-search-bar {{
|
|
1815
|
+
margin-bottom: 16px;
|
|
1816
|
+
display: flex;
|
|
1817
|
+
gap: 10px;
|
|
1818
|
+
align-items: center;
|
|
1819
|
+
}}
|
|
1820
|
+
.layers-container {{
|
|
1821
|
+
display: flex;
|
|
1822
|
+
flex-direction: column;
|
|
1823
|
+
gap: 12px;
|
|
1824
|
+
}}
|
|
1825
|
+
.layer-card {{
|
|
1826
|
+
background: var(--bg-card);
|
|
1827
|
+
border: 1px solid var(--border-subtle);
|
|
1828
|
+
border-radius: 6px;
|
|
1829
|
+
padding: 12px 14px;
|
|
1830
|
+
}}
|
|
1831
|
+
.layer-header {{
|
|
1832
|
+
display: flex;
|
|
1833
|
+
justify-content: space-between;
|
|
1834
|
+
align-items: center;
|
|
1835
|
+
margin-bottom: 10px;
|
|
1836
|
+
}}
|
|
1837
|
+
.layer-title-group {{ display: flex; align-items: center; gap: 8px; }}
|
|
1838
|
+
.layer-icon {{ font-size: 16px; }}
|
|
1839
|
+
.layer-name {{ font-size: 13px; font-weight: 700; color: var(--text-main); }}
|
|
1840
|
+
.layer-desc {{ font-size: 11px; color: var(--text-muted); }}
|
|
1841
|
+
.layer-badge {{
|
|
1842
|
+
font-size: 11px;
|
|
1843
|
+
font-weight: 600;
|
|
1844
|
+
padding: 2px 8px;
|
|
1845
|
+
border-radius: 10px;
|
|
1846
|
+
}}
|
|
1847
|
+
.layer-chips-grid {{
|
|
1848
|
+
display: flex;
|
|
1849
|
+
flex-wrap: wrap;
|
|
1850
|
+
gap: 6px;
|
|
1851
|
+
}}
|
|
1852
|
+
.inv-chip {{
|
|
1853
|
+
background: var(--bg-surface);
|
|
1854
|
+
border: 1px solid var(--border-subtle);
|
|
1855
|
+
border-radius: 4px;
|
|
1856
|
+
padding: 4px 8px;
|
|
1857
|
+
font-size: 11px;
|
|
1858
|
+
display: inline-flex;
|
|
1859
|
+
align-items: center;
|
|
1860
|
+
gap: 6px;
|
|
1861
|
+
cursor: pointer;
|
|
1862
|
+
transition: all 0.15s ease;
|
|
1863
|
+
}}
|
|
1864
|
+
.inv-chip:hover {{
|
|
1865
|
+
border-color: var(--accent-blue);
|
|
1866
|
+
transform: translateY(-1px);
|
|
1867
|
+
}}
|
|
1868
|
+
.inv-chip-safe {{ border-left: 2px solid var(--accent-green); }}
|
|
1869
|
+
.inv-chip-violated {{ border-left: 2px solid var(--accent-red); background: rgba(248, 81, 73, 0.08); }}
|
|
1870
|
+
.inv-dot {{ width: 6px; height: 6px; border-radius: 50%; }}
|
|
1871
|
+
.inv-code {{ font-family: monospace; font-weight: 600; color: var(--accent-blue); font-size: 11px; }}
|
|
1872
|
+
.inv-title {{ color: var(--text-main); max-width: 140px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
|
1873
|
+
|
|
1874
|
+
/* Modal / Drawer for Invariant Inspection */
|
|
1875
|
+
.modal-backdrop {{
|
|
1876
|
+
position: fixed;
|
|
1877
|
+
top: 0;
|
|
1878
|
+
left: 0;
|
|
1879
|
+
width: 100vw;
|
|
1880
|
+
height: 100vh;
|
|
1881
|
+
background: rgba(0, 0, 0, 0.7);
|
|
1882
|
+
backdrop-filter: blur(2px);
|
|
1883
|
+
z-index: 9999;
|
|
1884
|
+
display: flex;
|
|
1885
|
+
align-items: center;
|
|
1886
|
+
justify-content: center;
|
|
1887
|
+
padding: 20px;
|
|
1888
|
+
}}
|
|
1889
|
+
.modal-card {{
|
|
1890
|
+
background: var(--bg-surface);
|
|
1891
|
+
border: 1px solid var(--border-subtle);
|
|
1892
|
+
border-radius: 8px;
|
|
1893
|
+
max-width: 600px;
|
|
1894
|
+
width: 100%;
|
|
1895
|
+
padding: 22px;
|
|
1896
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
|
1897
|
+
}}
|
|
1898
|
+
.modal-header {{
|
|
1899
|
+
display: flex;
|
|
1900
|
+
justify-content: space-between;
|
|
1901
|
+
align-items: flex-start;
|
|
1902
|
+
margin-bottom: 14px;
|
|
1903
|
+
}}
|
|
1904
|
+
.modal-close-btn {{
|
|
1905
|
+
background: transparent;
|
|
1906
|
+
border: none;
|
|
1907
|
+
color: var(--text-muted);
|
|
1908
|
+
font-size: 18px;
|
|
1909
|
+
cursor: pointer;
|
|
1910
|
+
}}
|
|
1911
|
+
.modal-close-btn:hover {{ color: var(--text-main); }}
|
|
1912
|
+
.modal-body {{ font-size: 13px; color: var(--text-main); display: flex; flex-direction: column; gap: 12px; }}
|
|
1913
|
+
|
|
1914
|
+
/* Filter & Search Bar */
|
|
1915
|
+
.filter-bar {{
|
|
1916
|
+
display: flex;
|
|
1917
|
+
justify-content: space-between;
|
|
1918
|
+
align-items: center;
|
|
1919
|
+
margin-bottom: 8px;
|
|
1920
|
+
flex-wrap: wrap;
|
|
1921
|
+
gap: 10px;
|
|
1922
|
+
}}
|
|
1923
|
+
.filter-pills {{ display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }}
|
|
1924
|
+
.pill-group-label {{
|
|
1925
|
+
font-size: 11px;
|
|
1926
|
+
font-weight: 600;
|
|
1927
|
+
text-transform: uppercase;
|
|
1928
|
+
letter-spacing: 0.5px;
|
|
1929
|
+
color: var(--text-muted);
|
|
1930
|
+
margin-right: 2px;
|
|
1931
|
+
}}
|
|
1932
|
+
.pill {{
|
|
1933
|
+
background: var(--bg-card);
|
|
1934
|
+
border: 1px solid var(--border-subtle);
|
|
1935
|
+
color: var(--text-muted);
|
|
1936
|
+
padding: 4px 10px;
|
|
1937
|
+
border-radius: 14px;
|
|
1938
|
+
font-size: 12px;
|
|
1939
|
+
font-weight: 500;
|
|
1940
|
+
cursor: pointer;
|
|
1941
|
+
transition: all 0.15s ease;
|
|
1942
|
+
user-select: none;
|
|
1943
|
+
}}
|
|
1944
|
+
.pill:hover {{ color: var(--text-main); border-color: var(--text-muted); }}
|
|
1945
|
+
.pill.active {{
|
|
1946
|
+
background: var(--accent-blue);
|
|
1947
|
+
border-color: var(--accent-blue);
|
|
1948
|
+
color: #fff;
|
|
1949
|
+
}}
|
|
1950
|
+
.dir-filter-chip {{
|
|
1951
|
+
display: none;
|
|
1952
|
+
background: rgba(88, 166, 255, 0.15);
|
|
1953
|
+
border: 1px solid var(--accent-blue);
|
|
1954
|
+
color: var(--accent-blue);
|
|
1955
|
+
font-size: 11px;
|
|
1956
|
+
padding: 3px 8px;
|
|
1957
|
+
border-radius: 12px;
|
|
1958
|
+
cursor: pointer;
|
|
1959
|
+
}}
|
|
1960
|
+
.btn-reset-filters {{
|
|
1961
|
+
display: none;
|
|
1962
|
+
background: rgba(248, 81, 73, 0.12);
|
|
1963
|
+
border: 1px solid rgba(248, 81, 73, 0.35);
|
|
1964
|
+
color: var(--accent-red);
|
|
1965
|
+
font-size: 11px;
|
|
1966
|
+
font-weight: 600;
|
|
1967
|
+
padding: 3px 8px;
|
|
1968
|
+
border-radius: 12px;
|
|
1969
|
+
cursor: pointer;
|
|
1970
|
+
margin-left: 4px;
|
|
1971
|
+
transition: all 0.15s;
|
|
1972
|
+
}}
|
|
1973
|
+
.btn-reset-filters:hover {{ background: rgba(248, 81, 73, 0.25); }}
|
|
1974
|
+
.search-wrapper {{
|
|
1975
|
+
position: relative;
|
|
1976
|
+
display: inline-flex;
|
|
1977
|
+
align-items: center;
|
|
1978
|
+
}}
|
|
1979
|
+
.search-input {{
|
|
1980
|
+
background: var(--bg-card);
|
|
1981
|
+
border: 1px solid var(--border-subtle);
|
|
1982
|
+
color: var(--text-main);
|
|
1983
|
+
padding: 7px 28px 7px 12px;
|
|
1984
|
+
border-radius: 6px;
|
|
1985
|
+
font-size: 13px;
|
|
1986
|
+
width: 280px;
|
|
1987
|
+
outline: none;
|
|
1988
|
+
transition: border-color 0.15s;
|
|
1989
|
+
}}
|
|
1990
|
+
.search-input:focus {{ border-color: var(--accent-blue); }}
|
|
1991
|
+
.search-clear-btn {{
|
|
1992
|
+
position: absolute;
|
|
1993
|
+
right: 8px;
|
|
1994
|
+
background: none;
|
|
1995
|
+
border: none;
|
|
1996
|
+
color: var(--text-muted);
|
|
1997
|
+
cursor: pointer;
|
|
1998
|
+
font-size: 14px;
|
|
1999
|
+
line-height: 1;
|
|
2000
|
+
padding: 2px 4px;
|
|
2001
|
+
display: none;
|
|
2002
|
+
}}
|
|
2003
|
+
.search-clear-btn:hover {{ color: var(--text-main); }}
|
|
2004
|
+
.filter-stats-bar {{
|
|
2005
|
+
font-size: 12px;
|
|
2006
|
+
color: var(--text-muted);
|
|
2007
|
+
margin-bottom: 14px;
|
|
2008
|
+
padding-left: 2px;
|
|
2009
|
+
display: flex;
|
|
2010
|
+
align-items: center;
|
|
2011
|
+
justify-content: space-between;
|
|
2012
|
+
}}
|
|
2013
|
+
|
|
2014
|
+
/* Clean Baseline Card & Findings Archive */
|
|
2015
|
+
.clean-baseline-card {{
|
|
2016
|
+
background: var(--bg-surface);
|
|
2017
|
+
border: 1px solid var(--border-subtle);
|
|
2018
|
+
border-left: 4px solid var(--accent-green);
|
|
2019
|
+
border-radius: 8px;
|
|
2020
|
+
padding: 16px 20px;
|
|
2021
|
+
margin-bottom: 24px;
|
|
2022
|
+
transition: border-color 0.2s;
|
|
2023
|
+
}}
|
|
2024
|
+
.clean-baseline-header {{
|
|
2025
|
+
display: flex;
|
|
2026
|
+
justify-content: space-between;
|
|
2027
|
+
align-items: center;
|
|
2028
|
+
flex-wrap: wrap;
|
|
2029
|
+
gap: 14px;
|
|
2030
|
+
}}
|
|
2031
|
+
.btn-archive-toggle {{
|
|
2032
|
+
background: var(--bg-card);
|
|
2033
|
+
border: 1px solid var(--border-subtle);
|
|
2034
|
+
color: var(--text-main);
|
|
2035
|
+
padding: 6px 14px;
|
|
2036
|
+
border-radius: 6px;
|
|
2037
|
+
font-size: 12px;
|
|
2038
|
+
font-weight: 600;
|
|
2039
|
+
cursor: pointer;
|
|
2040
|
+
display: inline-flex;
|
|
2041
|
+
align-items: center;
|
|
2042
|
+
gap: 8px;
|
|
2043
|
+
transition: all 0.2s ease;
|
|
2044
|
+
}}
|
|
2045
|
+
.btn-archive-toggle:hover {{
|
|
2046
|
+
background: #30363d;
|
|
2047
|
+
border-color: var(--accent-blue);
|
|
2048
|
+
color: var(--accent-blue);
|
|
2049
|
+
}}
|
|
2050
|
+
body.light-theme .btn-archive-toggle:hover {{
|
|
2051
|
+
background: #e1e4e8;
|
|
2052
|
+
}}
|
|
2053
|
+
|
|
2054
|
+
/* Table & Drawers */
|
|
2055
|
+
.table-container {{
|
|
2056
|
+
background: var(--bg-surface);
|
|
2057
|
+
border: 1px solid var(--border-subtle);
|
|
2058
|
+
border-radius: 8px;
|
|
2059
|
+
overflow: hidden;
|
|
2060
|
+
margin-bottom: 24px;
|
|
2061
|
+
}}
|
|
2062
|
+
table {{ width: 100%; border-collapse: collapse; text-align: left; font-size: 13px; }}
|
|
2063
|
+
th {{
|
|
2064
|
+
background: var(--bg-card);
|
|
2065
|
+
padding: 10px 14px;
|
|
2066
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
2067
|
+
font-weight: 600;
|
|
2068
|
+
color: var(--text-muted);
|
|
2069
|
+
}}
|
|
2070
|
+
td {{ padding: 12px 14px; border-bottom: 1px solid var(--border-subtle); vertical-align: middle; }}
|
|
2071
|
+
tr.finding-row {{ cursor: pointer; transition: background 0.1s; }}
|
|
2072
|
+
tr.finding-row:hover {{ background: rgba(255, 255, 255, 0.02); }}
|
|
2073
|
+
body.light-theme tr.finding-row:hover {{ background: rgba(0, 0, 0, 0.02); }}
|
|
2074
|
+
tr.finding-row.expanded {{ background: rgba(88, 166, 255, 0.05); }}
|
|
2075
|
+
.drawer-chevron {{ display: inline-block; transition: transform 0.2s; font-size: 14px; color: var(--text-muted); }}
|
|
2076
|
+
tr.finding-row.expanded .drawer-chevron {{ transform: rotate(90deg); color: var(--accent-blue); }}
|
|
2077
|
+
|
|
2078
|
+
/* Badges */
|
|
2079
|
+
.badge {{
|
|
2080
|
+
display: inline-block;
|
|
2081
|
+
padding: 2px 8px;
|
|
2082
|
+
border-radius: 12px;
|
|
2083
|
+
font-size: 11px;
|
|
2084
|
+
font-weight: 600;
|
|
2085
|
+
text-transform: uppercase;
|
|
2086
|
+
letter-spacing: 0.3px;
|
|
2087
|
+
}}
|
|
2088
|
+
.badge-critical {{ background: rgba(248, 81, 73, 0.2); color: var(--accent-red); border: 1px solid var(--accent-red); }}
|
|
2089
|
+
.badge-high {{ background: rgba(219, 109, 40, 0.2); color: var(--accent-orange); border: 1px solid var(--accent-orange); }}
|
|
2090
|
+
.badge-medium {{ background: rgba(210, 153, 34, 0.2); color: var(--accent-yellow); border: 1px solid var(--accent-yellow); }}
|
|
2091
|
+
.badge-success {{ background: rgba(63, 185, 80, 0.2); color: var(--accent-green); border: 1px solid var(--accent-green); }}
|
|
2092
|
+
.badge-primary {{ background: rgba(88, 166, 255, 0.2); color: var(--accent-blue); border: 1px solid var(--accent-blue); }}
|
|
2093
|
+
.badge-canary {{ background: rgba(188, 140, 255, 0.2); color: var(--accent-purple); border: 1px solid var(--accent-purple); }}
|
|
2094
|
+
|
|
2095
|
+
.rule-id {{ color: var(--accent-blue); font-family: monospace; font-weight: 600; }}
|
|
2096
|
+
.conf-score {{ color: var(--text-muted); font-size: 12px; }}
|
|
2097
|
+
|
|
2098
|
+
/* Drawer Content */
|
|
2099
|
+
.drawer-content {{
|
|
2100
|
+
padding: 16px;
|
|
2101
|
+
background: var(--bg-dark);
|
|
2102
|
+
border-left: 3px solid var(--accent-blue);
|
|
2103
|
+
margin: 8px 12px;
|
|
2104
|
+
border-radius: 4px;
|
|
2105
|
+
}}
|
|
2106
|
+
body.light-theme .drawer-content {{ background: var(--bg-card); }}
|
|
2107
|
+
.drawer-header {{
|
|
2108
|
+
display: flex;
|
|
2109
|
+
justify-content: space-between;
|
|
2110
|
+
color: var(--text-muted);
|
|
2111
|
+
font-size: 12px;
|
|
2112
|
+
margin-bottom: 8px;
|
|
2113
|
+
}}
|
|
2114
|
+
.drawer-desc {{ font-size: 13px; color: var(--text-main); margin-bottom: 12px; }}
|
|
2115
|
+
.code-preview {{
|
|
2116
|
+
background: #000;
|
|
2117
|
+
border: 1px solid var(--border-subtle);
|
|
2118
|
+
border-radius: 4px;
|
|
2119
|
+
padding: 10px;
|
|
2120
|
+
font-family: monospace;
|
|
2121
|
+
font-size: 12px;
|
|
2122
|
+
overflow-x: auto;
|
|
2123
|
+
margin-bottom: 12px;
|
|
2124
|
+
}}
|
|
2125
|
+
body.light-theme .code-preview {{ background: #f6f8fa; border-color: var(--border-subtle); }}
|
|
2126
|
+
.code-preview code {{ color: #7ee787; font-family: monospace; }}
|
|
2127
|
+
body.light-theme .code-preview code {{ color: #1f2328; }}
|
|
2128
|
+
.drawer-actions {{ display: flex; justify-content: flex-end; gap: 8px; }}
|
|
2129
|
+
.cmd-box {{
|
|
2130
|
+
background: var(--bg-card);
|
|
2131
|
+
border: 1px solid var(--border-subtle);
|
|
2132
|
+
padding: 4px 8px;
|
|
2133
|
+
border-radius: 4px;
|
|
2134
|
+
display: flex;
|
|
2135
|
+
align-items: center;
|
|
2136
|
+
gap: 8px;
|
|
2137
|
+
font-size: 12px;
|
|
2138
|
+
font-family: monospace;
|
|
2139
|
+
}}
|
|
2140
|
+
.btn-copy {{
|
|
2141
|
+
background: transparent;
|
|
2142
|
+
border: none;
|
|
2143
|
+
color: var(--accent-blue);
|
|
2144
|
+
cursor: pointer;
|
|
2145
|
+
font-size: 11px;
|
|
2146
|
+
}}
|
|
2147
|
+
.btn-copy:hover {{ text-decoration: underline; }}
|
|
2148
|
+
.btn-copy.copied {{ color: var(--accent-green) !important; }}
|
|
2149
|
+
|
|
2150
|
+
/* Section Titles */
|
|
2151
|
+
.section-title {{
|
|
2152
|
+
font-size: 14px;
|
|
2153
|
+
font-weight: 700;
|
|
2154
|
+
text-transform: uppercase;
|
|
2155
|
+
letter-spacing: 0.5px;
|
|
2156
|
+
color: var(--text-muted);
|
|
2157
|
+
margin-bottom: 12px;
|
|
2158
|
+
display: flex;
|
|
2159
|
+
align-items: center;
|
|
2160
|
+
gap: 8px;
|
|
2161
|
+
}}
|
|
2162
|
+
|
|
2163
|
+
/* OWASP Grid */
|
|
2164
|
+
.owasp-card-wrapper {{
|
|
2165
|
+
padding: 10px 14px;
|
|
2166
|
+
display: flex;
|
|
2167
|
+
flex-direction: column;
|
|
2168
|
+
justify-content: flex-start;
|
|
2169
|
+
gap: 6px;
|
|
2170
|
+
}}
|
|
2171
|
+
.grid-owasp {{
|
|
2172
|
+
display: grid;
|
|
2173
|
+
grid-template-columns: repeat(4, 1fr);
|
|
2174
|
+
gap: 8px;
|
|
2175
|
+
margin-bottom: 0;
|
|
2176
|
+
}}
|
|
2177
|
+
@media (max-width: 950px) {{ .grid-owasp {{ grid-template-columns: repeat(2, 1fr); }} }}
|
|
2178
|
+
@media (max-width: 550px) {{ .grid-owasp {{ grid-template-columns: 1fr; }} }}
|
|
2179
|
+
.owasp-card {{
|
|
2180
|
+
background: var(--bg-surface);
|
|
2181
|
+
border: 1px solid var(--border-subtle);
|
|
2182
|
+
border-radius: 6px;
|
|
2183
|
+
padding: 8px 10px;
|
|
2184
|
+
}}
|
|
2185
|
+
.owasp-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }}
|
|
2186
|
+
.owasp-title {{ font-size: 11px; font-weight: 600; color: var(--text-main); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
|
2187
|
+
.owasp-badge {{ font-size: 9.5px; font-weight: 700; border: 1px solid; border-radius: 8px; padding: 1px 5px; white-space: nowrap; }}
|
|
2188
|
+
.owasp-bar-bg {{ width: 100%; height: 5px; background: var(--bg-card); border-radius: 3px; overflow: hidden; }}
|
|
2189
|
+
.owasp-bar {{ height: 100%; border-radius: 3px; }}
|
|
2190
|
+
|
|
2191
|
+
/* Compliance Framework Grid */
|
|
2192
|
+
.comp-card-wrapper {{
|
|
2193
|
+
padding: 10px 14px;
|
|
2194
|
+
display: flex;
|
|
2195
|
+
flex-direction: column;
|
|
2196
|
+
justify-content: flex-start;
|
|
2197
|
+
gap: 6px;
|
|
2198
|
+
}}
|
|
2199
|
+
.grid-compliance {{
|
|
2200
|
+
display: grid;
|
|
2201
|
+
grid-template-columns: repeat(3, 1fr);
|
|
2202
|
+
gap: 8px;
|
|
2203
|
+
margin-bottom: 0;
|
|
2204
|
+
}}
|
|
2205
|
+
@media (max-width: 900px) {{ .grid-compliance {{ grid-template-columns: 1fr; }} }}
|
|
2206
|
+
.comp-card {{ padding: 8px 10px; }}
|
|
2207
|
+
.comp-card-header {{
|
|
2208
|
+
display: flex;
|
|
2209
|
+
justify-content: space-between;
|
|
2210
|
+
align-items: center;
|
|
2211
|
+
margin-bottom: 6px;
|
|
2212
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
2213
|
+
padding-bottom: 4px;
|
|
2214
|
+
}}
|
|
2215
|
+
.comp-title {{ font-size: 12px; font-weight: 700; color: var(--text-main); }}
|
|
2216
|
+
.comp-sub {{ font-size: 9.5px; color: var(--text-muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; }}
|
|
2217
|
+
.comp-ctrl-list {{ display: flex; flex-direction: column; gap: 4px; }}
|
|
2218
|
+
.comp-ctrl-row {{ display: flex; justify-content: space-between; align-items: center; font-size: 10.5px; padding: 1px 0; }}
|
|
2219
|
+
.comp-ctrl-code {{ font-weight: 600; color: var(--text-main); font-size: 10px; }}
|
|
2220
|
+
.comp-ctrl-fams {{ color: var(--text-muted); font-size: 9px; line-height: 1.1; }}
|
|
2221
|
+
|
|
2222
|
+
/* Dual Grids */
|
|
2223
|
+
.grid-bottom {{
|
|
2224
|
+
display: grid;
|
|
2225
|
+
grid-template-columns: 1fr 1fr;
|
|
2226
|
+
gap: 20px;
|
|
2227
|
+
margin-bottom: 24px;
|
|
2228
|
+
}}
|
|
2229
|
+
@media (max-width: 850px) {{ .grid-bottom {{ grid-template-columns: 1fr; }} }}
|
|
2230
|
+
|
|
2231
|
+
.clusters-card {{
|
|
2232
|
+
padding: 10px 14px;
|
|
2233
|
+
display: flex;
|
|
2234
|
+
flex-direction: column;
|
|
2235
|
+
justify-content: flex-start;
|
|
2236
|
+
gap: 6px;
|
|
2237
|
+
}}
|
|
2238
|
+
.clusters-compact-list {{
|
|
2239
|
+
display: grid;
|
|
2240
|
+
grid-template-columns: repeat(2, 1fr);
|
|
2241
|
+
gap: 6px;
|
|
2242
|
+
max-height: 140px;
|
|
2243
|
+
overflow-y: auto;
|
|
2244
|
+
}}
|
|
2245
|
+
.clusters-compact-list .empty-state {{
|
|
2246
|
+
grid-column: 1 / -1;
|
|
2247
|
+
}}
|
|
2248
|
+
@media (max-width: 600px) {{ .clusters-compact-list {{ grid-template-columns: 1fr; }} }}
|
|
2249
|
+
.cluster-item {{
|
|
2250
|
+
background: var(--bg-card);
|
|
2251
|
+
border: 1px solid var(--border-subtle);
|
|
2252
|
+
border-radius: 5px;
|
|
2253
|
+
padding: 5px 8px;
|
|
2254
|
+
margin-bottom: 0;
|
|
2255
|
+
}}
|
|
2256
|
+
.cluster-header {{ display: flex; justify-content: space-between; align-items: center; font-size: 11px; gap: 4px; }}
|
|
2257
|
+
.cluster-header strong {{ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 130px; }}
|
|
2258
|
+
.cluster-sub {{ font-size: 9.5px; color: var(--text-muted); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
|
2259
|
+
|
|
2260
|
+
/* Compact Single-Page Layout */
|
|
2261
|
+
.header-compact {{
|
|
2262
|
+
display: flex;
|
|
2263
|
+
justify-content: space-between;
|
|
2264
|
+
align-items: center;
|
|
2265
|
+
padding-bottom: 8px;
|
|
2266
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
2267
|
+
margin-bottom: 4px;
|
|
2268
|
+
}}
|
|
2269
|
+
.row-top {{
|
|
2270
|
+
display: grid;
|
|
2271
|
+
grid-template-columns: 150px 1fr;
|
|
2272
|
+
gap: 12px;
|
|
2273
|
+
align-items: stretch;
|
|
2274
|
+
}}
|
|
2275
|
+
@media (max-width: 800px) {{ .row-top {{ grid-template-columns: 1fr; }} }}
|
|
2276
|
+
.gauge-card-compact {{
|
|
2277
|
+
display: flex;
|
|
2278
|
+
flex-direction: column;
|
|
2279
|
+
align-items: center;
|
|
2280
|
+
justify-content: center;
|
|
2281
|
+
padding: 10px;
|
|
2282
|
+
background: var(--bg-surface);
|
|
2283
|
+
border: 1px solid var(--border-subtle);
|
|
2284
|
+
border-radius: 8px;
|
|
2285
|
+
text-align: center;
|
|
2286
|
+
}}
|
|
2287
|
+
.row-middle {{
|
|
2288
|
+
display: grid;
|
|
2289
|
+
grid-template-columns: 1.15fr 0.85fr;
|
|
2290
|
+
gap: 12px;
|
|
2291
|
+
align-items: start;
|
|
2292
|
+
}}
|
|
2293
|
+
@media (max-width: 880px) {{ .row-middle {{ grid-template-columns: 1fr; }} }}
|
|
2294
|
+
.row-bottom {{
|
|
2295
|
+
display: grid;
|
|
2296
|
+
grid-template-columns: 1fr 1fr;
|
|
2297
|
+
gap: 12px;
|
|
2298
|
+
align-items: start;
|
|
2299
|
+
}}
|
|
2300
|
+
@media (max-width: 880px) {{ .row-bottom {{ grid-template-columns: 1fr; }} }}
|
|
2301
|
+
.footer-compact {{
|
|
2302
|
+
display: flex;
|
|
2303
|
+
justify-content: space-between;
|
|
2304
|
+
align-items: center;
|
|
2305
|
+
border-top: 1px solid var(--border-subtle);
|
|
2306
|
+
padding-top: 8px;
|
|
2307
|
+
margin-top: 4px;
|
|
2308
|
+
font-size: 11px;
|
|
2309
|
+
color: var(--text-muted);
|
|
2310
|
+
flex-wrap: wrap;
|
|
2311
|
+
gap: 8px;
|
|
2312
|
+
}}
|
|
2313
|
+
.footer-actions {{
|
|
2314
|
+
display: flex;
|
|
2315
|
+
align-items: center;
|
|
2316
|
+
gap: 8px;
|
|
2317
|
+
flex-wrap: wrap;
|
|
2318
|
+
}}
|
|
2319
|
+
.footer-meta {{
|
|
2320
|
+
display: flex;
|
|
2321
|
+
align-items: center;
|
|
2322
|
+
gap: 12px;
|
|
2323
|
+
}}
|
|
2324
|
+
.footer-links a {{
|
|
2325
|
+
color: var(--accent-blue);
|
|
2326
|
+
text-decoration: none;
|
|
2327
|
+
}}
|
|
2328
|
+
.footer-links a:hover {{
|
|
2329
|
+
text-decoration: underline;
|
|
2330
|
+
}}
|
|
2331
|
+
|
|
2332
|
+
/* 2D Directory Attack Surface Heatmap Matrix */
|
|
2333
|
+
.heatmap-card {{
|
|
2334
|
+
background: var(--bg-surface);
|
|
2335
|
+
border: 1px solid var(--border-subtle);
|
|
2336
|
+
border-radius: 8px;
|
|
2337
|
+
padding: 10px 14px;
|
|
2338
|
+
display: flex;
|
|
2339
|
+
flex-direction: column;
|
|
2340
|
+
justify-content: flex-start;
|
|
2341
|
+
gap: 6px;
|
|
2342
|
+
}}
|
|
2343
|
+
.card-compact-header {{
|
|
2344
|
+
display: flex;
|
|
2345
|
+
justify-content: space-between;
|
|
2346
|
+
align-items: center;
|
|
2347
|
+
margin-bottom: 6px;
|
|
2348
|
+
flex-wrap: wrap;
|
|
2349
|
+
gap: 6px;
|
|
2350
|
+
}}
|
|
2351
|
+
.heatmap-legend {{
|
|
2352
|
+
display: flex;
|
|
2353
|
+
align-items: center;
|
|
2354
|
+
gap: 8px;
|
|
2355
|
+
font-size: 10px;
|
|
2356
|
+
color: var(--text-muted);
|
|
2357
|
+
}}
|
|
2358
|
+
.heat-leg-item {{
|
|
2359
|
+
display: inline-flex;
|
|
2360
|
+
align-items: center;
|
|
2361
|
+
gap: 3px;
|
|
2362
|
+
}}
|
|
2363
|
+
.heat-leg-dot {{
|
|
2364
|
+
display: inline-flex;
|
|
2365
|
+
align-items: center;
|
|
2366
|
+
justify-content: center;
|
|
2367
|
+
width: 14px;
|
|
2368
|
+
height: 14px;
|
|
2369
|
+
border-radius: 3px;
|
|
2370
|
+
font-size: 9px;
|
|
2371
|
+
font-weight: 700;
|
|
2372
|
+
}}
|
|
2373
|
+
.heatmap-table-wrap {{
|
|
2374
|
+
overflow-x: auto;
|
|
2375
|
+
}}
|
|
2376
|
+
.heatmap-table {{
|
|
2377
|
+
width: 100%;
|
|
2378
|
+
border-collapse: separate;
|
|
2379
|
+
border-spacing: 4px;
|
|
2380
|
+
font-size: 11px;
|
|
2381
|
+
}}
|
|
2382
|
+
.heatmap-table th {{
|
|
2383
|
+
color: var(--text-muted);
|
|
2384
|
+
font-weight: 600;
|
|
2385
|
+
padding: 2px 4px;
|
|
2386
|
+
text-align: center;
|
|
2387
|
+
font-size: 10px;
|
|
2388
|
+
text-transform: uppercase;
|
|
2389
|
+
letter-spacing: 0.5px;
|
|
2390
|
+
}}
|
|
2391
|
+
.heatmap-row {{
|
|
2392
|
+
cursor: pointer;
|
|
2393
|
+
transition: background 0.15s ease;
|
|
2394
|
+
}}
|
|
2395
|
+
.heatmap-row:hover {{
|
|
2396
|
+
background: rgba(255, 255, 255, 0.04);
|
|
2397
|
+
}}
|
|
2398
|
+
body.light-theme .heatmap-row:hover {{
|
|
2399
|
+
background: rgba(0, 0, 0, 0.04);
|
|
2400
|
+
}}
|
|
2401
|
+
.heatmap-label-cell {{
|
|
2402
|
+
padding: 4px 6px;
|
|
2403
|
+
font-family: monospace;
|
|
2404
|
+
font-size: 11px;
|
|
2405
|
+
color: var(--text-main);
|
|
2406
|
+
white-space: nowrap;
|
|
2407
|
+
font-weight: 600;
|
|
2408
|
+
}}
|
|
2409
|
+
.heat-cell {{
|
|
2410
|
+
text-align: center;
|
|
2411
|
+
padding: 4px 6px;
|
|
2412
|
+
border-radius: 4px;
|
|
2413
|
+
font-weight: 700;
|
|
2414
|
+
font-size: 11px;
|
|
2415
|
+
transition: transform 0.1s ease, filter 0.1s ease;
|
|
2416
|
+
cursor: pointer;
|
|
2417
|
+
}}
|
|
2418
|
+
.heat-cell:hover {{
|
|
2419
|
+
transform: scale(1.08);
|
|
2420
|
+
filter: brightness(1.2);
|
|
2421
|
+
}}
|
|
2422
|
+
.heat-cell-safe {{
|
|
2423
|
+
background: rgba(46, 160, 67, 0.22);
|
|
2424
|
+
border: 1px solid rgba(63, 185, 80, 0.45);
|
|
2425
|
+
color: #7ee787;
|
|
2426
|
+
}}
|
|
2427
|
+
body.light-theme .heat-cell-safe {{
|
|
2428
|
+
background: rgba(46, 160, 67, 0.15);
|
|
2429
|
+
border-color: rgba(46, 160, 67, 0.4);
|
|
2430
|
+
color: #1a7f37;
|
|
2431
|
+
}}
|
|
2432
|
+
.heat-cell-warn {{
|
|
2433
|
+
background: rgba(210, 153, 34, 0.22);
|
|
2434
|
+
border: 1px solid rgba(210, 153, 34, 0.5);
|
|
2435
|
+
color: #e3b341;
|
|
2436
|
+
}}
|
|
2437
|
+
body.light-theme .heat-cell-warn {{
|
|
2438
|
+
background: rgba(210, 153, 34, 0.15);
|
|
2439
|
+
border-color: rgba(210, 153, 34, 0.4);
|
|
2440
|
+
color: #9a6700;
|
|
2441
|
+
}}
|
|
2442
|
+
.heat-cell-crit {{
|
|
2443
|
+
background: rgba(248, 81, 73, 0.25);
|
|
2444
|
+
border: 1px solid rgba(248, 81, 73, 0.55);
|
|
2445
|
+
color: #ffa198;
|
|
2446
|
+
}}
|
|
2447
|
+
body.light-theme .heat-cell-crit {{
|
|
2448
|
+
background: rgba(248, 81, 73, 0.15);
|
|
2449
|
+
border-color: rgba(248, 81, 73, 0.4);
|
|
2450
|
+
color: #cf222e;
|
|
2451
|
+
}}
|
|
2452
|
+
.heat-cell-idle {{
|
|
2453
|
+
background: var(--bg-card);
|
|
2454
|
+
border: 1px solid var(--border-subtle);
|
|
2455
|
+
color: var(--text-muted);
|
|
2456
|
+
opacity: 0.6;
|
|
2457
|
+
}}
|
|
2458
|
+
|
|
2459
|
+
/* Modal Large for Details Overlays */
|
|
2460
|
+
.modal-lg {{
|
|
2461
|
+
max-width: 1040px;
|
|
2462
|
+
width: 95%;
|
|
2463
|
+
max-height: 88vh;
|
|
2464
|
+
display: flex;
|
|
2465
|
+
flex-direction: column;
|
|
2466
|
+
padding: 18px 22px;
|
|
2467
|
+
}}
|
|
2468
|
+
.modal-lg .modal-body {{
|
|
2469
|
+
overflow-y: auto;
|
|
2470
|
+
flex: 1;
|
|
2471
|
+
padding-right: 6px;
|
|
2472
|
+
}}
|
|
2473
|
+
|
|
2474
|
+
/* Existing Heatmap Row fallback */
|
|
2475
|
+
.heatmap-row {{ display: table-row; }}
|
|
2476
|
+
|
|
2477
|
+
/* Golden Recipes & Diff Engine */
|
|
2478
|
+
.recipe-card {{
|
|
2479
|
+
background: var(--bg-card);
|
|
2480
|
+
border: 1px solid var(--border-subtle);
|
|
2481
|
+
border-radius: 6px;
|
|
2482
|
+
padding: 14px;
|
|
2483
|
+
margin-bottom: 12px;
|
|
2484
|
+
}}
|
|
2485
|
+
.recipe-header {{
|
|
2486
|
+
display: flex;
|
|
2487
|
+
justify-content: space-between;
|
|
2488
|
+
align-items: center;
|
|
2489
|
+
margin-bottom: 8px;
|
|
2490
|
+
flex-wrap: wrap;
|
|
2491
|
+
gap: 8px;
|
|
2492
|
+
font-size: 13px;
|
|
2493
|
+
}}
|
|
2494
|
+
.recipe-meta-badges {{ display: flex; align-items: center; gap: 6px; }}
|
|
2495
|
+
.chip-filetype {{ background: rgba(88, 166, 255, 0.1); border: 1px solid var(--accent-blue); color: var(--accent-blue); font-size: 10px; font-family: monospace; padding: 2px 6px; border-radius: 4px; }}
|
|
2496
|
+
.ponytail-badge {{ background: rgba(210, 153, 34, 0.15); border: 1px solid var(--accent-yellow); color: var(--accent-yellow); font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px; }}
|
|
2497
|
+
.verified-badge {{ background: rgba(63, 185, 80, 0.15); border: 1px solid var(--accent-green); color: var(--accent-green); font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px; }}
|
|
2498
|
+
.recipe-view-switcher {{
|
|
2499
|
+
display: flex;
|
|
2500
|
+
gap: 4px;
|
|
2501
|
+
margin-bottom: 8px;
|
|
2502
|
+
}}
|
|
2503
|
+
.recipe-tab-btn {{
|
|
2504
|
+
background: transparent;
|
|
2505
|
+
border: 1px solid var(--border-subtle);
|
|
2506
|
+
color: var(--text-muted);
|
|
2507
|
+
padding: 2px 8px;
|
|
2508
|
+
border-radius: 4px;
|
|
2509
|
+
font-size: 11px;
|
|
2510
|
+
cursor: pointer;
|
|
2511
|
+
}}
|
|
2512
|
+
.recipe-tab-btn.active {{
|
|
2513
|
+
background: var(--accent-blue);
|
|
2514
|
+
border-color: var(--accent-blue);
|
|
2515
|
+
color: #fff;
|
|
2516
|
+
}}
|
|
2517
|
+
.diff-line-header {{ color: var(--text-muted); font-weight: 600; display: block; }}
|
|
2518
|
+
.diff-line-hunk {{ color: var(--accent-blue); background: rgba(88, 166, 255, 0.1); display: block; padding: 1px 4px; border-radius: 2px; }}
|
|
2519
|
+
.diff-line-add {{ color: #7ee787; background: rgba(46, 160, 67, 0.18); display: block; padding: 1px 4px; border-radius: 2px; }}
|
|
2520
|
+
.diff-line-del {{ color: #ffa198; background: rgba(248, 81, 73, 0.18); display: block; padding: 1px 4px; border-radius: 2px; }}
|
|
2521
|
+
.diff-line-ctx {{ color: var(--text-muted); display: block; padding: 1px 4px; }}
|
|
2522
|
+
body.light-theme .diff-line-add {{ color: #1a7f37; background: rgba(46, 160, 67, 0.15); }}
|
|
2523
|
+
body.light-theme .diff-line-del {{ color: #cf222e; background: rgba(207, 34, 46, 0.15); }}
|
|
2524
|
+
body.light-theme .diff-line-hunk {{ color: #0969da; background: rgba(9, 105, 218, 0.1); }}
|
|
2525
|
+
|
|
2526
|
+
.recipe-split-container {{
|
|
2527
|
+
display: grid;
|
|
2528
|
+
grid-template-columns: 1fr 1fr;
|
|
2529
|
+
gap: 10px;
|
|
2530
|
+
margin-bottom: 12px;
|
|
2531
|
+
}}
|
|
2532
|
+
@media (max-width: 650px) {{ .recipe-split-container {{ grid-template-columns: 1fr; }} }}
|
|
2533
|
+
.split-pane .code-preview {{ margin-bottom: 0; min-height: 70px; }}
|
|
2534
|
+
.split-label {{ font-size: 11px; font-weight: 600; margin-bottom: 4px; }}
|
|
2535
|
+
.split-del {{ color: var(--accent-red); }}
|
|
2536
|
+
.split-add {{ color: var(--accent-green); }}
|
|
2537
|
+
.recipe-actions {{ display: flex; justify-content: space-between; align-items: center; margin-top: 8px; }}
|
|
2538
|
+
|
|
2539
|
+
/* Advisory Items */
|
|
2540
|
+
.advisory-item {{
|
|
2541
|
+
background: var(--bg-card);
|
|
2542
|
+
border: 1px solid var(--border-subtle);
|
|
2543
|
+
border-radius: 6px;
|
|
2544
|
+
padding: 12px;
|
|
2545
|
+
margin-bottom: 10px;
|
|
2546
|
+
}}
|
|
2547
|
+
.advisory-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }}
|
|
2548
|
+
.advisory-mitigates {{ font-size: 11px; color: var(--accent-blue); margin-bottom: 4px; }}
|
|
2549
|
+
.advisory-desc {{ font-size: 12px; color: var(--text-main); margin-bottom: 4px; }}
|
|
2550
|
+
.advisory-guarantee {{ font-size: 11px; color: var(--text-muted); margin-bottom: 6px; }}
|
|
2551
|
+
|
|
2552
|
+
.empty-state {{ text-align: center; padding: 20px; color: var(--text-muted); font-size: 13px; }}
|
|
2553
|
+
.empty-cell {{ text-align: center; color: var(--accent-green); padding: 24px; font-weight: 600; }}
|
|
2554
|
+
|
|
2555
|
+
footer {{
|
|
2556
|
+
margin-top: 36px;
|
|
2557
|
+
text-align: center;
|
|
2558
|
+
color: var(--text-muted);
|
|
2559
|
+
font-size: 12px;
|
|
2560
|
+
border-top: 1px solid var(--border-subtle);
|
|
2561
|
+
padding-top: 18px;
|
|
2562
|
+
}}
|
|
2563
|
+
|
|
2564
|
+
/* Print & PDF styling */
|
|
2565
|
+
@media print {{
|
|
2566
|
+
body {{ background: #fff !important; color: #000 !important; padding: 10px; }}
|
|
2567
|
+
.card, .owasp-card, .cluster-item, .defended-section, .table-container, .recipe-card, .advisory-item, .comp-card {{
|
|
2568
|
+
border: 1px solid #ccc !important;
|
|
2569
|
+
background: #fff !important;
|
|
2570
|
+
color: #000 !important;
|
|
2571
|
+
box-shadow: none !important;
|
|
2572
|
+
}}
|
|
2573
|
+
.header-actions, .filter-bar, .btn-copy, footer, .drawer-actions, .simulator-card, .recipe-view-switcher, .recipe-actions {{ display: none !important; }}
|
|
2574
|
+
.finding-row {{ background: #fff !important; color: #000 !important; }}
|
|
2575
|
+
.finding-title, .rule-id {{ color: #000 !important; }}
|
|
2576
|
+
.drawer-row {{ display: table-row !important; }}
|
|
2577
|
+
.drawer-content {{ background: #f6f8fa !important; border-left: 3px solid #000 !important; }}
|
|
2578
|
+
.code-preview {{ background: #eee !important; color: #000 !important; }}
|
|
2579
|
+
.code-preview code {{ color: #000 !important; }}
|
|
2580
|
+
.gauge-card {{ display: none !important; }}
|
|
2581
|
+
}}
|
|
2582
|
+
</style>
|
|
2583
|
+
</head>
|
|
2584
|
+
<body>
|
|
2585
|
+
<div class="container">
|
|
2586
|
+
<header class="header-compact">
|
|
2587
|
+
<div class="logo-group">
|
|
2588
|
+
<h1 style="font-size: 16px; margin: 0; display: flex; align-items: center; gap: 8px;">
|
|
2589
|
+
<span class="shield-icon">🛡</span> TorusGuard Security Posture
|
|
2590
|
+
</h1>
|
|
2591
|
+
<div class="project-meta">
|
|
2592
|
+
<strong>{html.escape(telemetry.get('project_name', 'Project'))}</strong> ·
|
|
2593
|
+
{html.escape(stack_label)} ·
|
|
2594
|
+
<span class="badge badge-subtle">v1.3.6</span>
|
|
2595
|
+
<span class="badge badge-success">● 100% HARDENED</span>
|
|
2596
|
+
</div>
|
|
2597
|
+
</div>
|
|
2598
|
+
<div class="header-actions">
|
|
2599
|
+
<button class="btn btn-sm" id="themeToggleBtn" onclick="toggleTheme()">☀️ Light Mode</button>
|
|
2600
|
+
<button class="btn btn-sm" onclick="downloadSarif()">📥 SARIF</button>
|
|
2601
|
+
<button class="btn btn-sm" onclick="downloadCsv()">📊 CSV</button>
|
|
2602
|
+
<button class="btn btn-sm" onclick="window.print()">🖶 Print</button>
|
|
2603
|
+
<div class="status-badge {status_badge_class}">{status_label}</div>
|
|
2604
|
+
</div>
|
|
2605
|
+
</header>
|
|
2606
|
+
|
|
2607
|
+
<!-- Row 1: Dynamic Circular Gauge & Governed Remediation Velocity Ledger -->
|
|
2608
|
+
<div class="row-top">
|
|
2609
|
+
<div class="card gauge-card-compact">
|
|
2610
|
+
<div style="position: relative; display: flex; align-items: center; justify-content: center;">
|
|
2611
|
+
<svg class="gauge-svg" style="width: 76px; height: 76px;" viewBox="0 0 100 100">
|
|
2612
|
+
<defs>
|
|
2613
|
+
<linearGradient id="gaugeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
2614
|
+
<stop offset="0%" stop-color="{grad_start}" />
|
|
2615
|
+
<stop offset="100%" stop-color="{grad_end}" />
|
|
2616
|
+
</linearGradient>
|
|
2617
|
+
<filter id="gaugeGlow" x="-20%" y="-20%" width="140%" height="140%">
|
|
2618
|
+
<feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="{glow_color}" flood-opacity="0.5"/>
|
|
2619
|
+
</filter>
|
|
2620
|
+
</defs>
|
|
2621
|
+
<circle class="gauge-bg" cx="50" cy="50" r="40" />
|
|
2622
|
+
<circle class="gauge-fill" id="gaugeCircleFill" cx="50" cy="50" r="40" stroke="url(#gaugeGradient)" filter="url(#gaugeGlow)" />
|
|
2623
|
+
</svg>
|
|
2624
|
+
<div class="gauge-value" id="postureScoreText" style="font-size: 20px;">0</div>
|
|
2625
|
+
</div>
|
|
2626
|
+
<div class="stat-label" style="font-size: 11px; color: var(--text-muted); text-transform: uppercase; margin-top: 4px; font-weight: 700;">Posture Score</div>
|
|
2627
|
+
</div>
|
|
2628
|
+
|
|
2629
|
+
{remediation_ledger_html}
|
|
2630
|
+
</div>
|
|
2631
|
+
|
|
2632
|
+
<!-- Row 2: 2D Directory Attack Surface Heatmap & Architectural Clusters -->
|
|
2633
|
+
<div class="row-middle">
|
|
2634
|
+
<div class="card heatmap-card">
|
|
2635
|
+
<div class="card-compact-header">
|
|
2636
|
+
<div class="section-title" style="margin-bottom: 0;">🔥 Directory Attack Surface Heatmap</div>
|
|
2637
|
+
<div class="heatmap-legend">
|
|
2638
|
+
<span class="heat-leg-item"><span class="heat-leg-dot heat-cell-safe">✔</span> Defended</span>
|
|
2639
|
+
<span class="heat-leg-item"><span class="heat-leg-dot heat-cell-warn">🟡</span> Warning</span>
|
|
2640
|
+
<span class="heat-leg-item"><span class="heat-leg-dot heat-cell-crit">🔴</span> High Risk</span>
|
|
2641
|
+
<span class="heat-leg-item"><span class="heat-leg-dot heat-cell-idle">—</span> Clean</span>
|
|
2642
|
+
</div>
|
|
2643
|
+
</div>
|
|
2644
|
+
<div class="heatmap-table-wrap">
|
|
2645
|
+
<table class="heatmap-table">
|
|
2646
|
+
<thead>
|
|
2647
|
+
<tr>
|
|
2648
|
+
<th style="text-align: left; width: 90px;">Module</th>
|
|
2649
|
+
<th>AUTH</th>
|
|
2650
|
+
<th>DB</th>
|
|
2651
|
+
<th>INPUT</th>
|
|
2652
|
+
<th>NET</th>
|
|
2653
|
+
<th>PLAT</th>
|
|
2654
|
+
<th>SEC</th>
|
|
2655
|
+
<th style="text-align: right; width: 85px;">Posture</th>
|
|
2656
|
+
</tr>
|
|
2657
|
+
</thead>
|
|
2658
|
+
<tbody>
|
|
2659
|
+
{heatmap_rows_html}
|
|
2660
|
+
</tbody>
|
|
2661
|
+
</table>
|
|
2662
|
+
</div>
|
|
2663
|
+
</div>
|
|
2664
|
+
|
|
2665
|
+
<div class="card clusters-card">
|
|
2666
|
+
<div class="section-title" style="margin-bottom: 8px;">🛠 Root-Cause Architectural Clusters</div>
|
|
2667
|
+
<div class="clusters-compact-list">
|
|
2668
|
+
{clusters_html}
|
|
2669
|
+
</div>
|
|
2670
|
+
</div>
|
|
2671
|
+
</div>
|
|
2672
|
+
|
|
2673
|
+
<!-- Row 3: OWASP Top 10 & Enterprise Compliance -->
|
|
2674
|
+
<div class="row-bottom">
|
|
2675
|
+
<div class="card owasp-card-wrapper">
|
|
2676
|
+
<div class="section-title" style="margin-bottom: 8px;">🛡 OWASP Top 10 (2021/2026) Compliance Matrix</div>
|
|
2677
|
+
<div class="grid-owasp">
|
|
2678
|
+
{owasp_cards_html}
|
|
2679
|
+
</div>
|
|
2680
|
+
</div>
|
|
2681
|
+
|
|
2682
|
+
<div class="card comp-card-wrapper">
|
|
2683
|
+
<div class="section-title" style="margin-bottom: 8px;">🏛 Enterprise Compliance Framework Mapping (SOC 2 · ISO 27001 · HIPAA)</div>
|
|
2684
|
+
<div class="grid-compliance" id="gridCompliance">
|
|
2685
|
+
{compliance_cards_html}
|
|
2686
|
+
</div>
|
|
2687
|
+
</div>
|
|
2688
|
+
</div>
|
|
2689
|
+
|
|
2690
|
+
<!-- Executive Action Footer Bar (Zero Scroll Navigation) -->
|
|
2691
|
+
<footer class="footer-compact">
|
|
2692
|
+
<div class="footer-actions">
|
|
2693
|
+
<button class="btn btn-sm" onclick="openFindingsModal()">🔍 View Findings & Evidence ({total_discovered})</button>
|
|
2694
|
+
<button class="btn btn-sm" onclick="openInvariantsModal()">🛡 74 Invariants Catalog</button>
|
|
2695
|
+
<button class="btn btn-sm" onclick="openRecipesModal()">✨ Golden Fix Recipes ({len(recipes)})</button>
|
|
2696
|
+
<button class="btn btn-sm" onclick="openAdvisoryModal()">💡 Defenses Advisory</button>
|
|
2697
|
+
</div>
|
|
2698
|
+
<div class="footer-meta">
|
|
2699
|
+
<span>TorusGuard v1.3.6 · 100% Local-First · Zero Cloud Telemetry</span>
|
|
2700
|
+
</div>
|
|
2701
|
+
</footer>
|
|
2702
|
+
</div>
|
|
2703
|
+
|
|
2704
|
+
<!-- Deep Inspection Modals -->
|
|
2705
|
+
<!-- 1. Findings & Evidence Modal -->
|
|
2706
|
+
<div id="findingsModal" class="modal-backdrop" style="display: none;" onclick="closeModal('findingsModal', event)">
|
|
2707
|
+
<div class="modal-card modal-lg" onclick="event.stopPropagation()">
|
|
2708
|
+
<div class="modal-header">
|
|
2709
|
+
<div>
|
|
2710
|
+
<h3 style="font-size: 16px; margin: 0; color: var(--text-main);">🔍 Governed Security Findings & Evidence Archive</h3>
|
|
2711
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
|
2712
|
+
{total_discovered} findings discovered across repository AST inspection.
|
|
2713
|
+
</div>
|
|
2714
|
+
</div>
|
|
2715
|
+
<button class="modal-close-btn" onclick="closeModal('findingsModal', event)">×</button>
|
|
2716
|
+
</div>
|
|
2717
|
+
<div class="modal-body" style="padding-top: 10px;">
|
|
2718
|
+
<!-- Interactive What-If Posture Score Simulator -->
|
|
2719
|
+
<div class="card simulator-card {'simulator-clean' if open_count == 0 else ''}" style="margin-bottom: 14px;">
|
|
2720
|
+
<div>
|
|
2721
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
2722
|
+
<strong style="font-size: 13px;">⚡ Interactive "What-If" Posture Score Simulator</strong>
|
|
2723
|
+
<span class="badge badge-primary">Dynamic Recalculation</span>
|
|
2724
|
+
</div>
|
|
2725
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">
|
|
2726
|
+
{'All 74 security invariants are currently defended. Simulator is ready for live remediation modeling during audit cycles.' if open_count == 0 else 'Simulate applying governed patches to preview projected posture improvement in real time:'}
|
|
2727
|
+
</div>
|
|
2728
|
+
</div>
|
|
2729
|
+
<div class="sim-stats">
|
|
2730
|
+
<div>Current: <strong id="simCurrentVal">{score}</strong>/100</div>
|
|
2731
|
+
<div>→</div>
|
|
2732
|
+
<div>Projected: <strong id="simProjectedVal" style="color: var(--accent-blue);">{score}</strong>/100 <span id="simDeltaBadge" class="badge badge-success">+0 pts</span></div>
|
|
2733
|
+
</div>
|
|
2734
|
+
<div class="sim-actions">
|
|
2735
|
+
<button class="btn btn-sm" onclick="simulateFixAll('Critical')">⚡ Simulate Criticals</button>
|
|
2736
|
+
<button class="btn btn-sm" onclick="simulateFixAll('all')">⚡ Simulate All Fixes</button>
|
|
2737
|
+
<button class="btn btn-sm" onclick="resetSimulatedFixes()">Reset Simulator</button>
|
|
2738
|
+
</div>
|
|
2739
|
+
</div>
|
|
2740
|
+
|
|
2741
|
+
{findings_section_html}
|
|
2742
|
+
</div>
|
|
2743
|
+
</div>
|
|
2744
|
+
</div>
|
|
2745
|
+
|
|
2746
|
+
<!-- 2. Active Defenses & 74 Invariants Catalog Modal -->
|
|
2747
|
+
<div id="invariantsCatalogModal" class="modal-backdrop" style="display: none;" onclick="closeModal('invariantsCatalogModal', event)">
|
|
2748
|
+
<div class="modal-card modal-lg" onclick="event.stopPropagation()">
|
|
2749
|
+
<div class="modal-header">
|
|
2750
|
+
<div>
|
|
2751
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
2752
|
+
<span class="badge badge-success">✔ 100% ENFORCED</span>
|
|
2753
|
+
<h3 style="font-size: 16px; margin: 0; color: var(--text-main);">🛡 Active Defenses & Protected Invariants ({defended_count} Passing)</h3>
|
|
2754
|
+
</div>
|
|
2755
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">
|
|
2756
|
+
All 74 architectural security rules enforced across 5 defensive layers. Zero security bypasses detected.
|
|
2757
|
+
</div>
|
|
2758
|
+
</div>
|
|
2759
|
+
<button class="modal-close-btn" onclick="closeModal('invariantsCatalogModal', event)">×</button>
|
|
2760
|
+
</div>
|
|
2761
|
+
<div class="modal-body" style="padding-top: 10px;">
|
|
2762
|
+
<div class="defended-section" id="defendedSection">
|
|
2763
|
+
<div class="coverage-bar-container" style="margin-bottom: 12px;">
|
|
2764
|
+
<div class="coverage-stats">
|
|
2765
|
+
<span><strong>{defended_count} of {total_rules} Invariants Enforced</strong> ({coverage_pct}% Defense Coverage)</span>
|
|
2766
|
+
<span class="badge {'badge-success' if coverage_pct >= 90 else 'badge-high'}">{coverage_pct}% Enforced</span>
|
|
2767
|
+
</div>
|
|
2768
|
+
<div class="coverage-progress-bg">
|
|
2769
|
+
<div class="coverage-progress-fill" style="width: {coverage_pct}%;"></div>
|
|
2770
|
+
</div>
|
|
2771
|
+
</div>
|
|
2772
|
+
<div class="inv-search-bar" style="margin-bottom: 12px;">
|
|
2773
|
+
<div class="search-wrapper" style="width: 100%;">
|
|
2774
|
+
<input type="text" id="invSearchInput" class="search-input" style="width: 100%;" placeholder="Search 74 invariants by ID, title, layer, or keyword..." oninput="filterInvariants()">
|
|
2775
|
+
<button class="search-clear-btn" id="invSearchClearBtn" onclick="clearInvSearch()">×</button>
|
|
2776
|
+
</div>
|
|
2777
|
+
<div id="invMatchCount" style="font-size: 11px; color: var(--accent-blue); white-space: nowrap;"></div>
|
|
2778
|
+
</div>
|
|
2779
|
+
<div class="layers-container" id="layersList">
|
|
2780
|
+
{layers_html}
|
|
2781
|
+
</div>
|
|
2782
|
+
</div>
|
|
2783
|
+
</div>
|
|
2784
|
+
</div>
|
|
2785
|
+
</div>
|
|
2786
|
+
|
|
2787
|
+
<!-- 3. Golden Fix Recipes Modal -->
|
|
2788
|
+
<div id="recipesModal" class="modal-backdrop" style="display: none;" onclick="closeModal('recipesModal', event)">
|
|
2789
|
+
<div class="modal-card modal-lg" id="recipesCard" onclick="event.stopPropagation()">
|
|
2790
|
+
<div class="modal-header">
|
|
2791
|
+
<div>
|
|
2792
|
+
<h3 style="font-size: 16px; margin: 0; color: var(--text-main);">✨ Golden Fix Recipes ({len(recipes)})</h3>
|
|
2793
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">
|
|
2794
|
+
Verified remediation patterns distilled from applied patches conforming to Ponytail line churn budget.
|
|
2795
|
+
</div>
|
|
2796
|
+
</div>
|
|
2797
|
+
<button class="modal-close-btn" onclick="closeModal('recipesModal', event)">×</button>
|
|
2798
|
+
</div>
|
|
2799
|
+
<div class="modal-body" style="padding-top: 10px;">
|
|
2800
|
+
<div class="filter-pills" style="margin-bottom: 12px;">
|
|
2801
|
+
{recipe_cat_pills_html}
|
|
2802
|
+
</div>
|
|
2803
|
+
<div id="recipeContainer">
|
|
2804
|
+
{recipes_html}
|
|
2805
|
+
</div>
|
|
2806
|
+
</div>
|
|
2807
|
+
</div>
|
|
2808
|
+
</div>
|
|
2809
|
+
|
|
2810
|
+
<!-- 4. Next Best Defenses Advisory Modal -->
|
|
2811
|
+
<div id="advisoryModal" class="modal-backdrop" style="display: none;" onclick="closeModal('advisoryModal', event)">
|
|
2812
|
+
<div class="modal-card modal-lg" onclick="event.stopPropagation()">
|
|
2813
|
+
<div class="modal-header">
|
|
2814
|
+
<div>
|
|
2815
|
+
<h3 style="font-size: 16px; margin: 0; color: var(--text-main);">💡 Prescriptive Next Best Defenses Advisory</h3>
|
|
2816
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">
|
|
2817
|
+
Stack-tailored recommendations to achieve hardened posture.
|
|
2818
|
+
</div>
|
|
2819
|
+
</div>
|
|
2820
|
+
<button class="modal-close-btn" onclick="closeModal('advisoryModal', event)">×</button>
|
|
2821
|
+
</div>
|
|
2822
|
+
<div class="modal-body" style="padding-top: 10px;">
|
|
2823
|
+
{advisory_html}
|
|
2824
|
+
</div>
|
|
2825
|
+
</div>
|
|
2826
|
+
</div>
|
|
2827
|
+
|
|
2828
|
+
<!-- 5. Invariant Guarantee Inspection Modal -->
|
|
2829
|
+
<div id="invariantModal" class="modal-backdrop" style="display: none;" onclick="closeInvariantModal(event)">
|
|
2830
|
+
<div class="modal-card" onclick="event.stopPropagation()">
|
|
2831
|
+
<div class="modal-header">
|
|
2832
|
+
<div>
|
|
2833
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
2834
|
+
<span id="modalInvSev" class="badge badge-medium">Medium</span>
|
|
2835
|
+
<code id="modalInvRule" class="rule-id" style="font-size: 14px;">TG-GEN</code>
|
|
2836
|
+
<span id="modalInvStatus" class="badge badge-success">Defended</span>
|
|
2837
|
+
</div>
|
|
2838
|
+
<h3 id="modalInvTitle" style="font-size: 16px; margin-top: 6px; color: var(--text-main);">Security Invariant</h3>
|
|
2839
|
+
</div>
|
|
2840
|
+
<button class="modal-close-btn" onclick="closeInvariantModal(event)">×</button>
|
|
2841
|
+
</div>
|
|
2842
|
+
<div class="modal-body">
|
|
2843
|
+
<div>
|
|
2844
|
+
<div style="font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 600;">Architectural Layer & Category</div>
|
|
2845
|
+
<div id="modalInvLayer" style="margin-top: 2px; font-weight: 500;">Application & LLM Defense</div>
|
|
2846
|
+
</div>
|
|
2847
|
+
<div>
|
|
2848
|
+
<div style="font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 600;">Invariant Guarantee</div>
|
|
2849
|
+
<div id="modalInvGuarantee" style="margin-top: 4px; line-height: 1.4; color: var(--text-main);">
|
|
2850
|
+
Enforces deterministic verification without bypasses.
|
|
2851
|
+
</div>
|
|
2852
|
+
</div>
|
|
2853
|
+
<div>
|
|
2854
|
+
<div style="font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 600;">Ponytail Churn Invariant</div>
|
|
2855
|
+
<div style="margin-top: 2px; color: var(--accent-yellow); font-size: 12px;">
|
|
2856
|
+
≤ 35 additions, ≤ 25 deletions per surgical patch bundle.
|
|
2857
|
+
</div>
|
|
2858
|
+
</div>
|
|
2859
|
+
<div>
|
|
2860
|
+
<div style="font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 600; margin-bottom: 4px;">Governed CLI Command</div>
|
|
2861
|
+
<div class="cmd-box" id="modalInvCmd">
|
|
2862
|
+
<code>npx torusguard harden --rule TG-GEN</code>
|
|
2863
|
+
<button class="btn-copy" onclick="copyText('npx torusguard harden --rule TG-GEN', this)">📋 Copy</button>
|
|
2864
|
+
</div>
|
|
2865
|
+
</div>
|
|
2866
|
+
</div>
|
|
2867
|
+
</div>
|
|
2868
|
+
</div>
|
|
2869
|
+
|
|
2870
|
+
<script>
|
|
2871
|
+
const RAW_FINDINGS = {findings_json_str};
|
|
2872
|
+
const SARIF_RULES = {sarif_rules_json_str};
|
|
2873
|
+
const INVARIANT_CATALOG = {rule_catalog_json_str};
|
|
2874
|
+
const VIOLATED_RULE_IDS = {violated_ids_json_str};
|
|
2875
|
+
|
|
2876
|
+
let currentSeverity = 'all';
|
|
2877
|
+
let currentStatus = 'all';
|
|
2878
|
+
let currentDirectory = '';
|
|
2879
|
+
let currentRecipeCategory = 'all';
|
|
2880
|
+
let BASE_POSTURE_SCORE = {score};
|
|
2881
|
+
|
|
2882
|
+
// ── Modal Dialog Engine ──
|
|
2883
|
+
function openModal(id) {{
|
|
2884
|
+
const m = document.getElementById(id);
|
|
2885
|
+
if (m) {{
|
|
2886
|
+
m.style.display = 'flex';
|
|
2887
|
+
document.body.style.overflow = 'hidden';
|
|
2888
|
+
}}
|
|
2889
|
+
}}
|
|
2890
|
+
function closeModal(id, event) {{
|
|
2891
|
+
if (event && event.target && event.target !== event.currentTarget && !event.target.classList.contains('modal-close-btn')) {{
|
|
2892
|
+
return;
|
|
2893
|
+
}}
|
|
2894
|
+
const m = document.getElementById(id);
|
|
2895
|
+
if (m) {{
|
|
2896
|
+
m.style.display = 'none';
|
|
2897
|
+
document.body.style.overflow = '';
|
|
2898
|
+
}}
|
|
2899
|
+
}}
|
|
2900
|
+
function openFindingsModal() {{ openModal('findingsModal'); }}
|
|
2901
|
+
function openInvariantsModal() {{ openModal('invariantsCatalogModal'); }}
|
|
2902
|
+
function openRecipesModal() {{ openModal('recipesModal'); }}
|
|
2903
|
+
function openAdvisoryModal() {{ openModal('advisoryModal'); }}
|
|
2904
|
+
|
|
2905
|
+
document.addEventListener('keydown', function(e) {{
|
|
2906
|
+
if (e.key === 'Escape') {{
|
|
2907
|
+
document.querySelectorAll('.modal-backdrop').forEach(m => {{
|
|
2908
|
+
m.style.display = 'none';
|
|
2909
|
+
}});
|
|
2910
|
+
document.body.style.overflow = '';
|
|
2911
|
+
}}
|
|
2912
|
+
}});
|
|
2913
|
+
|
|
2914
|
+
// ── Theme Switcher ──
|
|
2915
|
+
function initTheme() {{
|
|
2916
|
+
const saved = localStorage.getItem('tg_theme');
|
|
2917
|
+
const btn = document.getElementById('themeToggleBtn');
|
|
2918
|
+
if (saved === 'light') {{
|
|
2919
|
+
document.body.classList.add('light-theme');
|
|
2920
|
+
if (btn) btn.innerHTML = '🌙 Dark Mode';
|
|
2921
|
+
}} else {{
|
|
2922
|
+
document.body.classList.remove('light-theme');
|
|
2923
|
+
if (btn) btn.innerHTML = '☀️ Light Mode';
|
|
2924
|
+
}}
|
|
2925
|
+
}}
|
|
2926
|
+
|
|
2927
|
+
function toggleTheme() {{
|
|
2928
|
+
const isLight = document.body.classList.toggle('light-theme');
|
|
2929
|
+
localStorage.setItem('tg_theme', isLight ? 'light' : 'dark');
|
|
2930
|
+
const btn = document.getElementById('themeToggleBtn');
|
|
2931
|
+
if (btn) btn.innerHTML = isLight ? '🌙 Dark Mode' : '☀️ Light Mode';
|
|
2932
|
+
}}
|
|
2933
|
+
|
|
2934
|
+
document.addEventListener('DOMContentLoaded', initTheme);
|
|
2935
|
+
if (document.readyState === 'complete' || document.readyState === 'interactive') {{
|
|
2936
|
+
initTheme();
|
|
2937
|
+
}}
|
|
2938
|
+
|
|
2939
|
+
// ── What-If Posture Score Simulator ──
|
|
2940
|
+
function updateSimulatedScore() {{
|
|
2941
|
+
const checkboxes = document.querySelectorAll('.sim-fix-chk');
|
|
2942
|
+
const fixedIds = new Set();
|
|
2943
|
+
checkboxes.forEach(cb => {{
|
|
2944
|
+
if (cb.checked) {{
|
|
2945
|
+
fixedIds.add(cb.getAttribute('data-finding-id'));
|
|
2946
|
+
}}
|
|
2947
|
+
}});
|
|
2948
|
+
|
|
2949
|
+
let remCrit = 0, remHigh = 0, remMed = 0, remLow = 0;
|
|
2950
|
+
RAW_FINDINGS.forEach(f => {{
|
|
2951
|
+
if (f.is_canary) return;
|
|
2952
|
+
const st = (f.status || 'open').toLowerCase();
|
|
2953
|
+
if (st === 'resolved' || st === 'fixed' || st === 'confirmed_fixed') return;
|
|
2954
|
+
if (fixedIds.has(f.finding_id)) return; // Simulated as fixed
|
|
2955
|
+
const s = f.severity;
|
|
2956
|
+
if (s === 'Critical') remCrit++;
|
|
2957
|
+
else if (s === 'High') remHigh++;
|
|
2958
|
+
else if (s === 'Medium') remMed++;
|
|
2959
|
+
else if (s === 'Low') remLow++;
|
|
2960
|
+
}});
|
|
2961
|
+
|
|
2962
|
+
const penalty = (remCrit * 25) + (remHigh * 15) + (remMed * 5) + (remLow * 2);
|
|
2963
|
+
const newScore = Math.max(0, Math.min(100, 100 - penalty));
|
|
2964
|
+
|
|
2965
|
+
// Update projected UI labels
|
|
2966
|
+
const projEl = document.getElementById('simProjectedVal');
|
|
2967
|
+
const deltaEl = document.getElementById('simDeltaBadge');
|
|
2968
|
+
if (projEl) projEl.textContent = newScore;
|
|
2969
|
+
const delta = newScore - BASE_POSTURE_SCORE;
|
|
2970
|
+
if (deltaEl) {{
|
|
2971
|
+
deltaEl.textContent = (delta >= 0 ? '+' : '') + delta + ' pts';
|
|
2972
|
+
deltaEl.className = 'badge ' + (delta > 0 ? 'badge-success' : 'badge-primary');
|
|
2973
|
+
}}
|
|
2974
|
+
|
|
2975
|
+
// Animate SVG gauge to simulated score
|
|
2976
|
+
animateGaugeTo(newScore);
|
|
2977
|
+
}}
|
|
2978
|
+
|
|
2979
|
+
function simulateFixAll(sev) {{
|
|
2980
|
+
const checkboxes = document.querySelectorAll('.sim-fix-chk');
|
|
2981
|
+
checkboxes.forEach(cb => {{
|
|
2982
|
+
const cbSev = cb.getAttribute('data-severity');
|
|
2983
|
+
if (sev === 'all' || cbSev === sev) {{
|
|
2984
|
+
cb.checked = true;
|
|
2985
|
+
}}
|
|
2986
|
+
}});
|
|
2987
|
+
updateSimulatedScore();
|
|
2988
|
+
}}
|
|
2989
|
+
|
|
2990
|
+
function resetSimulatedFixes() {{
|
|
2991
|
+
const checkboxes = document.querySelectorAll('.sim-fix-chk');
|
|
2992
|
+
checkboxes.forEach(cb => {{ cb.checked = false; }});
|
|
2993
|
+
updateSimulatedScore();
|
|
2994
|
+
}}
|
|
2995
|
+
|
|
2996
|
+
function animateGaugeTo(targetScore) {{
|
|
2997
|
+
const circumference = 251.2;
|
|
2998
|
+
const targetOffset = circumference - (circumference * (targetScore / 100.0));
|
|
2999
|
+
const gaugeEl = document.getElementById('gaugeCircleFill');
|
|
3000
|
+
const textEl = document.getElementById('postureScoreText');
|
|
3001
|
+
|
|
3002
|
+
if (gaugeEl) {{
|
|
3003
|
+
gaugeEl.style.strokeDashoffset = targetOffset;
|
|
3004
|
+
}}
|
|
3005
|
+
if (textEl) {{
|
|
3006
|
+
textEl.textContent = targetScore;
|
|
3007
|
+
}}
|
|
3008
|
+
}}
|
|
3009
|
+
|
|
3010
|
+
// ── Invariant Matrix Filters & Inspection ──
|
|
3011
|
+
function filterInvariants() {{
|
|
3012
|
+
const input = document.getElementById('invSearchInput');
|
|
3013
|
+
const clearBtn = document.getElementById('invSearchClearBtn');
|
|
3014
|
+
const q = (input ? input.value : '').toLowerCase().trim();
|
|
3015
|
+
if (clearBtn) clearBtn.style.display = q ? 'block' : 'none';
|
|
3016
|
+
|
|
3017
|
+
const chips = document.querySelectorAll('.inv-chip');
|
|
3018
|
+
let matchCount = 0;
|
|
3019
|
+
chips.forEach(chip => {{
|
|
3020
|
+
const s = chip.getAttribute('data-search') || '';
|
|
3021
|
+
const visible = !q || s.includes(q);
|
|
3022
|
+
chip.style.display = visible ? 'inline-flex' : 'none';
|
|
3023
|
+
if (visible) matchCount++;
|
|
3024
|
+
}});
|
|
3025
|
+
|
|
3026
|
+
const countEl = document.getElementById('invMatchCount');
|
|
3027
|
+
if (countEl) {{
|
|
3028
|
+
countEl.textContent = q ? '(' + matchCount + ' matching)' : '';
|
|
3029
|
+
}}
|
|
3030
|
+
}}
|
|
3031
|
+
|
|
3032
|
+
function clearInvSearch() {{
|
|
3033
|
+
const input = document.getElementById('invSearchInput');
|
|
3034
|
+
if (input) input.value = '';
|
|
3035
|
+
filterInvariants();
|
|
3036
|
+
}}
|
|
3037
|
+
|
|
3038
|
+
function inspectInvariant(ruleId) {{
|
|
3039
|
+
const item = INVARIANT_CATALOG[ruleId];
|
|
3040
|
+
if (!item) return;
|
|
3041
|
+
const modal = document.getElementById('invariantModal');
|
|
3042
|
+
const isViolated = VIOLATED_RULE_IDS.includes(ruleId);
|
|
3043
|
+
|
|
3044
|
+
document.getElementById('modalInvRule').textContent = item.rule_id;
|
|
3045
|
+
document.getElementById('modalInvTitle').textContent = item.title;
|
|
3046
|
+
document.getElementById('modalInvLayer').textContent = item.layer_name + ' · ' + item.category;
|
|
3047
|
+
document.getElementById('modalInvGuarantee').textContent = item.guarantee;
|
|
3048
|
+
|
|
3049
|
+
const sevEl = document.getElementById('modalInvSev');
|
|
3050
|
+
sevEl.textContent = item.severity;
|
|
3051
|
+
sevEl.className = 'badge ' + (item.severity === 'Critical' ? 'badge-critical' : (item.severity === 'High' ? 'badge-high' : 'badge-medium'));
|
|
3052
|
+
|
|
3053
|
+
const statusEl = document.getElementById('modalInvStatus');
|
|
3054
|
+
statusEl.innerHTML = isViolated ? '⚠ Violated' : '✔ Defended';
|
|
3055
|
+
statusEl.className = 'badge ' + (isViolated ? 'badge-critical' : 'badge-success');
|
|
3056
|
+
|
|
3057
|
+
const cmdBox = document.getElementById('modalInvCmd');
|
|
3058
|
+
cmdBox.innerHTML = `<code>npx torusguard harden --rule ${{item.rule_id}}</code>
|
|
3059
|
+
<button class="btn-copy" onclick="copyText('npx torusguard harden --rule ${{item.rule_id}}', this); event.stopPropagation();">📋 Copy</button>`;
|
|
3060
|
+
|
|
3061
|
+
if (modal) modal.style.display = 'flex';
|
|
3062
|
+
}}
|
|
3063
|
+
|
|
3064
|
+
function closeInvariantModal(e) {{
|
|
3065
|
+
const modal = document.getElementById('invariantModal');
|
|
3066
|
+
if (modal) modal.style.display = 'none';
|
|
3067
|
+
}}
|
|
3068
|
+
|
|
3069
|
+
document.addEventListener('keydown', function(e) {{
|
|
3070
|
+
if (e.key === 'Escape') closeInvariantModal();
|
|
3071
|
+
}});
|
|
3072
|
+
|
|
3073
|
+
// ── Golden Recipes View Switcher & Category Filter ──
|
|
3074
|
+
function setRecipeView(recipeId, viewMode) {{
|
|
3075
|
+
const diffView = document.getElementById('recipe-view-diff-' + recipeId);
|
|
3076
|
+
const splitView = document.getElementById('recipe-view-split-' + recipeId);
|
|
3077
|
+
const btnDiff = document.getElementById('btn-view-diff-' + recipeId);
|
|
3078
|
+
const btnSplit = document.getElementById('btn-view-split-' + recipeId);
|
|
3079
|
+
|
|
3080
|
+
if (viewMode === 'diff') {{
|
|
3081
|
+
if (diffView) diffView.style.display = 'block';
|
|
3082
|
+
if (splitView) splitView.style.display = 'none';
|
|
3083
|
+
if (btnDiff) btnDiff.classList.add('active');
|
|
3084
|
+
if (btnSplit) btnSplit.classList.remove('active');
|
|
3085
|
+
}} else {{
|
|
3086
|
+
if (diffView) diffView.style.display = 'none';
|
|
3087
|
+
if (splitView) splitView.style.display = 'grid';
|
|
3088
|
+
if (btnDiff) btnDiff.classList.remove('active');
|
|
3089
|
+
if (btnSplit) btnSplit.classList.add('active');
|
|
3090
|
+
}}
|
|
3091
|
+
}}
|
|
3092
|
+
|
|
3093
|
+
function filterRecipeCategory(cat) {{
|
|
3094
|
+
currentRecipeCategory = cat;
|
|
3095
|
+
document.querySelectorAll('[data-recipe-cat-filter]').forEach(p => {{
|
|
3096
|
+
if (p.getAttribute('data-recipe-cat-filter') === cat) p.classList.add('active');
|
|
3097
|
+
else p.classList.remove('active');
|
|
3098
|
+
}});
|
|
3099
|
+
|
|
3100
|
+
document.querySelectorAll('.recipe-card').forEach(card => {{
|
|
3101
|
+
const cardCat = card.getAttribute('data-category') || '';
|
|
3102
|
+
if (cat === 'all' || cardCat === cat) {{
|
|
3103
|
+
card.style.display = '';
|
|
3104
|
+
}} else {{
|
|
3105
|
+
card.style.display = 'none';
|
|
3106
|
+
}}
|
|
3107
|
+
}});
|
|
3108
|
+
}}
|
|
3109
|
+
|
|
3110
|
+
// ── Findings Filtering & Drawer Lifecycle ──
|
|
3111
|
+
function filterBySeverity(sev) {{
|
|
3112
|
+
openFindingsModal();
|
|
3113
|
+
currentSeverity = sev;
|
|
3114
|
+
document.querySelectorAll('[data-sev-filter]').forEach(p => {{
|
|
3115
|
+
if (p.getAttribute('data-sev-filter') === sev) p.classList.add('active');
|
|
3116
|
+
else p.classList.remove('active');
|
|
3117
|
+
}});
|
|
3118
|
+
searchFindings();
|
|
3119
|
+
}}
|
|
3120
|
+
|
|
3121
|
+
function filterByStatus(status) {{
|
|
3122
|
+
currentStatus = status;
|
|
3123
|
+
document.querySelectorAll('[data-status-filter]').forEach(p => {{
|
|
3124
|
+
if (p.getAttribute('data-status-filter') === status) p.classList.add('active');
|
|
3125
|
+
else p.classList.remove('active');
|
|
3126
|
+
}});
|
|
3127
|
+
searchFindings();
|
|
3128
|
+
}}
|
|
3129
|
+
|
|
3130
|
+
function filterFindings(sev) {{
|
|
3131
|
+
filterBySeverity(sev);
|
|
3132
|
+
}}
|
|
3133
|
+
|
|
3134
|
+
function filterByDirectory(dir) {{
|
|
3135
|
+
openFindingsModal();
|
|
3136
|
+
currentDirectory = dir.replace(/\\/$/, '');
|
|
3137
|
+
const chip = document.getElementById('dirFilterChip');
|
|
3138
|
+
const chipName = document.getElementById('dirFilterName');
|
|
3139
|
+
if (chip && chipName) {{
|
|
3140
|
+
chipName.textContent = currentDirectory;
|
|
3141
|
+
chip.style.display = 'inline-flex';
|
|
3142
|
+
}}
|
|
3143
|
+
searchFindings();
|
|
3144
|
+
}}
|
|
3145
|
+
|
|
3146
|
+
function clearDirectoryFilter() {{
|
|
3147
|
+
currentDirectory = '';
|
|
3148
|
+
const chip = document.getElementById('dirFilterChip');
|
|
3149
|
+
if (chip) chip.style.display = 'none';
|
|
3150
|
+
searchFindings();
|
|
3151
|
+
}}
|
|
3152
|
+
|
|
3153
|
+
function clearSearch() {{
|
|
3154
|
+
const searchInput = document.getElementById('searchInput');
|
|
3155
|
+
if (searchInput) searchInput.value = '';
|
|
3156
|
+
searchFindings();
|
|
3157
|
+
}}
|
|
3158
|
+
|
|
3159
|
+
function resetAllFilters() {{
|
|
3160
|
+
currentSeverity = 'all';
|
|
3161
|
+
currentStatus = 'all';
|
|
3162
|
+
currentDirectory = '';
|
|
3163
|
+
const searchInput = document.getElementById('searchInput');
|
|
3164
|
+
if (searchInput) searchInput.value = '';
|
|
3165
|
+
|
|
3166
|
+
document.querySelectorAll('[data-sev-filter]').forEach(p => {{
|
|
3167
|
+
if (p.getAttribute('data-sev-filter') === 'all') p.classList.add('active');
|
|
3168
|
+
else p.classList.remove('active');
|
|
3169
|
+
}});
|
|
3170
|
+
document.querySelectorAll('[data-status-filter]').forEach(p => {{
|
|
3171
|
+
if (p.getAttribute('data-status-filter') === 'all') p.classList.add('active');
|
|
3172
|
+
else p.classList.remove('active');
|
|
3173
|
+
}});
|
|
3174
|
+
const chip = document.getElementById('dirFilterChip');
|
|
3175
|
+
if (chip) chip.style.display = 'none';
|
|
3176
|
+
|
|
3177
|
+
searchFindings();
|
|
3178
|
+
}}
|
|
3179
|
+
|
|
3180
|
+
function toggleDefendedVisibility() {{
|
|
3181
|
+
const body = document.getElementById('defendedMatrixBody');
|
|
3182
|
+
const icon = document.getElementById('defendedToggleIcon');
|
|
3183
|
+
if (!body) return;
|
|
3184
|
+
if (body.style.display === 'none') {{
|
|
3185
|
+
body.style.display = 'block';
|
|
3186
|
+
if (icon) icon.innerHTML = '▼ Collapse Details';
|
|
3187
|
+
}} else {{
|
|
3188
|
+
body.style.display = 'none';
|
|
3189
|
+
if (icon) icon.innerHTML = '▶ Expand 74 Invariant Details';
|
|
3190
|
+
}}
|
|
3191
|
+
}}
|
|
3192
|
+
|
|
3193
|
+
function toggleFindingsArchive() {{
|
|
3194
|
+
const body = document.getElementById('findingsArchiveBody');
|
|
3195
|
+
const icon = document.getElementById('archiveToggleIcon');
|
|
3196
|
+
const btn = document.getElementById('btnArchiveToggle');
|
|
3197
|
+
if (!body) return;
|
|
3198
|
+
if (body.style.display === 'none') {{
|
|
3199
|
+
body.style.display = 'block';
|
|
3200
|
+
if (icon) icon.innerHTML = '▼';
|
|
3201
|
+
if (btn) btn.classList.add('active');
|
|
3202
|
+
searchFindings();
|
|
3203
|
+
}} else {{
|
|
3204
|
+
body.style.display = 'none';
|
|
3205
|
+
if (icon) icon.innerHTML = '▶';
|
|
3206
|
+
if (btn) btn.classList.remove('active');
|
|
3207
|
+
}}
|
|
3208
|
+
}}
|
|
3209
|
+
|
|
3210
|
+
function toggleRecipesSection() {{
|
|
3211
|
+
const body = document.getElementById('recipesCardBody');
|
|
3212
|
+
const icon = document.getElementById('recipesToggleIcon');
|
|
3213
|
+
const btn = document.getElementById('btnRecipesToggle');
|
|
3214
|
+
if (!body) return;
|
|
3215
|
+
if (body.style.display === 'none') {{
|
|
3216
|
+
body.style.display = 'block';
|
|
3217
|
+
if (btn) btn.innerHTML = '<span id="recipesToggleIcon">▼</span> Collapse';
|
|
3218
|
+
}} else {{
|
|
3219
|
+
body.style.display = 'none';
|
|
3220
|
+
if (btn) btn.innerHTML = '<span id="recipesToggleIcon">▶</span> Expand ({len(recipes)})';
|
|
3221
|
+
}}
|
|
3222
|
+
}}
|
|
3223
|
+
|
|
3224
|
+
function toggleComplianceSection() {{
|
|
3225
|
+
const grid = document.getElementById('gridCompliance');
|
|
3226
|
+
const icon = document.getElementById('compToggleIcon');
|
|
3227
|
+
if (!grid) return;
|
|
3228
|
+
if (grid.style.display === 'none') {{
|
|
3229
|
+
grid.style.display = 'grid';
|
|
3230
|
+
if (icon) icon.innerHTML = '▼ Collapse';
|
|
3231
|
+
}} else {{
|
|
3232
|
+
grid.style.display = 'none';
|
|
3233
|
+
if (icon) icon.innerHTML = '▶ Expand Frameworks';
|
|
3234
|
+
}}
|
|
3235
|
+
}}
|
|
3236
|
+
|
|
3237
|
+
function toggleDefendedSection() {{
|
|
3238
|
+
openInvariantsModal();
|
|
3239
|
+
}}
|
|
3240
|
+
|
|
3241
|
+
function searchFindings() {{
|
|
3242
|
+
const searchInput = document.getElementById('searchInput');
|
|
3243
|
+
const clearBtn = document.getElementById('searchClearBtn');
|
|
3244
|
+
const query = (searchInput ? searchInput.value : '').toLowerCase().trim();
|
|
3245
|
+
|
|
3246
|
+
if (clearBtn) clearBtn.style.display = query ? 'block' : 'none';
|
|
3247
|
+
|
|
3248
|
+
const rows = document.querySelectorAll('.finding-row');
|
|
3249
|
+
let visibleCount = 0;
|
|
3250
|
+
|
|
3251
|
+
rows.forEach(r => {{
|
|
3252
|
+
const rSev = r.getAttribute('data-severity') || '';
|
|
3253
|
+
const rStatus = r.getAttribute('data-status') || 'open';
|
|
3254
|
+
const rSearch = r.getAttribute('data-search') || '';
|
|
3255
|
+
const rPath = r.getAttribute('data-path') || '';
|
|
3256
|
+
const drawerId = r.getAttribute('data-drawer-id');
|
|
3257
|
+
const drawer = document.getElementById(drawerId);
|
|
3258
|
+
|
|
3259
|
+
let matchSev = (currentSeverity === 'all') || (rSev === currentSeverity);
|
|
3260
|
+
if (currentSeverity === 'medium') {{
|
|
3261
|
+
matchSev = (rSev === 'medium' || rSev === 'low');
|
|
3262
|
+
}}
|
|
3263
|
+
|
|
3264
|
+
let matchStatus = (currentStatus === 'all') || (rStatus === currentStatus);
|
|
3265
|
+
|
|
3266
|
+
let matchDir = true;
|
|
3267
|
+
if (currentDirectory) {{
|
|
3268
|
+
matchDir = rPath.toLowerCase().startsWith(currentDirectory.toLowerCase());
|
|
3269
|
+
}}
|
|
3270
|
+
|
|
3271
|
+
const matchQuery = !query || rSearch.includes(query);
|
|
3272
|
+
const visible = matchSev && matchStatus && matchDir && matchQuery;
|
|
3273
|
+
|
|
3274
|
+
if (visible) visibleCount++;
|
|
3275
|
+
r.style.display = visible ? '' : 'none';
|
|
3276
|
+
if (drawer && !visible) {{
|
|
3277
|
+
drawer.style.display = 'none';
|
|
3278
|
+
r.classList.remove('expanded');
|
|
3279
|
+
}}
|
|
3280
|
+
}});
|
|
3281
|
+
|
|
3282
|
+
const countEl = document.getElementById('visibleCount');
|
|
3283
|
+
if (countEl) countEl.textContent = visibleCount;
|
|
3284
|
+
|
|
3285
|
+
const resetBtn = document.getElementById('btnResetFilters');
|
|
3286
|
+
const hasActiveFilters = currentSeverity !== 'all' || currentStatus !== 'all' || currentDirectory !== '' || query !== '';
|
|
3287
|
+
if (resetBtn) {{
|
|
3288
|
+
resetBtn.style.display = hasActiveFilters ? 'inline-block' : 'none';
|
|
3289
|
+
}}
|
|
3290
|
+
|
|
3291
|
+
const summaryEl = document.getElementById('activeFilterSummary');
|
|
3292
|
+
if (summaryEl) {{
|
|
3293
|
+
const parts = [];
|
|
3294
|
+
if (currentSeverity !== 'all') parts.push(`Sev: ${{currentSeverity}}`);
|
|
3295
|
+
if (currentStatus !== 'all') parts.push(`Status: ${{currentStatus}}`);
|
|
3296
|
+
if (currentDirectory) parts.push(`Dir: ${{currentDirectory}}`);
|
|
3297
|
+
if (query) parts.push(`Query: "${{query}}"`);
|
|
3298
|
+
summaryEl.textContent = parts.length ? `(${{parts.join(', ')}})` : '';
|
|
3299
|
+
}}
|
|
3300
|
+
}}
|
|
3301
|
+
|
|
3302
|
+
function initPostureGauge() {{
|
|
3303
|
+
const targetScore = {score};
|
|
3304
|
+
const circumference = 251.2;
|
|
3305
|
+
const targetOffset = circumference - (circumference * (targetScore / 100.0));
|
|
3306
|
+
const gaugeEl = document.getElementById('gaugeCircleFill');
|
|
3307
|
+
const textEl = document.getElementById('postureScoreText');
|
|
3308
|
+
|
|
3309
|
+
setTimeout(() => {{
|
|
3310
|
+
if (gaugeEl) {{
|
|
3311
|
+
gaugeEl.style.strokeDashoffset = targetOffset;
|
|
3312
|
+
}}
|
|
3313
|
+
}}, 50);
|
|
3314
|
+
|
|
3315
|
+
if (textEl) {{
|
|
3316
|
+
const duration = 800;
|
|
3317
|
+
const startTime = performance.now();
|
|
3318
|
+
function updateCount(now) {{
|
|
3319
|
+
const elapsed = now - startTime;
|
|
3320
|
+
const progress = Math.min(elapsed / duration, 1.0);
|
|
3321
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
3322
|
+
const val = Math.round(eased * targetScore);
|
|
3323
|
+
textEl.textContent = val;
|
|
3324
|
+
if (progress < 1.0) {{
|
|
3325
|
+
requestAnimationFrame(updateCount);
|
|
3326
|
+
}} else {{
|
|
3327
|
+
textEl.textContent = targetScore;
|
|
3328
|
+
}}
|
|
3329
|
+
}}
|
|
3330
|
+
requestAnimationFrame(updateCount);
|
|
3331
|
+
}}
|
|
3332
|
+
}}
|
|
3333
|
+
document.addEventListener('DOMContentLoaded', initPostureGauge);
|
|
3334
|
+
if (document.readyState === 'complete' || document.readyState === 'interactive') {{
|
|
3335
|
+
initPostureGauge();
|
|
3336
|
+
}}
|
|
3337
|
+
|
|
3338
|
+
function toggleDrawer(row) {{
|
|
3339
|
+
const drawerId = row.getAttribute('data-drawer-id');
|
|
3340
|
+
const drawer = document.getElementById(drawerId);
|
|
3341
|
+
if (!drawer) return;
|
|
3342
|
+
|
|
3343
|
+
const isExpanded = row.classList.contains('expanded');
|
|
3344
|
+
if (isExpanded) {{
|
|
3345
|
+
row.classList.remove('expanded');
|
|
3346
|
+
drawer.style.display = 'none';
|
|
3347
|
+
}} else {{
|
|
3348
|
+
row.classList.add('expanded');
|
|
3349
|
+
drawer.style.display = '';
|
|
3350
|
+
}}
|
|
3351
|
+
}}
|
|
3352
|
+
|
|
3353
|
+
function copyText(text, btn) {{
|
|
3354
|
+
const cleanText = text.replace(/\\\\n/g, '\\n');
|
|
3355
|
+
if (navigator.clipboard && window.isSecureContext) {{
|
|
3356
|
+
navigator.clipboard.writeText(cleanText).then(() => {{
|
|
3357
|
+
showCopied(btn);
|
|
3358
|
+
}}).catch(() => {{
|
|
3359
|
+
fallbackCopy(cleanText, btn);
|
|
3360
|
+
}});
|
|
3361
|
+
}} else {{
|
|
3362
|
+
fallbackCopy(cleanText, btn);
|
|
3363
|
+
}}
|
|
3364
|
+
}}
|
|
3365
|
+
|
|
3366
|
+
function fallbackCopy(text, btn) {{
|
|
3367
|
+
const ta = document.createElement('textarea');
|
|
3368
|
+
ta.value = text;
|
|
3369
|
+
ta.style.position = 'fixed';
|
|
3370
|
+
ta.style.opacity = '0';
|
|
3371
|
+
document.body.appendChild(ta);
|
|
3372
|
+
ta.select();
|
|
3373
|
+
try {{
|
|
3374
|
+
document.execCommand('copy');
|
|
3375
|
+
showCopied(btn);
|
|
3376
|
+
}} catch (e) {{}}
|
|
3377
|
+
document.body.removeChild(ta);
|
|
3378
|
+
}}
|
|
3379
|
+
|
|
3380
|
+
function showCopied(btn) {{
|
|
3381
|
+
const orig = btn.innerHTML;
|
|
3382
|
+
btn.innerHTML = '✔ Copied!';
|
|
3383
|
+
btn.classList.add('copied');
|
|
3384
|
+
setTimeout(() => {{
|
|
3385
|
+
btn.innerHTML = orig;
|
|
3386
|
+
btn.classList.remove('copied');
|
|
3387
|
+
}}, 2000);
|
|
3388
|
+
}}
|
|
3389
|
+
|
|
3390
|
+
function downloadSarif() {{
|
|
3391
|
+
const sarifData = {{
|
|
3392
|
+
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
3393
|
+
"version": "2.1.0",
|
|
3394
|
+
"runs": [{{
|
|
3395
|
+
"tool": {{
|
|
3396
|
+
"driver": {{
|
|
3397
|
+
"name": "TorusGuard",
|
|
3398
|
+
"semanticVersion": "1.3.6",
|
|
3399
|
+
"informationUri": "https://github.com/githubmofo/TorusGuard",
|
|
3400
|
+
"rules": SARIF_RULES
|
|
3401
|
+
}}
|
|
3402
|
+
}},
|
|
3403
|
+
"results": RAW_FINDINGS.map(f => ({{
|
|
3404
|
+
"ruleId": f.rule_id || "TG-GEN",
|
|
3405
|
+
"message": {{ "text": f.description || f.title || "Violation" }},
|
|
3406
|
+
"level": f.severity === "Critical" ? "error" : (f.severity === "High" ? "error" : "warning"),
|
|
3407
|
+
"locations": [{{
|
|
3408
|
+
"physicalLocation": {{
|
|
3409
|
+
"artifactLocation": {{ "uri": f.file_path || "unknown" }},
|
|
3410
|
+
"region": {{ "startLine": f.line_number || 1 }}
|
|
3411
|
+
}}
|
|
3412
|
+
}}]
|
|
3413
|
+
}}))
|
|
3414
|
+
}}]
|
|
3415
|
+
}};
|
|
3416
|
+
|
|
3417
|
+
const blob = new Blob([JSON.stringify(sarifData, null, 2)], {{ type: "application/json" }});
|
|
3418
|
+
const url = URL.createObjectURL(blob);
|
|
3419
|
+
const a = document.createElement("a");
|
|
3420
|
+
a.href = url;
|
|
3421
|
+
a.download = "torusguard-report.sarif";
|
|
3422
|
+
document.body.appendChild(a);
|
|
3423
|
+
a.click();
|
|
3424
|
+
document.body.removeChild(a);
|
|
3425
|
+
URL.revokeObjectURL(url);
|
|
3426
|
+
}}
|
|
3427
|
+
|
|
3428
|
+
function downloadCsv() {{
|
|
3429
|
+
let csv = "Finding ID,Rule ID,Severity,Title,File Path,Line Number,Confidence\\n";
|
|
3430
|
+
RAW_FINDINGS.forEach(f => {{
|
|
3431
|
+
const row = [
|
|
3432
|
+
`"${{(f.finding_id || '').replace(/"/g, '""')}}"`,
|
|
3433
|
+
`"${{(f.rule_id || '').replace(/"/g, '""')}}"`,
|
|
3434
|
+
`"${{(f.severity || '').replace(/"/g, '""')}}"`,
|
|
3435
|
+
`"${{(f.title || '').replace(/"/g, '""')}}"`,
|
|
3436
|
+
`"${{(f.file_path || '').replace(/"/g, '""')}}"`,
|
|
3437
|
+
f.line_number || 0,
|
|
3438
|
+
f.confidence_score || 70
|
|
3439
|
+
];
|
|
3440
|
+
csv += row.join(",") + "\\n";
|
|
3441
|
+
}});
|
|
3442
|
+
|
|
3443
|
+
const blob = new Blob([csv], {{ type: "text/csv;charset=utf-8;" }});
|
|
3444
|
+
const url = URL.createObjectURL(blob);
|
|
3445
|
+
const a = document.createElement("a");
|
|
3446
|
+
a.href = url;
|
|
3447
|
+
a.download = "torusguard-report.csv";
|
|
3448
|
+
document.body.appendChild(a);
|
|
3449
|
+
a.click();
|
|
3450
|
+
document.body.removeChild(a);
|
|
3451
|
+
URL.revokeObjectURL(url);
|
|
3452
|
+
}}
|
|
3453
|
+
</script>
|
|
3454
|
+
</body>
|
|
3455
|
+
</html>
|
|
3456
|
+
"""
|
|
3457
|
+
|
|
3458
|
+
|
|
3459
|
+
def emit_html_report(
|
|
3460
|
+
target_path: str | Path | None = None,
|
|
3461
|
+
root_dir: str | Path | None = None
|
|
3462
|
+
) -> dict[str, Any]:
|
|
3463
|
+
"""Emit the standalone HTML report to disk using atomic swapping with retry."""
|
|
3464
|
+
root = Path(root_dir or find_project_root()).resolve()
|
|
3465
|
+
telemetry = load_report_telemetry(root)
|
|
3466
|
+
html_content = generate_html_dashboard(telemetry)
|
|
3467
|
+
if target_path:
|
|
3468
|
+
out_file = Path(target_path)
|
|
3469
|
+
if not out_file.is_absolute():
|
|
3470
|
+
out_file = (root / out_file).resolve()
|
|
3471
|
+
else:
|
|
3472
|
+
out_file = root / "report.html"
|
|
3473
|
+
|
|
3474
|
+
out_file.parent.mkdir(parents=True, exist_ok=True)
|
|
3475
|
+
try:
|
|
3476
|
+
from report_sync import atomic_write_text
|
|
3477
|
+
atomic_write_text(out_file, html_content)
|
|
3478
|
+
except Exception:
|
|
3479
|
+
out_file.write_text(html_content, encoding="utf-8")
|
|
3480
|
+
|
|
3481
|
+
# Always mirror to .torusguard/runs/report-latest.html
|
|
3482
|
+
latest_run_html = root / ".torusguard" / "runs" / "report-latest.html"
|
|
3483
|
+
if out_file.resolve() != latest_run_html.resolve():
|
|
3484
|
+
try:
|
|
3485
|
+
latest_run_html.parent.mkdir(parents=True, exist_ok=True)
|
|
3486
|
+
from report_sync import atomic_write_text
|
|
3487
|
+
atomic_write_text(latest_run_html, html_content)
|
|
3488
|
+
except Exception:
|
|
3489
|
+
try:
|
|
3490
|
+
latest_run_html.write_text(html_content, encoding="utf-8")
|
|
3491
|
+
except Exception:
|
|
3492
|
+
pass
|
|
3493
|
+
|
|
3494
|
+
# If writing to a non-standard path and a root report.html exists, also keep it synchronized
|
|
3495
|
+
root_report = root / "report.html"
|
|
3496
|
+
if out_file.resolve() != root_report.resolve() and root_report.is_file():
|
|
3497
|
+
try:
|
|
3498
|
+
from report_sync import atomic_write_text
|
|
3499
|
+
atomic_write_text(root_report, html_content)
|
|
3500
|
+
except Exception:
|
|
3501
|
+
try:
|
|
3502
|
+
root_report.write_text(html_content, encoding="utf-8")
|
|
3503
|
+
except Exception:
|
|
3504
|
+
pass
|
|
3505
|
+
|
|
3506
|
+
all_rules = telemetry.get("all_rules", [])
|
|
3507
|
+
safe_rules = telemetry.get("safe_rules", [])
|
|
3508
|
+
violated_rules = telemetry.get("violated_rules", [])
|
|
3509
|
+
findings = telemetry.get("findings", [])
|
|
3510
|
+
|
|
3511
|
+
return {
|
|
3512
|
+
"status": "success",
|
|
3513
|
+
"report_path": str(out_file),
|
|
3514
|
+
"latest_mirror_path": str(latest_run_html),
|
|
3515
|
+
"file_size_bytes": len(html_content.encode("utf-8")),
|
|
3516
|
+
"posture_score": compute_posture_score(telemetry),
|
|
3517
|
+
"total_rules": len(all_rules),
|
|
3518
|
+
"defended_count": len(safe_rules),
|
|
3519
|
+
"harmed_count": len(violated_rules),
|
|
3520
|
+
"critical_count": sum(1 for f in findings if f.get("severity") == "Critical" and not f.get("is_canary") and f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")),
|
|
3521
|
+
"high_count": sum(1 for f in findings if f.get("severity") == "High" and not f.get("is_canary") and f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")),
|
|
3522
|
+
"medium_count": sum(1 for f in findings if f.get("severity") in ("Medium", "Low") and not f.get("is_canary") and f.get("status", "open") not in ("fixed", "resolved", "confirmed_fixed")),
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
|
|
3526
|
+
def main():
|
|
3527
|
+
parser = argparse.ArgumentParser(description="TorusGuard Visual HTML Report Emitter")
|
|
3528
|
+
parser.add_argument("--out", help="Output HTML file path (default: report.html at project root)")
|
|
3529
|
+
parser.add_argument("--root", help="Project root override")
|
|
3530
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON summary")
|
|
3531
|
+
|
|
3532
|
+
args = parser.parse_args()
|
|
3533
|
+
root = Path(args.root).resolve() if args.root else None
|
|
3534
|
+
|
|
3535
|
+
res = emit_html_report(target_path=args.out, root_dir=root)
|
|
3536
|
+
if args.json:
|
|
3537
|
+
print(json.dumps(res, indent=2))
|
|
3538
|
+
else:
|
|
3539
|
+
rep_path = Path(res["report_path"]).resolve()
|
|
3540
|
+
file_url = rep_path.as_uri()
|
|
3541
|
+
score = res["posture_score"]
|
|
3542
|
+
score_label = "Optimal Defense" if score >= 90 else ("Hardened" if score >= 75 else ("Degraded" if score >= 50 else "Critical Exposure"))
|
|
3543
|
+
score_color = "\033[32m" if score >= 90 else ("\033[36m" if score >= 75 else ("\033[33m" if score >= 50 else "\033[31m"))
|
|
3544
|
+
reset = "\033[0m"
|
|
3545
|
+
bold = "\033[1m"
|
|
3546
|
+
white = "\033[97m"
|
|
3547
|
+
cyan = "\033[36m"
|
|
3548
|
+
dim = "\033[2m"
|
|
3549
|
+
green = "\033[32m"
|
|
3550
|
+
red = "\033[31m"
|
|
3551
|
+
yellow = "\033[33m"
|
|
3552
|
+
|
|
3553
|
+
try:
|
|
3554
|
+
import term_ui as tui
|
|
3555
|
+
print()
|
|
3556
|
+
print(tui.card_header("🛡️ TORUSGUARD VISUAL SECURITY REPORT", "Single-File Offline Dashboard", "v2.3.0"))
|
|
3557
|
+
print(f"\n {bold}▸ Report URL:{reset} {cyan}{file_url}{reset}")
|
|
3558
|
+
print(f" {bold}▸ Output File:{reset} {white}{rep_path.name}{reset}")
|
|
3559
|
+
print(f" {bold}▸ Living Ledger:{reset} {green}security_report.md{reset}\n")
|
|
3560
|
+
|
|
3561
|
+
print(tui.card_border_top("Executive Posture"))
|
|
3562
|
+
print(tui.format_box_line(f"Posture Score: {bold}{score_color}{score}/100{reset} ({score_label})"))
|
|
3563
|
+
harmed = res.get("harmed_count", 0)
|
|
3564
|
+
defended = res.get("defended_count", 74)
|
|
3565
|
+
total = res.get("total_rules", 74)
|
|
3566
|
+
harmed_color = red if harmed > 0 else green
|
|
3567
|
+
print(tui.format_box_line(f"Invariants: {green}{defended} Defended{reset} · {harmed_color}{harmed} Harmed{reset} ({total} Rules)"))
|
|
3568
|
+
findings_str = f"{red}{res.get('critical_count', 0)} Critical{reset} · {yellow}{res.get('high_count', 0)} High{reset} · {cyan}{res.get('medium_count', 0)} Medium{reset}"
|
|
3569
|
+
print(tui.format_box_line(f"Exposure Status: {findings_str}"))
|
|
3570
|
+
print(tui.format_box_line(f"Dashboard Size: {white}{res['file_size_bytes']} bytes{reset} (Zero CDN, 100% Offline)"))
|
|
3571
|
+
print(tui.card_divider("Interactive Capabilities"))
|
|
3572
|
+
print(tui.format_box_line("• 5-Layer Architectural Invariant Matrix with modal inspection"))
|
|
3573
|
+
print(tui.format_box_line("• Interactive What-If Posture Simulator with live gauge updates"))
|
|
3574
|
+
print(tui.format_box_line("• Enterprise Compliance Framework Mapping (SOC 2, ISO, HIPAA)"))
|
|
3575
|
+
print(tui.format_box_line("• Golden Recipe Explorer with Unified Diff & Before/After views"))
|
|
3576
|
+
print(tui.format_box_line("• Zero-CDN Executive Dark / Light Mode Theme Switcher"))
|
|
3577
|
+
print(tui.card_border_bottom())
|
|
3578
|
+
print(f"\n {dim}Tip: Open {cyan}{file_url}{dim} directly in any browser to inspect.{reset}\n")
|
|
3579
|
+
except Exception:
|
|
3580
|
+
print(f"[SUCCESS] Emitted TorusGuard visual dashboard: {res['report_path']}")
|
|
3581
|
+
print(f"Posture Score: {res['posture_score']}/100 · Size: {res['file_size_bytes']} bytes")
|
|
3582
|
+
|
|
3583
|
+
|
|
3584
|
+
if __name__ == "__main__":
|
|
3585
|
+
main()
|