@upx-us/shield 0.4.36 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -561
- package/README.md +19 -24
- package/dist/index.js +153 -15
- package/dist/src/case-monitor.d.ts +46 -1
- package/dist/src/case-monitor.js +167 -19
- package/dist/src/cli-cases.js +46 -0
- package/dist/src/events/browser/enrich.js +1 -0
- package/dist/src/events/exec/enrich.d.ts +1 -0
- package/dist/src/events/exec/enrich.js +23 -0
- package/dist/src/events/file/enrich.js +7 -0
- package/dist/src/events/message/enrich.js +26 -0
- package/dist/src/exclusions.d.ts +16 -0
- package/dist/src/exclusions.js +122 -0
- package/dist/src/rpc/exclusion-handlers.d.ts +8 -0
- package/dist/src/rpc/exclusion-handlers.js +36 -0
- package/dist/src/rpc/handlers.d.ts +7 -7
- package/dist/src/rpc/handlers.js +138 -9
- package/dist/src/rpc/index.js +4 -0
- package/openclaw.plugin.json +3 -3
- package/package.json +2 -2
- package/skills/shield/README.md +10 -10
- package/skills/shield/SKILL.md +29 -8
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# OpenClaw Shield
|
|
2
2
|
|
|
3
3
|
> **OpenClaw Shield is a paid security service by UPX.**
|
|
4
|
-
> Start your **free 30-day trial** at [upx.com/
|
|
4
|
+
> Start your **free 30-day trial** at [upx.com/en/lp/openclaw-shield-upx](https://www.upx.com/en/lp/openclaw-shield-upx).
|
|
5
5
|
|
|
6
6
|
Real-time security monitoring for your OpenClaw agents — powered by the UPX Shield detection platform.
|
|
7
7
|
|
|
@@ -22,6 +22,7 @@ Your Agent → Shield (local: capture + redact) → UPX Platform (analysis, aler
|
|
|
22
22
|
| **Local event buffer** | Rolling log of recent events — inspect via `openclaw shield logs` without platform access |
|
|
23
23
|
| **Case notifications** | Automatic alerts when detection rules fire — agent notifies you of new cases |
|
|
24
24
|
| **Case resolution** | Close cases with categorization (resolution + root cause) directly from your agent |
|
|
25
|
+
| **False positive learning** | Mark cases as false positive — Shield remembers and auto-suppresses identical future alerts |
|
|
25
26
|
| **Host inventory** | Discovers all agents and workspaces on the machine — view via `openclaw shield vault show` |
|
|
26
27
|
| **Auto-update** | Patch and minor updates install automatically with rollback on failure |
|
|
27
28
|
| **Encrypted vault** | Redaction mappings stored locally with AES-256-GCM — UPX cannot reverse tokens |
|
|
@@ -54,6 +55,8 @@ Your Agent → Shield (local: capture + redact) → UPX Platform (analysis, aler
|
|
|
54
55
|
| `openclaw shield cases show <ID>` | Full case detail with events, rule info, and playbook |
|
|
55
56
|
| `openclaw shield cases resolve <ID>` | Resolve a case (--resolution, --root-cause, --comment) |
|
|
56
57
|
| `openclaw shield vault show` | Show host agent inventory (hashed IDs) |
|
|
58
|
+
| `openclaw shield exclusions` | List false positive exclusions |
|
|
59
|
+
| `openclaw shield exclusions remove <ID>` | Remove an exclusion to re-enable alerts |
|
|
57
60
|
|
|
58
61
|
**Agent RPCs** (used by the agent skill, not CLI):
|
|
59
62
|
|
|
@@ -68,10 +71,13 @@ Your Agent → Shield (local: capture + redact) → UPX Platform (analysis, aler
|
|
|
68
71
|
| `shield.case_detail` | Full case detail with events, rule, playbook |
|
|
69
72
|
| `shield.case_resolve` | Close a case with resolution and root cause |
|
|
70
73
|
| `shield.cases_ack` | Mark cases as notified |
|
|
74
|
+
| `shield.exclusions_list` | List false positive exclusions |
|
|
75
|
+
| `shield.exclusion_add` | Add an exclusion (rule + pattern hash) |
|
|
76
|
+
| `shield.exclusion_remove` | Remove an exclusion by ID |
|
|
71
77
|
|
|
72
78
|
---
|
|
73
79
|
|
|
74
|
-
### Option
|
|
80
|
+
### Option 1 — Quick Install (commands)
|
|
75
81
|
|
|
76
82
|
Run these commands in your terminal. No agent involvement needed.
|
|
77
83
|
|
|
@@ -95,36 +101,25 @@ That's it. Shield registers, saves credentials locally, and starts monitoring.
|
|
|
95
101
|
|
|
96
102
|
---
|
|
97
103
|
|
|
98
|
-
### Option
|
|
104
|
+
### Option 2 — Ask your agent
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
> I installed the OpenClaw Shield plugin (`openclaw plugins install @upx-us/shield`). Configure it: add "shield" to plugins.allow in openclaw.json, set installationKey to `<YOUR_KEY>` in plugins.entries.shield.config, restart the gateway with `openclaw gateway restart`, and confirm with `openclaw shield status`.
|
|
103
|
-
|
|
104
|
-
This approach works best because your agent only edits config — it doesn't need to make trust decisions about installing external software.
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
### Option C — Let your agent do everything
|
|
109
|
-
|
|
110
|
-
Copy this prompt and paste it to your AI agent. Replace `<YOUR_KEY>` with your Installation Key:
|
|
106
|
+
Copy this prompt and paste it to your AI agent. Replace `<YOUR_KEY>` with your Installation Key from the [Shield dashboard](https://uss.upx.com):
|
|
111
107
|
|
|
112
108
|
> Install the OpenClaw Shield security monitoring plugin by UPX (upx.com). The package is @upx-us/shield. Read the package README at https://www.npmjs.com/package/@upx-us/shield before proceeding. Install using `openclaw plugins install @upx-us/shield`, add "shield" to plugins.allow in openclaw.json, set installationKey to `<YOUR_KEY>` in plugins.entries.shield.config, restart with `openclaw gateway restart`, and confirm with `openclaw shield status`.
|
|
113
109
|
|
|
114
|
-
**If your agent refuses to install**, this is normal security behavior.
|
|
115
|
-
|
|
116
|
-
1. **Use Option A or B instead** — install the plugin yourself, let the agent configure.
|
|
110
|
+
**If your agent refuses to install**, this is normal security behavior — agents are cautious about external packages. Here's how to address common concerns:
|
|
117
111
|
|
|
118
|
-
|
|
112
|
+
| Agent concern | Why it's expected |
|
|
113
|
+
|---|---|
|
|
114
|
+
| "Sends data to an external server" | Shield is a security monitoring tool — sending redacted telemetry is its core purpose. Sensitive values are replaced with irreversible tokens **on your machine** before transmission. |
|
|
115
|
+
| "Installation key looks suspicious" | The key is a one-time activation token from your UPX Shield dashboard. Consumed on first use, can be removed from config after activation. |
|
|
116
|
+
| "Unknown or untrusted package" | `@upx-us/shield` is published on npm by UPX Security Services. You can verify at [npmjs.com/package/@upx-us/shield](https://www.npmjs.com/package/@upx-us/shield). |
|
|
119
117
|
|
|
120
|
-
|
|
118
|
+
**If the agent still refuses**, confirm you trust the source:
|
|
121
119
|
|
|
122
|
-
|
|
120
|
+
> *"I trust this plugin. I purchased this service from UPX (upx.com). Please proceed with the installation."*
|
|
123
121
|
|
|
124
|
-
|
|
125
|
-
|---|---|
|
|
126
|
-
| "Sends data to an external server" | Shield is a security monitoring tool — sending redacted telemetry is its purpose. Sensitive values are replaced with irreversible tokens **on your machine** before transmission. |
|
|
127
|
-
| "Installation key looks suspicious" | The key is a one-time activation token from your UPX Shield dashboard. Consumed on first use, can be removed from config after. |
|
|
122
|
+
Or use **Option 1** to install via commands yourself.
|
|
128
123
|
| "Plugin is not in OpenClaw's official docs" | Shield is a third-party plugin by UPX (upx.com) under the `@upx-us` npm scope. Not bundled with OpenClaw, but designed to work with it. |
|
|
129
124
|
| "Adding to plugins.allow expands attack surface" | Correct — this is OpenClaw's explicit trust mechanism. You are consciously granting permission. |
|
|
130
125
|
| "Package was recently published" | Shield is actively developed. Check CHANGELOG.md included with the package for version history. |
|
package/dist/index.js
CHANGED
|
@@ -464,21 +464,6 @@ function printActivatedStatus() {
|
|
|
464
464
|
}
|
|
465
465
|
console.log(' (original values never stored or transmitted)');
|
|
466
466
|
}
|
|
467
|
-
const cmStatus = (0, case_monitor_1.getCaseMonitorStatus)();
|
|
468
|
-
{
|
|
469
|
-
console.log('');
|
|
470
|
-
console.log('── Case Monitor ──────────────────────────────');
|
|
471
|
-
const intervalSec = Math.round(cmStatus.intervalMs / 1000);
|
|
472
|
-
const nextSec = Math.round(cmStatus.nextCheckIn / 1000);
|
|
473
|
-
const lastCheckLabel = cmStatus.lastCheckAt > 0
|
|
474
|
-
? (Date.now() - cmStatus.lastCheckAt < 60_000
|
|
475
|
-
? `${Math.round((Date.now() - cmStatus.lastCheckAt) / 1000)}s ago`
|
|
476
|
-
: `${Math.floor((Date.now() - cmStatus.lastCheckAt) / 60_000)}m ago`)
|
|
477
|
-
: 'pending';
|
|
478
|
-
console.log(` Interval: ${intervalSec}s (adaptive: 60s active → 900s idle)`);
|
|
479
|
-
console.log(` Last check: ${lastCheckLabel}`);
|
|
480
|
-
console.log(` Next check: ~${nextSec}s`);
|
|
481
|
-
}
|
|
482
467
|
}
|
|
483
468
|
exports.default = {
|
|
484
469
|
id: 'shield',
|
|
@@ -605,6 +590,62 @@ exports.default = {
|
|
|
605
590
|
state.lastSync = persistedStats.lastSync;
|
|
606
591
|
log.info('shield', `Starting monitoring bridge v${version_1.VERSION} (poll: ${config.pollIntervalMs}ms, dryRun: ${config.dryRun})`);
|
|
607
592
|
(0, case_monitor_1.initCaseMonitor)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'));
|
|
593
|
+
const runtime = api.runtime;
|
|
594
|
+
if (runtime?.system?.enqueueSystemEvent && runtime?.system?.requestHeartbeatNow) {
|
|
595
|
+
const config = api.config;
|
|
596
|
+
(0, case_monitor_1.setCaseNotificationDispatcher)({
|
|
597
|
+
enqueueSystemEvent: runtime.system.enqueueSystemEvent,
|
|
598
|
+
requestHeartbeatNow: runtime.system.requestHeartbeatNow,
|
|
599
|
+
agentId: config?.agents?.default ?? 'main',
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
log.warn('case-monitor', 'enqueueSystemEvent not available — system event notifications disabled (requires OpenClaw 2026.3+)');
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const channels = runtime?.channel;
|
|
607
|
+
const cfg = api.config;
|
|
608
|
+
const channelCandidates = [
|
|
609
|
+
{ name: 'telegram', sendFn: channels?.telegram?.sendMessageTelegram, configPath: 'telegram' },
|
|
610
|
+
{ name: 'discord', sendFn: channels?.discord?.sendMessageDiscord, configPath: 'discord' },
|
|
611
|
+
{ name: 'whatsapp', sendFn: channels?.whatsapp?.sendMessageWhatsApp, configPath: 'whatsapp' },
|
|
612
|
+
{ name: 'slack', sendFn: channels?.slack?.sendMessageSlack, configPath: 'slack' },
|
|
613
|
+
{ name: 'signal', sendFn: channels?.signal?.sendMessageSignal, configPath: 'signal' },
|
|
614
|
+
];
|
|
615
|
+
for (const candidate of channelCandidates) {
|
|
616
|
+
if (typeof candidate.sendFn !== 'function')
|
|
617
|
+
continue;
|
|
618
|
+
const channelCfg = cfg?.channels?.[candidate.configPath];
|
|
619
|
+
if (!channelCfg?.enabled && channelCfg?.enabled !== undefined)
|
|
620
|
+
continue;
|
|
621
|
+
let targetId = null;
|
|
622
|
+
if (candidate.name === 'telegram') {
|
|
623
|
+
const allowFrom = channelCfg?.allowFrom;
|
|
624
|
+
targetId = Array.isArray(allowFrom) && allowFrom.length > 0 ? String(allowFrom[0]) : null;
|
|
625
|
+
}
|
|
626
|
+
else if (candidate.name === 'discord') {
|
|
627
|
+
targetId = channelCfg?.defaultTo || (Array.isArray(channelCfg?.allowFrom) ? String(channelCfg.allowFrom[0]) : null);
|
|
628
|
+
}
|
|
629
|
+
else if (candidate.name === 'whatsapp') {
|
|
630
|
+
targetId = channelCfg?.defaultTo || (Array.isArray(channelCfg?.allowFrom) ? String(channelCfg.allowFrom[0]) : null);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
const allowFrom = channelCfg?.allowFrom;
|
|
634
|
+
targetId = Array.isArray(allowFrom) && allowFrom.length > 0 ? String(allowFrom[0]) : null;
|
|
635
|
+
}
|
|
636
|
+
if (targetId) {
|
|
637
|
+
(0, case_monitor_1.setDirectSendDispatcher)({
|
|
638
|
+
sendFn: candidate.sendFn,
|
|
639
|
+
to: targetId,
|
|
640
|
+
channel: candidate.name,
|
|
641
|
+
});
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
log.debug('case-monitor', `Direct send setup failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
648
|
+
}
|
|
608
649
|
const autoUpdateMode = pluginConfig.autoUpdate ?? true;
|
|
609
650
|
log.info('updater', `Startup update check (autoUpdate=${autoUpdateMode}, current=${version_1.VERSION})`);
|
|
610
651
|
const startupUpdate = (0, updater_1.performAutoUpdate)(autoUpdateMode, 0);
|
|
@@ -705,6 +746,10 @@ exports.default = {
|
|
|
705
746
|
state.lastPollAt = Date.now();
|
|
706
747
|
markStateDirty();
|
|
707
748
|
persistState();
|
|
749
|
+
const idlePlatformConfig = { apiUrl: config.credentials.apiUrl, instanceId: config.credentials.instanceId, hmacSecret: config.credentials.hmacSecret };
|
|
750
|
+
await (0, case_monitor_1.checkForNewCases)(idlePlatformConfig).catch(err => {
|
|
751
|
+
log.debug('case-monitor', `Check error: ${err instanceof Error ? err.message : String(err)}`);
|
|
752
|
+
});
|
|
708
753
|
return;
|
|
709
754
|
}
|
|
710
755
|
state.lastCaptureAt = Date.now();
|
|
@@ -866,9 +911,102 @@ exports.default = {
|
|
|
866
911
|
hmacSecret: rpcCreds?.hmacSecret || '',
|
|
867
912
|
};
|
|
868
913
|
(0, rpc_1.registerAllRpcs)(api, platformApiConfig);
|
|
914
|
+
api.registerCommand({
|
|
915
|
+
name: 'shieldcases',
|
|
916
|
+
description: 'Show pending Shield security cases',
|
|
917
|
+
requireAuth: true,
|
|
918
|
+
handler: () => {
|
|
919
|
+
const { getPendingCases } = require('./src/case-monitor');
|
|
920
|
+
const cases = getPendingCases();
|
|
921
|
+
if (cases.length === 0) {
|
|
922
|
+
return { text: '✅ No pending Shield security cases.' };
|
|
923
|
+
}
|
|
924
|
+
const severityEmoji = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
|
|
925
|
+
const lines = cases.map((c) => {
|
|
926
|
+
const emoji = severityEmoji[c.severity] || '⚠️';
|
|
927
|
+
const age = Math.floor((Date.now() - new Date(c.created_at).getTime()) / 60000);
|
|
928
|
+
const ageStr = age < 60 ? `${age}m` : `${Math.floor(age / 60)}h`;
|
|
929
|
+
return `${emoji} **${c.rule_title}** (${c.severity || 'unknown'}) — ${c.event_count} events — ${ageStr} ago`;
|
|
930
|
+
});
|
|
931
|
+
return {
|
|
932
|
+
text: [
|
|
933
|
+
`🛡️ **Shield — ${cases.length} Pending Case${cases.length > 1 ? 's' : ''}**`,
|
|
934
|
+
'',
|
|
935
|
+
...lines,
|
|
936
|
+
'',
|
|
937
|
+
'Use `openclaw shield cases show <id>` for details.',
|
|
938
|
+
].join('\n'),
|
|
939
|
+
};
|
|
940
|
+
},
|
|
941
|
+
});
|
|
869
942
|
api.registerCli(({ program }) => {
|
|
870
943
|
const shield = program.command('shield');
|
|
871
944
|
(0, cli_cases_1.registerCasesCli)(shield);
|
|
945
|
+
shield.command('monitor')
|
|
946
|
+
.description('Manage case notification monitoring via cron')
|
|
947
|
+
.option('--on', 'Enable case monitoring (creates cron job)')
|
|
948
|
+
.option('--off', 'Disable case monitoring (removes cron job)')
|
|
949
|
+
.option('--interval <minutes>', 'Check interval in minutes (default: 5)', '5')
|
|
950
|
+
.action(async (opts) => {
|
|
951
|
+
const { execSync } = require('child_process');
|
|
952
|
+
const CRON_NAME = 'shield-case-monitor';
|
|
953
|
+
let cronJob = null;
|
|
954
|
+
try {
|
|
955
|
+
const list = execSync('openclaw cron list --json 2>/dev/null', { encoding: 'utf8' });
|
|
956
|
+
const parsed = JSON.parse(list);
|
|
957
|
+
const jobs = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.jobs) ? parsed.jobs : []);
|
|
958
|
+
cronJob = jobs.find((j) => j.name === CRON_NAME) || null;
|
|
959
|
+
}
|
|
960
|
+
catch { }
|
|
961
|
+
if (opts.off) {
|
|
962
|
+
if (!cronJob) {
|
|
963
|
+
console.log('ℹ️ Shield case monitor is not active.');
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
execSync(`openclaw cron remove ${cronJob.id}`, { encoding: 'utf8', stdio: 'pipe' });
|
|
968
|
+
console.log('✅ Shield case monitor disabled.');
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
console.error(`❌ Failed to remove cron job ${cronJob.id}:`, err.message);
|
|
972
|
+
}
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (opts.on) {
|
|
976
|
+
const interval = parseInt(opts.interval || '5', 10);
|
|
977
|
+
if (isNaN(interval) || interval < 1) {
|
|
978
|
+
console.error('❌ Invalid interval. Must be at least 1 minute.');
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (cronJob) {
|
|
982
|
+
try {
|
|
983
|
+
execSync(`openclaw cron remove ${cronJob.id}`, { encoding: 'utf8', stdio: 'pipe' });
|
|
984
|
+
}
|
|
985
|
+
catch { }
|
|
986
|
+
}
|
|
987
|
+
const prompt = 'Check for new Shield security cases by running: openclaw shield cases list --status open. Report ONLY cases created in the last 10 minutes. If none are new, reply HEARTBEAT_OK.';
|
|
988
|
+
try {
|
|
989
|
+
execSync(`openclaw cron add --name "${CRON_NAME}" --every ${interval}m --message "${prompt.replace(/"/g, '\\"')}"`, { encoding: 'utf8', stdio: 'pipe' });
|
|
990
|
+
console.log(`✅ Shield case monitor enabled (every ${interval}m).`);
|
|
991
|
+
console.log(` Cron job: ${CRON_NAME}`);
|
|
992
|
+
console.log(' Disable with: openclaw shield monitor --off');
|
|
993
|
+
}
|
|
994
|
+
catch (err) {
|
|
995
|
+
console.error('❌ Failed to create cron job:', err.message);
|
|
996
|
+
}
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (cronJob) {
|
|
1000
|
+
console.log('🛡️ Shield case monitor: ✅ Active');
|
|
1001
|
+
console.log(` Cron job: ${CRON_NAME} (${cronJob.id})`);
|
|
1002
|
+
console.log(' Disable with: openclaw shield monitor --off');
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
console.log('🛡️ Shield case monitor: ❌ Inactive');
|
|
1006
|
+
console.log(' Enable with: openclaw shield monitor --on');
|
|
1007
|
+
console.log(' Options: --interval <minutes> (default: 5)');
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
872
1010
|
shield.command('status')
|
|
873
1011
|
.description('Show Shield monitoring status and activity')
|
|
874
1012
|
.action(async () => {
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { type PlatformApiConfig } from './rpc/client';
|
|
2
|
+
type EnqueueSystemEventFn = (text: string, options: {
|
|
3
|
+
sessionKey: string;
|
|
4
|
+
contextKey?: string | null;
|
|
5
|
+
}) => boolean;
|
|
6
|
+
type RequestHeartbeatNowFn = (opts?: {
|
|
7
|
+
reason?: string;
|
|
8
|
+
agentId?: string;
|
|
9
|
+
sessionKey?: string;
|
|
10
|
+
}) => void;
|
|
11
|
+
export declare function setCaseNotificationDispatcher(opts: {
|
|
12
|
+
enqueueSystemEvent: EnqueueSystemEventFn;
|
|
13
|
+
requestHeartbeatNow: RequestHeartbeatNowFn;
|
|
14
|
+
agentId?: string;
|
|
15
|
+
}): void;
|
|
16
|
+
type DirectSendFn = (to: string, text: string, opts?: Record<string, unknown>) => Promise<unknown>;
|
|
17
|
+
export declare function setDirectSendDispatcher(opts: {
|
|
18
|
+
sendFn: DirectSendFn;
|
|
19
|
+
to: string;
|
|
20
|
+
channel: string;
|
|
21
|
+
}): void;
|
|
2
22
|
export interface CaseSummary {
|
|
3
23
|
id: string;
|
|
4
24
|
rule_id: string;
|
|
@@ -7,9 +27,33 @@ export interface CaseSummary {
|
|
|
7
27
|
status: string;
|
|
8
28
|
created_at: string;
|
|
9
29
|
summary: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
mitre_technique?: string;
|
|
10
32
|
event_count: number;
|
|
11
33
|
agent_id?: string;
|
|
12
34
|
}
|
|
35
|
+
export interface CaseDetail extends CaseSummary {
|
|
36
|
+
attribution?: string;
|
|
37
|
+
events?: Array<{
|
|
38
|
+
timestamp: string;
|
|
39
|
+
event_type: string;
|
|
40
|
+
tool_name: string;
|
|
41
|
+
summary: string;
|
|
42
|
+
session_id?: string | null;
|
|
43
|
+
}>;
|
|
44
|
+
rule?: {
|
|
45
|
+
description?: string;
|
|
46
|
+
mitre_tactic?: string;
|
|
47
|
+
mitre_technique?: string;
|
|
48
|
+
};
|
|
49
|
+
playbook?: {
|
|
50
|
+
name?: string;
|
|
51
|
+
summary?: string;
|
|
52
|
+
steps?: string | null;
|
|
53
|
+
};
|
|
54
|
+
resolution?: string | null;
|
|
55
|
+
url?: string;
|
|
56
|
+
}
|
|
13
57
|
export declare function initCaseMonitor(dataDir: string): void;
|
|
14
58
|
export declare function notifyCaseMonitorActivity(): void;
|
|
15
59
|
export declare function getCaseMonitorStatus(): {
|
|
@@ -20,5 +64,6 @@ export declare function getCaseMonitorStatus(): {
|
|
|
20
64
|
export declare function checkForNewCases(config: PlatformApiConfig): Promise<void>;
|
|
21
65
|
export declare function getPendingCases(): CaseSummary[];
|
|
22
66
|
export declare function acknowledgeCases(caseIds: string[]): void;
|
|
23
|
-
export declare function formatCaseNotification(c: CaseSummary): string;
|
|
67
|
+
export declare function formatCaseNotification(c: CaseSummary | CaseDetail): string;
|
|
24
68
|
export declare function _resetForTesting(): void;
|
|
69
|
+
export {};
|
package/dist/src/case-monitor.js
CHANGED
|
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.setCaseNotificationDispatcher = setCaseNotificationDispatcher;
|
|
37
|
+
exports.setDirectSendDispatcher = setDirectSendDispatcher;
|
|
36
38
|
exports.initCaseMonitor = initCaseMonitor;
|
|
37
39
|
exports.notifyCaseMonitorActivity = notifyCaseMonitorActivity;
|
|
38
40
|
exports.getCaseMonitorStatus = getCaseMonitorStatus;
|
|
@@ -43,8 +45,113 @@ exports.formatCaseNotification = formatCaseNotification;
|
|
|
43
45
|
exports._resetForTesting = _resetForTesting;
|
|
44
46
|
const safe_io_1 = require("./safe-io");
|
|
45
47
|
const client_1 = require("./rpc/client");
|
|
48
|
+
const exclusions_1 = require("./exclusions");
|
|
46
49
|
const log = __importStar(require("./log"));
|
|
47
50
|
const path_1 = require("path");
|
|
51
|
+
let _enqueueSystemEvent = null;
|
|
52
|
+
let _requestHeartbeatNow = null;
|
|
53
|
+
let _agentId = 'main';
|
|
54
|
+
function setCaseNotificationDispatcher(opts) {
|
|
55
|
+
_enqueueSystemEvent = opts.enqueueSystemEvent;
|
|
56
|
+
_requestHeartbeatNow = opts.requestHeartbeatNow;
|
|
57
|
+
if (opts.agentId)
|
|
58
|
+
_agentId = opts.agentId;
|
|
59
|
+
log.info('case-monitor', 'Notification dispatcher configured');
|
|
60
|
+
}
|
|
61
|
+
function getMainSessionKey() {
|
|
62
|
+
return `agent:${_agentId}:main`;
|
|
63
|
+
}
|
|
64
|
+
async function enrichCases(config, cases) {
|
|
65
|
+
if (cases.length > 3)
|
|
66
|
+
return cases;
|
|
67
|
+
const enriched = [];
|
|
68
|
+
for (const cs of cases) {
|
|
69
|
+
try {
|
|
70
|
+
const result = await (0, client_1.callPlatformApi)(config, `/v1/agent/cases/${cs.id}`);
|
|
71
|
+
if (result.ok && result.data) {
|
|
72
|
+
enriched.push(result.data);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
enriched.push(cs);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
enriched.push(cs);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return enriched;
|
|
83
|
+
}
|
|
84
|
+
function isCaseDetail(c) {
|
|
85
|
+
return 'rule' in c || 'playbook' in c || 'events' in c;
|
|
86
|
+
}
|
|
87
|
+
let _directSendFn = null;
|
|
88
|
+
let _directSendTo = null;
|
|
89
|
+
let _directSendChannel = null;
|
|
90
|
+
function setDirectSendDispatcher(opts) {
|
|
91
|
+
_directSendFn = opts.sendFn;
|
|
92
|
+
_directSendTo = opts.to;
|
|
93
|
+
_directSendChannel = opts.channel;
|
|
94
|
+
log.info('case-monitor', `Direct send configured (channel: ${opts.channel}, to: ${opts.to})`);
|
|
95
|
+
}
|
|
96
|
+
function dispatchCaseNotifications(cases) {
|
|
97
|
+
if (cases.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
let text;
|
|
100
|
+
if (cases.length === 1) {
|
|
101
|
+
text = formatCaseNotification(cases[0]);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const lines = cases.map(c => {
|
|
105
|
+
const emoji = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
|
|
106
|
+
const sev = c.severity ? ` (${c.severity})` : '';
|
|
107
|
+
return `${emoji[c.severity] || '⚠️'} **${c.rule_title}**${sev} — ${c.event_count} events`;
|
|
108
|
+
});
|
|
109
|
+
text = [
|
|
110
|
+
`⚠️ **Shield Alert — ${cases.length} New Cases**`,
|
|
111
|
+
'',
|
|
112
|
+
...lines,
|
|
113
|
+
'',
|
|
114
|
+
'',
|
|
115
|
+
'💡 **Next steps:**',
|
|
116
|
+
...cases.map(c => ` • _"Investigate case ${c.id}"_`),
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
|
119
|
+
if (_directSendFn && _directSendTo) {
|
|
120
|
+
try {
|
|
121
|
+
const sendOpts = { textMode: 'markdown' };
|
|
122
|
+
_directSendFn(_directSendTo, text, sendOpts)
|
|
123
|
+
.then(() => {
|
|
124
|
+
log.info('case-monitor', `Direct notification sent for ${cases.length} case(s) via ${_directSendChannel} to ${_directSendTo}`);
|
|
125
|
+
acknowledgeCases(cases.map(c => c.id));
|
|
126
|
+
})
|
|
127
|
+
.catch((err) => {
|
|
128
|
+
log.warn('case-monitor', `Direct send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
|
+
dispatchViaSystemEvent(cases, text);
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
log.warn('case-monitor', `Direct send error: ${err instanceof Error ? err.message : String(err)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
dispatchViaSystemEvent(cases, text);
|
|
138
|
+
}
|
|
139
|
+
function dispatchViaSystemEvent(cases, text) {
|
|
140
|
+
if (!_enqueueSystemEvent || !_requestHeartbeatNow) {
|
|
141
|
+
log.debug('case-monitor', 'No notification dispatcher available — skipping');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const sessionKey = getMainSessionKey();
|
|
145
|
+
const contextKey = `shield:cases:${cases.map(c => c.id).sort().join(',')}`;
|
|
146
|
+
const enqueued = _enqueueSystemEvent(text, { sessionKey, contextKey });
|
|
147
|
+
if (enqueued) {
|
|
148
|
+
_requestHeartbeatNow({ reason: 'shield:new-cases' });
|
|
149
|
+
log.info('case-monitor', `System event dispatched for ${cases.length} case(s) to session ${sessionKey}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
log.debug('case-monitor', 'System event not enqueued (duplicate or no session)');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
48
155
|
const MIN_CHECK_INTERVAL_MS = 60_000;
|
|
49
156
|
const MAX_CHECK_INTERVAL_MS = 900_000;
|
|
50
157
|
const RECENT_EVENT_THRESHOLD_MS = 300_000;
|
|
@@ -98,11 +205,8 @@ async function checkForNewCases(config) {
|
|
|
98
205
|
try {
|
|
99
206
|
const params = {
|
|
100
207
|
status: 'open',
|
|
101
|
-
limit:
|
|
208
|
+
limit: 50,
|
|
102
209
|
};
|
|
103
|
-
if (state.lastCheckAt) {
|
|
104
|
-
params.since = state.lastCheckAt;
|
|
105
|
-
}
|
|
106
210
|
const result = await (0, client_1.callPlatformApi)(config, '/v1/agent/cases', params, 'GET');
|
|
107
211
|
if (!result.ok) {
|
|
108
212
|
if (!result.error?.includes('not configured')) {
|
|
@@ -113,10 +217,23 @@ async function checkForNewCases(config) {
|
|
|
113
217
|
const cases = result.data?.cases ?? [];
|
|
114
218
|
const ackSet = new Set(state.acknowledgedIds);
|
|
115
219
|
const pendingSet = new Set(state.pendingCases.map(c => c.id));
|
|
116
|
-
const
|
|
220
|
+
const newCasesRaw = cases.filter(c => !ackSet.has(c.id) && !pendingSet.has(c.id));
|
|
221
|
+
const newCases = [];
|
|
222
|
+
for (const c of newCasesRaw) {
|
|
223
|
+
const hash = (0, exclusions_1.computePatternHash)(c.rule_id, c.summary || '');
|
|
224
|
+
if ((0, exclusions_1.isExcluded)(c.rule_id, hash, c.agent_id)) {
|
|
225
|
+
log.info('case-monitor', `Case ${c.id} matched FP exclusion (rule=${c.rule_id}) — auto-acknowledging`);
|
|
226
|
+
state.acknowledgedIds = [...state.acknowledgedIds, c.id].slice(-MAX_ACKNOWLEDGED_IDS);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
newCases.push(c);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
117
232
|
if (newCases.length > 0) {
|
|
118
233
|
state.pendingCases = [...state.pendingCases, ...newCases];
|
|
119
234
|
log.info('case-monitor', `${newCases.length} new case(s) pending notification`);
|
|
235
|
+
const enriched = await enrichCases(config, newCases);
|
|
236
|
+
dispatchCaseNotifications(enriched);
|
|
120
237
|
}
|
|
121
238
|
state.lastCheckAt = new Date().toISOString();
|
|
122
239
|
(0, safe_io_1.writeJsonSafe)(stateFile, state);
|
|
@@ -151,28 +268,59 @@ function acknowledgeCases(caseIds) {
|
|
|
151
268
|
(0, safe_io_1.writeJsonSafe)(stateFile, state);
|
|
152
269
|
log.info('case-monitor', `Acknowledged ${caseIds.length} case(s), ${state.pendingCases.length} still pending`);
|
|
153
270
|
}
|
|
271
|
+
const MITRE_TACTICS = {
|
|
272
|
+
TA0001: 'Initial Access', TA0002: 'Execution', TA0003: 'Persistence',
|
|
273
|
+
TA0004: 'Privilege Escalation', TA0005: 'Defense Evasion', TA0006: 'Credential Access',
|
|
274
|
+
TA0007: 'Discovery', TA0008: 'Lateral Movement', TA0009: 'Collection',
|
|
275
|
+
TA0010: 'Exfiltration', TA0011: 'Command and Control', TA0040: 'Impact',
|
|
276
|
+
TA0042: 'Resource Development', TA0043: 'Reconnaissance',
|
|
277
|
+
};
|
|
278
|
+
const MITRE_TECHNIQUES = {
|
|
279
|
+
T1485: 'Data Destruction', T1489: 'Service Stop', T1059: 'Command and Scripting Interpreter',
|
|
280
|
+
T1070: 'Indicator Removal', T1105: 'Ingress Tool Transfer', T1136: 'Create Account',
|
|
281
|
+
T1548: 'Abuse Elevation Control', T1562: 'Impair Defenses', T1053: 'Scheduled Task/Job',
|
|
282
|
+
T1027: 'Obfuscated Files', T1082: 'System Information Discovery', T1518: 'Software Discovery',
|
|
283
|
+
};
|
|
284
|
+
function mitreLabel(code, map) {
|
|
285
|
+
if (!code)
|
|
286
|
+
return '';
|
|
287
|
+
const name = map[code];
|
|
288
|
+
return name ? `${code} ${name}` : code;
|
|
289
|
+
}
|
|
154
290
|
function formatCaseNotification(c) {
|
|
155
291
|
const severityEmoji = {
|
|
156
|
-
CRITICAL: '🔴',
|
|
157
|
-
HIGH: '🟠',
|
|
158
|
-
MEDIUM: '🟡',
|
|
159
|
-
LOW: '🔵',
|
|
160
|
-
INFO: 'ℹ️',
|
|
292
|
+
CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵', INFO: 'ℹ️',
|
|
161
293
|
};
|
|
162
294
|
const emoji = severityEmoji[c.severity] || '⚠️';
|
|
295
|
+
const caseId = c.id;
|
|
296
|
+
const detail = isCaseDetail(c) ? c : null;
|
|
163
297
|
const ago = getTimeAgo(c.created_at);
|
|
164
|
-
|
|
298
|
+
const lines = [
|
|
165
299
|
`${emoji} **Shield Alert — New Case**`,
|
|
166
300
|
``,
|
|
167
301
|
`**Rule:** ${c.rule_title}`,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
`**
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
302
|
+
];
|
|
303
|
+
if (c.severity) {
|
|
304
|
+
lines.push(`**Priority:** ${c.severity}`);
|
|
305
|
+
}
|
|
306
|
+
lines.push(`**Time:** ${ago}`, `**Events:** ${c.event_count}`, `**Case:** \`${caseId}\``);
|
|
307
|
+
if (detail?.rule?.description) {
|
|
308
|
+
lines.push(``, `> ${detail.rule.description}`);
|
|
309
|
+
}
|
|
310
|
+
if (detail?.rule?.mitre_tactic || detail?.rule?.mitre_technique) {
|
|
311
|
+
const tactic = mitreLabel(detail.rule.mitre_tactic, MITRE_TACTICS);
|
|
312
|
+
const technique = mitreLabel(detail.rule.mitre_technique, MITRE_TECHNIQUES);
|
|
313
|
+
const mitre = [tactic, technique].filter(Boolean).join(' · ');
|
|
314
|
+
lines.push(`> 🎯 MITRE: ${mitre}`);
|
|
315
|
+
}
|
|
316
|
+
if (detail?.playbook?.summary) {
|
|
317
|
+
lines.push(``, `⚠️ ${detail.playbook.summary}`);
|
|
318
|
+
}
|
|
319
|
+
lines.push(``, ``, `💡 **What's next:**`, ` • _"Investigate case ${caseId}"_`, ` • _"Close case ${caseId} as false positive"_`, ` • _"Resolve case ${caseId} — it was authorized maintenance"_`);
|
|
320
|
+
if (detail?.url) {
|
|
321
|
+
lines.push(``, `🔗 ${detail.url}`);
|
|
322
|
+
}
|
|
323
|
+
return lines.join('\n');
|
|
176
324
|
}
|
|
177
325
|
function getTimeAgo(isoDate) {
|
|
178
326
|
const diffMs = Date.now() - new Date(isoDate).getTime();
|
package/dist/src/cli-cases.js
CHANGED
|
@@ -35,6 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.registerCasesCli = registerCasesCli;
|
|
37
37
|
const config_1 = require("./config");
|
|
38
|
+
const exclusions_1 = require("./exclusions");
|
|
39
|
+
const path_1 = require("path");
|
|
40
|
+
const os_1 = require("os");
|
|
38
41
|
function hasValidCreds(creds) {
|
|
39
42
|
return !!(creds.apiUrl && creds.instanceId && creds.hmacSecret);
|
|
40
43
|
}
|
|
@@ -60,6 +63,16 @@ function registerCasesCli(shieldCommand) {
|
|
|
60
63
|
.argument('<id>', 'Case ID')
|
|
61
64
|
.option('--format <fmt>', 'Output format: table or json', 'table')
|
|
62
65
|
.action(showCase);
|
|
66
|
+
const exclusions = shieldCommand.command('exclusions')
|
|
67
|
+
.description('List and manage FP exclusions');
|
|
68
|
+
exclusions.action(() => {
|
|
69
|
+
listExclusionsCli();
|
|
70
|
+
});
|
|
71
|
+
exclusions
|
|
72
|
+
.command('remove')
|
|
73
|
+
.description('Remove an exclusion by ID')
|
|
74
|
+
.argument('<id>', 'Exclusion ID')
|
|
75
|
+
.action(removeExclusionCli);
|
|
63
76
|
cases
|
|
64
77
|
.command('resolve')
|
|
65
78
|
.description('Resolve/close a security case')
|
|
@@ -182,3 +195,36 @@ async function resolveCase(id, opts) {
|
|
|
182
195
|
console.log('Response:', JSON.stringify(result, null, 2));
|
|
183
196
|
}
|
|
184
197
|
}
|
|
198
|
+
function ensureExclusionsInit() {
|
|
199
|
+
const dataDir = (0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data');
|
|
200
|
+
(0, exclusions_1.initExclusions)(dataDir);
|
|
201
|
+
}
|
|
202
|
+
function listExclusionsCli() {
|
|
203
|
+
ensureExclusionsInit();
|
|
204
|
+
const exclusions = (0, exclusions_1.listExclusions)();
|
|
205
|
+
if (exclusions.length === 0) {
|
|
206
|
+
console.log('No exclusions configured. ✅');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
console.log(`Exclusions (${exclusions.length}):\n`);
|
|
210
|
+
for (const e of exclusions) {
|
|
211
|
+
const time = new Date(e.created_at).toLocaleString();
|
|
212
|
+
console.log(` ${e.id}`);
|
|
213
|
+
console.log(` Rule: ${e.rule_id} Hash: ${e.pattern_hash.slice(0, 12)}…`);
|
|
214
|
+
if (e.agent_id)
|
|
215
|
+
console.log(` Agent: ${e.agent_id}`);
|
|
216
|
+
if (e.reason)
|
|
217
|
+
console.log(` Reason: ${e.reason}`);
|
|
218
|
+
console.log(` Created: ${time}\n`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function removeExclusionCli(id) {
|
|
222
|
+
ensureExclusionsInit();
|
|
223
|
+
const removed = (0, exclusions_1.removeExclusion)(id);
|
|
224
|
+
if (removed) {
|
|
225
|
+
console.log(`✅ Exclusion ${id} removed.`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
console.error(`❌ Exclusion ${id} not found.`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RawToolCall, EnrichmentContext } from '../base';
|
|
2
2
|
import { ExecEvent } from './event';
|
|
3
|
+
export declare function detectSecretInCommand(cmd: string): boolean;
|
|
3
4
|
export declare function splitChainedCommands(cmd: string): string[];
|
|
4
5
|
export declare function enrich(tool: RawToolCall, ctx: EnrichmentContext): ExecEvent;
|