@contextfort-ai/openclaw-secure 0.1.7 → 0.1.9

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.
@@ -0,0 +1,515 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ContextFort Security Dashboard</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { background: #09090b; color: #fafafa; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; min-height: 100vh; display: flex; flex-direction: column; }
10
+ a { color: #60a5fa; text-decoration: none; }
11
+
12
+ /* Nav */
13
+ .nav { border-bottom: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.8); backdrop-filter: blur(12px); position: sticky; top: 0; z-index: 10; }
14
+ .nav-inner { padding: 0 24px; height: 56px; display: flex; align-items: center; justify-content: space-between; }
15
+ .nav-brand { display: flex; align-items: center; gap: 10px; }
16
+ .nav-brand .icon { width: 28px; height: 28px; background: #fff; display: flex; align-items: center; justify-content: center; }
17
+ .nav-brand .icon svg { width: 18px; height: 18px; }
18
+ .nav-brand span { font-size: 17px; font-weight: 700; letter-spacing: -0.5px; text-transform: uppercase; }
19
+ .nav-right { display: flex; align-items: center; gap: 12px; }
20
+ .muted { color: #a1a1aa; font-size: 13px; }
21
+
22
+ /* Layout */
23
+ .layout { display: flex; flex: 1; min-height: calc(100vh - 56px); }
24
+
25
+ /* Sidebar */
26
+ .sidebar { width: 200px; border-right: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.015); padding: 16px 10px; flex-shrink: 0; }
27
+ .sidebar-nav { display: flex; flex-direction: column; gap: 2px; }
28
+ .sidebar-btn { display: flex; align-items: center; gap: 10px; padding: 8px 12px; font-size: 13px; color: #a1a1aa; border: none; background: none; border-radius: 6px; cursor: pointer; width: 100%; text-align: left; transition: all 0.12s; }
29
+ .sidebar-btn:hover { color: #fff; background: rgba(255,255,255,0.05); }
30
+ .sidebar-btn.active { color: #fff; background: rgba(255,255,255,0.1); }
31
+ .sidebar-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
32
+ .sidebar-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #52525b; padding: 16px 12px 6px; }
33
+
34
+ /* Main */
35
+ .main { flex: 1; padding: 28px 32px; overflow-y: auto; }
36
+
37
+ /* Buttons */
38
+ .btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; font-size: 13px; color: #a1a1aa; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; background: transparent; cursor: pointer; transition: all 0.15s; }
39
+ .btn:hover { color: #fff; background: rgba(255,255,255,0.05); }
40
+ .btn.active { color: #fff; background: rgba(255,255,255,0.1); }
41
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
42
+
43
+ /* Cards */
44
+ .cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
45
+ .card { border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 20px; background: rgba(255,255,255,0.02); }
46
+ .card-label { font-size: 12px; color: #a1a1aa; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
47
+ .card-value { font-size: 28px; font-weight: 700; }
48
+ .card-sub { font-size: 12px; color: #a1a1aa; margin-top: 4px; }
49
+
50
+ /* Guard summary cards on home */
51
+ .guard-cards { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; }
52
+ .guard-card { border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 16px; background: rgba(255,255,255,0.02); cursor: pointer; transition: all 0.15s; }
53
+ .guard-card:hover { background: rgba(255,255,255,0.05); border-color: rgba(255,255,255,0.2); }
54
+ .guard-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
55
+ .guard-card-header svg { width: 16px; height: 16px; flex-shrink: 0; }
56
+ .guard-card-header span { font-size: 14px; font-weight: 600; }
57
+ .guard-stat { font-size: 12px; color: #a1a1aa; }
58
+ .guard-stat b { color: #fafafa; }
59
+ .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
60
+ .dot-green { background: #22c55e; box-shadow: 0 0 6px rgba(34,197,94,0.4); }
61
+ .dot-red { background: #ef4444; box-shadow: 0 0 6px rgba(239,68,68,0.4); }
62
+ .dot-yellow { background: #eab308; box-shadow: 0 0 6px rgba(234,179,8,0.4); }
63
+ .dot-gray { background: #52525b; }
64
+
65
+ /* Page header */
66
+ .page-header { margin-bottom: 24px; }
67
+ .page-header h1 { font-size: 24px; font-weight: 700; }
68
+ .page-header p { color: #a1a1aa; font-size: 14px; margin-top: 4px; }
69
+
70
+ /* Table */
71
+ .table-wrap { border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; overflow: hidden; }
72
+ .table-header { padding: 12px 16px; border-bottom: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.03); display: flex; align-items: center; justify-content: space-between; }
73
+ .table-header h3 { font-size: 14px; font-weight: 600; }
74
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
75
+ th { text-align: left; padding: 10px 16px; color: #a1a1aa; font-weight: 500; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.02); }
76
+ td { padding: 10px 16px; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: top; }
77
+ tr:hover td { background: rgba(255,255,255,0.02); }
78
+ .cmd { font-family: ui-monospace, monospace; font-size: 12px; max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
79
+
80
+ /* Badges */
81
+ .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 9999px; font-size: 11px; font-weight: 500; }
82
+ .badge-green { background: rgba(34,197,94,0.15); color: #4ade80; }
83
+ .badge-red { background: rgba(239,68,68,0.15); color: #f87171; }
84
+ .badge-yellow { background: rgba(234,179,8,0.15); color: #facc15; }
85
+ .badge-blue { background: rgba(59,130,246,0.15); color: #60a5fa; }
86
+ .badge-gray { background: rgba(255,255,255,0.08); color: #a1a1aa; }
87
+
88
+ /* Filter row */
89
+ .filter-row { display: flex; align-items: center; gap: 12px; }
90
+ select { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; padding: 5px 28px 5px 10px; font-size: 12px; color: #fff; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a1a1aa' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; }
91
+
92
+ /* Checkbox */
93
+ .check-label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #a1a1aa; cursor: pointer; user-select: none; }
94
+ .check-label input { accent-color: #60a5fa; }
95
+ .check-label:hover { color: #fff; }
96
+
97
+ /* Expandable payload */
98
+ .payload-toggle { cursor: pointer; color: #60a5fa; font-size: 12px; }
99
+ .payload-toggle:hover { text-decoration: underline; }
100
+ .payload-content { display: none; margin-top: 6px; padding: 8px 12px; background: rgba(255,255,255,0.03); border-radius: 4px; font-family: ui-monospace, monospace; font-size: 11px; color: #a1a1aa; white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow: auto; }
101
+ .payload-content.open { display: block; }
102
+
103
+ /* Empty state */
104
+ .empty { text-align: center; padding: 48px 24px; color: #52525b; font-size: 14px; }
105
+
106
+ /* Scroll */
107
+ .table-body { max-height: 600px; overflow-y: auto; }
108
+
109
+ /* Responsive */
110
+ @media (max-width: 900px) {
111
+ .cards { grid-template-columns: repeat(2, 1fr); }
112
+ .guard-cards { grid-template-columns: repeat(3, 1fr); }
113
+ .sidebar { width: 170px; }
114
+ }
115
+ </style>
116
+ </head>
117
+ <body>
118
+
119
+ <!-- Nav -->
120
+ <nav class="nav">
121
+ <div class="nav-inner">
122
+ <div class="nav-brand">
123
+ <div class="icon">
124
+ <svg fill="none" stroke="#000" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
125
+ </div>
126
+ <span>ContextFort</span>
127
+ </div>
128
+ <div class="nav-right">
129
+ <span class="muted" id="lastUpdated"></span>
130
+ <div style="display:flex;gap:4px;">
131
+ <button class="btn btn-sm" data-days="7" onclick="setDays(7)">7d</button>
132
+ <button class="btn btn-sm" data-days="30" onclick="setDays(30)">30d</button>
133
+ <button class="btn btn-sm active" data-days="365" onclick="setDays(365)">All</button>
134
+ </div>
135
+ <button class="btn" onclick="refresh()">
136
+ <svg style="width:14px;height:14px" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg>
137
+ Refresh
138
+ </button>
139
+ </div>
140
+ </div>
141
+ </nav>
142
+
143
+ <div class="layout">
144
+ <!-- Sidebar -->
145
+ <aside class="sidebar">
146
+ <div class="sidebar-nav">
147
+ <button class="sidebar-btn active" data-page="home" onclick="switchPage('home')">
148
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
149
+ Home
150
+ </button>
151
+
152
+ <div class="sidebar-label">Guards</div>
153
+
154
+ <button class="sidebar-btn" data-page="skill" onclick="switchPage('skill')">
155
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
156
+ Skill Scanner
157
+ <span class="dot dot-green" id="sb-skill-dot" style="margin-left:auto"></span>
158
+ </button>
159
+ <button class="sidebar-btn" data-page="bash" onclick="switchPage('bash')">
160
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
161
+ Bash Guard
162
+ <span class="dot dot-green" id="sb-bash-dot" style="margin-left:auto"></span>
163
+ </button>
164
+ <button class="sidebar-btn" data-page="pi" onclick="switchPage('pi')">
165
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
166
+ Prompt Injection
167
+ <span class="dot dot-green" id="sb-pi-dot" style="margin-left:auto"></span>
168
+ </button>
169
+ <button class="sidebar-btn" data-page="secrets" onclick="switchPage('secrets')">
170
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
171
+ Secrets Guard
172
+ <span class="dot dot-green" id="sb-sec-dot" style="margin-left:auto"></span>
173
+ </button>
174
+ <button class="sidebar-btn" data-page="exfil" onclick="switchPage('exfil')">
175
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
176
+ Exfil Monitor
177
+ <span class="dot dot-green" id="sb-exfil-dot" style="margin-left:auto"></span>
178
+ </button>
179
+ </div>
180
+ </aside>
181
+
182
+ <!-- Main content -->
183
+ <main class="main">
184
+
185
+ <!-- HOME PAGE -->
186
+ <div id="page-home">
187
+ <div class="page-header">
188
+ <h1>Overview</h1>
189
+ <p>Security status across all protection layers.</p>
190
+ </div>
191
+
192
+ <div class="cards">
193
+ <div class="card"><div class="card-label">Total Commands</div><div class="card-value" id="ov-total">-</div><div class="card-sub">intercepted by guard</div></div>
194
+ <div class="card"><div class="card-label">Blocked</div><div class="card-value" id="ov-blocked" style="color:#f87171">-</div><div class="card-sub">dangerous commands stopped</div></div>
195
+ <div class="card"><div class="card-label">Allow Rate</div><div class="card-value" id="ov-rate" style="color:#4ade80">-</div><div class="card-sub">commands passed all guards</div></div>
196
+ <div class="card"><div class="card-label">Active Since</div><div class="card-value" id="ov-since" style="font-size:16px">-</div><div class="card-sub">first hook loaded</div></div>
197
+ </div>
198
+
199
+ <div class="guard-cards">
200
+ <div class="guard-card" onclick="switchPage('skill')">
201
+ <div class="guard-card-header">
202
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
203
+ <span>Skill Scanner</span>
204
+ <span class="dot dot-green" id="g-skill-dot" style="margin-left:auto"></span>
205
+ </div>
206
+ <div class="guard-stat"><b id="g-skill-blocks">0</b> blocks</div>
207
+ </div>
208
+ <div class="guard-card" onclick="switchPage('bash')">
209
+ <div class="guard-card-header">
210
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
211
+ <span>Bash Guard</span>
212
+ <span class="dot dot-green" id="g-bash-dot" style="margin-left:auto"></span>
213
+ </div>
214
+ <div class="guard-stat"><b id="g-bash-blocks">0</b> blocks</div>
215
+ </div>
216
+ <div class="guard-card" onclick="switchPage('pi')">
217
+ <div class="guard-card-header">
218
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
219
+ <span>Prompt Injection</span>
220
+ <span class="dot dot-green" id="g-pi-dot" style="margin-left:auto"></span>
221
+ </div>
222
+ <div class="guard-stat"><b id="g-pi-blocks">0</b> blocks</div>
223
+ </div>
224
+ <div class="guard-card" onclick="switchPage('secrets')">
225
+ <div class="guard-card-header">
226
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
227
+ <span>Secrets Guard</span>
228
+ <span class="dot dot-green" id="g-sec-dot" style="margin-left:auto"></span>
229
+ </div>
230
+ <div class="guard-stat"><b id="g-sec-blocks">0</b> blocks &middot; <b id="g-sec-redactions">0</b> redactions</div>
231
+ </div>
232
+ <div class="guard-card" onclick="switchPage('exfil')">
233
+ <div class="guard-card-header">
234
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
235
+ <span>Exfil Monitor</span>
236
+ <span class="dot dot-green" id="g-exfil-dot" style="margin-left:auto"></span>
237
+ </div>
238
+ <div class="guard-stat"><b id="g-exfil-detections">0</b> detections</div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <!-- SKILL SCANNER PAGE -->
244
+ <div id="page-skill" style="display:none">
245
+ <div class="page-header">
246
+ <h1>Skill Scanner</h1>
247
+ <p>Skills cross-indexed and scanned for prompt injection patterns via Haiku.</p>
248
+ </div>
249
+ <div class="table-wrap">
250
+ <div class="table-header">
251
+ <h3>Events</h3>
252
+ <div class="filter-row">
253
+ <label class="check-label"><input type="checkbox" id="skill-blocked-only" onchange="renderGuardPage('skill')"> Only show blocked</label>
254
+ <label class="check-label"><input type="checkbox" id="skill-server-only" onchange="renderGuardPage('skill')"> Server sent events</label>
255
+ </div>
256
+ </div>
257
+ <div class="table-body" id="skill-table-body"></div>
258
+ </div>
259
+ </div>
260
+
261
+ <!-- BASH GUARD PAGE -->
262
+ <div id="page-bash" style="display:none">
263
+ <div class="page-header">
264
+ <h1>Bash Guard</h1>
265
+ <p>Commands blocked by static analysis rules (Tirith engine).</p>
266
+ </div>
267
+ <div class="table-wrap">
268
+ <div class="table-header">
269
+ <h3>Events</h3>
270
+ <div class="filter-row">
271
+ <label class="check-label"><input type="checkbox" id="bash-blocked-only" onchange="renderGuardPage('bash')"> Only show blocked</label>
272
+ <label class="check-label"><input type="checkbox" id="bash-server-only" onchange="renderGuardPage('bash')"> Server sent events</label>
273
+ </div>
274
+ </div>
275
+ <div class="table-body" id="bash-table-body"></div>
276
+ </div>
277
+ </div>
278
+
279
+ <!-- PROMPT INJECTION PAGE -->
280
+ <div id="page-pi" style="display:none">
281
+ <div class="page-header">
282
+ <h1>Prompt Injection Guard</h1>
283
+ <p>Command outputs scanned for prompt injection attempts via local Haiku.</p>
284
+ </div>
285
+ <div class="table-wrap">
286
+ <div class="table-header">
287
+ <h3>Events</h3>
288
+ <div class="filter-row">
289
+ <label class="check-label"><input type="checkbox" id="pi-blocked-only" onchange="renderGuardPage('pi')"> Only show blocked</label>
290
+ <label class="check-label"><input type="checkbox" id="pi-server-only" onchange="renderGuardPage('pi')"> Server sent events</label>
291
+ </div>
292
+ </div>
293
+ <div class="table-body" id="pi-table-body"></div>
294
+ </div>
295
+ </div>
296
+
297
+ <!-- SECRETS GUARD PAGE -->
298
+ <div id="page-secrets" style="display:none">
299
+ <div class="page-header">
300
+ <h1>Secrets Guard</h1>
301
+ <p>Env var leak prevention and output secret redaction.</p>
302
+ </div>
303
+ <div class="table-wrap">
304
+ <div class="table-header">
305
+ <h3>Events</h3>
306
+ <div class="filter-row">
307
+ <label class="check-label"><input type="checkbox" id="secrets-blocked-only" onchange="renderGuardPage('secrets')"> Only show blocked</label>
308
+ <label class="check-label"><input type="checkbox" id="secrets-server-only" onchange="renderGuardPage('secrets')"> Server sent events</label>
309
+ </div>
310
+ </div>
311
+ <div class="table-body" id="secrets-table-body"></div>
312
+ </div>
313
+ </div>
314
+
315
+ <!-- EXFIL MONITOR PAGE -->
316
+ <div id="page-exfil" style="display:none">
317
+ <div class="page-header">
318
+ <h1>Exfil Monitor</h1>
319
+ <p>Detects when sensitive environment variables are transmitted to external servers via curl, wget, or nc.</p>
320
+ </div>
321
+ <div class="table-wrap">
322
+ <div class="table-header">
323
+ <h3>Events</h3>
324
+ <div class="filter-row">
325
+ <label class="check-label"><input type="checkbox" id="exfil-blocked-only" onchange="renderGuardPage('exfil')"> Only show blocked</label>
326
+ <label class="check-label"><input type="checkbox" id="exfil-server-only" onchange="renderGuardPage('exfil')"> Server sent events</label>
327
+ </div>
328
+ </div>
329
+ <div class="table-body" id="exfil-table-body"></div>
330
+ </div>
331
+ </div>
332
+
333
+ </main>
334
+ </div>
335
+
336
+ <script>
337
+ let currentDays = 365;
338
+ let currentPage = 'home';
339
+ let localEvents = [];
340
+ let serverEvents = [];
341
+ let overviewData = {};
342
+
343
+ // Guard blocker name mapping
344
+ const GUARD_BLOCKER = { skill: 'skill', bash: 'tirith', pi: 'prompt_injection', secrets: 'env_var' };
345
+ const GUARD_EVENTS = {
346
+ skill: e => e.blocker === 'skill',
347
+ bash: e => e.blocker === 'tirith',
348
+ pi: e => e.blocker === 'prompt_injection',
349
+ secrets: e => e.blocker === 'env_var' || e.event === 'output_redacted',
350
+ exfil: e => e.event === 'exfil_attempt',
351
+ };
352
+ const SERVER_GUARD = {
353
+ skill: e => e.destination === 'supabase' || (e.destination === 'posthog' && e.event && e.event.includes('skill')),
354
+ bash: e => e.destination === 'posthog' && e.properties?.blocker === 'tirith',
355
+ pi: e => e.destination === 'posthog' && (e.event === 'output_scan_started' || e.event === 'output_scan_result' || e.properties?.blocker === 'prompt_injection'),
356
+ secrets: e => e.destination === 'posthog' && (e.properties?.blocker === 'env_var_leak' || e.event === 'output_secrets_redacted'),
357
+ exfil: e => e.destination === 'posthog' && e.event === 'exfil_attempt',
358
+ };
359
+
360
+ function setDays(d) {
361
+ currentDays = d;
362
+ document.querySelectorAll('[data-days]').forEach(b => b.classList.toggle('active', parseInt(b.dataset.days) === d));
363
+ refresh();
364
+ }
365
+
366
+ function switchPage(page) {
367
+ currentPage = page;
368
+ document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.toggle('active', b.dataset.page === page));
369
+ ['home','skill','bash','pi','secrets','exfil'].forEach(p => {
370
+ document.getElementById('page-' + p).style.display = p === page ? '' : 'none';
371
+ });
372
+ if (page !== 'home') renderGuardPage(page);
373
+ }
374
+
375
+ async function refresh() {
376
+ try {
377
+ const [ov, local, server] = await Promise.all([
378
+ fetch(`/api/overview?days=${currentDays}`).then(r => r.json()),
379
+ fetch(`/api/events?type=local&days=${currentDays}&limit=5000`).then(r => r.json()),
380
+ fetch(`/api/events?type=server_send&days=${currentDays}&limit=2000`).then(r => r.json()),
381
+ ]);
382
+ localEvents = local.events || [];
383
+ serverEvents = server.events || [];
384
+ overviewData = ov;
385
+ renderOverview(ov);
386
+ renderSidebarDots(ov);
387
+ if (currentPage !== 'home') renderGuardPage(currentPage);
388
+ document.getElementById('lastUpdated').textContent = 'Updated ' + new Date().toLocaleTimeString();
389
+ } catch (e) {
390
+ console.error('Refresh failed:', e);
391
+ }
392
+ }
393
+
394
+ function renderOverview(ov) {
395
+ document.getElementById('ov-total').textContent = (ov.total || 0).toLocaleString();
396
+ document.getElementById('ov-blocked').textContent = (ov.blocked || 0).toLocaleString();
397
+ const rate = ov.total > 0 ? (((ov.total - ov.blocked) / ov.total) * 100).toFixed(1) : '100.0';
398
+ document.getElementById('ov-rate').textContent = rate + '%';
399
+ document.getElementById('ov-since').textContent = ov.activeSince ? timeAgo(ov.activeSince) : 'No data';
400
+
401
+ const gs = ov.guardStatus || {};
402
+ document.getElementById('g-skill-blocks').textContent = gs.skill_scanner?.blocks || 0;
403
+ document.getElementById('g-bash-blocks').textContent = gs.bash_guard?.blocks || 0;
404
+ document.getElementById('g-pi-blocks').textContent = gs.prompt_injection?.blocks || 0;
405
+ document.getElementById('g-sec-blocks').textContent = gs.secrets_guard?.blocks || 0;
406
+ document.getElementById('g-sec-redactions').textContent = gs.secrets_guard?.redactions || 0;
407
+ document.getElementById('g-exfil-detections').textContent = gs.exfil_monitor?.detections || 0;
408
+
409
+ const hasData = ov.total > 0;
410
+ setDot('g-skill-dot', hasData, gs.skill_scanner?.blocks > 0);
411
+ setDot('g-bash-dot', hasData, gs.bash_guard?.blocks > 0);
412
+ setDot('g-pi-dot', hasData, gs.prompt_injection?.blocks > 0);
413
+ setDot('g-sec-dot', hasData, (gs.secrets_guard?.blocks > 0) || (gs.secrets_guard?.redactions > 0));
414
+ setDot('g-exfil-dot', hasData, gs.exfil_monitor?.detections > 0, 'dot-yellow');
415
+ }
416
+
417
+ function renderSidebarDots(ov) {
418
+ const gs = ov.guardStatus || {};
419
+ const hasData = ov.total > 0;
420
+ setDot('sb-skill-dot', hasData, gs.skill_scanner?.blocks > 0);
421
+ setDot('sb-bash-dot', hasData, gs.bash_guard?.blocks > 0);
422
+ setDot('sb-pi-dot', hasData, gs.prompt_injection?.blocks > 0);
423
+ setDot('sb-sec-dot', hasData, (gs.secrets_guard?.blocks > 0) || (gs.secrets_guard?.redactions > 0));
424
+ setDot('sb-exfil-dot', hasData, gs.exfil_monitor?.detections > 0, 'dot-yellow');
425
+ }
426
+
427
+ function setDot(id, hasData, hasBlocks, activeClass) {
428
+ const el = document.getElementById(id);
429
+ if (!el) return;
430
+ el.className = 'dot ' + (hasData ? (hasBlocks ? (activeClass || 'dot-red') : 'dot-green') : 'dot-gray');
431
+ }
432
+
433
+ function renderGuardPage(guard) {
434
+ const serverOnly = document.getElementById(guard + '-server-only')?.checked;
435
+ const blockedOnly = document.getElementById(guard + '-blocked-only')?.checked;
436
+ const container = document.getElementById(guard + '-table-body');
437
+
438
+ if (serverOnly) {
439
+ // Show server send events filtered to this guard
440
+ const filtered = serverEvents.filter(SERVER_GUARD[guard] || (() => false));
441
+ if (filtered.length === 0) {
442
+ container.innerHTML = '<div class="empty">No server send events for this guard.</div>';
443
+ return;
444
+ }
445
+ container.innerHTML = `<table><thead><tr><th>Time</th><th>Destination</th><th>Event</th><th>Payload</th></tr></thead><tbody>` +
446
+ filtered.map((e, i) => {
447
+ const dest = e.destination === 'posthog' ? '<span class="badge badge-blue">PostHog</span>' : '<span class="badge badge-yellow">Supabase</span>';
448
+ const payloadStr = JSON.stringify(e.properties || e.payload || {}, null, 2);
449
+ const uid = guard + '-sv-' + i;
450
+ return `<tr>
451
+ <td style="white-space:nowrap;color:#a1a1aa">${formatTime(e.ts)}</td>
452
+ <td>${dest}</td>
453
+ <td>${esc(e.event || '-')}</td>
454
+ <td><span class="payload-toggle" onclick="document.getElementById('${uid}').classList.toggle('open')">View payload</span><div class="payload-content" id="${uid}">${esc(payloadStr)}</div></td>
455
+ </tr>`;
456
+ }).join('') + '</tbody></table>';
457
+ } else {
458
+ // Show all command events; optionally filter to only blocked by this guard
459
+ const guardBlockerFn = GUARD_EVENTS[guard] || (() => false);
460
+ let filtered;
461
+ if (blockedOnly) {
462
+ filtered = localEvents.filter(e => guardBlockerFn(e));
463
+ } else {
464
+ // Show all command_check + command_blocked + output_redacted events
465
+ filtered = localEvents.filter(e => e.event === 'command_check' || e.event === 'command_blocked' || e.event === 'output_redacted');
466
+ }
467
+ if (filtered.length === 0) {
468
+ container.innerHTML = '<div class="empty">' + (blockedOnly ? 'No blocked events for this guard.' : 'No events yet. Events appear when openclaw-secure is active.') + '</div>';
469
+ return;
470
+ }
471
+ container.innerHTML = `<table><thead><tr><th>Time</th><th>Decision</th><th>Command</th><th>Blocker</th><th>Detail</th></tr></thead><tbody>` +
472
+ filtered.map(e => {
473
+ const badge = e.decision === 'block' ? 'badge-red' : e.decision === 'redact' ? 'badge-yellow' : 'badge-green';
474
+ const label = e.decision === 'block' ? 'Blocked' : e.decision === 'redact' ? 'Redacted' : 'Allowed';
475
+ const blocker = e.blocker ? esc(e.blocker) : '-';
476
+ const detail = e.reason ? esc(e.reason) : (e.secrets_count ? `${e.secrets_count} secret(s) redacted` : '');
477
+ return `<tr>
478
+ <td style="white-space:nowrap;color:#a1a1aa">${formatTime(e.ts)}</td>
479
+ <td><span class="badge ${badge}">${label}</span></td>
480
+ <td class="cmd" title="${esc(e.command || '')}">${esc(e.command || '-')}</td>
481
+ <td style="font-size:12px">${blocker}</td>
482
+ <td style="font-size:12px;color:#a1a1aa;max-width:300px;overflow:hidden;text-overflow:ellipsis">${detail}</td>
483
+ </tr>`;
484
+ }).join('') + '</tbody></table>';
485
+ }
486
+ }
487
+
488
+ function formatTime(ts) {
489
+ if (!ts) return '-';
490
+ const d = new Date(ts);
491
+ const now = new Date();
492
+ if (d.toDateString() === now.toDateString()) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
493
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
494
+ }
495
+
496
+ function timeAgo(ts) {
497
+ const diff = Date.now() - new Date(ts).getTime();
498
+ const mins = Math.floor(diff / 60000);
499
+ if (mins < 1) return 'Just now';
500
+ if (mins < 60) return mins + 'm ago';
501
+ const hrs = Math.floor(mins / 60);
502
+ if (hrs < 24) return hrs + 'h ago';
503
+ return Math.floor(hrs / 24) + 'd ago';
504
+ }
505
+
506
+ function esc(s) {
507
+ if (!s) return '';
508
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
509
+ }
510
+
511
+ refresh();
512
+ setInterval(refresh, 10000);
513
+ </script>
514
+ </body>
515
+ </html>