@node9/proxy 1.43.0 → 1.45.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/dist/cli.js +10681 -9922
- package/dist/cli.mjs +10659 -9903
- package/dist/dashboard.mjs +6 -1
- package/dist/index.js +131 -9
- package/dist/index.mjs +131 -9
- package/package.json +1 -1
package/dist/dashboard.mjs
CHANGED
|
@@ -1264,7 +1264,12 @@ var init_dist = __esm({
|
|
|
1264
1264
|
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
1265
1265
|
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
1266
1266
|
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
1267
|
-
"review-drop-truncate-shell"
|
|
1267
|
+
"review-drop-truncate-shell",
|
|
1268
|
+
// chmod 777 is now owned by the AST detector (analyzeChmod777) so the raw-
|
|
1269
|
+
// regex smart rule is suppressed for bash — it matched `chmod 777` inside a
|
|
1270
|
+
// `node -e` / `python -c` string literal (a detection pattern, not a run
|
|
1271
|
+
// command) → false positive.
|
|
1272
|
+
"shield:filesystem:review-chmod-777"
|
|
1268
1273
|
]);
|
|
1269
1274
|
FS_OP_CACHE_MAX = 5e3;
|
|
1270
1275
|
fsOpCache = /* @__PURE__ */ new Map();
|
package/dist/index.js
CHANGED
|
@@ -75,6 +75,7 @@ __export(audit_exports, {
|
|
|
75
75
|
appendLocalAudit: () => appendLocalAudit,
|
|
76
76
|
appendToLog: () => appendToLog,
|
|
77
77
|
buildArgsPreview: () => buildArgsPreview,
|
|
78
|
+
filePathFromArgs: () => filePathFromArgs,
|
|
78
79
|
generateEventId: () => generateEventId,
|
|
79
80
|
redactSecrets: () => redactSecrets
|
|
80
81
|
});
|
|
@@ -131,6 +132,25 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
131
132
|
cwd: process.cwd()
|
|
132
133
|
});
|
|
133
134
|
}
|
|
135
|
+
function filePathFromArgs(args) {
|
|
136
|
+
let obj = args;
|
|
137
|
+
if (typeof obj === "string") {
|
|
138
|
+
const t = obj.trim();
|
|
139
|
+
if (!t.startsWith("{") || !t.endsWith("}")) return void 0;
|
|
140
|
+
if (!t.includes("file_path") && !t.includes("notebook_path")) return void 0;
|
|
141
|
+
try {
|
|
142
|
+
obj = JSON.parse(t);
|
|
143
|
+
} catch {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
148
|
+
const o = obj;
|
|
149
|
+
const v = o.file_path ?? o.notebook_path;
|
|
150
|
+
if (typeof v === "string" && v) return v.slice(0, 1024);
|
|
151
|
+
}
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
134
154
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
135
155
|
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
|
|
136
156
|
const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
@@ -143,10 +163,19 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
143
163
|
const workingDirField = meta?.workingDir ? { workingDir: meta.workingDir } : {};
|
|
144
164
|
const shell = process.env.SHELL ? import_path.default.basename(process.env.SHELL) : void 0;
|
|
145
165
|
const shellTypeField = shell ? { shellType: shell } : {};
|
|
166
|
+
const editFilePath = filePathFromArgs(args);
|
|
167
|
+
const editFilePathField = editFilePath ? { editFilePath } : {};
|
|
168
|
+
const loopCountField = typeof meta?.loopCount === "number" ? { loopCount: meta.loopCount } : {};
|
|
169
|
+
const transcriptPathField = meta?.transcriptPath ? { transcriptPath: meta.transcriptPath } : {};
|
|
170
|
+
const taintFields = meta?.taintFromEid ? {
|
|
171
|
+
taintFromEid: meta.taintFromEid,
|
|
172
|
+
...meta.taintSource && { taintSource: meta.taintSource }
|
|
173
|
+
} : {};
|
|
174
|
+
const eid = generateEventId();
|
|
146
175
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
147
176
|
// eid first: the outbox shipper dedups on it, and a fixed leading field
|
|
148
177
|
// makes the JSONL easy to eyeball.
|
|
149
|
-
eid
|
|
178
|
+
eid,
|
|
150
179
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151
180
|
tool: toolName,
|
|
152
181
|
...agentToolNameField,
|
|
@@ -158,6 +187,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
158
187
|
...cloudLinkField,
|
|
159
188
|
...workingDirField,
|
|
160
189
|
...shellTypeField,
|
|
190
|
+
...editFilePathField,
|
|
191
|
+
...loopCountField,
|
|
192
|
+
...transcriptPathField,
|
|
193
|
+
...taintFields,
|
|
161
194
|
...testRun,
|
|
162
195
|
agent: meta?.agent,
|
|
163
196
|
mcpServer: meta?.mcpServer,
|
|
@@ -165,6 +198,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
165
198
|
hostname: import_os.default.hostname(),
|
|
166
199
|
platform: import_os.default.platform()
|
|
167
200
|
});
|
|
201
|
+
return eid;
|
|
168
202
|
}
|
|
169
203
|
function appendConfigAudit(entry) {
|
|
170
204
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
@@ -1278,7 +1312,12 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
|
1278
1312
|
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
1279
1313
|
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
1280
1314
|
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
1281
|
-
"review-drop-truncate-shell"
|
|
1315
|
+
"review-drop-truncate-shell",
|
|
1316
|
+
// chmod 777 is now owned by the AST detector (analyzeChmod777) so the raw-
|
|
1317
|
+
// regex smart rule is suppressed for bash — it matched `chmod 777` inside a
|
|
1318
|
+
// `node -e` / `python -c` string literal (a detection pattern, not a run
|
|
1319
|
+
// command) → false positive.
|
|
1320
|
+
"shield:filesystem:review-chmod-777"
|
|
1282
1321
|
]);
|
|
1283
1322
|
var SQL_DB_CLIS = /* @__PURE__ */ new Set([
|
|
1284
1323
|
"psql",
|
|
@@ -1303,6 +1342,62 @@ function analyzeSqlDestructive(command) {
|
|
|
1303
1342
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1304
1343
|
};
|
|
1305
1344
|
}
|
|
1345
|
+
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
|
|
1346
|
+
var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1347
|
+
"sudo",
|
|
1348
|
+
"doas",
|
|
1349
|
+
"env",
|
|
1350
|
+
"xargs",
|
|
1351
|
+
"time",
|
|
1352
|
+
"nice",
|
|
1353
|
+
"ionice",
|
|
1354
|
+
"nohup",
|
|
1355
|
+
"setsid",
|
|
1356
|
+
"stdbuf",
|
|
1357
|
+
"timeout",
|
|
1358
|
+
"command",
|
|
1359
|
+
"exec"
|
|
1360
|
+
]);
|
|
1361
|
+
function chmodHasOpenPermMode(command) {
|
|
1362
|
+
const f = parseShared(command);
|
|
1363
|
+
if (f === PARSE_FAIL) return false;
|
|
1364
|
+
let found = false;
|
|
1365
|
+
try {
|
|
1366
|
+
syntax.Walk(f, (node) => {
|
|
1367
|
+
if (!node || found) return false;
|
|
1368
|
+
const n = node;
|
|
1369
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1370
|
+
const words = (n.Args || []).map((a) => resolveWordLiteral(a));
|
|
1371
|
+
if (words.length === 0) return true;
|
|
1372
|
+
const name = (words[0] ?? "").toLowerCase();
|
|
1373
|
+
let idx = -1;
|
|
1374
|
+
if (name === "chmod") idx = 0;
|
|
1375
|
+
else if (COMMAND_WRAPPERS.has(name))
|
|
1376
|
+
idx = words.findIndex((w, i) => i > 0 && w?.toLowerCase() === "chmod");
|
|
1377
|
+
if (idx < 0) return true;
|
|
1378
|
+
for (let i = idx + 1; i < words.length; i++) {
|
|
1379
|
+
const w = words[i];
|
|
1380
|
+
if (w !== null && w.startsWith("-")) continue;
|
|
1381
|
+
if (w !== null && CHMOD_OPEN_PERM_TOKENS.has(w.toLowerCase())) found = true;
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
return true;
|
|
1385
|
+
});
|
|
1386
|
+
} catch {
|
|
1387
|
+
return found;
|
|
1388
|
+
}
|
|
1389
|
+
return found;
|
|
1390
|
+
}
|
|
1391
|
+
function analyzeChmod777(command) {
|
|
1392
|
+
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1393
|
+
if (!chmodHasOpenPermMode(command)) return null;
|
|
1394
|
+
return {
|
|
1395
|
+
ruleName: "shield:filesystem:review-chmod-777",
|
|
1396
|
+
verdict: "review",
|
|
1397
|
+
reason: "chmod 777 requires human approval (filesystem shield)",
|
|
1398
|
+
description: "The AI wants to make a file world-writable/executable (chmod 777). This removes the permission protection on the file so any user or process can modify or run it."
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1306
1401
|
function isProtectedHomePath(rawPath) {
|
|
1307
1402
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
1308
1403
|
let underHome = false;
|
|
@@ -2268,6 +2363,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2268
2363
|
ruleDescription: sqlVerdict.description
|
|
2269
2364
|
};
|
|
2270
2365
|
}
|
|
2366
|
+
const chmodVerdict = analyzeChmod777(bashCommand);
|
|
2367
|
+
if (chmodVerdict) {
|
|
2368
|
+
return {
|
|
2369
|
+
decision: chmodVerdict.verdict,
|
|
2370
|
+
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2371
|
+
reason: chmodVerdict.reason,
|
|
2372
|
+
tier: 2,
|
|
2373
|
+
ruleName: chmodVerdict.ruleName,
|
|
2374
|
+
ruleDescription: chmodVerdict.description
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2271
2377
|
}
|
|
2272
2378
|
if (config.policy.smartRules.length > 0) {
|
|
2273
2379
|
const matchedRule = config.policy.smartRules.find(
|
|
@@ -4370,14 +4476,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
|
|
|
4370
4476
|
const { id, allowCount } = await res.json();
|
|
4371
4477
|
return { id, allowCount: allowCount ?? 1 };
|
|
4372
4478
|
}
|
|
4373
|
-
async function notifyTaint(filePath, source) {
|
|
4479
|
+
async function notifyTaint(filePath, source, fromEid) {
|
|
4374
4480
|
if (!isDaemonRunning()) return;
|
|
4375
4481
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
4376
4482
|
try {
|
|
4377
4483
|
await fetch(`${base}/taint`, {
|
|
4378
4484
|
method: "POST",
|
|
4379
4485
|
headers: { "Content-Type": "application/json" },
|
|
4380
|
-
|
|
4486
|
+
// Phase D2 — fromEid is the audit eid that created this taint (edge source).
|
|
4487
|
+
body: JSON.stringify({ path: filePath, source, ...fromEid && { fromEid } }),
|
|
4381
4488
|
signal: AbortSignal.timeout(1e3)
|
|
4382
4489
|
});
|
|
4383
4490
|
} catch {
|
|
@@ -5065,7 +5172,11 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5065
5172
|
if (filePaths.length > 0) {
|
|
5066
5173
|
const taintResult = await checkTaint(filePaths);
|
|
5067
5174
|
if (taintResult.tainted && taintResult.record) {
|
|
5068
|
-
const {
|
|
5175
|
+
const {
|
|
5176
|
+
path: taintedPath,
|
|
5177
|
+
source: taintSource,
|
|
5178
|
+
fromEid: taintFromEid
|
|
5179
|
+
} = taintResult.record;
|
|
5069
5180
|
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
5070
5181
|
if (config.policy.egress?.enabled) {
|
|
5071
5182
|
const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
@@ -5079,7 +5190,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5079
5190
|
args,
|
|
5080
5191
|
"deny",
|
|
5081
5192
|
isObserveMode ? "observe-mode-taint-egress-would-block" : "taint-egress-block",
|
|
5082
|
-
|
|
5193
|
+
// Phase D2 — the causal taint edge: from the row that created
|
|
5194
|
+
// the taint (taintFromEid) to this block row.
|
|
5195
|
+
{ ...meta, ruleName: `taint-egress:${eg.host}`, taintFromEid, taintSource },
|
|
5083
5196
|
hashAuditArgs
|
|
5084
5197
|
);
|
|
5085
5198
|
if (isObserveMode) {
|
|
@@ -5116,8 +5229,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5116
5229
|
if (dlpMatch) {
|
|
5117
5230
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
5118
5231
|
if (dlpMatch.severity === "block") {
|
|
5232
|
+
let dlpEid;
|
|
5119
5233
|
if (!isManual)
|
|
5120
|
-
appendLocalAudit(
|
|
5234
|
+
dlpEid = appendLocalAudit(
|
|
5121
5235
|
toolName,
|
|
5122
5236
|
args,
|
|
5123
5237
|
"deny",
|
|
@@ -5130,7 +5244,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5130
5244
|
true
|
|
5131
5245
|
);
|
|
5132
5246
|
if (isWriteTool(toolName) && filePath) {
|
|
5133
|
-
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}
|
|
5247
|
+
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`, dlpEid);
|
|
5134
5248
|
}
|
|
5135
5249
|
if (isObserveMode) {
|
|
5136
5250
|
return {
|
|
@@ -5222,7 +5336,15 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5222
5336
|
if (loopResult.looping) {
|
|
5223
5337
|
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
|
5224
5338
|
if (!isManual)
|
|
5225
|
-
appendLocalAudit(
|
|
5339
|
+
appendLocalAudit(
|
|
5340
|
+
toolName,
|
|
5341
|
+
args,
|
|
5342
|
+
"deny",
|
|
5343
|
+
"loop-detected",
|
|
5344
|
+
// Phase B — carry the loop magnitude so the SaaS shows "looped Nx".
|
|
5345
|
+
{ ...meta, loopCount: loopResult.count },
|
|
5346
|
+
hashAuditArgs
|
|
5347
|
+
);
|
|
5226
5348
|
return {
|
|
5227
5349
|
approved: false,
|
|
5228
5350
|
reason,
|
package/dist/index.mjs
CHANGED
|
@@ -52,6 +52,7 @@ __export(audit_exports, {
|
|
|
52
52
|
appendLocalAudit: () => appendLocalAudit,
|
|
53
53
|
appendToLog: () => appendToLog,
|
|
54
54
|
buildArgsPreview: () => buildArgsPreview,
|
|
55
|
+
filePathFromArgs: () => filePathFromArgs,
|
|
55
56
|
generateEventId: () => generateEventId,
|
|
56
57
|
redactSecrets: () => redactSecrets
|
|
57
58
|
});
|
|
@@ -112,6 +113,25 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
112
113
|
cwd: process.cwd()
|
|
113
114
|
});
|
|
114
115
|
}
|
|
116
|
+
function filePathFromArgs(args) {
|
|
117
|
+
let obj = args;
|
|
118
|
+
if (typeof obj === "string") {
|
|
119
|
+
const t = obj.trim();
|
|
120
|
+
if (!t.startsWith("{") || !t.endsWith("}")) return void 0;
|
|
121
|
+
if (!t.includes("file_path") && !t.includes("notebook_path")) return void 0;
|
|
122
|
+
try {
|
|
123
|
+
obj = JSON.parse(t);
|
|
124
|
+
} catch {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
129
|
+
const o = obj;
|
|
130
|
+
const v = o.file_path ?? o.notebook_path;
|
|
131
|
+
if (typeof v === "string" && v) return v.slice(0, 1024);
|
|
132
|
+
}
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
115
135
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
116
136
|
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
|
|
117
137
|
const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
@@ -124,10 +144,19 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
124
144
|
const workingDirField = meta?.workingDir ? { workingDir: meta.workingDir } : {};
|
|
125
145
|
const shell = process.env.SHELL ? path.basename(process.env.SHELL) : void 0;
|
|
126
146
|
const shellTypeField = shell ? { shellType: shell } : {};
|
|
147
|
+
const editFilePath = filePathFromArgs(args);
|
|
148
|
+
const editFilePathField = editFilePath ? { editFilePath } : {};
|
|
149
|
+
const loopCountField = typeof meta?.loopCount === "number" ? { loopCount: meta.loopCount } : {};
|
|
150
|
+
const transcriptPathField = meta?.transcriptPath ? { transcriptPath: meta.transcriptPath } : {};
|
|
151
|
+
const taintFields = meta?.taintFromEid ? {
|
|
152
|
+
taintFromEid: meta.taintFromEid,
|
|
153
|
+
...meta.taintSource && { taintSource: meta.taintSource }
|
|
154
|
+
} : {};
|
|
155
|
+
const eid = generateEventId();
|
|
127
156
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
128
157
|
// eid first: the outbox shipper dedups on it, and a fixed leading field
|
|
129
158
|
// makes the JSONL easy to eyeball.
|
|
130
|
-
eid
|
|
159
|
+
eid,
|
|
131
160
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
132
161
|
tool: toolName,
|
|
133
162
|
...agentToolNameField,
|
|
@@ -139,6 +168,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
139
168
|
...cloudLinkField,
|
|
140
169
|
...workingDirField,
|
|
141
170
|
...shellTypeField,
|
|
171
|
+
...editFilePathField,
|
|
172
|
+
...loopCountField,
|
|
173
|
+
...transcriptPathField,
|
|
174
|
+
...taintFields,
|
|
142
175
|
...testRun,
|
|
143
176
|
agent: meta?.agent,
|
|
144
177
|
mcpServer: meta?.mcpServer,
|
|
@@ -146,6 +179,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
146
179
|
hostname: os.hostname(),
|
|
147
180
|
platform: os.platform()
|
|
148
181
|
});
|
|
182
|
+
return eid;
|
|
149
183
|
}
|
|
150
184
|
function appendConfigAudit(entry) {
|
|
151
185
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
@@ -1248,7 +1282,12 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
|
1248
1282
|
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
1249
1283
|
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
1250
1284
|
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
1251
|
-
"review-drop-truncate-shell"
|
|
1285
|
+
"review-drop-truncate-shell",
|
|
1286
|
+
// chmod 777 is now owned by the AST detector (analyzeChmod777) so the raw-
|
|
1287
|
+
// regex smart rule is suppressed for bash — it matched `chmod 777` inside a
|
|
1288
|
+
// `node -e` / `python -c` string literal (a detection pattern, not a run
|
|
1289
|
+
// command) → false positive.
|
|
1290
|
+
"shield:filesystem:review-chmod-777"
|
|
1252
1291
|
]);
|
|
1253
1292
|
var SQL_DB_CLIS = /* @__PURE__ */ new Set([
|
|
1254
1293
|
"psql",
|
|
@@ -1273,6 +1312,62 @@ function analyzeSqlDestructive(command) {
|
|
|
1273
1312
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1274
1313
|
};
|
|
1275
1314
|
}
|
|
1315
|
+
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
|
|
1316
|
+
var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1317
|
+
"sudo",
|
|
1318
|
+
"doas",
|
|
1319
|
+
"env",
|
|
1320
|
+
"xargs",
|
|
1321
|
+
"time",
|
|
1322
|
+
"nice",
|
|
1323
|
+
"ionice",
|
|
1324
|
+
"nohup",
|
|
1325
|
+
"setsid",
|
|
1326
|
+
"stdbuf",
|
|
1327
|
+
"timeout",
|
|
1328
|
+
"command",
|
|
1329
|
+
"exec"
|
|
1330
|
+
]);
|
|
1331
|
+
function chmodHasOpenPermMode(command) {
|
|
1332
|
+
const f = parseShared(command);
|
|
1333
|
+
if (f === PARSE_FAIL) return false;
|
|
1334
|
+
let found = false;
|
|
1335
|
+
try {
|
|
1336
|
+
syntax.Walk(f, (node) => {
|
|
1337
|
+
if (!node || found) return false;
|
|
1338
|
+
const n = node;
|
|
1339
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1340
|
+
const words = (n.Args || []).map((a) => resolveWordLiteral(a));
|
|
1341
|
+
if (words.length === 0) return true;
|
|
1342
|
+
const name = (words[0] ?? "").toLowerCase();
|
|
1343
|
+
let idx = -1;
|
|
1344
|
+
if (name === "chmod") idx = 0;
|
|
1345
|
+
else if (COMMAND_WRAPPERS.has(name))
|
|
1346
|
+
idx = words.findIndex((w, i) => i > 0 && w?.toLowerCase() === "chmod");
|
|
1347
|
+
if (idx < 0) return true;
|
|
1348
|
+
for (let i = idx + 1; i < words.length; i++) {
|
|
1349
|
+
const w = words[i];
|
|
1350
|
+
if (w !== null && w.startsWith("-")) continue;
|
|
1351
|
+
if (w !== null && CHMOD_OPEN_PERM_TOKENS.has(w.toLowerCase())) found = true;
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
return true;
|
|
1355
|
+
});
|
|
1356
|
+
} catch {
|
|
1357
|
+
return found;
|
|
1358
|
+
}
|
|
1359
|
+
return found;
|
|
1360
|
+
}
|
|
1361
|
+
function analyzeChmod777(command) {
|
|
1362
|
+
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1363
|
+
if (!chmodHasOpenPermMode(command)) return null;
|
|
1364
|
+
return {
|
|
1365
|
+
ruleName: "shield:filesystem:review-chmod-777",
|
|
1366
|
+
verdict: "review",
|
|
1367
|
+
reason: "chmod 777 requires human approval (filesystem shield)",
|
|
1368
|
+
description: "The AI wants to make a file world-writable/executable (chmod 777). This removes the permission protection on the file so any user or process can modify or run it."
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1276
1371
|
function isProtectedHomePath(rawPath) {
|
|
1277
1372
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
1278
1373
|
let underHome = false;
|
|
@@ -2238,6 +2333,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2238
2333
|
ruleDescription: sqlVerdict.description
|
|
2239
2334
|
};
|
|
2240
2335
|
}
|
|
2336
|
+
const chmodVerdict = analyzeChmod777(bashCommand);
|
|
2337
|
+
if (chmodVerdict) {
|
|
2338
|
+
return {
|
|
2339
|
+
decision: chmodVerdict.verdict,
|
|
2340
|
+
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2341
|
+
reason: chmodVerdict.reason,
|
|
2342
|
+
tier: 2,
|
|
2343
|
+
ruleName: chmodVerdict.ruleName,
|
|
2344
|
+
ruleDescription: chmodVerdict.description
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2241
2347
|
}
|
|
2242
2348
|
if (config.policy.smartRules.length > 0) {
|
|
2243
2349
|
const matchedRule = config.policy.smartRules.find(
|
|
@@ -4340,14 +4446,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
|
|
|
4340
4446
|
const { id, allowCount } = await res.json();
|
|
4341
4447
|
return { id, allowCount: allowCount ?? 1 };
|
|
4342
4448
|
}
|
|
4343
|
-
async function notifyTaint(filePath, source) {
|
|
4449
|
+
async function notifyTaint(filePath, source, fromEid) {
|
|
4344
4450
|
if (!isDaemonRunning()) return;
|
|
4345
4451
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
4346
4452
|
try {
|
|
4347
4453
|
await fetch(`${base}/taint`, {
|
|
4348
4454
|
method: "POST",
|
|
4349
4455
|
headers: { "Content-Type": "application/json" },
|
|
4350
|
-
|
|
4456
|
+
// Phase D2 — fromEid is the audit eid that created this taint (edge source).
|
|
4457
|
+
body: JSON.stringify({ path: filePath, source, ...fromEid && { fromEid } }),
|
|
4351
4458
|
signal: AbortSignal.timeout(1e3)
|
|
4352
4459
|
});
|
|
4353
4460
|
} catch {
|
|
@@ -5035,7 +5142,11 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5035
5142
|
if (filePaths.length > 0) {
|
|
5036
5143
|
const taintResult = await checkTaint(filePaths);
|
|
5037
5144
|
if (taintResult.tainted && taintResult.record) {
|
|
5038
|
-
const {
|
|
5145
|
+
const {
|
|
5146
|
+
path: taintedPath,
|
|
5147
|
+
source: taintSource,
|
|
5148
|
+
fromEid: taintFromEid
|
|
5149
|
+
} = taintResult.record;
|
|
5039
5150
|
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
5040
5151
|
if (config.policy.egress?.enabled) {
|
|
5041
5152
|
const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
@@ -5049,7 +5160,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5049
5160
|
args,
|
|
5050
5161
|
"deny",
|
|
5051
5162
|
isObserveMode ? "observe-mode-taint-egress-would-block" : "taint-egress-block",
|
|
5052
|
-
|
|
5163
|
+
// Phase D2 — the causal taint edge: from the row that created
|
|
5164
|
+
// the taint (taintFromEid) to this block row.
|
|
5165
|
+
{ ...meta, ruleName: `taint-egress:${eg.host}`, taintFromEid, taintSource },
|
|
5053
5166
|
hashAuditArgs
|
|
5054
5167
|
);
|
|
5055
5168
|
if (isObserveMode) {
|
|
@@ -5086,8 +5199,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5086
5199
|
if (dlpMatch) {
|
|
5087
5200
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
5088
5201
|
if (dlpMatch.severity === "block") {
|
|
5202
|
+
let dlpEid;
|
|
5089
5203
|
if (!isManual)
|
|
5090
|
-
appendLocalAudit(
|
|
5204
|
+
dlpEid = appendLocalAudit(
|
|
5091
5205
|
toolName,
|
|
5092
5206
|
args,
|
|
5093
5207
|
"deny",
|
|
@@ -5100,7 +5214,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5100
5214
|
true
|
|
5101
5215
|
);
|
|
5102
5216
|
if (isWriteTool(toolName) && filePath) {
|
|
5103
|
-
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}
|
|
5217
|
+
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`, dlpEid);
|
|
5104
5218
|
}
|
|
5105
5219
|
if (isObserveMode) {
|
|
5106
5220
|
return {
|
|
@@ -5192,7 +5306,15 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5192
5306
|
if (loopResult.looping) {
|
|
5193
5307
|
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
|
5194
5308
|
if (!isManual)
|
|
5195
|
-
appendLocalAudit(
|
|
5309
|
+
appendLocalAudit(
|
|
5310
|
+
toolName,
|
|
5311
|
+
args,
|
|
5312
|
+
"deny",
|
|
5313
|
+
"loop-detected",
|
|
5314
|
+
// Phase B — carry the loop magnitude so the SaaS shows "looped Nx".
|
|
5315
|
+
{ ...meta, loopCount: loopResult.count },
|
|
5316
|
+
hashAuditArgs
|
|
5317
|
+
);
|
|
5196
5318
|
return {
|
|
5197
5319
|
approved: false,
|
|
5198
5320
|
reason,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|