@upx-us/shield 0.8.0 → 0.8.1
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/CHANGELOG.md +14 -0
- package/README.md +44 -27
- package/dist/index.js +148 -54
- package/dist/src/case-monitor.d.ts +10 -1
- package/dist/src/case-monitor.js +50 -10
- package/dist/src/events/browser/validations.js +8 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/skills/shield/SKILL.md +14 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.8.1] — 2026-03-17
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Disconnected status after failed startup no longer wipes prior healthy activity** — when a new plugin runtime starts but fails before completing its first sync, Shield now preserves the last known-good state (poll time, sync time, session activity) instead of replacing it with a blank slate. Instances that appeared disconnected after a gateway reload now show accurate prior activity.
|
|
12
|
+
- **Plugin no longer advances into startup with partially-formed credentials** — a change in v0.7.5 caused the canonical ingest URL to be treated as a valid credential even when instance ID and HMAC secret were missing, allowing startup to proceed further than it should before failing and writing zero-state. Credential validation now requires all three fields.
|
|
13
|
+
- **Rapid version bumps no longer trigger multiple zero-state writes** — consecutive `openclaw.plugin.json` version changes (as seen in the v0.7.3→v0.7.6 series) could trigger multiple gateway hot-reload cycles, each writing a blank initial state before credential checks failed. The startup guard now prevents redundant state resets during re-registration.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Status output shows last known-good activity when current runtime is disconnected** — if Shield is not running, the status command now surfaces when the last healthy poll and sync occurred, giving a clearer picture of what happened before the disconnect.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
7
21
|
## [0.8.0] — 2026-03-17
|
|
8
22
|
|
|
9
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
# OpenClaw Shield
|
|
2
2
|
|
|
3
3
|
> **OpenClaw Shield is a paid security service by UPX.**
|
|
4
|
-
> Start your **free
|
|
4
|
+
> Start your **free 60-day trial (no credit card required)** at [upx.com/en/lp/openclaw-shield-upx](https://www.upx.com/en/lp/openclaw-shield-upx).
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
**OpenClaw Shield** is the first SIEM-powered security solution built for AI agents — not to control what they *can* do, but to detect what they *did*.
|
|
7
|
+
|
|
8
|
+
While agent sandboxes block at the gate, Shield watches from inside: every tool call, file read, command execution, and outbound request is ingested, enriched, and matched against behavioral detection rules in real time. Anomalies become cases. Cases get playbooks.
|
|
9
|
+
|
|
10
|
+
**What you get:**
|
|
11
|
+
- 🔍 **90+ detection rules** tuned for OpenClaw agents — continuously curated and expanded by UPX security experts
|
|
12
|
+
- 📋 Automated case generation with remediation playbooks
|
|
13
|
+
- 🔒 Cryptographic event integrity (HMAC-SHA256, AES-256-GCM)
|
|
14
|
+
- 🏷️ Full trigger attribution — who prompted what, when, and why
|
|
15
|
+
- 🧹 Built-in data redaction before events leave the host
|
|
16
|
+
- ☁️ **Powered by Google SecOps (Chronicle)** — the same enterprise SIEM trusted by security teams worldwide
|
|
17
|
+
|
|
18
|
+
> Shield is designed to complement the security controls built into OpenClaw and any Claw-based solution — not replace them. Zero-trust policies, sandboxed execution, and private routing control what your agent *can* do. Shield adds what they can't: a continuous forensic record of what it *did*, with behavioral detection and automated case management on top.
|
|
19
|
+
>
|
|
20
|
+
> Works alongside any hardening strategy, out of the box.
|
|
21
|
+
> *For example, solutions like NemoClaw control the perimeter. Shield owns the forensics.*
|
|
22
|
+
|
|
23
|
+
---
|
|
7
24
|
|
|
8
25
|
**Shield is a local collector.** It captures agent activity, redacts sensitive data, and forwards clean telemetry to the UPX platform at [uss.upx.com](https://uss.upx.com) — where security rules, correlation, alerting, playbooks, and case management give your team full visibility. The plugin itself stays lean and transparent; all the heavy lifting happens on the platform side.
|
|
9
26
|
|
|
10
27
|
```
|
|
11
|
-
Your Agent → Shield (local: capture + redact) → UPX Platform (
|
|
28
|
+
Your Agent → Shield (local: capture + redact) → UPX Platform → Google SecOps (detection, cases, forensics)
|
|
12
29
|
```
|
|
13
30
|
|
|
14
31
|
---
|
|
@@ -176,6 +193,8 @@ openclaw gateway restart
|
|
|
176
193
|
|
|
177
194
|
## What to expect after activation
|
|
178
195
|
|
|
196
|
+
Shield's agent skill communicates with you in your language. Alerts, case summaries, and recommendations are presented in whichever language you use — raw command output and technical identifiers (rule names, case IDs, field names) are always kept as-is.
|
|
197
|
+
|
|
179
198
|
After restart, Shield exchanges your key for permanent credentials — this takes a few seconds. You should see your first events within the first poll cycle (~30 seconds). Within 1–2 minutes, those events will appear on the platform at [uss.upx.com](https://uss.upx.com).
|
|
180
199
|
|
|
181
200
|
Run `openclaw shield status` to confirm:
|
|
@@ -183,41 +202,39 @@ Run `openclaw shield status` to confirm:
|
|
|
183
202
|
**Just activated (first minute):**
|
|
184
203
|
|
|
185
204
|
```
|
|
186
|
-
OpenClaw Shield — v0.
|
|
205
|
+
OpenClaw Shield — v0.8.x (5s ago)
|
|
187
206
|
|
|
188
|
-
──
|
|
189
|
-
|
|
190
|
-
Version: 0.
|
|
207
|
+
── Status ────────────────────────────────────
|
|
208
|
+
✅ Running · Connected
|
|
209
|
+
Version: 0.8.x
|
|
191
210
|
Instance: a1b2c3d4…
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
Events sent
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
Session: 0m
|
|
211
|
+
|
|
212
|
+
── Monitoring ────────────────────────────────
|
|
213
|
+
Events: 3 sent · 0 quarantined
|
|
214
|
+
Failures: 0 poll · 0 telemetry
|
|
215
|
+
Session: 0m active
|
|
216
|
+
Last data: poll 5s ago · capture 5s ago
|
|
199
217
|
```
|
|
200
218
|
|
|
201
|
-
A low event count and a recent `Last
|
|
219
|
+
A low event count and a recent `Last data` time means Shield is working correctly. Events accumulate as your agent uses tools.
|
|
202
220
|
|
|
203
221
|
> **Note:** The Activity and Redactions sections appear after your first sync cycle (~30s). If your status looks shorter than the "extended use" example below, that's normal — they'll populate automatically.
|
|
204
222
|
|
|
205
223
|
**After extended use:**
|
|
206
224
|
|
|
207
225
|
```
|
|
208
|
-
OpenClaw Shield — v0.
|
|
226
|
+
OpenClaw Shield — v0.8.x (5s ago)
|
|
209
227
|
|
|
210
|
-
──
|
|
211
|
-
|
|
212
|
-
Version: 0.
|
|
228
|
+
── Status ────────────────────────────────────
|
|
229
|
+
✅ Running · Connected
|
|
230
|
+
Version: 0.8.x
|
|
213
231
|
Instance: a1b2c3d4…
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Events
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Session: 4h 12m
|
|
232
|
+
|
|
233
|
+
── Monitoring ────────────────────────────────
|
|
234
|
+
Events: 1,842 sent · 0 quarantined
|
|
235
|
+
Failures: 0 poll · 0 telemetry
|
|
236
|
+
Session: 4h 12m active
|
|
237
|
+
Last data: poll 5s ago · capture 14s ago
|
|
221
238
|
|
|
222
239
|
── Activity ──────────────────────────────────
|
|
223
240
|
|
|
@@ -238,7 +255,7 @@ OpenClaw Shield — v0.3.x (5s ago)
|
|
|
238
255
|
**When not activated:**
|
|
239
256
|
|
|
240
257
|
```
|
|
241
|
-
OpenClaw Shield — v0.
|
|
258
|
+
OpenClaw Shield — v0.8.x
|
|
242
259
|
|
|
243
260
|
Status: Loaded (not activated)
|
|
244
261
|
|
package/dist/index.js
CHANGED
|
@@ -287,6 +287,27 @@ function hasMeaningfulRuntimeState(snapshot) {
|
|
|
287
287
|
snapshot.lastStartupCheckpoint ||
|
|
288
288
|
snapshot.instanceId);
|
|
289
289
|
}
|
|
290
|
+
function hasHealthyRuntimeState(snapshot) {
|
|
291
|
+
return Boolean(snapshot.lastSuccessfulPollAt || snapshot.lastSync?.at);
|
|
292
|
+
}
|
|
293
|
+
function buildLastKnownGoodSnapshot(updatedAt) {
|
|
294
|
+
return {
|
|
295
|
+
pid: process.pid,
|
|
296
|
+
startedAt: state.startedAt,
|
|
297
|
+
lastPollAt: state.lastPollAt,
|
|
298
|
+
lastSuccessfulPollAt: state.lastSuccessfulPollAt,
|
|
299
|
+
lastCaptureAt: state.lastCaptureAt,
|
|
300
|
+
lastSync: state.lastSync,
|
|
301
|
+
instanceId: state.instanceId,
|
|
302
|
+
eventsProcessed: state.eventsProcessed,
|
|
303
|
+
quarantineCount: state.quarantineCount,
|
|
304
|
+
updatedAt,
|
|
305
|
+
version: version_1.VERSION,
|
|
306
|
+
lastLifecyclePhase: state.lastLifecyclePhase,
|
|
307
|
+
lastStopReason: state.lastStopReason,
|
|
308
|
+
lastStartupCheckpoint: state.lastStartupCheckpoint,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
290
311
|
function persistState(extra = {}) {
|
|
291
312
|
flushAllTimeStats();
|
|
292
313
|
if (!_stateDirty)
|
|
@@ -295,6 +316,7 @@ function persistState(extra = {}) {
|
|
|
295
316
|
const dir = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data');
|
|
296
317
|
if (!(0, fs_1.existsSync)(dir))
|
|
297
318
|
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
319
|
+
const previous = (0, safe_io_1.readJsonSafe)(STATUS_FILE, {}, 'status-merge');
|
|
298
320
|
let countersSnapshot = {};
|
|
299
321
|
try {
|
|
300
322
|
countersSnapshot = {
|
|
@@ -305,13 +327,22 @@ function persistState(extra = {}) {
|
|
|
305
327
|
};
|
|
306
328
|
}
|
|
307
329
|
catch { }
|
|
308
|
-
|
|
330
|
+
const updatedAt = Date.now();
|
|
331
|
+
const mergedCurrent = {
|
|
309
332
|
...state, ...extra,
|
|
310
333
|
version: version_1.VERSION,
|
|
311
|
-
updatedAt
|
|
334
|
+
updatedAt,
|
|
312
335
|
pid: process.pid,
|
|
313
336
|
counters: countersSnapshot,
|
|
314
337
|
allTime: readAllTimeStats(),
|
|
338
|
+
};
|
|
339
|
+
const previousLastKnownGood = previous.lastKnownGood ?? null;
|
|
340
|
+
const nextLastKnownGood = hasHealthyRuntimeState(state)
|
|
341
|
+
? buildLastKnownGoodSnapshot(updatedAt)
|
|
342
|
+
: previousLastKnownGood;
|
|
343
|
+
(0, safe_io_1.writeJsonSafe)(STATUS_FILE, {
|
|
344
|
+
...mergedCurrent,
|
|
345
|
+
lastKnownGood: nextLastKnownGood,
|
|
315
346
|
});
|
|
316
347
|
_stateDirty = false;
|
|
317
348
|
}
|
|
@@ -428,10 +459,10 @@ function printActivatedStatus() {
|
|
|
428
459
|
if (!s) {
|
|
429
460
|
console.log(`OpenClaw Shield — v${version_1.VERSION}`);
|
|
430
461
|
console.log('');
|
|
431
|
-
console.log('──
|
|
432
|
-
console.log('
|
|
462
|
+
console.log('── Status ────────────────────────────────────');
|
|
463
|
+
console.log(' ⚠️ Unknown · Status unavailable');
|
|
433
464
|
console.log(` Version: ${version_1.VERSION}`);
|
|
434
|
-
console.log(`
|
|
465
|
+
console.log(` Note: ${snapshot.message ?? 'Shield has not persisted runtime state yet.'}`);
|
|
435
466
|
console.log(' - This usually means the gateway has not started the plugin yet, or state persistence is unavailable.');
|
|
436
467
|
console.log(' - If this persists, run: openclaw gateway restart');
|
|
437
468
|
return;
|
|
@@ -445,46 +476,85 @@ function printActivatedStatus() {
|
|
|
445
476
|
const shortId = instanceId ? `${instanceId.slice(0, 8)}…` : '';
|
|
446
477
|
const lifecyclePhase = s.lastLifecyclePhase ?? 'unknown';
|
|
447
478
|
const stopReason = s.lastStopReason ?? 'unknown';
|
|
479
|
+
const lastKnownGood = (s.lastKnownGood ?? null);
|
|
448
480
|
console.log(`OpenClaw Shield — v${s.version ?? version_1.VERSION}${ageLabel ? ` (${ageLabel})` : ''}`);
|
|
481
|
+
const cachedUpdate = (0, updater_1.loadUpdateState)();
|
|
449
482
|
console.log('');
|
|
450
|
-
console.log('── Plugin Health ─────────────────────────────');
|
|
451
|
-
console.log(` Connection: ${isRunning ? '✅ Connected' : snapshot.source === 'stale' ? '⚠️ Status stale' : '❌ Disconnected'}`);
|
|
452
|
-
console.log(` Version: ${s.version ?? version_1.VERSION}`);
|
|
453
|
-
if (shortId)
|
|
454
|
-
console.log(` Instance: ${shortId}`);
|
|
455
|
-
console.log(` Last poll: ${lastPollLabel}`);
|
|
456
483
|
const lastCaptureMs = s.lastCaptureAt ? Date.now() - s.lastCaptureAt : null;
|
|
457
484
|
const lastCaptureLabel = s.lastCaptureAt ? fmtTime(lastCaptureMs) : null;
|
|
458
|
-
|
|
459
|
-
console.log(` Last capture: ${lastCaptureLabel}`);
|
|
460
|
-
const updateState = (0, updater_1.loadUpdateState)();
|
|
485
|
+
const updateState = cachedUpdate;
|
|
461
486
|
const allTime = (s.allTime ?? readAllTimeStats());
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
487
|
+
const phaseIcon = isRunning ? '✅' : (snapshot.source === 'stale' ? '⚠️' : '❌');
|
|
488
|
+
const phaseDisplay = lifecyclePhase.charAt(0).toUpperCase() + lifecyclePhase.slice(1);
|
|
489
|
+
const connectionDisplay = isRunning ? 'Connected' : snapshot.source === 'stale' ? 'Status stale' : 'Disconnected';
|
|
490
|
+
console.log('── Status ────────────────────────────────────');
|
|
491
|
+
console.log(` ${phaseIcon} ${phaseDisplay} · ${connectionDisplay}`);
|
|
492
|
+
console.log(` Version: ${s.version ?? version_1.VERSION}`);
|
|
493
|
+
if (shortId)
|
|
494
|
+
console.log(` Instance: ${shortId}`);
|
|
495
|
+
if (snapshot.message)
|
|
496
|
+
console.log(` Note: ${snapshot.message}`);
|
|
469
497
|
if (stopReason && stopReason !== 'none')
|
|
470
498
|
console.log(` Stop reason: ${stopReason}`);
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
console.log(` Daemon PID: ${s.pid}${pidAlive ? '' : ' ⚠️ stale (process not running)'}`);
|
|
479
|
-
}
|
|
480
|
-
if (snapshot.message) {
|
|
481
|
-
console.log(` Status note: ${snapshot.message}`);
|
|
499
|
+
if (lastKnownGood && !isRunning) {
|
|
500
|
+
const lastGoodPoll = lastKnownGood.lastPollAt ? fmtTime(Date.now() - lastKnownGood.lastPollAt) : 'never';
|
|
501
|
+
const lastGoodCapture = lastKnownGood.lastCaptureAt ? fmtTime(Date.now() - lastKnownGood.lastCaptureAt) : null;
|
|
502
|
+
const lastGoodParts = [`poll ${lastGoodPoll}`];
|
|
503
|
+
if (lastGoodCapture)
|
|
504
|
+
lastGoodParts.push(`capture ${lastGoodCapture}`);
|
|
505
|
+
console.log(` Last healthy: ${lastGoodParts.join(' · ')}`);
|
|
482
506
|
}
|
|
483
|
-
|
|
484
|
-
|
|
507
|
+
console.log('');
|
|
508
|
+
const telFailures = s.telemetryConsecutiveFailures ?? 0;
|
|
509
|
+
const pollFailures = s.consecutiveFailures ?? 0;
|
|
510
|
+
const sessionMs = s.startedAt ? Date.now() - s.startedAt : null;
|
|
511
|
+
const sessionLabel = sessionMs != null
|
|
512
|
+
? sessionMs < 60_000 ? `${Math.round(sessionMs / 1000)}s`
|
|
513
|
+
: sessionMs < 3_600_000 ? `${Math.floor(sessionMs / 60_000)}m`
|
|
514
|
+
: `${(sessionMs / 3_600_000).toFixed(1)}h`
|
|
515
|
+
: null;
|
|
516
|
+
const lastDataParts = [];
|
|
517
|
+
if (lastPollLabel !== 'never')
|
|
518
|
+
lastDataParts.push(`poll ${lastPollLabel}`);
|
|
519
|
+
if (lastCaptureLabel)
|
|
520
|
+
lastDataParts.push(`capture ${lastCaptureLabel}`);
|
|
521
|
+
console.log('── Monitoring ────────────────────────────────');
|
|
522
|
+
console.log(` Events: ${allTime.eventsProcessed.toLocaleString()} sent · ${allTime.quarantineCount.toLocaleString()} quarantined`);
|
|
523
|
+
console.log(` Failures: ${pollFailures} poll · ${telFailures} telemetry`);
|
|
524
|
+
const checkpointHuman = (() => {
|
|
525
|
+
const cp = s.lastStartupCheckpoint ?? '';
|
|
526
|
+
if (cp.includes('not_activated'))
|
|
527
|
+
return 'Not activated';
|
|
528
|
+
if (cp.includes('activation_failed'))
|
|
529
|
+
return 'Activation failed';
|
|
530
|
+
if (cp.includes('update_restart'))
|
|
531
|
+
return 'Restarting for update';
|
|
532
|
+
if (cp === 'checkpoint:11' || cp === 'checkpoint:9' || cp === 'checkpoint:9a')
|
|
533
|
+
return 'Monitoring active';
|
|
534
|
+
if (cp === 'checkpoint:8')
|
|
535
|
+
return 'Starting up';
|
|
536
|
+
if (cp.includes('checkpoint:') || cp === 'SIGTERM')
|
|
537
|
+
return 'Starting up';
|
|
538
|
+
if (cp.includes('registration_invalid') || cp.includes('poll_registration_invalid'))
|
|
539
|
+
return 'Registration invalid';
|
|
540
|
+
return null;
|
|
541
|
+
})();
|
|
542
|
+
if (sessionLabel)
|
|
543
|
+
console.log(` Session: ${sessionLabel} active${checkpointHuman ? ` (${checkpointHuman})` : ''}`);
|
|
544
|
+
if (lastDataParts.length > 0)
|
|
545
|
+
console.log(` Last data: ${lastDataParts.join(' · ')}`);
|
|
546
|
+
if (lastKnownGood && !isRunning) {
|
|
547
|
+
const lastGoodSync = lastKnownGood.lastSync?.at ? fmtTime(Date.now() - lastKnownGood.lastSync.at) : null;
|
|
548
|
+
const lastGoodSession = lastKnownGood.startedAt ? fmtTime(Date.now() - lastKnownGood.startedAt) : null;
|
|
549
|
+
const lastGoodLabel = [
|
|
550
|
+
lastGoodSession ? `session ${lastGoodSession}` : null,
|
|
551
|
+
lastGoodSync ? `sync ${lastGoodSync}` : null,
|
|
552
|
+
].filter(Boolean).join(' · ');
|
|
553
|
+
if (lastGoodLabel)
|
|
554
|
+
console.log(` Last good: ${lastGoodLabel}`);
|
|
485
555
|
}
|
|
486
|
-
if (s.
|
|
487
|
-
console.log(`
|
|
556
|
+
if (s.eventsRetainedForRetry && Number(s.eventsRetainedForRetry) > 0) {
|
|
557
|
+
console.log(` Retry backlog:${String(s.eventsRetainedForRetry).padStart(3)} event(s) retained`);
|
|
488
558
|
}
|
|
489
559
|
if (updateState.pendingRestart || updateState.lastFailureStage || updateState.rollbackPending) {
|
|
490
560
|
console.log(` Update: ${updateState.pendingRestart ? 'pending restart' : updateState.rollbackPending ? 'rollback needed' : 'attention required'}`);
|
|
@@ -493,11 +563,19 @@ function printActivatedStatus() {
|
|
|
493
563
|
if (updateState.lastError)
|
|
494
564
|
console.log(` Update error: ${updateState.lastError}`);
|
|
495
565
|
}
|
|
566
|
+
if (s.lastError) {
|
|
567
|
+
console.log(` Last error: ${s.lastError}`);
|
|
568
|
+
}
|
|
496
569
|
if (s.lastDeliveryIssue) {
|
|
497
570
|
console.log(` Delivery: ${s.lastDeliveryIssue}`);
|
|
498
571
|
}
|
|
499
|
-
if (
|
|
500
|
-
console.log(
|
|
572
|
+
if (cachedUpdate.updateAvailable && cachedUpdate.latestVersion) {
|
|
573
|
+
console.log('');
|
|
574
|
+
console.log(`⚡ Update available: v${cachedUpdate.latestVersion}`);
|
|
575
|
+
console.log(` To update, run:`);
|
|
576
|
+
console.log(` openclaw plugins update shield`);
|
|
577
|
+
console.log(` openclaw gateway restart`);
|
|
578
|
+
console.log(` Or tell your agent: "Update the OpenClaw Shield plugin to the latest version"`);
|
|
501
579
|
}
|
|
502
580
|
const statusWarnings = getStatusWarnings({
|
|
503
581
|
running: isRunning,
|
|
@@ -525,14 +603,6 @@ function printActivatedStatus() {
|
|
|
525
603
|
console.log(' - If this persists, run: openclaw gateway restart');
|
|
526
604
|
}
|
|
527
605
|
const startedAt = s.startedAt;
|
|
528
|
-
if (startedAt) {
|
|
529
|
-
const uptimeMs = Date.now() - startedAt;
|
|
530
|
-
const uptimeLabel = uptimeMs < 3_600_000
|
|
531
|
-
? `${Math.floor(uptimeMs / 60_000)}m`
|
|
532
|
-
: `${(uptimeMs / 3_600_000).toFixed(1)}h`;
|
|
533
|
-
console.log(` Session: ${uptimeLabel}`);
|
|
534
|
-
}
|
|
535
|
-
console.log('');
|
|
536
606
|
console.log('── Activity ──────────────────────────────────');
|
|
537
607
|
const BAR_CHARS = '████████████████████';
|
|
538
608
|
const BAR_MAX = 8;
|
|
@@ -554,7 +624,12 @@ function printActivatedStatus() {
|
|
|
554
624
|
}
|
|
555
625
|
else {
|
|
556
626
|
console.log('📡 Last sync');
|
|
557
|
-
|
|
627
|
+
if (lastKnownGood?.lastSync?.at && !isRunning) {
|
|
628
|
+
console.log(` Current runtime has no sync yet. Last healthy sync was ${fmtTime(Date.now() - lastKnownGood.lastSync.at)}.`);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
console.log(` ${snapshot.source === 'stale' ? 'Last known sync is unavailable because persisted state is stale.' : 'No sync yet. Bridge will send on the next poll cycle.'}`);
|
|
632
|
+
}
|
|
558
633
|
}
|
|
559
634
|
const counters = (s.counters ?? {});
|
|
560
635
|
const sessionEvents = counters.totalEvents ?? 0;
|
|
@@ -562,10 +637,10 @@ function printActivatedStatus() {
|
|
|
562
637
|
const sessionRows = Object.entries(sessionTypes).sort(([, a], [, b]) => b - a);
|
|
563
638
|
const sessionMax = sessionRows[0]?.[1] ?? 0;
|
|
564
639
|
console.log('');
|
|
565
|
-
const
|
|
640
|
+
const sessionActivityLabel = startedAt
|
|
566
641
|
? `since restart ${fmtTime(Date.now() - startedAt)}`
|
|
567
642
|
: 'this session';
|
|
568
|
-
console.log(`📊 This session (${
|
|
643
|
+
console.log(`📊 This session (${sessionActivityLabel} — ${sessionEvents} event${sessionEvents !== 1 ? 's' : ''})`);
|
|
569
644
|
if (sessionRows.length === 0) {
|
|
570
645
|
console.log(' No events recorded yet.');
|
|
571
646
|
}
|
|
@@ -704,11 +779,15 @@ exports.default = {
|
|
|
704
779
|
catch { }
|
|
705
780
|
}
|
|
706
781
|
};
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
782
|
+
const inheritedRuntimeTeardown = teardownPreviousRuntime;
|
|
783
|
+
let inheritedRuntimeHandedOff = false;
|
|
784
|
+
const currentRuntimeTeardown = () => cleanupRuntime({
|
|
785
|
+
markStopped: true,
|
|
786
|
+
resetGuard: true,
|
|
787
|
+
flushRedactor: false,
|
|
788
|
+
stopReason: 'runtime_replaced',
|
|
789
|
+
});
|
|
790
|
+
teardownPreviousRuntime = inheritedRuntimeTeardown;
|
|
712
791
|
const deactivateForRegistrationInvalidation = async (checkpoint, message) => {
|
|
713
792
|
setLifecyclePhase('deactivated', checkpoint);
|
|
714
793
|
setStopReason('registration_invalidated');
|
|
@@ -922,8 +1001,23 @@ exports.default = {
|
|
|
922
1001
|
log.info('shield', '[checkpoint:8] Dynamic imports loaded');
|
|
923
1002
|
if (config.redactionEnabled)
|
|
924
1003
|
initRedactor();
|
|
1004
|
+
if (inheritedRuntimeTeardown && !inheritedRuntimeHandedOff) {
|
|
1005
|
+
try {
|
|
1006
|
+
log.info('shield', '[checkpoint:8a] Taking over previous healthy runtime');
|
|
1007
|
+
pendingTeardown = inheritedRuntimeTeardown()
|
|
1008
|
+
.catch((err) => log.warn('shield', `Previous runtime teardown during takeover failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1009
|
+
await pendingTeardown.catch(() => { });
|
|
1010
|
+
pendingTeardown = null;
|
|
1011
|
+
inheritedRuntimeHandedOff = true;
|
|
1012
|
+
}
|
|
1013
|
+
catch (err) {
|
|
1014
|
+
log.warn('shield', `Takeover handoff failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1015
|
+
throw err;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
925
1018
|
state.running = true;
|
|
926
1019
|
setLifecyclePhase('running', 'checkpoint:9');
|
|
1020
|
+
teardownPreviousRuntime = currentRuntimeTeardown;
|
|
927
1021
|
persistState();
|
|
928
1022
|
log.info('shield', '[checkpoint:9] state.running=true — entering poll loop');
|
|
929
1023
|
const runTelemetry = async () => {
|
|
@@ -19,6 +19,11 @@ export declare function setDirectSendDispatcher(opts: {
|
|
|
19
19
|
to: string;
|
|
20
20
|
channel: string;
|
|
21
21
|
}): void;
|
|
22
|
+
declare const SEVERITY_ORDER: Record<string, number>;
|
|
23
|
+
declare function formatCaseTimestamp(isoDate: string): string;
|
|
24
|
+
declare function buildCaseUrl(c: CaseSummary | CaseDetail): string;
|
|
25
|
+
declare function formatBatchCaseBlock(c: CaseSummary | CaseDetail): string;
|
|
26
|
+
declare function dispatchCaseNotifications(cases: (CaseSummary | CaseDetail)[]): void;
|
|
22
27
|
export interface CaseSummary {
|
|
23
28
|
id: string;
|
|
24
29
|
rule_id: string;
|
|
@@ -77,4 +82,8 @@ export declare function formatCaseNotification(c: CaseSummary | CaseDetail): str
|
|
|
77
82
|
export declare function _resetForTesting(): void;
|
|
78
83
|
export declare function _simulateFailuresForTesting(count: number, firstFailureAgeMs: number): void;
|
|
79
84
|
export declare function _simulateSuccessForTesting(): void;
|
|
80
|
-
export {};
|
|
85
|
+
export { SEVERITY_ORDER as _SEVERITY_ORDER };
|
|
86
|
+
export declare const _formatCaseTimestamp: typeof formatCaseTimestamp;
|
|
87
|
+
export declare const _buildCaseUrl: typeof buildCaseUrl;
|
|
88
|
+
export declare const _formatBatchCaseBlock: typeof formatBatchCaseBlock;
|
|
89
|
+
export declare const _dispatchCaseNotifications: typeof dispatchCaseNotifications;
|
package/dist/src/case-monitor.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports._dispatchCaseNotifications = exports._formatBatchCaseBlock = exports._buildCaseUrl = exports._formatCaseTimestamp = exports._SEVERITY_ORDER = void 0;
|
|
36
37
|
exports.setCaseNotificationDispatcher = setCaseNotificationDispatcher;
|
|
37
38
|
exports.setDirectSendDispatcher = setDirectSendDispatcher;
|
|
38
39
|
exports.initCaseMonitor = initCaseMonitor;
|
|
@@ -96,6 +97,42 @@ function setDirectSendDispatcher(opts) {
|
|
|
96
97
|
_directSendChannel = opts.channel;
|
|
97
98
|
log.info('case-monitor', `Direct send configured (channel: ${opts.channel}, to: ${opts.to})`);
|
|
98
99
|
}
|
|
100
|
+
const SEVERITY_ORDER = {
|
|
101
|
+
CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3, INFO: 4,
|
|
102
|
+
};
|
|
103
|
+
exports._SEVERITY_ORDER = SEVERITY_ORDER;
|
|
104
|
+
function formatCaseTimestamp(isoDate) {
|
|
105
|
+
const d = new Date(isoDate);
|
|
106
|
+
if (isNaN(d.getTime()))
|
|
107
|
+
return isoDate;
|
|
108
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
109
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;
|
|
110
|
+
}
|
|
111
|
+
function buildCaseUrl(c) {
|
|
112
|
+
if (isCaseDetail(c) && c.url)
|
|
113
|
+
return c.url;
|
|
114
|
+
return `https://uss.upx.com/cases/${c.id}`;
|
|
115
|
+
}
|
|
116
|
+
function formatBatchCaseBlock(c) {
|
|
117
|
+
const severityEmoji = {
|
|
118
|
+
CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵', INFO: 'ℹ️',
|
|
119
|
+
};
|
|
120
|
+
const emoji = severityEmoji[c.severity] || '⚠️';
|
|
121
|
+
const sev = c.severity || '';
|
|
122
|
+
const description = (isCaseDetail(c) && c.rule?.description)
|
|
123
|
+
? c.rule.description
|
|
124
|
+
: (c.description || c.summary || '');
|
|
125
|
+
const ts = formatCaseTimestamp(c.created_at);
|
|
126
|
+
const url = buildCaseUrl(c);
|
|
127
|
+
const lines = [
|
|
128
|
+
`${emoji} ${c.rule_title} · ${sev}`,
|
|
129
|
+
];
|
|
130
|
+
if (description)
|
|
131
|
+
lines.push(description);
|
|
132
|
+
lines.push(`🕐 ${ts}`);
|
|
133
|
+
lines.push(`🔗 View case: ${url}`);
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
99
136
|
function dispatchCaseNotifications(cases) {
|
|
100
137
|
if (cases.length === 0)
|
|
101
138
|
return;
|
|
@@ -104,19 +141,18 @@ function dispatchCaseNotifications(cases) {
|
|
|
104
141
|
text = formatCaseNotification(cases[0]);
|
|
105
142
|
}
|
|
106
143
|
else {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
return
|
|
144
|
+
const sorted = [...cases].sort((a, b) => {
|
|
145
|
+
const oa = SEVERITY_ORDER[a.severity] ?? 99;
|
|
146
|
+
const ob = SEVERITY_ORDER[b.severity] ?? 99;
|
|
147
|
+
return oa - ob;
|
|
111
148
|
});
|
|
149
|
+
const agentLabel = _agentId || 'agent';
|
|
150
|
+
const separator = '─────────────────────';
|
|
151
|
+
const blocks = sorted.map(c => `${separator}\n${formatBatchCaseBlock(c)}`);
|
|
112
152
|
text = [
|
|
113
|
-
`⚠️
|
|
114
|
-
'',
|
|
115
|
-
...lines,
|
|
116
|
-
'',
|
|
153
|
+
`⚠️ Shield — ${cases.length} new cases · ${agentLabel}`,
|
|
117
154
|
'',
|
|
118
|
-
|
|
119
|
-
...cases.map(c => ` • _"Investigate case ${c.id}"_`),
|
|
155
|
+
...blocks,
|
|
120
156
|
].join('\n');
|
|
121
157
|
}
|
|
122
158
|
if (_directSendFn && _directSendTo) {
|
|
@@ -456,3 +492,7 @@ function _simulateSuccessForTesting() {
|
|
|
456
492
|
_degradedSinceMs = null;
|
|
457
493
|
}
|
|
458
494
|
}
|
|
495
|
+
exports._formatCaseTimestamp = formatCaseTimestamp;
|
|
496
|
+
exports._buildCaseUrl = buildCaseUrl;
|
|
497
|
+
exports._formatBatchCaseBlock = formatBatchCaseBlock;
|
|
498
|
+
exports._dispatchCaseNotifications = dispatchCaseNotifications;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validate = validate;
|
|
4
|
+
const URL_REQUIRED_ACTIONS = new Set(['navigate', 'open']);
|
|
4
5
|
function validate(event) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const action = event.tool_metadata?.browser_action ?? null;
|
|
7
|
+
if (URL_REQUIRED_ACTIONS.has(action)) {
|
|
8
|
+
if (!event.url)
|
|
9
|
+
return { valid: false, field: 'url', error: 'missing url' };
|
|
10
|
+
if (!event.target?.url)
|
|
11
|
+
return { valid: false, field: 'target.url', error: 'missing target.url' };
|
|
12
|
+
}
|
|
9
13
|
return { valid: true };
|
|
10
14
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upx-us/shield",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Security monitoring
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "Security monitoring and SIEM integration for OpenClaw agents — behavioral detection, case generation, and forensic audit trail via Google SecOps (Chronicle).",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": {
|
package/skills/shield/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: openclaw-shield-upx
|
|
3
|
-
description: "Security monitoring and threat detection for OpenClaw agents —
|
|
3
|
+
description: "Security monitoring and threat detection for OpenClaw agents — powered by Google SecOps (Chronicle). Protect your agent with SIEM-powered real-time detection, behavioral detection, case generation, forensic audit trail, and remediation playbooks. Use when: user asks about security status, Shield health, event logs, redaction vault, setting up agent protection, enabling SIEM, detecting threats, monitoring agent activity, or auditing agent actions. NOT for: general OS hardening, firewall config, or network security unrelated to OpenClaw agents."
|
|
4
4
|
homepage: https://www.upx.com/en/lp/openclaw-shield-upx
|
|
5
5
|
source: https://www.npmjs.com/package/@upx-us/shield
|
|
6
6
|
license: "Proprietary — UPX Technologies, Inc. All rights reserved."
|
|
@@ -16,7 +16,7 @@ Security monitoring for OpenClaw agents by [UPX](https://www.upx.com). Shield ru
|
|
|
16
16
|
Shield requires the `@upx-us/shield` plugin and an active subscription.
|
|
17
17
|
|
|
18
18
|
- **Plugin**: [@upx-us/shield](https://www.npmjs.com/package/@upx-us/shield)
|
|
19
|
-
- **Subscribe / Free
|
|
19
|
+
- **Subscribe / Free 60-day trial (no credit card required)**: [upx.com/en/lp/openclaw-shield-upx](https://www.upx.com/en/lp/openclaw-shield-upx)
|
|
20
20
|
- **Dashboard**: [uss.upx.com](https://uss.upx.com)
|
|
21
21
|
|
|
22
22
|
## Commands
|
|
@@ -43,7 +43,7 @@ Before responding to ANY Shield-related request, run `openclaw shield status` an
|
|
|
43
43
|
### State A — Plugin not installed
|
|
44
44
|
(command not found, or exit code non-zero with plugin_not_found)
|
|
45
45
|
Respond with:
|
|
46
|
-
> Shield is not installed yet. Start your free
|
|
46
|
+
> Shield is not installed yet. Start your free 60-day trial (no credit card required) at:
|
|
47
47
|
> https://www.upx.com/en/lp/openclaw-shield-upx
|
|
48
48
|
>
|
|
49
49
|
> Then install the plugin:
|
|
@@ -79,6 +79,10 @@ Proceed normally. No onboarding message needed.
|
|
|
79
79
|
|
|
80
80
|
**Data flow disclosure**: Shield captures agent activity locally and sends redacted telemetry to the UPX detection platform for security monitoring. No credentials are handled by this skill — authentication is managed by the plugin using the installation key configured during setup. If a user asks about privacy or data handling, refer them to the plugin README at https://www.npmjs.com/package/@upx-us/shield for full details.
|
|
81
81
|
|
|
82
|
+
## Presentation Language
|
|
83
|
+
|
|
84
|
+
Always present Shield information, alerts, and case summaries to the user in the language they use to communicate. Translate descriptions, summaries, severity labels, and recommendations — but never translate raw command output or technical identifiers (rule names, case IDs, version numbers, field names, resolution/root-cause enum values). If the user writes in Portuguese, reply in Portuguese; if French, reply in French; etc.
|
|
85
|
+
|
|
82
86
|
## Responding to Security Cases
|
|
83
87
|
|
|
84
88
|
When a Shield case fires or the user asks about an alert: use `openclaw shield cases` to list open cases and `openclaw shield cases --id <id>` for full detail (timeline, matched events, playbook). Severity guidance: **CRITICAL/HIGH** → surface immediately and ask if they want to investigate; **MEDIUM** → present and offer a playbook walkthrough; **LOW/INFO** → mention without interrupting the current task. Always include: rule name, what it detects, when it fired, and the first recommended remediation step. Confirm with the user before resolving — never resolve autonomously.
|
|
@@ -109,6 +113,7 @@ When asked "is my agent secure?", "am I protected?", or "what's being detected?"
|
|
|
109
113
|
|
|
110
114
|
Real-time alerts (notifications or inline messages) are high priority: acknowledge immediately, retrieve full case detail, summarise in plain language, present the recommended next step from the playbook, and ask the user how to proceed. Do not take remediation action without explicit approval.
|
|
111
115
|
|
|
116
|
+
|
|
112
117
|
## When to use this skill
|
|
113
118
|
|
|
114
119
|
- "Is Shield running?" → `openclaw shield status`
|
|
@@ -123,10 +128,13 @@ Real-time alerts (notifications or inline messages) are high priority: acknowled
|
|
|
123
128
|
|
|
124
129
|
After running `openclaw shield status`, check:
|
|
125
130
|
|
|
126
|
-
-
|
|
127
|
-
- **
|
|
128
|
-
-
|
|
131
|
+
- **✅ Running · Connected** → healthy, nothing to do
|
|
132
|
+
- **⚠️ Degraded · Connected** → capturing but sync issues; try `openclaw shield flush`
|
|
133
|
+
- **❌ Disconnected** → gateway may need a restart
|
|
134
|
+
- **Failures: N poll** → platform connectivity issue, usually self-recovers; try `openclaw shield flush`
|
|
135
|
+
- **Failures: N telemetry** → instance reporting failing, monitoring still active
|
|
129
136
|
- **Rising quarantine** → possible version mismatch, suggest checking for plugin updates
|
|
137
|
+
- **Last data: capture Xm ago** (stale) → agent may be idle, or capture pipeline issue
|
|
130
138
|
|
|
131
139
|
## RPCs
|
|
132
140
|
|