@clawdstrike/openclaw 0.1.1 → 0.1.2
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 +3 -1
- package/clawdstrike-security.js +1 -0
- package/dist/audit/adapter-logger.d.ts +24 -0
- package/dist/audit/adapter-logger.d.ts.map +1 -0
- package/dist/audit/adapter-logger.js +42 -0
- package/dist/audit/adapter-logger.js.map +1 -0
- package/dist/classification.d.ts +41 -0
- package/dist/classification.d.ts.map +1 -0
- package/dist/classification.js +102 -0
- package/dist/classification.js.map +1 -0
- package/dist/cli/commands/policy.js +1 -1
- package/dist/cli/commands/policy.js.map +1 -1
- package/dist/e2e/openclaw-e2e.js +3 -3
- package/dist/e2e/openclaw-e2e.js.map +1 -1
- package/dist/engine-holder.d.ts +28 -0
- package/dist/engine-holder.d.ts.map +1 -0
- package/dist/engine-holder.js +38 -0
- package/dist/engine-holder.js.map +1 -0
- package/dist/guards/egress.d.ts.map +1 -1
- package/dist/guards/egress.js +20 -1
- package/dist/guards/egress.js.map +1 -1
- package/dist/guards/forbidden-path.d.ts.map +1 -1
- package/dist/guards/forbidden-path.js +6 -0
- package/dist/guards/forbidden-path.js.map +1 -1
- package/dist/guards/secret-leak.d.ts.map +1 -1
- package/dist/guards/secret-leak.js +21 -0
- package/dist/guards/secret-leak.js.map +1 -1
- package/dist/hooks/agent-bootstrap/handler.d.ts +4 -0
- package/dist/hooks/agent-bootstrap/handler.d.ts.map +1 -1
- package/dist/hooks/agent-bootstrap/handler.js +7 -7
- package/dist/hooks/agent-bootstrap/handler.js.map +1 -1
- package/dist/hooks/approval-state.d.ts +31 -0
- package/dist/hooks/approval-state.d.ts.map +1 -0
- package/dist/hooks/approval-state.js +189 -0
- package/dist/hooks/approval-state.js.map +1 -0
- package/dist/hooks/approval-utils.d.ts +5 -0
- package/dist/hooks/approval-utils.d.ts.map +1 -0
- package/dist/hooks/approval-utils.js +77 -0
- package/dist/hooks/approval-utils.js.map +1 -0
- package/dist/hooks/audit-logger/handler.d.ts +4 -0
- package/dist/hooks/audit-logger/handler.d.ts.map +1 -1
- package/dist/hooks/audit-logger/handler.js +4 -0
- package/dist/hooks/audit-logger/handler.js.map +1 -1
- package/dist/hooks/cua-bridge/handler.d.ts +57 -0
- package/dist/hooks/cua-bridge/handler.d.ts.map +1 -0
- package/dist/hooks/cua-bridge/handler.js +369 -0
- package/dist/hooks/cua-bridge/handler.js.map +1 -0
- package/dist/hooks/tool-guard/handler.d.ts +17 -2
- package/dist/hooks/tool-guard/handler.d.ts.map +1 -1
- package/dist/hooks/tool-guard/handler.js +200 -75
- package/dist/hooks/tool-guard/handler.js.map +1 -1
- package/dist/hooks/tool-preflight/handler.d.ts +34 -0
- package/dist/hooks/tool-preflight/handler.d.ts.map +1 -0
- package/dist/hooks/tool-preflight/handler.js +426 -0
- package/dist/hooks/tool-preflight/handler.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/openclaw-adapter.d.ts +48 -0
- package/dist/openclaw-adapter.d.ts.map +1 -0
- package/dist/openclaw-adapter.js +81 -0
- package/dist/openclaw-adapter.js.map +1 -0
- package/dist/plugin.d.ts +40 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +125 -32
- package/dist/plugin.js.map +1 -1
- package/dist/policy/engine.d.ts +5 -0
- package/dist/policy/engine.d.ts.map +1 -1
- package/dist/policy/engine.js +580 -84
- package/dist/policy/engine.js.map +1 -1
- package/dist/policy/loader.js +57 -0
- package/dist/policy/loader.js.map +1 -1
- package/dist/policy/validator.d.ts.map +1 -1
- package/dist/policy/validator.js +97 -3
- package/dist/policy/validator.js.map +1 -1
- package/dist/receipt/signer.d.ts +42 -0
- package/dist/receipt/signer.d.ts.map +1 -0
- package/dist/receipt/signer.js +134 -0
- package/dist/receipt/signer.js.map +1 -0
- package/dist/receipt/types.d.ts +50 -0
- package/dist/receipt/types.d.ts.map +1 -0
- package/dist/receipt/types.js +9 -0
- package/dist/receipt/types.js.map +1 -0
- package/dist/security-prompt.js +1 -1
- package/dist/tools/policy-check.d.ts +2 -2
- package/dist/tools/policy-check.d.ts.map +1 -1
- package/dist/tools/policy-check.js +4 -7
- package/dist/tools/policy-check.js.map +1 -1
- package/dist/translator/openclaw-translator.d.ts +31 -0
- package/dist/translator/openclaw-translator.d.ts.map +1 -0
- package/dist/translator/openclaw-translator.js +314 -0
- package/dist/translator/openclaw-translator.js.map +1 -0
- package/dist/types.d.ts +86 -170
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/package.json +5 -3
- package/rulesets/ai-agent-minimal.yaml +25 -0
- package/rulesets/ai-agent.yaml +25 -0
package/dist/policy/engine.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { parseNetworkTarget } from '@clawdstrike/adapter-core';
|
|
3
4
|
import { createPolicyEngineFromPolicy } from '@clawdstrike/policy';
|
|
4
5
|
import { mergeConfig } from '../config.js';
|
|
5
6
|
import { EgressGuard, ForbiddenPathGuard, PatchIntegrityGuard, SecretLeakGuard } from '../guards/index.js';
|
|
@@ -12,6 +13,176 @@ function expandHome(p) {
|
|
|
12
13
|
function normalizePathForPrefix(p) {
|
|
13
14
|
return path.resolve(expandHome(p));
|
|
14
15
|
}
|
|
16
|
+
function cleanPathToken(t) {
|
|
17
|
+
return t.trim().replace(/^[("'`]+/, '').replace(/[)"'`;,\]}]+$/, '');
|
|
18
|
+
}
|
|
19
|
+
function isRedirectionOp(t) {
|
|
20
|
+
return t === '>' || t === '>>' || t === '1>' || t === '1>>' || t === '2>' || t === '2>>' || t === '<' || t === '<<';
|
|
21
|
+
}
|
|
22
|
+
function splitInlineRedirection(t) {
|
|
23
|
+
// Support forms like ">/path", "2>>/path", "<input".
|
|
24
|
+
const m = t.match(/^(?:\d)?(?:>>|>)\s*(.+)$/);
|
|
25
|
+
if (m?.[1])
|
|
26
|
+
return m[1];
|
|
27
|
+
const mi = t.match(/^(?:<<|<)\s*(.+)$/);
|
|
28
|
+
if (mi?.[1])
|
|
29
|
+
return mi[1];
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function looksLikePathToken(t) {
|
|
33
|
+
if (!t)
|
|
34
|
+
return false;
|
|
35
|
+
if (t.includes('://'))
|
|
36
|
+
return false;
|
|
37
|
+
if (t.startsWith('/') || t.startsWith('~') || t.startsWith('./') || t.startsWith('../'))
|
|
38
|
+
return true;
|
|
39
|
+
if (t === '.env' || t.startsWith('.env.'))
|
|
40
|
+
return true;
|
|
41
|
+
if (t.includes('/.ssh/') || t.includes('/.aws/') || t.includes('/.gnupg/') || t.includes('/.kube/'))
|
|
42
|
+
return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const WRITE_PATH_FLAG_NAMES = new Set([
|
|
46
|
+
// Common output flags
|
|
47
|
+
'o',
|
|
48
|
+
'out',
|
|
49
|
+
'output',
|
|
50
|
+
'outfile',
|
|
51
|
+
'output-file',
|
|
52
|
+
// Common log file flags
|
|
53
|
+
'log-file',
|
|
54
|
+
'logfile',
|
|
55
|
+
'log-path',
|
|
56
|
+
'logpath',
|
|
57
|
+
]);
|
|
58
|
+
function isWritePathFlagToken(t) {
|
|
59
|
+
if (!t)
|
|
60
|
+
return false;
|
|
61
|
+
if (!t.startsWith('-'))
|
|
62
|
+
return false;
|
|
63
|
+
const normalized = t.replace(/^-+/, '').toLowerCase().replace(/_/g, '-');
|
|
64
|
+
return WRITE_PATH_FLAG_NAMES.has(normalized);
|
|
65
|
+
}
|
|
66
|
+
function extractCommandPathCandidates(command, args) {
|
|
67
|
+
const tokens = [command, ...args].map((t) => String(t ?? '')).filter(Boolean);
|
|
68
|
+
const reads = [];
|
|
69
|
+
const writes = [];
|
|
70
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
71
|
+
const t = tokens[i];
|
|
72
|
+
// Redirection operators: treat as write/read targets.
|
|
73
|
+
if (isRedirectionOp(t)) {
|
|
74
|
+
const next = tokens[i + 1];
|
|
75
|
+
if (typeof next === 'string' && next.length > 0) {
|
|
76
|
+
const cleaned = cleanPathToken(next);
|
|
77
|
+
if (cleaned) {
|
|
78
|
+
if (t.startsWith('>') || t === '>' || t === '>>' || t === '1>' || t === '1>>' || t === '2>' || t === '2>>') {
|
|
79
|
+
writes.push(cleaned);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
reads.push(cleaned);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const inline = splitInlineRedirection(t);
|
|
89
|
+
if (inline) {
|
|
90
|
+
const cleaned = cleanPathToken(inline);
|
|
91
|
+
if (cleaned) {
|
|
92
|
+
if (t.includes('>'))
|
|
93
|
+
writes.push(cleaned);
|
|
94
|
+
else
|
|
95
|
+
reads.push(cleaned);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Flags like --output /path or -o /path (write targets)
|
|
100
|
+
if (isWritePathFlagToken(t)) {
|
|
101
|
+
const next = tokens[i + 1];
|
|
102
|
+
if (typeof next === 'string' && next.length > 0) {
|
|
103
|
+
const cleaned = cleanPathToken(next);
|
|
104
|
+
if (looksLikePathToken(cleaned)) {
|
|
105
|
+
writes.push(cleaned);
|
|
106
|
+
i += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Flags like --output=/path
|
|
112
|
+
const eq = t.indexOf('=');
|
|
113
|
+
if (eq > 0) {
|
|
114
|
+
const lhs = t.slice(0, eq);
|
|
115
|
+
const rhs = cleanPathToken(t.slice(eq + 1));
|
|
116
|
+
if (looksLikePathToken(rhs)) {
|
|
117
|
+
if (isWritePathFlagToken(lhs))
|
|
118
|
+
writes.push(rhs);
|
|
119
|
+
else
|
|
120
|
+
reads.push(rhs);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const cleanedToken = cleanPathToken(t);
|
|
124
|
+
if (looksLikePathToken(cleanedToken)) {
|
|
125
|
+
reads.push(cleanedToken);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const uniq = (xs) => Array.from(new Set(xs.filter(Boolean)));
|
|
129
|
+
return { reads: uniq(reads), writes: uniq(writes) };
|
|
130
|
+
}
|
|
131
|
+
const POLICY_REASON_CODES = {
|
|
132
|
+
POLICY_DENY: 'ADC_POLICY_DENY',
|
|
133
|
+
POLICY_WARN: 'ADC_POLICY_WARN',
|
|
134
|
+
GUARD_ERROR: 'ADC_GUARD_ERROR',
|
|
135
|
+
CUA_MALFORMED_EVENT: 'OCLAW_CUA_MALFORMED_EVENT',
|
|
136
|
+
CUA_COMPUTER_USE_CONFIG_MISSING: 'OCLAW_CUA_COMPUTER_USE_CONFIG_MISSING',
|
|
137
|
+
CUA_COMPUTER_USE_DISABLED: 'OCLAW_CUA_COMPUTER_USE_DISABLED',
|
|
138
|
+
CUA_ACTION_NOT_ALLOWED: 'OCLAW_CUA_ACTION_NOT_ALLOWED',
|
|
139
|
+
CUA_MODE_UNSUPPORTED: 'OCLAW_CUA_MODE_UNSUPPORTED',
|
|
140
|
+
CUA_CONNECT_METADATA_MISSING: 'OCLAW_CUA_CONNECT_METADATA_MISSING',
|
|
141
|
+
CUA_SIDE_CHANNEL_CONFIG_MISSING: 'OCLAW_CUA_SIDE_CHANNEL_CONFIG_MISSING',
|
|
142
|
+
CUA_SIDE_CHANNEL_DISABLED: 'OCLAW_CUA_SIDE_CHANNEL_DISABLED',
|
|
143
|
+
CUA_SIDE_CHANNEL_POLICY_DENY: 'OCLAW_CUA_SIDE_CHANNEL_POLICY_DENY',
|
|
144
|
+
CUA_TRANSFER_SIZE_CONFIG_INVALID: 'OCLAW_CUA_TRANSFER_SIZE_CONFIG_INVALID',
|
|
145
|
+
CUA_TRANSFER_SIZE_MISSING: 'OCLAW_CUA_TRANSFER_SIZE_MISSING',
|
|
146
|
+
CUA_TRANSFER_SIZE_EXCEEDED: 'OCLAW_CUA_TRANSFER_SIZE_EXCEEDED',
|
|
147
|
+
CUA_INPUT_CONFIG_MISSING: 'OCLAW_CUA_INPUT_CONFIG_MISSING',
|
|
148
|
+
CUA_INPUT_DISABLED: 'OCLAW_CUA_INPUT_DISABLED',
|
|
149
|
+
CUA_INPUT_TYPE_MISSING: 'OCLAW_CUA_INPUT_TYPE_MISSING',
|
|
150
|
+
CUA_INPUT_TYPE_NOT_ALLOWED: 'OCLAW_CUA_INPUT_TYPE_NOT_ALLOWED',
|
|
151
|
+
CUA_POSTCONDITION_PROBE_REQUIRED: 'OCLAW_CUA_POSTCONDITION_PROBE_REQUIRED',
|
|
152
|
+
FILESYSTEM_WRITE_ROOT_DENY: 'OCLAW_FILESYSTEM_WRITE_ROOT_DENY',
|
|
153
|
+
TOOL_DENIED: 'OCLAW_TOOL_DENIED',
|
|
154
|
+
TOOL_NOT_ALLOWLISTED: 'OCLAW_TOOL_NOT_ALLOWLISTED',
|
|
155
|
+
};
|
|
156
|
+
function denyDecision(reason_code, reason, guard, severity = 'high') {
|
|
157
|
+
return {
|
|
158
|
+
status: 'deny',
|
|
159
|
+
reason_code,
|
|
160
|
+
reason,
|
|
161
|
+
message: reason,
|
|
162
|
+
...(guard !== undefined && { guard }),
|
|
163
|
+
...(severity !== undefined && { severity }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function warnDecision(reason_code, reason, guard, severity = 'medium') {
|
|
167
|
+
return {
|
|
168
|
+
status: 'warn',
|
|
169
|
+
reason_code,
|
|
170
|
+
reason,
|
|
171
|
+
message: reason,
|
|
172
|
+
...(guard !== undefined && { guard }),
|
|
173
|
+
...(severity !== undefined && { severity }),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function ensureReasonCode(decision) {
|
|
177
|
+
if (decision.status === 'allow')
|
|
178
|
+
return decision;
|
|
179
|
+
if (typeof decision.reason_code === 'string' && decision.reason_code.trim().length > 0)
|
|
180
|
+
return decision;
|
|
181
|
+
return {
|
|
182
|
+
...decision,
|
|
183
|
+
reason_code: decision.status === 'warn' ? POLICY_REASON_CODES.POLICY_WARN : POLICY_REASON_CODES.GUARD_ERROR,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
15
186
|
export class PolicyEngine {
|
|
16
187
|
config;
|
|
17
188
|
policy;
|
|
@@ -68,13 +239,11 @@ export class PolicyEngine {
|
|
|
68
239
|
async evaluate(event) {
|
|
69
240
|
const base = this.evaluateDeterministic(event);
|
|
70
241
|
// Fail fast on deterministic violations to avoid unnecessary external calls.
|
|
71
|
-
|
|
72
|
-
const baseWarn = base.status === 'warn' || base.warn;
|
|
73
|
-
if (baseDenied || baseWarn) {
|
|
242
|
+
if (base.status === 'deny' || base.status === 'warn') {
|
|
74
243
|
return this.applyMode(base, this.config.mode);
|
|
75
244
|
}
|
|
76
245
|
if (this.threatIntelEngine) {
|
|
77
|
-
const ti = await this.threatIntelEngine.evaluate(
|
|
246
|
+
const ti = await this.threatIntelEngine.evaluate(event);
|
|
78
247
|
const tiApplied = this.applyOnViolation(ti);
|
|
79
248
|
const combined = combineDecisions(base, tiApplied);
|
|
80
249
|
return this.applyMode(combined, this.config.mode);
|
|
@@ -83,25 +252,58 @@ export class PolicyEngine {
|
|
|
83
252
|
}
|
|
84
253
|
applyMode(result, mode) {
|
|
85
254
|
if (mode === 'audit') {
|
|
86
|
-
return { status: 'allow', allowed: true, denied: false, warn: false };
|
|
87
|
-
}
|
|
88
|
-
const isDenied = result.status === 'deny' || result.denied;
|
|
89
|
-
if (mode === 'advisory' && isDenied) {
|
|
90
255
|
return {
|
|
91
|
-
status: '
|
|
92
|
-
|
|
93
|
-
denied: false,
|
|
94
|
-
warn: true,
|
|
256
|
+
status: 'allow',
|
|
257
|
+
reason_code: result.reason_code,
|
|
95
258
|
reason: result.reason,
|
|
259
|
+
message: `[audit] Original decision: ${result.status} — ${result.message ?? result.reason ?? 'no reason'}`,
|
|
96
260
|
guard: result.guard,
|
|
97
261
|
severity: result.severity,
|
|
98
|
-
message: result.reason,
|
|
99
262
|
};
|
|
100
263
|
}
|
|
101
|
-
|
|
264
|
+
if (mode === 'advisory' && result.status === 'deny') {
|
|
265
|
+
return ensureReasonCode(warnDecision(result.reason_code, result.reason ?? result.message ?? 'policy deny converted to advisory warning', result.guard, result.severity ?? 'medium'));
|
|
266
|
+
}
|
|
267
|
+
return ensureReasonCode(result);
|
|
268
|
+
}
|
|
269
|
+
getExpectedDataType(eventType) {
|
|
270
|
+
switch (eventType) {
|
|
271
|
+
case 'file_read':
|
|
272
|
+
case 'file_write':
|
|
273
|
+
return 'file';
|
|
274
|
+
case 'command_exec':
|
|
275
|
+
return 'command';
|
|
276
|
+
case 'network_egress':
|
|
277
|
+
return 'network';
|
|
278
|
+
case 'tool_call':
|
|
279
|
+
return 'tool';
|
|
280
|
+
case 'patch_apply':
|
|
281
|
+
return 'patch';
|
|
282
|
+
case 'secret_access':
|
|
283
|
+
return 'secret';
|
|
284
|
+
case 'custom':
|
|
285
|
+
return undefined;
|
|
286
|
+
default:
|
|
287
|
+
// CUA event types (starting with 'remote.' or 'input.')
|
|
288
|
+
if (eventType.startsWith('remote.') || eventType.startsWith('input.')) {
|
|
289
|
+
return 'cua';
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
102
293
|
}
|
|
103
294
|
evaluateDeterministic(event) {
|
|
104
|
-
const allowed = { status: 'allow'
|
|
295
|
+
const allowed = { status: 'allow' };
|
|
296
|
+
// Validate eventType/data.type consistency to prevent guard bypass
|
|
297
|
+
const expectedDataType = this.getExpectedDataType(event.eventType);
|
|
298
|
+
if (expectedDataType && event.data.type !== expectedDataType) {
|
|
299
|
+
return {
|
|
300
|
+
status: 'deny',
|
|
301
|
+
reason_code: 'event_type_mismatch',
|
|
302
|
+
reason: `Event type "${event.eventType}" requires data.type "${expectedDataType}" but got "${event.data.type}"`,
|
|
303
|
+
guard: 'policy_engine',
|
|
304
|
+
severity: 'critical',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
105
307
|
switch (event.eventType) {
|
|
106
308
|
case 'file_read':
|
|
107
309
|
case 'file_write':
|
|
@@ -114,20 +316,159 @@ export class PolicyEngine {
|
|
|
114
316
|
return this.checkToolCall(event);
|
|
115
317
|
case 'patch_apply':
|
|
116
318
|
return this.checkPatch(event);
|
|
319
|
+
case 'remote.session.connect':
|
|
320
|
+
case 'remote.session.disconnect':
|
|
321
|
+
case 'remote.session.reconnect':
|
|
322
|
+
case 'input.inject':
|
|
323
|
+
case 'remote.clipboard':
|
|
324
|
+
case 'remote.file_transfer':
|
|
325
|
+
case 'remote.audio':
|
|
326
|
+
case 'remote.drive_mapping':
|
|
327
|
+
case 'remote.printing':
|
|
328
|
+
case 'remote.session_share':
|
|
329
|
+
return this.checkCua(event);
|
|
117
330
|
default:
|
|
118
331
|
return allowed;
|
|
119
332
|
}
|
|
120
333
|
}
|
|
334
|
+
checkCua(event) {
|
|
335
|
+
if (event.data.type !== 'cua') {
|
|
336
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_MALFORMED_EVENT, `Malformed CUA event payload for ${event.eventType}: data.type must be 'cua'`, 'computer_use', 'high'));
|
|
337
|
+
}
|
|
338
|
+
const cuaData = event.data;
|
|
339
|
+
const connectEgressDecision = this.checkCuaConnectEgress(event, cuaData);
|
|
340
|
+
if (connectEgressDecision.status === 'deny' || connectEgressDecision.status === 'warn') {
|
|
341
|
+
return connectEgressDecision;
|
|
342
|
+
}
|
|
343
|
+
const computerUse = this.policy.guards?.computer_use;
|
|
344
|
+
if (!computerUse) {
|
|
345
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_COMPUTER_USE_CONFIG_MISSING, `CUA action '${event.eventType}' denied: missing guards.computer_use policy config`, 'computer_use', 'high'));
|
|
346
|
+
}
|
|
347
|
+
if (computerUse.enabled === false) {
|
|
348
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_COMPUTER_USE_DISABLED, `CUA action '${event.eventType}' denied: computer_use guard is disabled`, 'computer_use', 'high'));
|
|
349
|
+
}
|
|
350
|
+
const mode = computerUse.mode ?? 'guardrail';
|
|
351
|
+
const allowedActions = normalizeStringList(computerUse.allowed_actions);
|
|
352
|
+
const actionAllowed = allowedActions.length === 0 || allowedActions.includes(event.eventType);
|
|
353
|
+
if (!actionAllowed) {
|
|
354
|
+
const reason = `CUA action '${event.eventType}' is not listed in guards.computer_use.allowed_actions`;
|
|
355
|
+
if (mode === 'observe' || mode === 'guardrail') {
|
|
356
|
+
return warnDecision(POLICY_REASON_CODES.CUA_ACTION_NOT_ALLOWED, reason, 'computer_use', 'medium');
|
|
357
|
+
}
|
|
358
|
+
if (mode !== 'fail_closed') {
|
|
359
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_MODE_UNSUPPORTED, `CUA action '${event.eventType}' denied: unsupported computer_use mode '${mode}'`, 'computer_use', 'high'));
|
|
360
|
+
}
|
|
361
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_ACTION_NOT_ALLOWED, reason, 'computer_use', 'high'));
|
|
362
|
+
}
|
|
363
|
+
const sideChannelDecision = this.checkRemoteDesktopSideChannel(event, cuaData);
|
|
364
|
+
if (sideChannelDecision.status === 'deny' || sideChannelDecision.status === 'warn') {
|
|
365
|
+
return sideChannelDecision;
|
|
366
|
+
}
|
|
367
|
+
const inputDecision = this.checkInputInjectionCapability(event, cuaData);
|
|
368
|
+
if (inputDecision.status === 'deny' || inputDecision.status === 'warn') {
|
|
369
|
+
return inputDecision;
|
|
370
|
+
}
|
|
371
|
+
return { status: 'allow' };
|
|
372
|
+
}
|
|
373
|
+
checkCuaConnectEgress(event, data) {
|
|
374
|
+
if (event.eventType !== 'remote.session.connect') {
|
|
375
|
+
return { status: 'allow' };
|
|
376
|
+
}
|
|
377
|
+
if (!this.config.guards.egress) {
|
|
378
|
+
return { status: 'allow' };
|
|
379
|
+
}
|
|
380
|
+
const target = extractCuaNetworkTarget(data);
|
|
381
|
+
if (!target) {
|
|
382
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_CONNECT_METADATA_MISSING, "CUA connect action denied: missing destination host/url metadata required for egress evaluation", 'egress', 'high'));
|
|
383
|
+
}
|
|
384
|
+
const egressEvent = {
|
|
385
|
+
eventId: `${event.eventId}:cua-connect-egress`,
|
|
386
|
+
eventType: 'network_egress',
|
|
387
|
+
timestamp: event.timestamp,
|
|
388
|
+
sessionId: event.sessionId,
|
|
389
|
+
data: {
|
|
390
|
+
type: 'network',
|
|
391
|
+
host: target.host,
|
|
392
|
+
port: target.port,
|
|
393
|
+
...(target.protocol ? { protocol: target.protocol } : {}),
|
|
394
|
+
...(target.url ? { url: target.url } : {}),
|
|
395
|
+
},
|
|
396
|
+
metadata: {
|
|
397
|
+
...(event.metadata ?? {}),
|
|
398
|
+
derivedFrom: event.eventType,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
return this.checkEgress(egressEvent);
|
|
402
|
+
}
|
|
403
|
+
checkRemoteDesktopSideChannel(event, data) {
|
|
404
|
+
const sideChannelFlag = eventTypeToSideChannelFlag(event.eventType);
|
|
405
|
+
if (!sideChannelFlag) {
|
|
406
|
+
return { status: 'allow' };
|
|
407
|
+
}
|
|
408
|
+
const cfg = this.policy.guards?.remote_desktop_side_channel;
|
|
409
|
+
if (!cfg) {
|
|
410
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_SIDE_CHANNEL_CONFIG_MISSING, `CUA side-channel action '${event.eventType}' denied: missing guards.remote_desktop_side_channel policy config`, 'remote_desktop_side_channel', 'high'));
|
|
411
|
+
}
|
|
412
|
+
if (cfg.enabled === false) {
|
|
413
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_SIDE_CHANNEL_DISABLED, `CUA side-channel action '${event.eventType}' denied: remote_desktop_side_channel guard is disabled`, 'remote_desktop_side_channel', 'high'));
|
|
414
|
+
}
|
|
415
|
+
if (cfg[sideChannelFlag] === false) {
|
|
416
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_SIDE_CHANNEL_POLICY_DENY, `CUA side-channel action '${event.eventType}' denied by policy`, 'remote_desktop_side_channel', 'high'));
|
|
417
|
+
}
|
|
418
|
+
if (event.eventType === 'remote.file_transfer') {
|
|
419
|
+
const maxBytes = cfg.max_transfer_size_bytes;
|
|
420
|
+
if (maxBytes !== undefined) {
|
|
421
|
+
if (typeof maxBytes !== 'number' || !Number.isFinite(maxBytes) || maxBytes < 0) {
|
|
422
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_TRANSFER_SIZE_CONFIG_INVALID, `CUA file transfer denied: invalid max_transfer_size_bytes '${String(maxBytes)}'`, 'remote_desktop_side_channel', 'high'));
|
|
423
|
+
}
|
|
424
|
+
const transferSize = extractTransferSize(data);
|
|
425
|
+
if (transferSize === null) {
|
|
426
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_TRANSFER_SIZE_MISSING, 'CUA file transfer denied: missing required transfer_size metadata', 'remote_desktop_side_channel', 'high'));
|
|
427
|
+
}
|
|
428
|
+
if (transferSize > maxBytes) {
|
|
429
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_TRANSFER_SIZE_EXCEEDED, `CUA file transfer size ${transferSize} exceeds max_transfer_size_bytes ${maxBytes}`, 'remote_desktop_side_channel', 'high'));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { status: 'allow' };
|
|
434
|
+
}
|
|
435
|
+
checkInputInjectionCapability(event, data) {
|
|
436
|
+
if (event.eventType !== 'input.inject') {
|
|
437
|
+
return { status: 'allow' };
|
|
438
|
+
}
|
|
439
|
+
const cfg = this.policy.guards?.input_injection_capability;
|
|
440
|
+
if (!cfg) {
|
|
441
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_INPUT_CONFIG_MISSING, `CUA input action '${event.eventType}' denied: missing guards.input_injection_capability policy config`, 'input_injection_capability', 'high'));
|
|
442
|
+
}
|
|
443
|
+
if (cfg.enabled === false) {
|
|
444
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_INPUT_DISABLED, `CUA input action '${event.eventType}' denied: input_injection_capability guard is disabled`, 'input_injection_capability', 'high'));
|
|
445
|
+
}
|
|
446
|
+
const allowedInputTypes = normalizeStringList(cfg.allowed_input_types);
|
|
447
|
+
const inputType = extractInputType(data);
|
|
448
|
+
if (allowedInputTypes.length > 0) {
|
|
449
|
+
if (!inputType) {
|
|
450
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_INPUT_TYPE_MISSING, "CUA input action denied: missing required 'input_type'", 'input_injection_capability', 'high'));
|
|
451
|
+
}
|
|
452
|
+
if (!allowedInputTypes.includes(inputType)) {
|
|
453
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_INPUT_TYPE_NOT_ALLOWED, `CUA input action denied: input_type '${inputType}' is not allowed`, 'input_injection_capability', 'high'));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (cfg.require_postcondition_probe === true) {
|
|
457
|
+
const probeHash = data.postconditionProbeHash;
|
|
458
|
+
if (typeof probeHash !== 'string' || probeHash.trim().length === 0) {
|
|
459
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.CUA_POSTCONDITION_PROBE_REQUIRED, 'CUA input action denied: postcondition probe hash is required', 'input_injection_capability', 'high'));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return { status: 'allow' };
|
|
463
|
+
}
|
|
121
464
|
checkFilesystem(event) {
|
|
122
465
|
if (!this.config.guards.forbidden_path) {
|
|
123
|
-
return { status: 'allow'
|
|
466
|
+
return { status: 'allow' };
|
|
124
467
|
}
|
|
125
468
|
// First, enforce forbidden path patterns.
|
|
126
469
|
const forbidden = this.forbiddenPathGuard.checkSync(event, this.policy);
|
|
127
470
|
const mapped = this.guardResultToDecision(forbidden);
|
|
128
|
-
|
|
129
|
-
const mappedWarn = mapped.status === 'warn' || mapped.warn;
|
|
130
|
-
if (mappedDenied || mappedWarn) {
|
|
471
|
+
if (mapped.status === 'deny' || mapped.status === 'warn') {
|
|
131
472
|
return this.applyOnViolation(mapped);
|
|
132
473
|
}
|
|
133
474
|
// Then, enforce write roots if configured.
|
|
@@ -140,31 +481,62 @@ export class PolicyEngine {
|
|
|
140
481
|
return filePath === rootPath || filePath.startsWith(rootPath + path.sep);
|
|
141
482
|
});
|
|
142
483
|
if (!ok) {
|
|
143
|
-
return this.applyOnViolation(
|
|
144
|
-
status: 'deny',
|
|
145
|
-
allowed: false,
|
|
146
|
-
denied: true,
|
|
147
|
-
warn: false,
|
|
148
|
-
reason: 'Write path not in allowed roots',
|
|
149
|
-
guard: 'forbidden_path',
|
|
150
|
-
severity: 'high',
|
|
151
|
-
});
|
|
484
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.FILESYSTEM_WRITE_ROOT_DENY, 'Write path not in allowed roots', 'forbidden_path', 'high'));
|
|
152
485
|
}
|
|
153
486
|
}
|
|
154
487
|
}
|
|
155
|
-
return { status: 'allow'
|
|
488
|
+
return { status: 'allow' };
|
|
156
489
|
}
|
|
157
490
|
checkEgress(event) {
|
|
158
491
|
if (!this.config.guards.egress) {
|
|
159
|
-
return { status: 'allow'
|
|
492
|
+
return { status: 'allow' };
|
|
160
493
|
}
|
|
161
494
|
const res = this.egressGuard.checkSync(event, this.policy);
|
|
162
495
|
const mapped = this.guardResultToDecision(res);
|
|
163
496
|
return this.applyOnViolation(mapped);
|
|
164
497
|
}
|
|
165
498
|
checkExecution(event) {
|
|
499
|
+
// Defense in depth: shell/command execution can still touch the filesystem.
|
|
500
|
+
// Best-effort extract path-like tokens (including redirections) and run them through the
|
|
501
|
+
// filesystem policy checks (forbidden paths + allowed write roots).
|
|
502
|
+
if (this.config.guards.forbidden_path && event.data.type === 'command') {
|
|
503
|
+
const { reads, writes } = extractCommandPathCandidates(event.data.command, event.data.args);
|
|
504
|
+
const maxChecks = 64;
|
|
505
|
+
let checks = 0;
|
|
506
|
+
// Check likely writes first so allowed_write_roots is enforced.
|
|
507
|
+
for (const p of writes) {
|
|
508
|
+
if (checks++ >= maxChecks)
|
|
509
|
+
break;
|
|
510
|
+
const synthetic = {
|
|
511
|
+
eventId: `${event.eventId}:cmdwrite:${checks}`,
|
|
512
|
+
eventType: 'file_write',
|
|
513
|
+
timestamp: event.timestamp,
|
|
514
|
+
sessionId: event.sessionId,
|
|
515
|
+
data: { type: 'file', path: p, operation: 'write' },
|
|
516
|
+
metadata: { ...event.metadata, derivedFrom: 'command_exec' },
|
|
517
|
+
};
|
|
518
|
+
const d = this.checkFilesystem(synthetic);
|
|
519
|
+
if (d.status === 'deny' || d.status === 'warn')
|
|
520
|
+
return d;
|
|
521
|
+
}
|
|
522
|
+
for (const p of reads) {
|
|
523
|
+
if (checks++ >= maxChecks)
|
|
524
|
+
break;
|
|
525
|
+
const synthetic = {
|
|
526
|
+
eventId: `${event.eventId}:cmdread:${checks}`,
|
|
527
|
+
eventType: 'file_read',
|
|
528
|
+
timestamp: event.timestamp,
|
|
529
|
+
sessionId: event.sessionId,
|
|
530
|
+
data: { type: 'file', path: p, operation: 'read' },
|
|
531
|
+
metadata: { ...event.metadata, derivedFrom: 'command_exec' },
|
|
532
|
+
};
|
|
533
|
+
const d = this.checkFilesystem(synthetic);
|
|
534
|
+
if (d.status === 'deny' || d.status === 'warn')
|
|
535
|
+
return d;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
166
538
|
if (!this.config.guards.patch_integrity) {
|
|
167
|
-
return { status: 'allow'
|
|
539
|
+
return { status: 'allow' };
|
|
168
540
|
}
|
|
169
541
|
const res = this.patchIntegrityGuard.checkSync(event, this.policy);
|
|
170
542
|
const mapped = this.guardResultToDecision(res);
|
|
@@ -175,33 +547,37 @@ export class PolicyEngine {
|
|
|
175
547
|
if (event.data.type === 'tool') {
|
|
176
548
|
const tools = this.policy.tools;
|
|
177
549
|
const toolName = event.data.toolName.toLowerCase();
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
180
|
-
return this.applyOnViolation({
|
|
181
|
-
status: 'deny',
|
|
182
|
-
allowed: false,
|
|
183
|
-
denied: true,
|
|
184
|
-
warn: false,
|
|
185
|
-
reason: `Tool '${event.data.toolName}' is denied by policy`,
|
|
186
|
-
guard: 'mcp_tool',
|
|
187
|
-
severity: 'high',
|
|
188
|
-
});
|
|
550
|
+
const deniedTools = tools?.denied?.map((x) => x.toLowerCase()) ?? [];
|
|
551
|
+
if (deniedTools.includes(toolName)) {
|
|
552
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.TOOL_DENIED, `Tool '${event.data.toolName}' is denied by policy`, 'mcp_tool', 'high'));
|
|
189
553
|
}
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
return this.applyOnViolation({
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
554
|
+
const allowedTools = tools?.allowed?.map((x) => x.toLowerCase()) ?? [];
|
|
555
|
+
if (allowedTools.length > 0 && !allowedTools.includes(toolName)) {
|
|
556
|
+
return this.applyOnViolation(denyDecision(POLICY_REASON_CODES.TOOL_NOT_ALLOWLISTED, `Tool '${event.data.toolName}' is not in allowed tool list`, 'mcp_tool', 'high'));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Also check forbidden paths in tool parameters (defense in depth).
|
|
560
|
+
if (this.config.guards.forbidden_path && event.data.type === 'tool') {
|
|
561
|
+
const params = event.data.parameters ?? {};
|
|
562
|
+
const pathKeys = ['path', 'file', 'file_path', 'filepath', 'filename', 'target'];
|
|
563
|
+
for (const key of pathKeys) {
|
|
564
|
+
const val = params[key];
|
|
565
|
+
if (typeof val === 'string' && val.length > 0) {
|
|
566
|
+
const pathEvent = {
|
|
567
|
+
...event,
|
|
568
|
+
eventType: 'file_write',
|
|
569
|
+
data: { type: 'file', path: val, operation: 'write' },
|
|
570
|
+
};
|
|
571
|
+
const pathCheck = this.forbiddenPathGuard.checkSync(pathEvent, this.policy);
|
|
572
|
+
const pathDecision = this.guardResultToDecision(pathCheck);
|
|
573
|
+
if (pathDecision.status === 'deny' || pathDecision.status === 'warn') {
|
|
574
|
+
return this.applyOnViolation(pathDecision);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
201
577
|
}
|
|
202
578
|
}
|
|
203
579
|
if (!this.config.guards.secret_leak) {
|
|
204
|
-
return { status: 'allow'
|
|
580
|
+
return { status: 'allow' };
|
|
205
581
|
}
|
|
206
582
|
const res = this.secretLeakGuard.checkSync(event, this.policy);
|
|
207
583
|
const mapped = this.guardResultToDecision(res);
|
|
@@ -212,48 +588,37 @@ export class PolicyEngine {
|
|
|
212
588
|
const r1 = this.patchIntegrityGuard.checkSync(event, this.policy);
|
|
213
589
|
const mapped1 = this.guardResultToDecision(r1);
|
|
214
590
|
const applied1 = this.applyOnViolation(mapped1);
|
|
215
|
-
|
|
216
|
-
const applied1Warn = applied1.status === 'warn' || applied1.warn;
|
|
217
|
-
if (applied1Denied || applied1Warn)
|
|
591
|
+
if (applied1.status === 'deny' || applied1.status === 'warn')
|
|
218
592
|
return applied1;
|
|
219
593
|
}
|
|
220
594
|
if (this.config.guards.secret_leak) {
|
|
221
595
|
const r2 = this.secretLeakGuard.checkSync(event, this.policy);
|
|
222
596
|
const mapped2 = this.guardResultToDecision(r2);
|
|
223
597
|
const applied2 = this.applyOnViolation(mapped2);
|
|
224
|
-
|
|
225
|
-
const applied2Warn = applied2.status === 'warn' || applied2.warn;
|
|
226
|
-
if (applied2Denied || applied2Warn)
|
|
598
|
+
if (applied2.status === 'deny' || applied2.status === 'warn')
|
|
227
599
|
return applied2;
|
|
228
600
|
}
|
|
229
|
-
return { status: 'allow'
|
|
601
|
+
return { status: 'allow' };
|
|
230
602
|
}
|
|
231
603
|
applyOnViolation(decision) {
|
|
232
604
|
const action = this.policy.on_violation;
|
|
233
|
-
|
|
234
|
-
if (!isDenied)
|
|
605
|
+
if (decision.status !== 'deny')
|
|
235
606
|
return decision;
|
|
236
607
|
if (action === 'warn') {
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
warn: true,
|
|
242
|
-
reason: decision.reason,
|
|
243
|
-
guard: decision.guard,
|
|
244
|
-
severity: decision.severity,
|
|
245
|
-
message: decision.reason,
|
|
246
|
-
};
|
|
608
|
+
return warnDecision(decision.reason_code, decision.reason ?? decision.message ?? 'Policy violation downgraded to warning', decision.guard, decision.severity ?? 'medium');
|
|
609
|
+
}
|
|
610
|
+
if (action && action !== 'cancel') {
|
|
611
|
+
console.warn(`[clawdstrike] Unhandled on_violation action: "${action}" — treating as deny`);
|
|
247
612
|
}
|
|
248
613
|
return decision;
|
|
249
614
|
}
|
|
250
615
|
guardResultToDecision(result) {
|
|
251
616
|
if (result.status === 'allow')
|
|
252
|
-
return { status: 'allow'
|
|
617
|
+
return { status: 'allow' };
|
|
253
618
|
if (result.status === 'warn') {
|
|
254
|
-
return
|
|
619
|
+
return warnDecision(POLICY_REASON_CODES.POLICY_WARN, result.reason ?? `${result.guard} returned warning`, result.guard, 'medium');
|
|
255
620
|
}
|
|
256
|
-
return
|
|
621
|
+
return denyDecision(POLICY_REASON_CODES.GUARD_ERROR, result.reason ?? `${result.guard} denied request`, result.guard, result.severity ?? 'high');
|
|
257
622
|
}
|
|
258
623
|
}
|
|
259
624
|
function buildThreatIntelEngine(policy) {
|
|
@@ -261,22 +626,153 @@ function buildThreatIntelEngine(policy) {
|
|
|
261
626
|
if (!Array.isArray(custom) || custom.length === 0) {
|
|
262
627
|
return null;
|
|
263
628
|
}
|
|
629
|
+
// The openclaw Policy types `custom` as `unknown`; the canonical Policy
|
|
630
|
+
// expects `CustomGuardSpec[]`. We've validated it's an array above.
|
|
631
|
+
// GuardConfigs has an index signature so `unknown[]` is assignable.
|
|
264
632
|
const canonicalPolicy = {
|
|
265
633
|
version: '1.1.0',
|
|
266
634
|
guards: { custom },
|
|
267
635
|
};
|
|
268
636
|
return createPolicyEngineFromPolicy(canonicalPolicy);
|
|
269
637
|
}
|
|
270
|
-
function toCanonicalEvent(event) {
|
|
271
|
-
// OpenClaw events are compatible with adapter-core's PolicyEvent shape. Keep the
|
|
272
|
-
// raw eventId/timestamp/metadata for audit trails.
|
|
273
|
-
return event;
|
|
274
|
-
}
|
|
275
638
|
function combineDecisions(base, next) {
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
639
|
+
const rank = { deny: 2, warn: 1, allow: 0 };
|
|
640
|
+
const baseRank = rank[base.status] ?? 0;
|
|
641
|
+
const nextRank = rank[next.status] ?? 0;
|
|
642
|
+
if (nextRank > baseRank)
|
|
279
643
|
return next;
|
|
644
|
+
if (nextRank === baseRank && nextRank > 0 && next.reason) {
|
|
645
|
+
// On ties for non-allow decisions, merge the reasons
|
|
646
|
+
return {
|
|
647
|
+
...base,
|
|
648
|
+
message: base.message
|
|
649
|
+
? `${base.message}; ${next.message ?? next.reason}`
|
|
650
|
+
: next.message ?? next.reason,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
280
653
|
return base;
|
|
281
654
|
}
|
|
655
|
+
function normalizeStringList(values) {
|
|
656
|
+
if (!Array.isArray(values))
|
|
657
|
+
return [];
|
|
658
|
+
const out = [];
|
|
659
|
+
for (const value of values) {
|
|
660
|
+
if (typeof value !== 'string')
|
|
661
|
+
continue;
|
|
662
|
+
const normalized = value.trim();
|
|
663
|
+
if (normalized.length > 0)
|
|
664
|
+
out.push(normalized);
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
function extractInputType(data) {
|
|
669
|
+
const candidates = [data.input_type, data.inputType];
|
|
670
|
+
for (const candidate of candidates) {
|
|
671
|
+
if (typeof candidate === 'string') {
|
|
672
|
+
const normalized = candidate.trim().toLowerCase();
|
|
673
|
+
if (normalized.length > 0)
|
|
674
|
+
return normalized;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
function extractTransferSize(data) {
|
|
680
|
+
const candidates = [
|
|
681
|
+
data.transfer_size,
|
|
682
|
+
data.transferSize,
|
|
683
|
+
data.size_bytes,
|
|
684
|
+
data.sizeBytes,
|
|
685
|
+
];
|
|
686
|
+
for (const candidate of candidates) {
|
|
687
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate >= 0) {
|
|
688
|
+
return candidate;
|
|
689
|
+
}
|
|
690
|
+
if (typeof candidate === 'string') {
|
|
691
|
+
const parsed = Number.parseInt(candidate, 10);
|
|
692
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
693
|
+
return parsed;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
function parsePort(value) {
|
|
700
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
701
|
+
const port = Math.trunc(value);
|
|
702
|
+
if (port > 0 && port <= 65535)
|
|
703
|
+
return port;
|
|
704
|
+
}
|
|
705
|
+
if (typeof value === 'string') {
|
|
706
|
+
const trimmed = value.trim();
|
|
707
|
+
if (/^[0-9]+$/.test(trimmed)) {
|
|
708
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
709
|
+
if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535)
|
|
710
|
+
return parsed;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
function firstNonEmptyString(values) {
|
|
716
|
+
for (const value of values) {
|
|
717
|
+
if (typeof value !== 'string')
|
|
718
|
+
continue;
|
|
719
|
+
const trimmed = value.trim();
|
|
720
|
+
if (trimmed.length > 0)
|
|
721
|
+
return trimmed;
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
function extractCuaNetworkTarget(data) {
|
|
726
|
+
const url = firstNonEmptyString([
|
|
727
|
+
data.url,
|
|
728
|
+
data.endpoint,
|
|
729
|
+
data.href,
|
|
730
|
+
data.target_url,
|
|
731
|
+
data.targetUrl,
|
|
732
|
+
]);
|
|
733
|
+
const parsed = parseNetworkTarget(url ?? '', { emptyPort: 'default' });
|
|
734
|
+
const host = firstNonEmptyString([
|
|
735
|
+
data.host,
|
|
736
|
+
data.hostname,
|
|
737
|
+
data.remote_host,
|
|
738
|
+
data.remoteHost,
|
|
739
|
+
data.destination_host,
|
|
740
|
+
data.destinationHost,
|
|
741
|
+
parsed.host,
|
|
742
|
+
])?.toLowerCase();
|
|
743
|
+
if (!host) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
const protocol = firstNonEmptyString([data.protocol, data.scheme])?.toLowerCase();
|
|
747
|
+
const explicitPort = parsePort(data.port
|
|
748
|
+
?? data.remote_port
|
|
749
|
+
?? data.remotePort
|
|
750
|
+
?? data.destination_port
|
|
751
|
+
?? data.destinationPort);
|
|
752
|
+
const port = explicitPort ?? (parsed.host ? parsed.port : protocol === 'http' ? 80 : 443);
|
|
753
|
+
return {
|
|
754
|
+
host,
|
|
755
|
+
port,
|
|
756
|
+
...(protocol ? { protocol } : {}),
|
|
757
|
+
...(url ? { url } : {}),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function eventTypeToSideChannelFlag(eventType) {
|
|
761
|
+
switch (eventType) {
|
|
762
|
+
case 'remote.clipboard':
|
|
763
|
+
return 'clipboard_enabled';
|
|
764
|
+
case 'remote.file_transfer':
|
|
765
|
+
return 'file_transfer_enabled';
|
|
766
|
+
case 'remote.audio':
|
|
767
|
+
return 'audio_enabled';
|
|
768
|
+
case 'remote.drive_mapping':
|
|
769
|
+
return 'drive_mapping_enabled';
|
|
770
|
+
case 'remote.printing':
|
|
771
|
+
return 'printing_enabled';
|
|
772
|
+
case 'remote.session_share':
|
|
773
|
+
return 'session_share_enabled';
|
|
774
|
+
default:
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
282
778
|
//# sourceMappingURL=engine.js.map
|