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