@contextfort-ai/openclaw-secure 0.1.9 → 0.1.11
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/README.md +65 -0
- package/bin/openclaw-secure.js +71 -0
- package/monitor/dashboard/public/index.html +998 -37
- package/monitor/dashboard/scan-worker.js +21 -0
- package/monitor/dashboard/server.js +253 -7
- package/monitor/exfil_guard/index.js +79 -7
- package/monitor/prompt_injection_guard/index.js +17 -2
- package/monitor/secrets_guard/index.js +148 -96
- package/monitor/skills_guard/index.js +58 -0
- package/openclaw-secure.js +111 -25
- package/package.json +1 -1
- package/test/guard-test-200.js +1312 -0
|
@@ -75,7 +75,7 @@
|
|
|
75
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
76
|
td { padding: 10px 16px; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: top; }
|
|
77
77
|
tr:hover td { background: rgba(255,255,255,0.02); }
|
|
78
|
-
.cmd { font-family: ui-monospace, monospace; font-size: 12px; max-width: 400px;
|
|
78
|
+
.cmd { font-family: ui-monospace, monospace; font-size: 12px; max-width: 400px; white-space: pre-wrap; word-break: break-all; }
|
|
79
79
|
|
|
80
80
|
/* Badges */
|
|
81
81
|
.badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 9999px; font-size: 11px; font-weight: 500; }
|
|
@@ -148,6 +148,10 @@
|
|
|
148
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
149
|
Home
|
|
150
150
|
</button>
|
|
151
|
+
<button class="sidebar-btn" data-page="scan" onclick="switchPage('scan')">
|
|
152
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
153
|
+
Secret Scanner
|
|
154
|
+
</button>
|
|
151
155
|
|
|
152
156
|
<div class="sidebar-label">Guards</div>
|
|
153
157
|
|
|
@@ -173,7 +177,7 @@
|
|
|
173
177
|
</button>
|
|
174
178
|
<button class="sidebar-btn" data-page="exfil" onclick="switchPage('exfil')">
|
|
175
179
|
<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
|
|
180
|
+
Secrets Exfil Monitor
|
|
177
181
|
<span class="dot dot-green" id="sb-exfil-dot" style="margin-left:auto"></span>
|
|
178
182
|
</button>
|
|
179
183
|
</div>
|
|
@@ -196,6 +200,37 @@
|
|
|
196
200
|
<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
201
|
</div>
|
|
198
202
|
|
|
203
|
+
<!-- Active Block Banner (shown when agent is blocked) -->
|
|
204
|
+
<div id="home-block-banner" style="display:none;margin-bottom:24px;border:1px solid rgba(239,68,68,0.3);border-radius:8px;padding:16px 20px;background:rgba(239,68,68,0.08)">
|
|
205
|
+
<div style="display:flex;align-items:flex-start;gap:12px">
|
|
206
|
+
<svg style="width:20px;height:20px;flex-shrink:0;margin-top:2px" fill="none" stroke="#f87171" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
207
|
+
<div style="flex:1">
|
|
208
|
+
<div style="font-size:15px;font-weight:600;color:#f87171;margin-bottom:4px" id="home-block-title">Agent Blocked</div>
|
|
209
|
+
<div style="font-size:13px;color:#a1a1aa;margin-bottom:12px" id="home-block-reason"></div>
|
|
210
|
+
<div style="display:flex;gap:8px">
|
|
211
|
+
<button class="btn btn-sm" id="home-block-view-btn" style="color:#f87171;border-color:rgba(239,68,68,0.3)" onclick="">View Details</button>
|
|
212
|
+
<button class="btn btn-sm" id="home-block-remove-btn" style="color:#4ade80;border-color:rgba(34,197,94,0.3)" onclick="removeBlock()">Remove Block & Allow OpenClaw</button>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<!-- Anthropic API Key box (shown when not set) -->
|
|
219
|
+
<div id="anthropic-key-box" style="display:none;margin-bottom:24px;border:1px solid rgba(250,204,21,0.3);border-radius:8px;padding:16px 20px;background:rgba(250,204,21,0.06)">
|
|
220
|
+
<div style="display:flex;align-items:flex-start;gap:12px">
|
|
221
|
+
<svg style="width:20px;height:20px;flex-shrink:0;margin-top:2px" fill="none" stroke="#facc15" 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>
|
|
222
|
+
<div style="flex:1">
|
|
223
|
+
<div style="font-size:15px;font-weight:600;color:#facc15;margin-bottom:4px">Prompt Injection Detection Inactive</div>
|
|
224
|
+
<div style="font-size:13px;color:#a1a1aa;margin-bottom:12px">An Anthropic API key is required for Haiku to scan command output for prompt injection. Enter your key below or set <code style="background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px;font-size:12px">ANTHROPIC_API_KEY</code> in your shell.</div>
|
|
225
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
226
|
+
<input type="password" id="anthropic-key-input" placeholder="sk-ant-..." style="flex:1;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:8px 12px;color:#e4e4e7;font-size:13px;font-family:ui-monospace,monospace;outline:none" />
|
|
227
|
+
<button class="btn btn-sm" id="anthropic-key-save-btn" style="color:#facc15;border-color:rgba(250,204,21,0.3);white-space:nowrap" onclick="saveAnthropicKey()">Save Key</button>
|
|
228
|
+
</div>
|
|
229
|
+
<div id="anthropic-key-msg" style="font-size:12px;margin-top:8px;display:none"></div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
199
234
|
<div class="guard-cards">
|
|
200
235
|
<div class="guard-card" onclick="switchPage('skill')">
|
|
201
236
|
<div class="guard-card-header">
|
|
@@ -232,12 +267,82 @@
|
|
|
232
267
|
<div class="guard-card" onclick="switchPage('exfil')">
|
|
233
268
|
<div class="guard-card-header">
|
|
234
269
|
<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>
|
|
270
|
+
<span>Secrets Exfil Monitor</span>
|
|
236
271
|
<span class="dot dot-green" id="g-exfil-dot" style="margin-left:auto"></span>
|
|
237
272
|
</div>
|
|
238
273
|
<div class="guard-stat"><b id="g-exfil-detections">0</b> detections</div>
|
|
239
274
|
</div>
|
|
240
275
|
</div>
|
|
276
|
+
|
|
277
|
+
<!-- Secret Scanner Summary -->
|
|
278
|
+
<div style="margin-top:24px">
|
|
279
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
|
|
280
|
+
<h2 style="font-size:16px;font-weight:600">Secret Scanner</h2>
|
|
281
|
+
<button class="btn btn-sm" onclick="switchPage('scan')">View Details</button>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="card" id="home-scan-card">
|
|
284
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
285
|
+
<svg style="width:20px;height:20px;flex-shrink:0" fill="none" stroke="#a1a1aa" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
286
|
+
<div>
|
|
287
|
+
<div style="font-size:14px" id="home-scan-status">Loading...</div>
|
|
288
|
+
<div style="font-size:12px;color:#a1a1aa;margin-top:2px" id="home-scan-detail"></div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<!-- SECRET SCANNER PAGE -->
|
|
296
|
+
<div id="page-scan" style="display:none">
|
|
297
|
+
<div class="page-header">
|
|
298
|
+
<h1>Secret Scanner</h1>
|
|
299
|
+
<p>Scan directories accessible to OpenClaw for hardcoded secrets using TruffleHog.</p>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div class="cards" style="margin-bottom:24px">
|
|
303
|
+
<div class="card"><div class="card-label">TruffleHog</div><div class="card-value" id="scan-thog-status" style="font-size:16px">Checking...</div></div>
|
|
304
|
+
<div class="card"><div class="card-label">Last Scan</div><div class="card-value" id="scan-last-time" style="font-size:16px">Never</div></div>
|
|
305
|
+
<div class="card"><div class="card-label">LIVE Keys</div><div class="card-value" id="scan-verified" style="color:#f87171">-</div></div>
|
|
306
|
+
<div class="card"><div class="card-label">Total Findings</div><div class="card-value" id="scan-total">-</div></div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div style="display:flex;gap:12px;align-items:center;margin-bottom:24px">
|
|
310
|
+
<button class="btn" id="scan-run-btn" onclick="runScan()">
|
|
311
|
+
<svg style="width:14px;height:14px" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
312
|
+
Run Scan
|
|
313
|
+
</button>
|
|
314
|
+
<button class="btn" id="scan-solve-btn" onclick="runSolve()" style="display:none">
|
|
315
|
+
<svg style="width:14px;height:14px" 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>
|
|
316
|
+
Scrub Selected
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<!-- Scan targets -->
|
|
321
|
+
<div id="scan-targets-section" style="display:none;margin-bottom:24px">
|
|
322
|
+
<h3 style="font-size:14px;font-weight:600;margin-bottom:8px">Scanned Directories</h3>
|
|
323
|
+
<div id="scan-targets-list"></div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<!-- Findings table -->
|
|
327
|
+
<div class="table-wrap">
|
|
328
|
+
<div class="table-header">
|
|
329
|
+
<h3>Findings</h3>
|
|
330
|
+
<div class="filter-row">
|
|
331
|
+
<label class="check-label" id="scan-select-all-wrap" style="display:none"><input type="checkbox" id="scan-select-all" onchange="toggleScanSelectAll()"> Select all</label>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="table-body" id="scan-findings-body">
|
|
335
|
+
<div class="empty">No scan results yet. Click "Run Scan" to start.</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!-- Solve results -->
|
|
340
|
+
<div id="scan-solve-results" style="display:none;margin-top:24px">
|
|
341
|
+
<div class="table-wrap">
|
|
342
|
+
<div class="table-header"><h3>Scrub Results</h3></div>
|
|
343
|
+
<div class="table-body" id="scan-solve-body"></div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
241
346
|
</div>
|
|
242
347
|
|
|
243
348
|
<!-- SKILL SCANNER PAGE -->
|
|
@@ -246,12 +351,35 @@
|
|
|
246
351
|
<h1>Skill Scanner</h1>
|
|
247
352
|
<p>Skills cross-indexed and scanned for prompt injection patterns via Haiku.</p>
|
|
248
353
|
</div>
|
|
354
|
+
|
|
355
|
+
<!-- Skills Status (top section) -->
|
|
356
|
+
<div class="table-wrap" style="margin-bottom:24px">
|
|
357
|
+
<div class="table-header">
|
|
358
|
+
<h3>Skills Overview</h3>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="table-body" id="skill-status-body">
|
|
361
|
+
<div class="empty">Loading skills...</div>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<!-- File Viewer Modal -->
|
|
366
|
+
<div id="skill-file-modal" style="display:none;position:fixed;inset:0;z-index:50;background:rgba(0,0,0,0.7);display:none;align-items:center;justify-content:center;padding:24px">
|
|
367
|
+
<div style="background:#18181b;border:1px solid rgba(255,255,255,0.1);border-radius:8px;width:100%;max-width:720px;max-height:80vh;display:flex;flex-direction:column">
|
|
368
|
+
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid rgba(255,255,255,0.1)">
|
|
369
|
+
<span style="font-size:14px;font-weight:600" id="skill-modal-title">Files</span>
|
|
370
|
+
<button class="btn btn-sm" onclick="closeSkillModal()">×</button>
|
|
371
|
+
</div>
|
|
372
|
+
<div id="skill-modal-content" style="overflow:auto;padding:16px;flex:1"></div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<!-- Activity Log (bottom section) -->
|
|
249
377
|
<div class="table-wrap">
|
|
250
378
|
<div class="table-header">
|
|
251
|
-
<h3>
|
|
379
|
+
<h3>Activity Log</h3>
|
|
252
380
|
<div class="filter-row">
|
|
253
|
-
<label class="check-label"><input type="checkbox" id="skill-blocked-only" onchange="
|
|
254
|
-
<label class="check-label"><input type="checkbox" id="skill-server-only" onchange="
|
|
381
|
+
<label class="check-label"><input type="checkbox" id="skill-blocked-only" onchange="renderSkillPage()"> Only flagged/blocked</label>
|
|
382
|
+
<label class="check-label"><input type="checkbox" id="skill-server-only" onchange="renderSkillPage()"> Server sent events</label>
|
|
255
383
|
</div>
|
|
256
384
|
</div>
|
|
257
385
|
<div class="table-body" id="skill-table-body"></div>
|
|
@@ -268,7 +396,8 @@
|
|
|
268
396
|
<div class="table-header">
|
|
269
397
|
<h3>Events</h3>
|
|
270
398
|
<div class="filter-row">
|
|
271
|
-
<label class="check-label"><input type="checkbox" id="bash-blocked-only" onchange="renderGuardPage('bash')">
|
|
399
|
+
<label class="check-label"><input type="checkbox" id="bash-blocked-only" onchange="renderGuardPage('bash')"> Show only blocked</label>
|
|
400
|
+
<label class="check-label"><input type="checkbox" id="bash-show-system" onchange="renderGuardPage('bash')"> Show system commands</label>
|
|
272
401
|
<label class="check-label"><input type="checkbox" id="bash-server-only" onchange="renderGuardPage('bash')"> Server sent events</label>
|
|
273
402
|
</div>
|
|
274
403
|
</div>
|
|
@@ -284,10 +413,10 @@
|
|
|
284
413
|
</div>
|
|
285
414
|
<div class="table-wrap">
|
|
286
415
|
<div class="table-header">
|
|
287
|
-
<h3>Events</h3>
|
|
416
|
+
<h3>Scan Events</h3>
|
|
288
417
|
<div class="filter-row">
|
|
289
|
-
<label class="check-label"><input type="checkbox" id="pi-blocked-only" onchange="
|
|
290
|
-
<label class="check-label"><input type="checkbox" id="pi-server-only" onchange="
|
|
418
|
+
<label class="check-label"><input type="checkbox" id="pi-blocked-only" onchange="renderPiPage()"> Only flagged</label>
|
|
419
|
+
<label class="check-label"><input type="checkbox" id="pi-server-only" onchange="renderPiPage()"> Server sent events</label>
|
|
291
420
|
</div>
|
|
292
421
|
</div>
|
|
293
422
|
<div class="table-body" id="pi-table-body"></div>
|
|
@@ -304,8 +433,8 @@
|
|
|
304
433
|
<div class="table-header">
|
|
305
434
|
<h3>Events</h3>
|
|
306
435
|
<div class="filter-row">
|
|
307
|
-
<label class="check-label"><input type="checkbox" id="secrets-blocked-only" onchange="
|
|
308
|
-
<label class="check-label"><input type="checkbox" id="secrets-server-only" onchange="
|
|
436
|
+
<label class="check-label"><input type="checkbox" id="secrets-blocked-only" onchange="renderSecretsPage()"> Only show blocked</label>
|
|
437
|
+
<label class="check-label"><input type="checkbox" id="secrets-server-only" onchange="renderSecretsPage()"> Server sent events</label>
|
|
309
438
|
</div>
|
|
310
439
|
</div>
|
|
311
440
|
<div class="table-body" id="secrets-table-body"></div>
|
|
@@ -315,14 +444,35 @@
|
|
|
315
444
|
<!-- EXFIL MONITOR PAGE -->
|
|
316
445
|
<div id="page-exfil" style="display:none">
|
|
317
446
|
<div class="page-header">
|
|
318
|
-
<h1>Exfil Monitor</h1>
|
|
447
|
+
<h1>Secrets Exfil Monitor</h1>
|
|
319
448
|
<p>Detects when sensitive environment variables are transmitted to external servers via curl, wget, or nc.</p>
|
|
320
449
|
</div>
|
|
450
|
+
|
|
451
|
+
<!-- Destination Allowlist -->
|
|
452
|
+
<div class="card" style="margin-bottom:24px;padding:20px">
|
|
453
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
|
|
454
|
+
<div>
|
|
455
|
+
<h3 style="font-size:15px;font-weight:600;margin-bottom:4px">Destination Allowlist</h3>
|
|
456
|
+
<div style="font-size:12px;color:#a1a1aa">When enabled, commands sending secrets to domains NOT in this list will be blocked.</div>
|
|
457
|
+
</div>
|
|
458
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
459
|
+
<span id="exfil-al-status-badge"></span>
|
|
460
|
+
<button class="btn btn-sm" id="exfil-al-toggle-btn" onclick="toggleExfilAllowlist()">Enable</button>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
<div id="exfil-al-domains" style="margin-bottom:12px"></div>
|
|
464
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
465
|
+
<input type="text" id="exfil-al-input" placeholder="e.g. api.notion.com or *.supabase.co" style="flex:1;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:8px 12px;color:#e4e4e7;font-size:13px;font-family:ui-monospace,monospace;outline:none" />
|
|
466
|
+
<button class="btn btn-sm" onclick="addExfilDomain()">Add</button>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
321
470
|
<div class="table-wrap">
|
|
322
471
|
<div class="table-header">
|
|
323
472
|
<h3>Events</h3>
|
|
324
473
|
<div class="filter-row">
|
|
325
474
|
<label class="check-label"><input type="checkbox" id="exfil-blocked-only" onchange="renderGuardPage('exfil')"> Only show blocked</label>
|
|
475
|
+
<label class="check-label"><input type="checkbox" id="exfil-show-passed" onchange="renderGuardPage('exfil')"> Show passed</label>
|
|
326
476
|
<label class="check-label"><input type="checkbox" id="exfil-server-only" onchange="renderGuardPage('exfil')"> Server sent events</label>
|
|
327
477
|
</div>
|
|
328
478
|
</div>
|
|
@@ -340,14 +490,23 @@ let localEvents = [];
|
|
|
340
490
|
let serverEvents = [];
|
|
341
491
|
let overviewData = {};
|
|
342
492
|
|
|
343
|
-
//
|
|
344
|
-
const
|
|
493
|
+
// Map tab name → guard field value in log events
|
|
494
|
+
const GUARD_KEY = { skill: 'skill', bash: 'tirith', pi: 'prompt_injection', secrets: 'env_var', exfil: 'exfil' };
|
|
495
|
+
// All events for this guard (passed + blocked + special)
|
|
345
496
|
const GUARD_EVENTS = {
|
|
346
|
-
skill: e => e.
|
|
347
|
-
bash: e => e.
|
|
348
|
-
pi: e => e.
|
|
349
|
-
secrets: e => e.
|
|
350
|
-
exfil: e => e.
|
|
497
|
+
skill: e => e.guard === 'skill',
|
|
498
|
+
bash: e => e.guard === 'tirith',
|
|
499
|
+
pi: e => e.guard === 'prompt_injection',
|
|
500
|
+
secrets: e => e.guard === 'env_var' || e.event === 'output_redacted',
|
|
501
|
+
exfil: e => e.guard === 'exfil',
|
|
502
|
+
};
|
|
503
|
+
// Only blocked/flagged/detected events for this guard
|
|
504
|
+
const GUARD_BLOCKED = {
|
|
505
|
+
skill: e => e.guard === 'skill' && (e.decision === 'block' || e.decision === 'scan_flagged' || e.decision === 'binary_detected'),
|
|
506
|
+
bash: e => e.guard === 'tirith' && e.decision === 'block',
|
|
507
|
+
pi: e => e.guard === 'prompt_injection' && (e.decision === 'block' || e.decision === 'scan_flagged'),
|
|
508
|
+
secrets: e => (e.guard === 'env_var' && e.decision === 'block') || e.event === 'output_redacted',
|
|
509
|
+
exfil: e => e.guard === 'exfil',
|
|
351
510
|
};
|
|
352
511
|
const SERVER_GUARD = {
|
|
353
512
|
skill: e => e.destination === 'supabase' || (e.destination === 'posthog' && e.event && e.event.includes('skill')),
|
|
@@ -366,10 +525,15 @@ function setDays(d) {
|
|
|
366
525
|
function switchPage(page) {
|
|
367
526
|
currentPage = page;
|
|
368
527
|
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.toggle('active', b.dataset.page === page));
|
|
369
|
-
['home','skill','bash','pi','secrets','exfil'].forEach(p => {
|
|
528
|
+
['home','scan','skill','bash','pi','secrets','exfil'].forEach(p => {
|
|
370
529
|
document.getElementById('page-' + p).style.display = p === page ? '' : 'none';
|
|
371
530
|
});
|
|
372
|
-
if (page
|
|
531
|
+
if (page === 'scan') loadScanStatus();
|
|
532
|
+
else if (page === 'skill') renderSkillPage();
|
|
533
|
+
else if (page === 'pi') renderPiPage();
|
|
534
|
+
else if (page === 'secrets') renderSecretsPage();
|
|
535
|
+
else if (page === 'exfil') { loadExfilAllowlist(); renderGuardPage('exfil'); }
|
|
536
|
+
else if (page !== 'home') renderGuardPage(page);
|
|
373
537
|
}
|
|
374
538
|
|
|
375
539
|
async function refresh() {
|
|
@@ -384,7 +548,15 @@ async function refresh() {
|
|
|
384
548
|
overviewData = ov;
|
|
385
549
|
renderOverview(ov);
|
|
386
550
|
renderSidebarDots(ov);
|
|
387
|
-
|
|
551
|
+
renderBlockBanner();
|
|
552
|
+
checkAnthropicKey();
|
|
553
|
+
if (currentPage === 'scan') loadScanStatus();
|
|
554
|
+
else if (currentPage === 'skill') renderSkillPage();
|
|
555
|
+
else if (currentPage === 'pi') renderPiPage();
|
|
556
|
+
else if (currentPage === 'secrets') renderSecretsPage();
|
|
557
|
+
else if (currentPage === 'exfil') { loadExfilAllowlist(); renderGuardPage('exfil'); }
|
|
558
|
+
else if (currentPage !== 'home') renderGuardPage(currentPage);
|
|
559
|
+
loadScanStatus(); // always update home scan summary
|
|
388
560
|
document.getElementById('lastUpdated').textContent = 'Updated ' + new Date().toLocaleTimeString();
|
|
389
561
|
} catch (e) {
|
|
390
562
|
console.error('Refresh failed:', e);
|
|
@@ -430,13 +602,567 @@ function setDot(id, hasData, hasBlocks, activeClass) {
|
|
|
430
602
|
el.className = 'dot ' + (hasData ? (hasBlocks ? (activeClass || 'dot-red') : 'dot-green') : 'dot-gray');
|
|
431
603
|
}
|
|
432
604
|
|
|
605
|
+
// =============================================
|
|
606
|
+
// Home — Active Block Banner
|
|
607
|
+
// =============================================
|
|
608
|
+
function renderBlockBanner() {
|
|
609
|
+
const banner = document.getElementById('home-block-banner');
|
|
610
|
+
if (!banner) return;
|
|
611
|
+
|
|
612
|
+
// Find the most recent block event (scan_flagged from skill or prompt_injection, or binary_detected)
|
|
613
|
+
const blockEvent = localEvents.find(e =>
|
|
614
|
+
e.event === 'guard_check' &&
|
|
615
|
+
((e.guard === 'skill' && (e.decision === 'scan_flagged' || e.decision === 'binary_detected')) ||
|
|
616
|
+
(e.guard === 'prompt_injection' && e.decision === 'scan_flagged'))
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
// Check if block was already removed
|
|
620
|
+
const unblockEvent = localEvents.find(e => e.event === 'block_removed');
|
|
621
|
+
if (unblockEvent && blockEvent && new Date(unblockEvent.ts) > new Date(blockEvent.ts)) {
|
|
622
|
+
banner.style.display = 'none';
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (!blockEvent) {
|
|
627
|
+
banner.style.display = 'none';
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
banner.style.display = '';
|
|
632
|
+
const guard = blockEvent.guard === 'skill' ? 'Skill Scanner' : 'Prompt Injection';
|
|
633
|
+
const guardPage = blockEvent.guard === 'skill' ? 'skill' : 'pi';
|
|
634
|
+
document.getElementById('home-block-title').textContent = `Agent Blocked — ${guard}`;
|
|
635
|
+
document.getElementById('home-block-reason').textContent = blockEvent.reason || 'A security threat was detected. All commands are blocked.';
|
|
636
|
+
document.getElementById('home-block-view-btn').setAttribute('onclick', `switchPage('${guardPage}')`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function removeBlock() {
|
|
640
|
+
if (!confirm('Remove the active block and allow OpenClaw to proceed?\n\nOnly do this if you have reviewed and resolved the security issue.')) return;
|
|
641
|
+
try {
|
|
642
|
+
const res = await fetch('/api/unblock', { method: 'POST' });
|
|
643
|
+
const data = await res.json();
|
|
644
|
+
if (data.error) { alert('Error: ' + data.error); return; }
|
|
645
|
+
document.getElementById('home-block-banner').style.display = 'none';
|
|
646
|
+
refresh();
|
|
647
|
+
} catch (e) {
|
|
648
|
+
alert('Failed to remove block: ' + e.message);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// =============================================
|
|
653
|
+
// Anthropic API Key Box
|
|
654
|
+
// =============================================
|
|
655
|
+
async function checkAnthropicKey() {
|
|
656
|
+
try {
|
|
657
|
+
const res = await fetch('/api/anthropic-key');
|
|
658
|
+
const data = await res.json();
|
|
659
|
+
const box = document.getElementById('anthropic-key-box');
|
|
660
|
+
if (!data.hasKey) {
|
|
661
|
+
box.style.display = '';
|
|
662
|
+
} else {
|
|
663
|
+
box.style.display = 'none';
|
|
664
|
+
}
|
|
665
|
+
} catch {}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function saveAnthropicKey() {
|
|
669
|
+
const input = document.getElementById('anthropic-key-input');
|
|
670
|
+
const msg = document.getElementById('anthropic-key-msg');
|
|
671
|
+
const key = input.value.trim();
|
|
672
|
+
if (!key) return;
|
|
673
|
+
try {
|
|
674
|
+
const res = await fetch('/api/anthropic-key', {
|
|
675
|
+
method: 'POST',
|
|
676
|
+
headers: { 'Content-Type': 'application/json' },
|
|
677
|
+
body: JSON.stringify({ key }),
|
|
678
|
+
});
|
|
679
|
+
const data = await res.json();
|
|
680
|
+
msg.style.display = '';
|
|
681
|
+
if (data.error) {
|
|
682
|
+
msg.style.color = '#f87171';
|
|
683
|
+
msg.textContent = data.error;
|
|
684
|
+
} else {
|
|
685
|
+
msg.style.color = '#4ade80';
|
|
686
|
+
msg.textContent = 'Key saved. Restart openclaw for prompt injection detection to activate.';
|
|
687
|
+
input.value = '';
|
|
688
|
+
setTimeout(() => { document.getElementById('anthropic-key-box').style.display = 'none'; }, 3000);
|
|
689
|
+
}
|
|
690
|
+
} catch (e) {
|
|
691
|
+
msg.style.display = '';
|
|
692
|
+
msg.style.color = '#f87171';
|
|
693
|
+
msg.textContent = 'Failed: ' + e.message;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// =============================================
|
|
698
|
+
// Skill Scanner Page
|
|
699
|
+
// =============================================
|
|
700
|
+
function renderSkillPage() {
|
|
701
|
+
renderSkillStatus();
|
|
702
|
+
renderSkillActivityLog();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function renderSkillStatus() {
|
|
706
|
+
const container = document.getElementById('skill-status-body');
|
|
707
|
+
// Get skill events that tell us about skills (init_skill, scanning, scan_clean, scan_flagged, binary_detected, removed, modified, deleted)
|
|
708
|
+
const skillEvents = localEvents.filter(e => e.guard === 'skill' && e.decision !== 'allow' && e.decision !== 'init');
|
|
709
|
+
|
|
710
|
+
// Build latest status per skill by path
|
|
711
|
+
const skillMap = new Map();
|
|
712
|
+
// Process in chronological order (events are newest-first, so reverse)
|
|
713
|
+
const chronological = [...skillEvents].reverse();
|
|
714
|
+
for (const e of chronological) {
|
|
715
|
+
const sp = e.detail?.skill_path;
|
|
716
|
+
if (!sp) continue;
|
|
717
|
+
const name = e.detail?.skill_name || sp.split('/').pop();
|
|
718
|
+
if (!skillMap.has(sp)) {
|
|
719
|
+
skillMap.set(sp, { name, path: sp, dir: e.detail?.skill_dir || '', status: 'clean', reason: '', fileCount: 0, fileNames: [], binaryFiles: [], fileContents: [], ts: e.ts, decision: e.decision });
|
|
720
|
+
}
|
|
721
|
+
const skill = skillMap.get(sp);
|
|
722
|
+
skill.ts = e.ts; // update to latest timestamp
|
|
723
|
+
skill.decision = e.decision;
|
|
724
|
+
if (e.detail?.file_count !== undefined) skill.fileCount = e.detail.file_count;
|
|
725
|
+
if (e.detail?.file_names) skill.fileNames = e.detail.file_names;
|
|
726
|
+
if (e.detail?.binary_files) skill.binaryFiles = e.detail.binary_files;
|
|
727
|
+
if (e.detail?.file_contents) skill.fileContents = e.detail.file_contents;
|
|
728
|
+
// Determine status
|
|
729
|
+
if (e.decision === 'scan_flagged') { skill.status = 'malicious'; skill.reason = e.reason || ''; }
|
|
730
|
+
else if (e.decision === 'binary_detected') { skill.status = 'binary'; skill.reason = e.reason || ''; }
|
|
731
|
+
else if (e.decision === 'scan_clean') { skill.status = 'clean'; skill.reason = ''; }
|
|
732
|
+
else if (e.decision === 'removed' || e.decision === 'deleted') { skill.status = 'removed'; skill.reason = ''; }
|
|
733
|
+
else if (e.decision === 'modified') { skill.status = 'scanning'; }
|
|
734
|
+
else if (e.decision === 'scanning') { skill.status = 'scanning'; }
|
|
735
|
+
else if (e.decision === 'init_skill') {
|
|
736
|
+
skill.status = e.detail?.status || 'clean';
|
|
737
|
+
skill.reason = e.detail?.flagged_reason || '';
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const skills = Array.from(skillMap.values()).filter(s => s.status !== 'removed');
|
|
742
|
+
|
|
743
|
+
if (skills.length === 0) {
|
|
744
|
+
container.innerHTML = '<div class="empty">No skills discovered yet. Skills appear when openclaw-secure is active.</div>';
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
container.innerHTML = `<table><thead><tr>
|
|
749
|
+
<th style="width:40px"></th>
|
|
750
|
+
<th>Skill</th>
|
|
751
|
+
<th>Path</th>
|
|
752
|
+
<th>Status</th>
|
|
753
|
+
<th>Reason</th>
|
|
754
|
+
<th style="text-align:center">Files</th>
|
|
755
|
+
<th>Last Scanned</th>
|
|
756
|
+
</tr></thead><tbody>` +
|
|
757
|
+
skills.map((s, i) => {
|
|
758
|
+
const statusBadge = s.status === 'malicious'
|
|
759
|
+
? '<span class="badge badge-red">Malicious</span>'
|
|
760
|
+
: s.status === 'binary'
|
|
761
|
+
? '<span class="badge badge-red">Binary</span>'
|
|
762
|
+
: s.status === 'scanning'
|
|
763
|
+
? '<span class="badge badge-blue">Scanning</span>'
|
|
764
|
+
: '<span class="badge badge-green">Clean</span>';
|
|
765
|
+
const deleteBtn = (s.status === 'malicious' || s.status === 'binary')
|
|
766
|
+
? `<button class="btn btn-sm" style="color:#f87171;border-color:rgba(239,68,68,0.3)" onclick="deleteSkill('${esc(s.path.replace(/'/g, "\\'"))}')">Delete</button>`
|
|
767
|
+
: '';
|
|
768
|
+
const viewFiles = s.fileNames.length > 0 || s.fileContents.length > 0
|
|
769
|
+
? `<span class="payload-toggle" onclick="openSkillFiles(${i})">${s.fileCount || s.fileNames.length}</span>`
|
|
770
|
+
: `<span style="color:#a1a1aa">${s.fileCount || 0}</span>`;
|
|
771
|
+
return `<tr>
|
|
772
|
+
<td>${deleteBtn}</td>
|
|
773
|
+
<td style="font-weight:500;font-family:ui-monospace,monospace;font-size:13px">${esc(s.name)}</td>
|
|
774
|
+
<td style="font-size:12px;color:#a1a1aa;max-width:250px;word-break:break-all">${esc(s.path)}</td>
|
|
775
|
+
<td>${statusBadge}</td>
|
|
776
|
+
<td style="font-size:12px;color:#a1a1aa;max-width:300px">${esc(s.reason)}</td>
|
|
777
|
+
<td style="text-align:center">${viewFiles}</td>
|
|
778
|
+
<td style="white-space:nowrap;color:#a1a1aa;font-size:12px">${formatTime(s.ts)}</td>
|
|
779
|
+
</tr>`;
|
|
780
|
+
}).join('') + '</tbody></table>';
|
|
781
|
+
|
|
782
|
+
// Store skills data for file modal
|
|
783
|
+
window._skillStatusData = skills;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function openSkillFiles(idx) {
|
|
787
|
+
const skill = window._skillStatusData?.[idx];
|
|
788
|
+
if (!skill) return;
|
|
789
|
+
const modal = document.getElementById('skill-file-modal');
|
|
790
|
+
const title = document.getElementById('skill-modal-title');
|
|
791
|
+
const content = document.getElementById('skill-modal-content');
|
|
792
|
+
title.textContent = 'Files in ' + skill.name;
|
|
793
|
+
|
|
794
|
+
if (skill.fileContents && skill.fileContents.length > 0) {
|
|
795
|
+
content.innerHTML = skill.fileContents.map(f =>
|
|
796
|
+
`<div style="border:1px solid rgba(255,255,255,0.1);border-radius:6px;overflow:hidden;margin-bottom:12px">
|
|
797
|
+
<div style="background:rgba(255,255,255,0.05);padding:6px 12px;font-family:ui-monospace,monospace;font-size:12px;color:#a1a1aa;border-bottom:1px solid rgba(255,255,255,0.1)">${esc(f.path)}</div>
|
|
798
|
+
<pre style="padding:8px 12px;font-size:12px;font-family:ui-monospace,monospace;white-space:pre-wrap;word-break:break-all;max-height:256px;overflow:auto;margin:0">${esc(f.content)}</pre>
|
|
799
|
+
</div>`
|
|
800
|
+
).join('');
|
|
801
|
+
} else if (skill.fileNames && skill.fileNames.length > 0) {
|
|
802
|
+
content.innerHTML = '<div style="padding:8px 0">' + skill.fileNames.map(f =>
|
|
803
|
+
`<div style="padding:4px 0;font-family:ui-monospace,monospace;font-size:13px;color:#a1a1aa">${esc(f)}</div>`
|
|
804
|
+
).join('') + '</div>';
|
|
805
|
+
} else {
|
|
806
|
+
content.innerHTML = '<div class="empty">No file data available for this skill.</div>';
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
modal.style.display = 'flex';
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function closeSkillModal() {
|
|
813
|
+
document.getElementById('skill-file-modal').style.display = 'none';
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function deleteSkill(skillPath) {
|
|
817
|
+
if (!confirm('Delete this skill directory permanently?\n\n' + skillPath + '\n\nThis cannot be undone.')) return;
|
|
818
|
+
try {
|
|
819
|
+
const res = await fetch('/api/skill/delete', {
|
|
820
|
+
method: 'POST',
|
|
821
|
+
headers: { 'Content-Type': 'application/json' },
|
|
822
|
+
body: JSON.stringify({ skillPath }),
|
|
823
|
+
});
|
|
824
|
+
const data = await res.json();
|
|
825
|
+
if (data.error) { alert('Error: ' + data.error); return; }
|
|
826
|
+
// Refresh
|
|
827
|
+
refresh();
|
|
828
|
+
} catch (e) {
|
|
829
|
+
alert('Failed to delete skill: ' + e.message);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function renderSkillActivityLog() {
|
|
834
|
+
const serverOnly = document.getElementById('skill-server-only')?.checked;
|
|
835
|
+
const blockedOnly = document.getElementById('skill-blocked-only')?.checked;
|
|
836
|
+
const container = document.getElementById('skill-table-body');
|
|
837
|
+
|
|
838
|
+
if (serverOnly) {
|
|
839
|
+
const filtered = serverEvents.filter(SERVER_GUARD.skill || (() => false));
|
|
840
|
+
if (filtered.length === 0) {
|
|
841
|
+
container.innerHTML = '<div class="empty">No server send events for skill scanner.</div>';
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
container.innerHTML = `<table><thead><tr><th>Time</th><th>Destination</th><th>Event</th><th>Payload</th></tr></thead><tbody>` +
|
|
845
|
+
filtered.map((e, i) => {
|
|
846
|
+
const dest = e.destination === 'posthog' ? '<span class="badge badge-blue">PostHog</span>' : '<span class="badge badge-yellow">Supabase</span>';
|
|
847
|
+
const payloadStr = JSON.stringify(e.properties || e.payload || {}, null, 2);
|
|
848
|
+
const uid = 'skill-sv-' + i;
|
|
849
|
+
return `<tr>
|
|
850
|
+
<td style="white-space:nowrap;color:#a1a1aa">${formatTime(e.ts)}</td>
|
|
851
|
+
<td>${dest}</td>
|
|
852
|
+
<td>${esc(e.event || '-')}</td>
|
|
853
|
+
<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>
|
|
854
|
+
</tr>`;
|
|
855
|
+
}).join('') + '</tbody></table>';
|
|
856
|
+
} else {
|
|
857
|
+
// Show all skill-specific events (not "allow" per-command checks)
|
|
858
|
+
const skillDecisions = new Set(['init', 'init_skill', 'scanning', 'scan_clean', 'scan_flagged', 'binary_detected', 'removed', 'modified', 'deleted']);
|
|
859
|
+
let filtered = localEvents.filter(e => e.guard === 'skill' && skillDecisions.has(e.decision));
|
|
860
|
+
if (blockedOnly) {
|
|
861
|
+
filtered = filtered.filter(e => e.decision === 'scan_flagged' || e.decision === 'binary_detected');
|
|
862
|
+
}
|
|
863
|
+
if (filtered.length === 0) {
|
|
864
|
+
container.innerHTML = '<div class="empty">' + (blockedOnly ? 'No flagged skill events.' : 'No skill activity yet.') + '</div>';
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
container.innerHTML = `<table><thead><tr>
|
|
868
|
+
<th>Time</th>
|
|
869
|
+
<th>Event</th>
|
|
870
|
+
<th>Skill</th>
|
|
871
|
+
<th>Path</th>
|
|
872
|
+
<th>Files</th>
|
|
873
|
+
<th>Detail</th>
|
|
874
|
+
</tr></thead><tbody>` +
|
|
875
|
+
filtered.map((e, i) => {
|
|
876
|
+
const badgeMap = { init: 'badge-gray', init_skill: 'badge-gray', scanning: 'badge-blue', scan_clean: 'badge-green', scan_flagged: 'badge-red', binary_detected: 'badge-red', removed: 'badge-yellow', modified: 'badge-blue', deleted: 'badge-yellow' };
|
|
877
|
+
const labelMap = { init: 'Init', init_skill: 'Discovered', scanning: 'Scanning', scan_clean: 'Clean', scan_flagged: 'Flagged', binary_detected: 'Binary', removed: 'Removed', modified: 'Modified', deleted: 'Deleted' };
|
|
878
|
+
const badge = badgeMap[e.decision] || 'badge-gray';
|
|
879
|
+
const label = labelMap[e.decision] || e.decision;
|
|
880
|
+
const skillName = e.detail?.skill_name || '-';
|
|
881
|
+
const skillPath = e.detail?.skill_path || '';
|
|
882
|
+
const fileCount = e.detail?.file_count ?? e.detail?.file_names?.length ?? '';
|
|
883
|
+
const fileNames = e.detail?.file_names || [];
|
|
884
|
+
const binaryFiles = e.detail?.binary_files || [];
|
|
885
|
+
const uid = 'skill-log-' + i;
|
|
886
|
+
// Build detail string
|
|
887
|
+
let detailParts = [];
|
|
888
|
+
if (e.reason && e.decision !== 'init') detailParts.push(esc(e.reason));
|
|
889
|
+
let detailExtra = '';
|
|
890
|
+
if (fileNames.length > 0 || binaryFiles.length > 0 || e.detail) {
|
|
891
|
+
const detailObj = {};
|
|
892
|
+
if (fileNames.length > 0) detailObj.files = fileNames;
|
|
893
|
+
if (binaryFiles.length > 0) detailObj.binary_files = binaryFiles;
|
|
894
|
+
if (e.detail?.directories) detailObj.directories = e.detail.directories;
|
|
895
|
+
if (e.detail?.cached !== undefined) detailObj.cached = e.detail.cached;
|
|
896
|
+
if (Object.keys(detailObj).length > 0) {
|
|
897
|
+
detailExtra = ` <span class="payload-toggle" onclick="document.getElementById('${uid}').classList.toggle('open')">[detail]</span><div class="payload-content" id="${uid}">${esc(JSON.stringify(detailObj, null, 2))}</div>`;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return `<tr>
|
|
901
|
+
<td style="white-space:nowrap;color:#a1a1aa">${formatTime(e.ts)}</td>
|
|
902
|
+
<td><span class="badge ${badge}">${label}</span></td>
|
|
903
|
+
<td style="font-family:ui-monospace,monospace;font-size:13px">${esc(skillName)}</td>
|
|
904
|
+
<td style="font-size:12px;color:#a1a1aa;max-width:200px;word-break:break-all">${esc(skillPath)}</td>
|
|
905
|
+
<td style="text-align:center;color:#a1a1aa">${fileCount}</td>
|
|
906
|
+
<td style="font-size:12px;color:#a1a1aa;max-width:350px">${detailParts.join('') + detailExtra}</td>
|
|
907
|
+
</tr>`;
|
|
908
|
+
}).join('') + '</tbody></table>';
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// =============================================
|
|
913
|
+
// Prompt Injection Page
|
|
914
|
+
// =============================================
|
|
915
|
+
function renderPiPage() {
|
|
916
|
+
const serverOnly = document.getElementById('pi-server-only')?.checked;
|
|
917
|
+
const blockedOnly = document.getElementById('pi-blocked-only')?.checked;
|
|
918
|
+
const container = document.getElementById('pi-table-body');
|
|
919
|
+
|
|
920
|
+
if (serverOnly) {
|
|
921
|
+
const filtered = serverEvents.filter(SERVER_GUARD.pi || (() => false));
|
|
922
|
+
if (filtered.length === 0) {
|
|
923
|
+
container.innerHTML = '<div class="empty">No server send events for prompt injection guard.</div>';
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
container.innerHTML = `<table><thead><tr><th>Time</th><th>Destination</th><th>Event</th><th>Payload</th></tr></thead><tbody>` +
|
|
927
|
+
filtered.map((e, i) => {
|
|
928
|
+
const dest = e.destination === 'posthog' ? '<span class="badge badge-blue">PostHog</span>' : '<span class="badge badge-yellow">Supabase</span>';
|
|
929
|
+
const payloadStr = JSON.stringify(e.properties || e.payload || {}, null, 2);
|
|
930
|
+
const uid = 'pi-sv-' + i;
|
|
931
|
+
return `<tr>
|
|
932
|
+
<td style="white-space:nowrap;color:#a1a1aa">${formatTime(e.ts)}</td>
|
|
933
|
+
<td>${dest}</td>
|
|
934
|
+
<td>${esc(e.event || '-')}</td>
|
|
935
|
+
<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>
|
|
936
|
+
</tr>`;
|
|
937
|
+
}).join('') + '</tbody></table>';
|
|
938
|
+
} else {
|
|
939
|
+
// Show only PI-specific scan events (not per-command allow checks)
|
|
940
|
+
const piDecisions = new Set(['scanning', 'scan_clean', 'scan_flagged']);
|
|
941
|
+
let filtered = localEvents.filter(e => e.guard === 'prompt_injection' && piDecisions.has(e.decision));
|
|
942
|
+
if (blockedOnly) {
|
|
943
|
+
filtered = filtered.filter(e => e.decision === 'scan_flagged');
|
|
944
|
+
}
|
|
945
|
+
if (filtered.length === 0) {
|
|
946
|
+
container.innerHTML = '<div class="empty">' + (blockedOnly ? 'No flagged prompt injection events.' : 'No prompt injection scans yet. Scans trigger when matching command output is detected.') + '</div>';
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
container.innerHTML = `<table><thead><tr>
|
|
950
|
+
<th>Time</th>
|
|
951
|
+
<th>Decision</th>
|
|
952
|
+
<th>Pattern Matched</th>
|
|
953
|
+
<th>Command</th>
|
|
954
|
+
<th>Haiku Decision</th>
|
|
955
|
+
<th>Haiku Reason</th>
|
|
956
|
+
<th>I/O</th>
|
|
957
|
+
</tr></thead><tbody>` +
|
|
958
|
+
filtered.map((e, i) => {
|
|
959
|
+
const badgeMap = { scanning: 'badge-blue', scan_clean: 'badge-green', scan_flagged: 'badge-red' };
|
|
960
|
+
const labelMap = { scanning: 'Scanning', scan_clean: 'Clean', scan_flagged: 'Flagged' };
|
|
961
|
+
const badge = badgeMap[e.decision] || 'badge-gray';
|
|
962
|
+
const label = labelMap[e.decision] || e.decision;
|
|
963
|
+
const pattern = e.detail?.matched_pattern || '-';
|
|
964
|
+
const modelOutput = e.detail?.model_output;
|
|
965
|
+
const haikuDecision = modelOutput ? (modelOutput.suspicious ? '<span class="badge badge-red">Suspicious</span>' : '<span class="badge badge-green">Clean</span>') : (e.decision === 'scanning' ? '<span class="badge badge-blue">Pending</span>' : '-');
|
|
966
|
+
const haikuReason = modelOutput?.reason || '-';
|
|
967
|
+
// I/O toggle
|
|
968
|
+
const uid = 'pi-io-' + i;
|
|
969
|
+
const modelInput = e.detail?.model_input || '';
|
|
970
|
+
const modelOutputStr = modelOutput ? JSON.stringify(modelOutput, null, 2) : '';
|
|
971
|
+
const hasIO = modelInput || modelOutputStr;
|
|
972
|
+
const ioHtml = hasIO
|
|
973
|
+
? `<span class="payload-toggle" onclick="document.getElementById('${uid}').classList.toggle('open')">View</span><div class="payload-content" id="${uid}"><b>Input (command output):</b>\n${esc(typeof modelInput === 'string' ? modelInput.slice(0, 3000) : JSON.stringify(modelInput).slice(0, 3000))}\n\n<b>Haiku response:</b>\n${esc(modelOutputStr)}</div>`
|
|
974
|
+
: '-';
|
|
975
|
+
return `<tr>
|
|
976
|
+
<td style="white-space:nowrap;color:#a1a1aa">${formatTime(e.ts)}</td>
|
|
977
|
+
<td><span class="badge ${badge}">${label}</span></td>
|
|
978
|
+
<td style="font-family:ui-monospace,monospace;font-size:12px;max-width:200px;word-break:break-all">${esc(pattern)}</td>
|
|
979
|
+
<td class="cmd" style="max-width:250px" title="${esc(e.command || '')}">${esc(e.command || '-')}</td>
|
|
980
|
+
<td>${haikuDecision}</td>
|
|
981
|
+
<td style="font-size:12px;color:#a1a1aa;max-width:300px">${esc(haikuReason)}</td>
|
|
982
|
+
<td>${ioHtml}</td>
|
|
983
|
+
</tr>`;
|
|
984
|
+
}).join('') + '</tbody></table>';
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// =============================================
|
|
989
|
+
// Secrets Guard Page
|
|
990
|
+
// =============================================
|
|
991
|
+
function renderSecretsPage() {
|
|
992
|
+
const serverOnly = document.getElementById('secrets-server-only')?.checked;
|
|
993
|
+
const blockedOnly = document.getElementById('secrets-blocked-only')?.checked;
|
|
994
|
+
const container = document.getElementById('secrets-table-body');
|
|
995
|
+
|
|
996
|
+
if (serverOnly) {
|
|
997
|
+
const filtered = serverEvents.filter(SERVER_GUARD.secrets || (() => false));
|
|
998
|
+
if (filtered.length === 0) {
|
|
999
|
+
container.innerHTML = '<div class="empty">No server send events for secrets guard.</div>';
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
container.innerHTML = `<table><thead><tr><th>Time</th><th>Destination</th><th>Event</th><th>Payload</th></tr></thead><tbody>` +
|
|
1003
|
+
filtered.map((e, i) => {
|
|
1004
|
+
const dest = e.destination === 'posthog' ? '<span class="badge badge-blue">PostHog</span>' : '<span class="badge badge-yellow">Supabase</span>';
|
|
1005
|
+
const payloadStr = JSON.stringify(e.properties || e.payload || {}, null, 2);
|
|
1006
|
+
const uid = 'sec-sv-' + i;
|
|
1007
|
+
return `<tr>
|
|
1008
|
+
<td style="white-space:nowrap;color:#a1a1aa">${formatTime(e.ts)}</td>
|
|
1009
|
+
<td>${dest}</td>
|
|
1010
|
+
<td>${esc(e.event || '-')}</td>
|
|
1011
|
+
<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>
|
|
1012
|
+
</tr>`;
|
|
1013
|
+
}).join('') + '</tbody></table>';
|
|
1014
|
+
} else {
|
|
1015
|
+
let filtered = localEvents.filter(e => (e.guard === 'env_var' || e.event === 'output_redacted') && e.decision !== 'allow');
|
|
1016
|
+
if (blockedOnly) {
|
|
1017
|
+
filtered = filtered.filter(e => e.decision === 'block' || e.event === 'output_redacted');
|
|
1018
|
+
}
|
|
1019
|
+
// Hide system commands
|
|
1020
|
+
filtered = filtered.filter(e => !e.command || !/^(networksetup|arp|defaults)\b/.test(e.command.trim()));
|
|
1021
|
+
if (filtered.length === 0) {
|
|
1022
|
+
container.innerHTML = '<div class="empty">' + (blockedOnly ? 'No blocked/redacted events.' : 'No secrets guard events yet.') + '</div>';
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
container.innerHTML = `<table><thead><tr>
|
|
1026
|
+
<th>Time</th>
|
|
1027
|
+
<th>Decision</th>
|
|
1028
|
+
<th>Type</th>
|
|
1029
|
+
<th>Pattern Matched</th>
|
|
1030
|
+
<th>Vars / Secrets</th>
|
|
1031
|
+
<th>Command</th>
|
|
1032
|
+
<th>Reason</th>
|
|
1033
|
+
</tr></thead><tbody>` +
|
|
1034
|
+
filtered.map((e, i) => {
|
|
1035
|
+
const badgeMap = { block: 'badge-red', redact: 'badge-yellow', log: 'badge-yellow', allow: 'badge-green' };
|
|
1036
|
+
const labelMap = { block: 'Blocked', redact: 'Redacted', log: 'Logged', allow: 'Allowed' };
|
|
1037
|
+
const badge = badgeMap[e.decision] || 'badge-gray';
|
|
1038
|
+
const label = labelMap[e.decision] || (e.decision || '-');
|
|
1039
|
+
|
|
1040
|
+
// Type column
|
|
1041
|
+
const type = e.detail?.type || (e.event === 'output_redacted' ? 'output_redacted' : '-');
|
|
1042
|
+
const typeLabel = { env_dump: 'Env Dump', value_exposed: 'Value Exposed', lang_env_access: 'Lang API', env_ref_logged: 'Env Ref', output_redacted: 'Output Redacted' }[type] || type;
|
|
1043
|
+
|
|
1044
|
+
// Pattern matched
|
|
1045
|
+
const pattern = e.detail?.matched_pattern || (e.detail?.matched_patterns ? e.detail.matched_patterns.join(', ') : (e.detail?.pattern_category || '-'));
|
|
1046
|
+
|
|
1047
|
+
// Vars / secrets
|
|
1048
|
+
const vars = e.detail?.vars || e.detail?.secrets || [];
|
|
1049
|
+
const varsStr = Array.isArray(vars) ? vars.join(', ') : String(vars);
|
|
1050
|
+
|
|
1051
|
+
const uid = 'sec-lc-' + i;
|
|
1052
|
+
const detailObj = e.detail ? JSON.stringify(e.detail, null, 2) : '';
|
|
1053
|
+
const detailHtml = detailObj ? ` <span class="payload-toggle" onclick="document.getElementById('${uid}').classList.toggle('open')">[detail]</span><div class="payload-content" id="${uid}">${esc(detailObj)}</div>` : '';
|
|
1054
|
+
|
|
1055
|
+
return `<tr>
|
|
1056
|
+
<td style="white-space:nowrap;color:#a1a1aa">${formatTime(e.ts)}</td>
|
|
1057
|
+
<td><span class="badge ${badge}">${label}</span></td>
|
|
1058
|
+
<td style="font-size:12px"><span class="badge badge-gray">${esc(typeLabel)}</span></td>
|
|
1059
|
+
<td style="font-family:ui-monospace,monospace;font-size:11px;max-width:200px;word-break:break-all">${esc(pattern)}</td>
|
|
1060
|
+
<td style="font-family:ui-monospace,monospace;font-size:12px;color:#facc15;max-width:180px;word-break:break-all">${esc(varsStr)}</td>
|
|
1061
|
+
<td class="cmd" style="max-width:250px" title="${esc(e.command || '')}">${esc(e.command || '-')}</td>
|
|
1062
|
+
<td style="font-size:12px;color:#a1a1aa;max-width:300px">${esc(e.reason || '-')}${detailHtml}</td>
|
|
1063
|
+
</tr>`;
|
|
1064
|
+
}).join('') + '</tbody></table>';
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// =============================================
|
|
1069
|
+
// Exfil Allowlist
|
|
1070
|
+
// =============================================
|
|
1071
|
+
let exfilAllowlist = { enabled: false, domains: [] };
|
|
1072
|
+
|
|
1073
|
+
async function loadExfilAllowlist() {
|
|
1074
|
+
try {
|
|
1075
|
+
const res = await fetch('/api/exfil-allowlist');
|
|
1076
|
+
exfilAllowlist = await res.json();
|
|
1077
|
+
} catch { exfilAllowlist = { enabled: false, domains: [] }; }
|
|
1078
|
+
renderExfilAllowlist();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function renderExfilAllowlist() {
|
|
1082
|
+
const al = exfilAllowlist || { enabled: false, domains: [] };
|
|
1083
|
+
const statusBadge = document.getElementById('exfil-al-status-badge');
|
|
1084
|
+
const toggleBtn = document.getElementById('exfil-al-toggle-btn');
|
|
1085
|
+
const domainsDiv = document.getElementById('exfil-al-domains');
|
|
1086
|
+
if (!statusBadge) return;
|
|
1087
|
+
|
|
1088
|
+
if (al.enabled) {
|
|
1089
|
+
statusBadge.innerHTML = '<span class="badge badge-green">Enabled</span>';
|
|
1090
|
+
toggleBtn.textContent = 'Disable';
|
|
1091
|
+
toggleBtn.style.color = '#facc15';
|
|
1092
|
+
toggleBtn.style.borderColor = 'rgba(250,204,21,0.3)';
|
|
1093
|
+
} else {
|
|
1094
|
+
statusBadge.innerHTML = '<span class="badge badge-gray">Disabled</span>';
|
|
1095
|
+
toggleBtn.textContent = 'Enable';
|
|
1096
|
+
toggleBtn.style.color = '#4ade80';
|
|
1097
|
+
toggleBtn.style.borderColor = 'rgba(34,197,94,0.3)';
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (al.domains.length === 0) {
|
|
1101
|
+
domainsDiv.innerHTML = '<div style="font-size:13px;color:#52525b;padding:8px 0">No domains configured.</div>';
|
|
1102
|
+
} else {
|
|
1103
|
+
domainsDiv.innerHTML = al.domains.map(d =>
|
|
1104
|
+
`<div style="display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:4px 10px;margin:3px 4px 3px 0;font-size:13px;font-family:ui-monospace,monospace">
|
|
1105
|
+
${esc(d)}
|
|
1106
|
+
<span style="cursor:pointer;color:#a1a1aa;font-size:16px;line-height:1" onclick="removeExfilDomain('${esc(d)}')" title="Remove">×</span>
|
|
1107
|
+
</div>`
|
|
1108
|
+
).join('');
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
async function addExfilDomain() {
|
|
1113
|
+
const input = document.getElementById('exfil-al-input');
|
|
1114
|
+
const domain = (input.value || '').trim();
|
|
1115
|
+
if (!domain) return;
|
|
1116
|
+
try {
|
|
1117
|
+
const res = await fetch('/api/exfil-allowlist', {
|
|
1118
|
+
method: 'POST',
|
|
1119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1120
|
+
body: JSON.stringify({ action: 'add', domain }),
|
|
1121
|
+
});
|
|
1122
|
+
const data = await res.json();
|
|
1123
|
+
if (data.allowlist) exfilAllowlist = data.allowlist;
|
|
1124
|
+
input.value = '';
|
|
1125
|
+
renderExfilAllowlist();
|
|
1126
|
+
} catch (e) { alert('Failed: ' + e.message); }
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async function removeExfilDomain(domain) {
|
|
1130
|
+
try {
|
|
1131
|
+
const res = await fetch('/api/exfil-allowlist', {
|
|
1132
|
+
method: 'POST',
|
|
1133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1134
|
+
body: JSON.stringify({ action: 'remove', domain }),
|
|
1135
|
+
});
|
|
1136
|
+
const data = await res.json();
|
|
1137
|
+
if (data.allowlist) exfilAllowlist = data.allowlist;
|
|
1138
|
+
renderExfilAllowlist();
|
|
1139
|
+
} catch (e) { alert('Failed: ' + e.message); }
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function toggleExfilAllowlist() {
|
|
1143
|
+
const action = exfilAllowlist.enabled ? 'disable' : 'enable';
|
|
1144
|
+
try {
|
|
1145
|
+
const res = await fetch('/api/exfil-allowlist', {
|
|
1146
|
+
method: 'POST',
|
|
1147
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1148
|
+
body: JSON.stringify({ action }),
|
|
1149
|
+
});
|
|
1150
|
+
const data = await res.json();
|
|
1151
|
+
if (data.allowlist) exfilAllowlist = data.allowlist;
|
|
1152
|
+
renderExfilAllowlist();
|
|
1153
|
+
} catch (e) { alert('Failed: ' + e.message); }
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const SYSTEM_COMMANDS = /^(networksetup|arp|defaults)\b/;
|
|
1157
|
+
|
|
433
1158
|
function renderGuardPage(guard) {
|
|
434
1159
|
const serverOnly = document.getElementById(guard + '-server-only')?.checked;
|
|
435
1160
|
const blockedOnly = document.getElementById(guard + '-blocked-only')?.checked;
|
|
1161
|
+
const showPassed = document.getElementById(guard + '-show-passed')?.checked;
|
|
1162
|
+
const showSystem = document.getElementById(guard + '-show-system')?.checked;
|
|
436
1163
|
const container = document.getElementById(guard + '-table-body');
|
|
437
1164
|
|
|
438
1165
|
if (serverOnly) {
|
|
439
|
-
// Show server send events filtered to this guard
|
|
440
1166
|
const filtered = serverEvents.filter(SERVER_GUARD[guard] || (() => false));
|
|
441
1167
|
if (filtered.length === 0) {
|
|
442
1168
|
container.innerHTML = '<div class="empty">No server send events for this guard.</div>';
|
|
@@ -455,31 +1181,38 @@ function renderGuardPage(guard) {
|
|
|
455
1181
|
</tr>`;
|
|
456
1182
|
}).join('') + '</tbody></table>';
|
|
457
1183
|
} else {
|
|
458
|
-
|
|
459
|
-
const
|
|
1184
|
+
const guardAllFn = GUARD_EVENTS[guard] || (() => false);
|
|
1185
|
+
const guardBlockedFn = GUARD_BLOCKED[guard] || (() => false);
|
|
460
1186
|
let filtered;
|
|
461
1187
|
if (blockedOnly) {
|
|
462
|
-
filtered = localEvents.filter(e =>
|
|
1188
|
+
filtered = localEvents.filter(e => guardBlockedFn(e));
|
|
463
1189
|
} else {
|
|
464
|
-
|
|
465
|
-
|
|
1190
|
+
filtered = localEvents.filter(e => guardAllFn(e));
|
|
1191
|
+
}
|
|
1192
|
+
// Hide system commands (networksetup, arp, defaults) unless checkbox is checked
|
|
1193
|
+
if (!showSystem) {
|
|
1194
|
+
filtered = filtered.filter(e => !e.command || !SYSTEM_COMMANDS.test(e.command.trim()));
|
|
466
1195
|
}
|
|
467
1196
|
if (filtered.length === 0) {
|
|
468
1197
|
container.innerHTML = '<div class="empty">' + (blockedOnly ? 'No blocked events for this guard.' : 'No events yet. Events appear when openclaw-secure is active.') + '</div>';
|
|
469
1198
|
return;
|
|
470
1199
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
const
|
|
476
|
-
const
|
|
1200
|
+
const cmdHeader = guard === 'skill' ? 'Skill File' : 'Command';
|
|
1201
|
+
container.innerHTML = `<table><thead><tr><th>Time</th><th>Decision</th><th>${cmdHeader}</th><th>Detail</th></tr></thead><tbody>` +
|
|
1202
|
+
filtered.map((e, i) => {
|
|
1203
|
+
const badgeMap = { block: 'badge-red', scan_flagged: 'badge-red', binary_detected: 'badge-red', redact: 'badge-yellow', log: 'badge-yellow', scanning: 'badge-blue', scan_clean: 'badge-green', allow: 'badge-green', init: 'badge-gray' };
|
|
1204
|
+
const labelMap = { block: 'Blocked', scan_flagged: 'Flagged', binary_detected: 'Binary', redact: 'Redacted', log: 'Logged', scanning: 'Scanning', scan_clean: 'Clean', allow: 'Allowed', init: 'Init' };
|
|
1205
|
+
const badge = badgeMap[e.decision] || 'badge-gray';
|
|
1206
|
+
const label = labelMap[e.decision] || (e.decision || '-');
|
|
1207
|
+
const reason = e.reason ? esc(e.reason) : (e.secrets_count ? `${e.secrets_count} secret(s) redacted` : '');
|
|
1208
|
+
const detailObj = e.detail ? JSON.stringify(e.detail, null, 2) : '';
|
|
1209
|
+
const uid = guard + '-lc-' + i;
|
|
1210
|
+
const detailHtml = reason + (detailObj ? ` <span class="payload-toggle" onclick="document.getElementById('${uid}').classList.toggle('open')">[detail]</span><div class="payload-content" id="${uid}">${esc(detailObj)}</div>` : '');
|
|
477
1211
|
return `<tr>
|
|
478
1212
|
<td style="white-space:nowrap;color:#a1a1aa">${formatTime(e.ts)}</td>
|
|
479
1213
|
<td><span class="badge ${badge}">${label}</span></td>
|
|
480
1214
|
<td class="cmd" title="${esc(e.command || '')}">${esc(e.command || '-')}</td>
|
|
481
|
-
<td style="font-size:12px">${
|
|
482
|
-
<td style="font-size:12px;color:#a1a1aa;max-width:300px;overflow:hidden;text-overflow:ellipsis">${detail}</td>
|
|
1215
|
+
<td style="font-size:12px;color:#a1a1aa;max-width:400px">${detailHtml}</td>
|
|
483
1216
|
</tr>`;
|
|
484
1217
|
}).join('') + '</tbody></table>';
|
|
485
1218
|
}
|
|
@@ -508,8 +1241,236 @@ function esc(s) {
|
|
|
508
1241
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
509
1242
|
}
|
|
510
1243
|
|
|
1244
|
+
// =============================================
|
|
1245
|
+
// Secret Scanner
|
|
1246
|
+
// =============================================
|
|
1247
|
+
let scanData = null; // last scan results from API
|
|
1248
|
+
|
|
1249
|
+
async function loadScanStatus() {
|
|
1250
|
+
try {
|
|
1251
|
+
const res = await fetch('/api/scan');
|
|
1252
|
+
const data = await res.json();
|
|
1253
|
+
renderScanStatus(data);
|
|
1254
|
+
} catch (e) {
|
|
1255
|
+
console.error('Failed to load scan status:', e);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function renderScanStatus(data) {
|
|
1260
|
+
const el = (id) => document.getElementById(id);
|
|
1261
|
+
// TruffleHog status
|
|
1262
|
+
if (el('scan-thog-status')) {
|
|
1263
|
+
el('scan-thog-status').innerHTML = data.installed ? 'Installed' : 'Not installed<div style="font-size:11px;color:#a1a1aa;margin-top:4px">Run: <code style="background:#27272a;padding:2px 6px;border-radius:4px">brew install trufflehog</code></div>';
|
|
1264
|
+
el('scan-thog-status').style.color = data.installed ? '#4ade80' : '#f87171';
|
|
1265
|
+
}
|
|
1266
|
+
// Home page summary + findings from fresh scan data
|
|
1267
|
+
if (data.fresh) {
|
|
1268
|
+
const f = data.fresh;
|
|
1269
|
+
const verified = (f.findings || []).filter(x => x.verified).length;
|
|
1270
|
+
const total = (f.findings || []).length;
|
|
1271
|
+
const ago = f.summary && f.summary.scannedAt ? timeAgo(f.summary.scannedAt) : 'Just now';
|
|
1272
|
+
if (el('scan-last-time')) el('scan-last-time').textContent = ago;
|
|
1273
|
+
if (el('scan-verified')) el('scan-verified').textContent = verified;
|
|
1274
|
+
if (el('scan-total')) el('scan-total').textContent = total;
|
|
1275
|
+
if (el('home-scan-status')) {
|
|
1276
|
+
if (verified > 0) {
|
|
1277
|
+
el('home-scan-status').innerHTML = `<span style="color:#f87171">${verified} live secret${verified > 1 ? 's' : ''} found</span>`;
|
|
1278
|
+
} else if (total > 0) {
|
|
1279
|
+
el('home-scan-status').textContent = `${total} potential finding${total > 1 ? 's' : ''} (none verified)`;
|
|
1280
|
+
} else {
|
|
1281
|
+
el('home-scan-status').innerHTML = '<span style="color:#4ade80">No hardcoded secrets found</span>';
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (el('home-scan-detail')) {
|
|
1285
|
+
el('home-scan-detail').textContent = `Last scan: ${ago}` + (f.summary ? ` · ${f.summary.targetsScanned || 0} directories scanned` : '');
|
|
1286
|
+
}
|
|
1287
|
+
// Reset Run Scan button
|
|
1288
|
+
if (el('scan-run-btn')) { el('scan-run-btn').disabled = false; el('scan-run-btn').innerHTML = '<svg style="width:14px;height:14px" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Run Scan'; }
|
|
1289
|
+
// Populate findings table if not already set by a manual scan
|
|
1290
|
+
if (!scanData) {
|
|
1291
|
+
scanData = f;
|
|
1292
|
+
renderScanFindings();
|
|
1293
|
+
}
|
|
1294
|
+
} else if (data.scanning) {
|
|
1295
|
+
if (el('scan-last-time')) el('scan-last-time').textContent = 'Scanning...';
|
|
1296
|
+
if (el('scan-verified')) el('scan-verified').textContent = '-';
|
|
1297
|
+
if (el('scan-total')) el('scan-total').textContent = '-';
|
|
1298
|
+
if (el('home-scan-status')) el('home-scan-status').textContent = 'Scanning directories for secrets...';
|
|
1299
|
+
if (el('home-scan-detail')) el('home-scan-detail').textContent = '';
|
|
1300
|
+
// Update scan page too
|
|
1301
|
+
if (el('scan-findings-body')) el('scan-findings-body').innerHTML = '<div class="empty">Scanning... this may take a minute.</div>';
|
|
1302
|
+
if (el('scan-run-btn')) { el('scan-run-btn').disabled = true; el('scan-run-btn').innerHTML = '<svg style="width:14px;height:14px;animation:spin 1s linear infinite" 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"/></svg> Scanning...'; }
|
|
1303
|
+
if (!document.getElementById('spin-style')) { const style = document.createElement('style'); style.id = 'spin-style'; style.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }'; document.head.appendChild(style); }
|
|
1304
|
+
if (el('scan-solve-btn')) el('scan-solve-btn').style.display = 'none';
|
|
1305
|
+
if (el('scan-select-all-wrap')) el('scan-select-all-wrap').style.display = 'none';
|
|
1306
|
+
} else {
|
|
1307
|
+
if (el('scan-last-time')) el('scan-last-time').textContent = 'Never';
|
|
1308
|
+
if (el('scan-verified')) el('scan-verified').textContent = '-';
|
|
1309
|
+
if (el('scan-total')) el('scan-total').textContent = '-';
|
|
1310
|
+
if (el('home-scan-status')) el('home-scan-status').textContent = data.installed ? 'No scan data yet. Scan starting...' : 'TruffleHog not installed. Install with: brew install trufflehog';
|
|
1311
|
+
if (el('home-scan-detail')) el('home-scan-detail').textContent = '';
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function runScan() {
|
|
1316
|
+
const btn = document.getElementById('scan-run-btn');
|
|
1317
|
+
btn.disabled = true;
|
|
1318
|
+
btn.innerHTML = '<svg style="width:14px;height:14px;animation:spin 1s linear infinite" 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"/></svg> Scanning...';
|
|
1319
|
+
if (!document.getElementById('spin-style')) {
|
|
1320
|
+
const style = document.createElement('style');
|
|
1321
|
+
style.id = 'spin-style';
|
|
1322
|
+
style.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
|
|
1323
|
+
document.head.appendChild(style);
|
|
1324
|
+
}
|
|
1325
|
+
document.getElementById('scan-findings-body').innerHTML = '<div class="empty">Scanning... this may take a minute.</div>';
|
|
1326
|
+
try {
|
|
1327
|
+
await fetch('/api/scan', { method: 'POST' });
|
|
1328
|
+
// Scan runs in background on server — poll until done
|
|
1329
|
+
await pollScanResults();
|
|
1330
|
+
} catch (e) {
|
|
1331
|
+
document.getElementById('scan-findings-body').innerHTML = '<div class="empty">Scan request sent. Polling for results...</div>';
|
|
1332
|
+
await pollScanResults();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
async function pollScanResults() {
|
|
1337
|
+
const btn = document.getElementById('scan-run-btn');
|
|
1338
|
+
const maxPolls = 120; // 4 minutes max (2s intervals)
|
|
1339
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
1340
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1341
|
+
try {
|
|
1342
|
+
const res = await fetch('/api/scan');
|
|
1343
|
+
const data = await res.json();
|
|
1344
|
+
if (data.scanning) continue; // still in progress
|
|
1345
|
+
// Scan complete — use fresh results if available
|
|
1346
|
+
if (data.fresh) {
|
|
1347
|
+
scanData = data.fresh;
|
|
1348
|
+
}
|
|
1349
|
+
// Update status cards
|
|
1350
|
+
renderScanStatus(data);
|
|
1351
|
+
if (scanData && scanData.targets) {
|
|
1352
|
+
document.getElementById('scan-targets-section').style.display = '';
|
|
1353
|
+
document.getElementById('scan-targets-list').innerHTML = scanData.targets.map(t =>
|
|
1354
|
+
`<div style="display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;color:#a1a1aa"><span style="color:#4ade80">✓</span> ${esc(t.label)}</div>`
|
|
1355
|
+
).join('');
|
|
1356
|
+
}
|
|
1357
|
+
renderScanFindings();
|
|
1358
|
+
btn.disabled = false;
|
|
1359
|
+
btn.innerHTML = '<svg style="width:14px;height:14px" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Run Scan';
|
|
1360
|
+
return;
|
|
1361
|
+
} catch {}
|
|
1362
|
+
}
|
|
1363
|
+
btn.disabled = false;
|
|
1364
|
+
btn.innerHTML = '<svg style="width:14px;height:14px" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Run Scan';
|
|
1365
|
+
document.getElementById('scan-findings-body').innerHTML = '<div class="empty">Scan timed out. Check server logs.</div>';
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function renderScanFindings() {
|
|
1369
|
+
const container = document.getElementById('scan-findings-body');
|
|
1370
|
+
const solveBtn = document.getElementById('scan-solve-btn');
|
|
1371
|
+
const selectAllWrap = document.getElementById('scan-select-all-wrap');
|
|
1372
|
+
if (!scanData || !scanData.findings || scanData.findings.length === 0) {
|
|
1373
|
+
container.innerHTML = '<div class="empty">No findings. Your codebase is clean!</div>';
|
|
1374
|
+
solveBtn.style.display = 'none';
|
|
1375
|
+
selectAllWrap.style.display = 'none';
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const findings = scanData.findings;
|
|
1379
|
+
const hasVerified = findings.some(f => f.verified);
|
|
1380
|
+
solveBtn.style.display = hasVerified ? '' : 'none';
|
|
1381
|
+
selectAllWrap.style.display = hasVerified ? '' : 'none';
|
|
1382
|
+
container.innerHTML = `<table><thead><tr>
|
|
1383
|
+
<th style="width:32px"></th>
|
|
1384
|
+
<th>Detector</th>
|
|
1385
|
+
<th>File</th>
|
|
1386
|
+
<th>Line</th>
|
|
1387
|
+
<th>Status</th>
|
|
1388
|
+
<th>Secret (redacted)</th>
|
|
1389
|
+
<th>Scan Target</th>
|
|
1390
|
+
</tr></thead><tbody>` +
|
|
1391
|
+
findings.map((f, i) => {
|
|
1392
|
+
const verified = f.verified ? '<span class="badge badge-red">LIVE</span>' : '<span class="badge badge-gray">Inactive</span>';
|
|
1393
|
+
const checkDisabled = f.verified ? '' : 'disabled';
|
|
1394
|
+
return `<tr>
|
|
1395
|
+
<td><input type="checkbox" class="scan-check" data-index="${f.index}" ${checkDisabled}></td>
|
|
1396
|
+
<td style="font-weight:500">${esc(f.detectorName)}</td>
|
|
1397
|
+
<td class="cmd" title="${esc(f.file || '')}">${esc(f.file ? f.file.replace(/^\/Users\/[^/]+/, '~') : '-')}</td>
|
|
1398
|
+
<td style="color:#a1a1aa">${f.line || '-'}</td>
|
|
1399
|
+
<td>${verified}</td>
|
|
1400
|
+
<td style="font-family:ui-monospace,monospace;font-size:12px;color:#a1a1aa">${esc(f.raw)}</td>
|
|
1401
|
+
<td style="font-size:12px;color:#a1a1aa">${esc(f.scanTarget || '')}</td>
|
|
1402
|
+
</tr>`;
|
|
1403
|
+
}).join('') + '</tbody></table>';
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function toggleScanSelectAll() {
|
|
1407
|
+
const checked = document.getElementById('scan-select-all')?.checked;
|
|
1408
|
+
document.querySelectorAll('.scan-check:not(:disabled)').forEach(cb => { cb.checked = checked; });
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async function runSolve() {
|
|
1412
|
+
const checks = document.querySelectorAll('.scan-check:checked');
|
|
1413
|
+
const indices = [...checks].map(cb => parseInt(cb.dataset.index));
|
|
1414
|
+
if (indices.length === 0) {
|
|
1415
|
+
alert('No findings selected. Check the boxes next to verified secrets to scrub.');
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (!confirm(`Replace ${indices.length} secret(s) in files with dummy values? This cannot be undone.`)) return;
|
|
1419
|
+
const btn = document.getElementById('scan-solve-btn');
|
|
1420
|
+
btn.disabled = true;
|
|
1421
|
+
btn.textContent = 'Scrubbing...';
|
|
1422
|
+
try {
|
|
1423
|
+
const res = await fetch('/api/solve', {
|
|
1424
|
+
method: 'POST',
|
|
1425
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1426
|
+
body: JSON.stringify({ indices }),
|
|
1427
|
+
});
|
|
1428
|
+
const data = await res.json();
|
|
1429
|
+
if (data.error) {
|
|
1430
|
+
alert('Solve error: ' + data.error);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
// Show results
|
|
1434
|
+
const resultsDiv = document.getElementById('scan-solve-results');
|
|
1435
|
+
const resultsBody = document.getElementById('scan-solve-body');
|
|
1436
|
+
resultsDiv.style.display = '';
|
|
1437
|
+
const succeeded = (data.results || []).filter(r => r.success);
|
|
1438
|
+
const failed = (data.results || []).filter(r => !r.success);
|
|
1439
|
+
resultsBody.innerHTML = `<table><thead><tr><th>Status</th><th>File</th><th>Detector</th><th>Original</th><th>Replaced With</th></tr></thead><tbody>` +
|
|
1440
|
+
(data.results || []).map(r => {
|
|
1441
|
+
const status = r.success ? '<span class="badge badge-green">Replaced</span>' : '<span class="badge badge-red">Failed</span>';
|
|
1442
|
+
return `<tr>
|
|
1443
|
+
<td>${status}</td>
|
|
1444
|
+
<td class="cmd">${esc((r.file || '').replace(/^\/Users\/[^/]+/, '~'))}</td>
|
|
1445
|
+
<td>${esc(r.detectorName || '')}</td>
|
|
1446
|
+
<td style="font-family:ui-monospace,monospace;font-size:12px;color:#a1a1aa">${esc(r.original || r.error || '')}</td>
|
|
1447
|
+
<td style="font-family:ui-monospace,monospace;font-size:12px;color:#4ade80">${esc(r.dummy || '')}</td>
|
|
1448
|
+
</tr>`;
|
|
1449
|
+
}).join('') + '</tbody></table>';
|
|
1450
|
+
if (succeeded.length > 0) {
|
|
1451
|
+
resultsBody.innerHTML += `<div style="padding:16px;font-size:13px;color:#a1a1aa">
|
|
1452
|
+
${succeeded.length} secret(s) replaced with dummy values. Remember to rotate the REAL secrets in their original services.
|
|
1453
|
+
</div>`;
|
|
1454
|
+
}
|
|
1455
|
+
// Clear scan data since files changed
|
|
1456
|
+
scanData = null;
|
|
1457
|
+
document.getElementById('scan-findings-body').innerHTML = '<div class="empty">Scan data cleared after scrub. Run a new scan to verify.</div>';
|
|
1458
|
+
document.getElementById('scan-solve-btn').style.display = 'none';
|
|
1459
|
+
document.getElementById('scan-select-all-wrap').style.display = 'none';
|
|
1460
|
+
// Refresh home summary
|
|
1461
|
+
loadScanStatus();
|
|
1462
|
+
} catch (e) {
|
|
1463
|
+
alert('Solve failed: ' + e.message);
|
|
1464
|
+
} finally {
|
|
1465
|
+
btn.disabled = false;
|
|
1466
|
+
btn.innerHTML = '<svg style="width:14px;height:14px" 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> Scrub Selected';
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
511
1470
|
refresh();
|
|
512
1471
|
setInterval(refresh, 10000);
|
|
513
1472
|
</script>
|
|
514
1473
|
</body>
|
|
515
1474
|
</html>
|
|
1475
|
+
|
|
1476
|
+
|