@node9/proxy 1.29.0 → 1.31.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 +1653 -908
- package/dist/cli.mjs +1622 -878
- package/dist/dashboard.mjs +29 -4
- package/dist/index.js +478 -155
- package/dist/index.mjs +475 -152
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -51,11 +51,17 @@ __export(audit_exports, {
|
|
|
51
51
|
appendHookDebug: () => appendHookDebug,
|
|
52
52
|
appendLocalAudit: () => appendLocalAudit,
|
|
53
53
|
appendToLog: () => appendToLog,
|
|
54
|
+
buildArgsPreview: () => buildArgsPreview,
|
|
55
|
+
generateEventId: () => generateEventId,
|
|
54
56
|
redactSecrets: () => redactSecrets
|
|
55
57
|
});
|
|
56
58
|
import fs from "fs";
|
|
57
59
|
import path from "path";
|
|
58
60
|
import os from "os";
|
|
61
|
+
import crypto from "crypto";
|
|
62
|
+
function generateEventId() {
|
|
63
|
+
return `${Date.now().toString(36)}-${crypto.randomBytes(6).toString("hex")}`;
|
|
64
|
+
}
|
|
59
65
|
function isTestCall(toolName, args) {
|
|
60
66
|
if (toolName !== "Bash" && toolName !== "bash") return false;
|
|
61
67
|
const cmd = args?.command;
|
|
@@ -74,6 +80,17 @@ function redactSecrets(text) {
|
|
|
74
80
|
);
|
|
75
81
|
return redacted;
|
|
76
82
|
}
|
|
83
|
+
function buildArgsPreview(args) {
|
|
84
|
+
try {
|
|
85
|
+
const o = args && typeof args === "object" ? args : null;
|
|
86
|
+
const primary = o && (o.command ?? o.file_path ?? o.path ?? o.url ?? o.query);
|
|
87
|
+
const text = typeof primary === "string" ? primary : args ? JSON.stringify(args) : "";
|
|
88
|
+
if (!text) return void 0;
|
|
89
|
+
return redactSecrets(text).slice(0, 120);
|
|
90
|
+
} catch {
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
77
94
|
function appendToLog(logPath, entry) {
|
|
78
95
|
try {
|
|
79
96
|
const dir = path.dirname(logPath);
|
|
@@ -96,11 +113,18 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
96
113
|
});
|
|
97
114
|
}
|
|
98
115
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
99
|
-
const
|
|
116
|
+
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
|
|
117
|
+
const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
118
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
100
119
|
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
101
120
|
const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
|
|
102
121
|
const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
|
|
122
|
+
const dlpFields = meta?.dlpPattern ? { dlpPattern: meta.dlpPattern, dlpSample: meta.dlpSample } : {};
|
|
123
|
+
const cloudLinkField = meta?.cloudRequestId ? { cloudRequestId: meta.cloudRequestId } : {};
|
|
103
124
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
125
|
+
// eid first: the outbox shipper dedups on it, and a fixed leading field
|
|
126
|
+
// makes the JSONL easy to eyeball.
|
|
127
|
+
eid: generateEventId(),
|
|
104
128
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
129
|
tool: toolName,
|
|
106
130
|
...agentToolNameField,
|
|
@@ -108,6 +132,8 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
108
132
|
decision,
|
|
109
133
|
checkedBy,
|
|
110
134
|
...ruleNameField,
|
|
135
|
+
...dlpFields,
|
|
136
|
+
...cloudLinkField,
|
|
111
137
|
...testRun,
|
|
112
138
|
agent: meta?.agent,
|
|
113
139
|
mcpServer: meta?.mcpServer,
|
|
@@ -216,7 +242,13 @@ var ConfigFileSchema = z.object({
|
|
|
216
242
|
allowGlobalPause: z.boolean().optional(),
|
|
217
243
|
auditHashArgs: z.boolean().optional(),
|
|
218
244
|
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
219
|
-
cloudSyncIntervalHours: z.number().positive().optional()
|
|
245
|
+
cloudSyncIntervalHours: z.number().positive().optional(),
|
|
246
|
+
// Outbox shipper (audit.log → SaaS batch ingest). enabled defaults
|
|
247
|
+
// to true; set false to fall back to local-only auditing.
|
|
248
|
+
shipper: z.object({
|
|
249
|
+
enabled: z.boolean().optional(),
|
|
250
|
+
intervalSeconds: z.number().min(5).optional()
|
|
251
|
+
}).optional()
|
|
220
252
|
}).optional(),
|
|
221
253
|
policy: z.object({
|
|
222
254
|
sandboxPaths: z.array(z.string()).optional(),
|
|
@@ -231,7 +263,15 @@ var ConfigFileSchema = z.object({
|
|
|
231
263
|
}).optional(),
|
|
232
264
|
dlp: z.object({
|
|
233
265
|
enabled: z.boolean().optional(),
|
|
234
|
-
scanIgnoredTools: z.boolean().optional()
|
|
266
|
+
scanIgnoredTools: z.boolean().optional(),
|
|
267
|
+
pii: z.enum(["off", "block"]).optional()
|
|
268
|
+
}).optional(),
|
|
269
|
+
egress: z.object({
|
|
270
|
+
enabled: z.boolean().optional(),
|
|
271
|
+
mode: z.enum(["off", "review", "block"]).optional(),
|
|
272
|
+
allow: z.array(z.string()).optional(),
|
|
273
|
+
deny: z.array(z.string()).optional(),
|
|
274
|
+
allowPrivate: z.boolean().optional()
|
|
235
275
|
}).optional(),
|
|
236
276
|
loopDetection: z.object({
|
|
237
277
|
enabled: z.boolean().optional(),
|
|
@@ -284,7 +324,7 @@ import mvdanSh from "mvdan-sh";
|
|
|
284
324
|
import pm from "picomatch";
|
|
285
325
|
import safeRegex2 from "safe-regex2";
|
|
286
326
|
import safeRegex3 from "safe-regex2";
|
|
287
|
-
import
|
|
327
|
+
import crypto2 from "crypto";
|
|
288
328
|
var ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
|
|
289
329
|
function isAssignmentContext(text) {
|
|
290
330
|
return ASSIGNMENT_CONTEXT_RE.test(text);
|
|
@@ -1213,6 +1253,201 @@ function extractLiteralArgs(callExpr) {
|
|
|
1213
1253
|
}
|
|
1214
1254
|
return { name, flags, paths };
|
|
1215
1255
|
}
|
|
1256
|
+
var NET_BINARIES = /* @__PURE__ */ new Set(["curl", "wget", "scp", "ssh", "nc", "ncat", "netcat"]);
|
|
1257
|
+
var VALUE_FLAGS = {
|
|
1258
|
+
curl: /* @__PURE__ */ new Set([
|
|
1259
|
+
"-d",
|
|
1260
|
+
"--data",
|
|
1261
|
+
"--data-ascii",
|
|
1262
|
+
"--data-binary",
|
|
1263
|
+
"--data-raw",
|
|
1264
|
+
"--data-urlencode",
|
|
1265
|
+
"-F",
|
|
1266
|
+
"--form",
|
|
1267
|
+
"-H",
|
|
1268
|
+
"--header",
|
|
1269
|
+
"-X",
|
|
1270
|
+
"--request",
|
|
1271
|
+
"-o",
|
|
1272
|
+
"--output",
|
|
1273
|
+
"-T",
|
|
1274
|
+
"--upload-file",
|
|
1275
|
+
"-u",
|
|
1276
|
+
"--user",
|
|
1277
|
+
"-e",
|
|
1278
|
+
"--referer",
|
|
1279
|
+
"-A",
|
|
1280
|
+
"--user-agent",
|
|
1281
|
+
"-b",
|
|
1282
|
+
"--cookie",
|
|
1283
|
+
"-c",
|
|
1284
|
+
"--cookie-jar",
|
|
1285
|
+
"--connect-to",
|
|
1286
|
+
"--resolve",
|
|
1287
|
+
"--cacert",
|
|
1288
|
+
"--cert",
|
|
1289
|
+
"--key",
|
|
1290
|
+
"-x",
|
|
1291
|
+
"--proxy",
|
|
1292
|
+
"-m",
|
|
1293
|
+
"--max-time",
|
|
1294
|
+
"--retry"
|
|
1295
|
+
]),
|
|
1296
|
+
wget: /* @__PURE__ */ new Set([
|
|
1297
|
+
"-O",
|
|
1298
|
+
"--output-document",
|
|
1299
|
+
"--post-data",
|
|
1300
|
+
"--post-file",
|
|
1301
|
+
"--header",
|
|
1302
|
+
"-U",
|
|
1303
|
+
"--user-agent",
|
|
1304
|
+
"--user",
|
|
1305
|
+
"--password",
|
|
1306
|
+
"-o",
|
|
1307
|
+
"--output-file",
|
|
1308
|
+
"-P",
|
|
1309
|
+
"--directory-prefix",
|
|
1310
|
+
"-t",
|
|
1311
|
+
"--tries",
|
|
1312
|
+
"-T",
|
|
1313
|
+
"--timeout"
|
|
1314
|
+
]),
|
|
1315
|
+
scp: /* @__PURE__ */ new Set(["-i", "-F", "-l", "-o", "-c", "-S", "-P", "-J", "-D", "-W"]),
|
|
1316
|
+
ssh: /* @__PURE__ */ new Set([
|
|
1317
|
+
"-i",
|
|
1318
|
+
"-p",
|
|
1319
|
+
"-o",
|
|
1320
|
+
"-l",
|
|
1321
|
+
"-F",
|
|
1322
|
+
"-c",
|
|
1323
|
+
"-L",
|
|
1324
|
+
"-R",
|
|
1325
|
+
"-D",
|
|
1326
|
+
"-W",
|
|
1327
|
+
"-b",
|
|
1328
|
+
"-e",
|
|
1329
|
+
"-m",
|
|
1330
|
+
"-O",
|
|
1331
|
+
"-Q",
|
|
1332
|
+
"-S",
|
|
1333
|
+
"-J",
|
|
1334
|
+
"-w",
|
|
1335
|
+
"-B",
|
|
1336
|
+
"-I",
|
|
1337
|
+
"-E"
|
|
1338
|
+
]),
|
|
1339
|
+
nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
|
|
1340
|
+
};
|
|
1341
|
+
function resolveWordLiteral(w) {
|
|
1342
|
+
const parts = w?.Parts || [];
|
|
1343
|
+
let s = "";
|
|
1344
|
+
for (const p of parts) {
|
|
1345
|
+
const t = syntax.NodeType(p);
|
|
1346
|
+
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
1347
|
+
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
1348
|
+
else if (t === "DblQuoted") {
|
|
1349
|
+
const inner = p.Parts || [];
|
|
1350
|
+
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
1351
|
+
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
1352
|
+
} else {
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return s;
|
|
1357
|
+
}
|
|
1358
|
+
function parseDestHost(token) {
|
|
1359
|
+
if (!token) return null;
|
|
1360
|
+
let t = token.trim();
|
|
1361
|
+
if (!t || t.startsWith("-")) return null;
|
|
1362
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) {
|
|
1363
|
+
try {
|
|
1364
|
+
const h = new URL(t).hostname.toLowerCase();
|
|
1365
|
+
return h || null;
|
|
1366
|
+
} catch {
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
const at = t.lastIndexOf("@");
|
|
1371
|
+
if (at >= 0) t = t.slice(at + 1);
|
|
1372
|
+
t = t.split("/")[0];
|
|
1373
|
+
t = t.replace(/:\d+$/, "");
|
|
1374
|
+
t = t.split(":")[0];
|
|
1375
|
+
t = t.toLowerCase();
|
|
1376
|
+
if (t.length > 253) return null;
|
|
1377
|
+
if (t === "localhost") return t;
|
|
1378
|
+
if (/^[a-z0-9.-]+\.[a-z0-9.-]+$/.test(t)) return t;
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
function destTokensForBinary(binary, args) {
|
|
1382
|
+
const valueFlags = VALUE_FLAGS[binary] ?? /* @__PURE__ */ new Set();
|
|
1383
|
+
const positionals = [];
|
|
1384
|
+
const urlFlagValues = [];
|
|
1385
|
+
for (let i = 0; i < args.length; i++) {
|
|
1386
|
+
const tok = args[i];
|
|
1387
|
+
if (tok === null) continue;
|
|
1388
|
+
if (tok.startsWith("-")) {
|
|
1389
|
+
if (tok.startsWith("--url=")) {
|
|
1390
|
+
urlFlagValues.push(tok.slice("--url=".length));
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
if (tok === "--url") {
|
|
1394
|
+
const next = args[i + 1];
|
|
1395
|
+
if (typeof next === "string") urlFlagValues.push(next);
|
|
1396
|
+
i++;
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (tok.includes("=")) continue;
|
|
1400
|
+
if (valueFlags.has(tok)) i++;
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
positionals.push(tok);
|
|
1404
|
+
}
|
|
1405
|
+
switch (binary) {
|
|
1406
|
+
case "curl":
|
|
1407
|
+
case "wget":
|
|
1408
|
+
return [...urlFlagValues, ...positionals];
|
|
1409
|
+
case "ssh":
|
|
1410
|
+
return positionals.slice(0, 1);
|
|
1411
|
+
case "scp":
|
|
1412
|
+
return positionals.filter((p) => p.includes(":") || p.includes("@"));
|
|
1413
|
+
case "nc":
|
|
1414
|
+
case "ncat":
|
|
1415
|
+
case "netcat":
|
|
1416
|
+
return positionals.slice(0, 1);
|
|
1417
|
+
default:
|
|
1418
|
+
return [];
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function extractShellDestinations(command) {
|
|
1422
|
+
const f = parseShared(command);
|
|
1423
|
+
if (f === PARSE_FAIL) return [];
|
|
1424
|
+
const out = [];
|
|
1425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1426
|
+
try {
|
|
1427
|
+
syntax.Walk(f, (node) => {
|
|
1428
|
+
if (!node) return false;
|
|
1429
|
+
const n = node;
|
|
1430
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1431
|
+
const callArgs = n.Args || [];
|
|
1432
|
+
if (callArgs.length === 0) return true;
|
|
1433
|
+
const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
|
|
1434
|
+
if (!NET_BINARIES.has(name)) return true;
|
|
1435
|
+
const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
|
|
1436
|
+
for (const raw of destTokensForBinary(name, rest)) {
|
|
1437
|
+
const host = parseDestHost(raw);
|
|
1438
|
+
if (!host) continue;
|
|
1439
|
+
const key = `${name}:${host}`;
|
|
1440
|
+
if (seen.has(key)) continue;
|
|
1441
|
+
seen.add(key);
|
|
1442
|
+
out.push({ host, binary: name, raw });
|
|
1443
|
+
}
|
|
1444
|
+
return true;
|
|
1445
|
+
});
|
|
1446
|
+
} catch {
|
|
1447
|
+
return out;
|
|
1448
|
+
}
|
|
1449
|
+
return out;
|
|
1450
|
+
}
|
|
1216
1451
|
var FS_OP_CACHE_MAX = 5e3;
|
|
1217
1452
|
var fsOpCache = /* @__PURE__ */ new Map();
|
|
1218
1453
|
function analyzeFsOperation(command) {
|
|
@@ -1342,6 +1577,83 @@ function analyzeShellCommand(command) {
|
|
|
1342
1577
|
}
|
|
1343
1578
|
return { actions, paths, allTokens };
|
|
1344
1579
|
}
|
|
1580
|
+
var DEFAULT_EGRESS_ALLOWLIST = [
|
|
1581
|
+
"*.github.com",
|
|
1582
|
+
"*.githubusercontent.com",
|
|
1583
|
+
"*.npmjs.org",
|
|
1584
|
+
"pypi.org",
|
|
1585
|
+
"*.pythonhosted.org",
|
|
1586
|
+
"crates.io",
|
|
1587
|
+
"*.crates.io",
|
|
1588
|
+
"rubygems.org",
|
|
1589
|
+
"proxy.golang.org",
|
|
1590
|
+
"sum.golang.org",
|
|
1591
|
+
"*.anthropic.com",
|
|
1592
|
+
"*.openai.com",
|
|
1593
|
+
"*.googleapis.com",
|
|
1594
|
+
"*.docker.io",
|
|
1595
|
+
"*.docker.com",
|
|
1596
|
+
"deb.debian.org",
|
|
1597
|
+
"*.ubuntu.com"
|
|
1598
|
+
];
|
|
1599
|
+
function hostMatches(host, pattern) {
|
|
1600
|
+
const h = host.toLowerCase();
|
|
1601
|
+
const p = pattern.toLowerCase().trim();
|
|
1602
|
+
if (!p) return false;
|
|
1603
|
+
if (p === "*") return true;
|
|
1604
|
+
if (p.startsWith("*.")) {
|
|
1605
|
+
const suffix = p.slice(2);
|
|
1606
|
+
return h === suffix || h.endsWith("." + suffix);
|
|
1607
|
+
}
|
|
1608
|
+
return h === p;
|
|
1609
|
+
}
|
|
1610
|
+
function matchesAny(host, patterns) {
|
|
1611
|
+
for (const p of patterns) if (hostMatches(host, p)) return true;
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
function isPrivateHost(host) {
|
|
1615
|
+
const h = host.toLowerCase();
|
|
1616
|
+
if (h === "localhost" || h === "0.0.0.0") return true;
|
|
1617
|
+
if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localhost")) return true;
|
|
1618
|
+
if (/^127\./.test(h)) return true;
|
|
1619
|
+
if (/^10\./.test(h)) return true;
|
|
1620
|
+
if (/^192\.168\./.test(h)) return true;
|
|
1621
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
|
|
1622
|
+
return false;
|
|
1623
|
+
}
|
|
1624
|
+
function evaluateEgress(dests, policy) {
|
|
1625
|
+
if (!policy.enabled) return null;
|
|
1626
|
+
let review = null;
|
|
1627
|
+
for (const d of dests) {
|
|
1628
|
+
if (matchesAny(d.host, policy.deny)) {
|
|
1629
|
+
return {
|
|
1630
|
+
verdict: "block",
|
|
1631
|
+
host: d.host,
|
|
1632
|
+
binary: d.binary,
|
|
1633
|
+
reason: `Egress to ${d.host} is on the deny list.`
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
if (policy.allowPrivate && isPrivateHost(d.host)) continue;
|
|
1637
|
+
if (matchesAny(d.host, policy.allow) || matchesAny(d.host, DEFAULT_EGRESS_ALLOWLIST)) continue;
|
|
1638
|
+
if (policy.mode === "block") {
|
|
1639
|
+
return {
|
|
1640
|
+
verdict: "block",
|
|
1641
|
+
host: d.host,
|
|
1642
|
+
binary: d.binary,
|
|
1643
|
+
reason: `Egress to unknown host ${d.host} is blocked (egress policy: block).`
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
if (policy.mode === "review" && !review) {
|
|
1647
|
+
review = {
|
|
1648
|
+
verdict: "review",
|
|
1649
|
+
host: d.host,
|
|
1650
|
+
binary: d.binary,
|
|
1651
|
+
reason: `${d.binary} is sending data to an unrecognized host (${d.host}). Approve this destination?`
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return review;
|
|
1656
|
+
}
|
|
1345
1657
|
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1346
1658
|
"cat",
|
|
1347
1659
|
"head",
|
|
@@ -1907,6 +2219,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1907
2219
|
}
|
|
1908
2220
|
const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
|
|
1909
2221
|
if (ptVerdict) return ptVerdict;
|
|
2222
|
+
if (config.policy.egress?.enabled) {
|
|
2223
|
+
const dests = extractShellDestinations(shellCommand);
|
|
2224
|
+
if (dests.length > 0) {
|
|
2225
|
+
const eg = evaluateEgress(dests, config.policy.egress);
|
|
2226
|
+
if (eg) {
|
|
2227
|
+
return {
|
|
2228
|
+
decision: eg.verdict,
|
|
2229
|
+
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
2230
|
+
reason: eg.reason,
|
|
2231
|
+
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
2232
|
+
ruleDescription: eg.reason,
|
|
2233
|
+
tier: eg.verdict === "block" ? 3 : 4
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
1910
2238
|
const firstToken = analyzed.actions[0] ?? "";
|
|
1911
2239
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1912
2240
|
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
@@ -2817,7 +3145,7 @@ assertBuiltinShieldRegexesAreSafe();
|
|
|
2817
3145
|
var LOOP_MAX_RECORDS = 500;
|
|
2818
3146
|
function computeArgsHash(args) {
|
|
2819
3147
|
const str = JSON.stringify(args ?? "");
|
|
2820
|
-
return
|
|
3148
|
+
return crypto2.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
2821
3149
|
}
|
|
2822
3150
|
function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
2823
3151
|
const hash = computeArgsHash(args);
|
|
@@ -2828,6 +3156,32 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
|
2828
3156
|
const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
|
|
2829
3157
|
return { nextRecords, count, looping: count >= threshold };
|
|
2830
3158
|
}
|
|
3159
|
+
var PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
3160
|
+
var PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
3161
|
+
var PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
3162
|
+
var PII_CC_RE = /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
|
|
3163
|
+
function detectPii(text) {
|
|
3164
|
+
const found = /* @__PURE__ */ new Set();
|
|
3165
|
+
if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
|
|
3166
|
+
if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
|
|
3167
|
+
if (PII_PHONE_RE.test(text)) found.add("Phone");
|
|
3168
|
+
if (PII_CC_RE.test(text)) found.add("Credit Card");
|
|
3169
|
+
return [...found];
|
|
3170
|
+
}
|
|
3171
|
+
var REALTIME_PII_PATTERNS = ["SSN", "Credit Card"];
|
|
3172
|
+
var MAX_PII_SCAN_BYTES = 1e5;
|
|
3173
|
+
function detectArgsPii(args) {
|
|
3174
|
+
if (args === null || args === void 0) return [];
|
|
3175
|
+
let text;
|
|
3176
|
+
try {
|
|
3177
|
+
text = typeof args === "string" ? args : JSON.stringify(args);
|
|
3178
|
+
} catch {
|
|
3179
|
+
return [];
|
|
3180
|
+
}
|
|
3181
|
+
if (typeof text !== "string") return [];
|
|
3182
|
+
if (text.length > MAX_PII_SCAN_BYTES) text = text.slice(0, MAX_PII_SCAN_BYTES);
|
|
3183
|
+
return detectPii(text).filter((p) => REALTIME_PII_PATTERNS.includes(p));
|
|
3184
|
+
}
|
|
2831
3185
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
2832
3186
|
|
|
2833
3187
|
// src/shields.ts
|
|
@@ -2930,7 +3284,8 @@ var DEFAULT_CONFIG = {
|
|
|
2930
3284
|
flightRecorder: true,
|
|
2931
3285
|
auditHashArgs: true,
|
|
2932
3286
|
approvers: { native: true, browser: false, cloud: false, terminal: true },
|
|
2933
|
-
cloudSyncIntervalHours: 5
|
|
3287
|
+
cloudSyncIntervalHours: 5,
|
|
3288
|
+
shipper: { enabled: true, intervalSeconds: 20 }
|
|
2934
3289
|
},
|
|
2935
3290
|
policy: {
|
|
2936
3291
|
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
@@ -3114,7 +3469,8 @@ var DEFAULT_CONFIG = {
|
|
|
3114
3469
|
description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
|
|
3115
3470
|
}
|
|
3116
3471
|
],
|
|
3117
|
-
dlp: { enabled: true, scanIgnoredTools: true },
|
|
3472
|
+
dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
|
|
3473
|
+
egress: { enabled: false, mode: "review", allow: [], deny: [], allowPrivate: true },
|
|
3118
3474
|
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
3119
3475
|
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
3120
3476
|
},
|
|
@@ -3225,7 +3581,8 @@ function getConfig(cwd) {
|
|
|
3225
3581
|
const projectConfig = tryLoadConfig(projectPath);
|
|
3226
3582
|
const mergedSettings = {
|
|
3227
3583
|
...DEFAULT_CONFIG.settings,
|
|
3228
|
-
approvers: { ...DEFAULT_CONFIG.settings.approvers }
|
|
3584
|
+
approvers: { ...DEFAULT_CONFIG.settings.approvers },
|
|
3585
|
+
shipper: { ...DEFAULT_CONFIG.settings.shipper }
|
|
3229
3586
|
};
|
|
3230
3587
|
const mergedPolicy = {
|
|
3231
3588
|
sandboxPaths: [...DEFAULT_CONFIG.policy.sandboxPaths],
|
|
@@ -3239,6 +3596,11 @@ function getConfig(cwd) {
|
|
|
3239
3596
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
3240
3597
|
},
|
|
3241
3598
|
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
3599
|
+
egress: {
|
|
3600
|
+
...DEFAULT_CONFIG.policy.egress,
|
|
3601
|
+
allow: [...DEFAULT_CONFIG.policy.egress.allow],
|
|
3602
|
+
deny: [...DEFAULT_CONFIG.policy.egress.deny]
|
|
3603
|
+
},
|
|
3242
3604
|
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
3243
3605
|
skillPinning: {
|
|
3244
3606
|
...DEFAULT_CONFIG.policy.skillPinning,
|
|
@@ -3256,6 +3618,7 @@ function getConfig(cwd) {
|
|
|
3256
3618
|
if (s.enableHookLogDebug !== void 0)
|
|
3257
3619
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
3258
3620
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
3621
|
+
if (s.shipper) mergedSettings.shipper = { ...mergedSettings.shipper, ...s.shipper };
|
|
3259
3622
|
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
3260
3623
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
3261
3624
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
@@ -3288,6 +3651,15 @@ function getConfig(cwd) {
|
|
|
3288
3651
|
const d = p.dlp;
|
|
3289
3652
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
3290
3653
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
3654
|
+
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
3655
|
+
}
|
|
3656
|
+
if (p.egress) {
|
|
3657
|
+
const e = p.egress;
|
|
3658
|
+
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
3659
|
+
if (e.mode !== void 0) mergedPolicy.egress.mode = e.mode;
|
|
3660
|
+
if (Array.isArray(e.allow)) mergedPolicy.egress.allow.push(...e.allow);
|
|
3661
|
+
if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
|
|
3662
|
+
if (e.allowPrivate !== void 0) mergedPolicy.egress.allowPrivate = e.allowPrivate;
|
|
3291
3663
|
}
|
|
3292
3664
|
if (p.loopDetection) {
|
|
3293
3665
|
const ld = p.loopDetection;
|
|
@@ -4030,6 +4402,15 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
4030
4402
|
var isTestEnv = () => {
|
|
4031
4403
|
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
4032
4404
|
};
|
|
4405
|
+
var MIN_INTERACTION_MS = 400;
|
|
4406
|
+
function resolveNativeDecision(opts) {
|
|
4407
|
+
const { code, output, elapsedMs, locked } = opts;
|
|
4408
|
+
if (locked) return "deny";
|
|
4409
|
+
const tooFast = elapsedMs < MIN_INTERACTION_MS;
|
|
4410
|
+
if (output.includes("Always Allow")) return tooFast ? "deny" : "always_allow";
|
|
4411
|
+
if (code === 0) return tooFast ? "deny" : "allow";
|
|
4412
|
+
return "deny";
|
|
4413
|
+
}
|
|
4033
4414
|
function formatArgs(args, matchedField, matchedWord) {
|
|
4034
4415
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
4035
4416
|
let parsed = args;
|
|
@@ -4173,6 +4554,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
4173
4554
|
);
|
|
4174
4555
|
return new Promise((resolve) => {
|
|
4175
4556
|
let childProcess = null;
|
|
4557
|
+
const startedAt = Date.now();
|
|
4176
4558
|
const onAbort = () => {
|
|
4177
4559
|
if (childProcess && childProcess.pid) {
|
|
4178
4560
|
try {
|
|
@@ -4230,13 +4612,14 @@ end run`;
|
|
|
4230
4612
|
}
|
|
4231
4613
|
let output = "";
|
|
4232
4614
|
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
4233
|
-
childProcess?.on("
|
|
4615
|
+
childProcess?.on("error", () => {
|
|
4234
4616
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
4235
|
-
if (locked) return resolve("deny");
|
|
4236
|
-
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
4237
|
-
if (code === 0) return resolve("allow");
|
|
4238
4617
|
resolve("deny");
|
|
4239
4618
|
});
|
|
4619
|
+
childProcess?.on("close", (code) => {
|
|
4620
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
4621
|
+
resolve(resolveNativeDecision({ code, output, elapsedMs: Date.now() - startedAt, locked }));
|
|
4622
|
+
});
|
|
4240
4623
|
} catch {
|
|
4241
4624
|
resolve("deny");
|
|
4242
4625
|
}
|
|
@@ -4251,91 +4634,6 @@ init_audit();
|
|
|
4251
4634
|
import fs9 from "fs";
|
|
4252
4635
|
import os8 from "os";
|
|
4253
4636
|
import path11 from "path";
|
|
4254
|
-
var DLP_SAMPLE_MAX_LEN = 200;
|
|
4255
|
-
var DLP_PATTERN_MAX_LEN = 100;
|
|
4256
|
-
var KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
|
|
4257
|
-
"dlp-block",
|
|
4258
|
-
"observe-mode-dlp-would-block",
|
|
4259
|
-
"dlp-review-flagged",
|
|
4260
|
-
"loop-detected",
|
|
4261
|
-
"audit-mode",
|
|
4262
|
-
"local-policy",
|
|
4263
|
-
"smart-rule-block",
|
|
4264
|
-
// Smart-rule block was downgraded to review because the daemon was
|
|
4265
|
-
// running and we're not in CI. The block attempt is still recorded;
|
|
4266
|
-
// the user got a popup. Distinct from 'smart-rule-block' so the
|
|
4267
|
-
// dashboard can show "block rule overridden" separately from a hard
|
|
4268
|
-
// block that fired with no human in the loop.
|
|
4269
|
-
"smart-rule-block-override",
|
|
4270
|
-
"persistent",
|
|
4271
|
-
"trust",
|
|
4272
|
-
"observe-mode",
|
|
4273
|
-
"observe-mode-would-block"
|
|
4274
|
-
]);
|
|
4275
|
-
function validateApiUrl(raw) {
|
|
4276
|
-
let u;
|
|
4277
|
-
try {
|
|
4278
|
-
u = new URL(raw);
|
|
4279
|
-
} catch {
|
|
4280
|
-
return null;
|
|
4281
|
-
}
|
|
4282
|
-
if (u.username || u.password) return null;
|
|
4283
|
-
if (u.protocol === "https:") return u;
|
|
4284
|
-
if (u.protocol === "http:") {
|
|
4285
|
-
const h = u.hostname;
|
|
4286
|
-
if (h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]") return u;
|
|
4287
|
-
}
|
|
4288
|
-
return null;
|
|
4289
|
-
}
|
|
4290
|
-
function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
|
|
4291
|
-
const validated = validateApiUrl(creds.apiUrl);
|
|
4292
|
-
if (!validated) {
|
|
4293
|
-
try {
|
|
4294
|
-
fs9.appendFileSync(
|
|
4295
|
-
HOOK_DEBUG_LOG,
|
|
4296
|
-
`[audit] refused to send: invalid apiUrl scheme/host (got "${String(creds.apiUrl).slice(0, 200)}")
|
|
4297
|
-
`
|
|
4298
|
-
);
|
|
4299
|
-
} catch {
|
|
4300
|
-
}
|
|
4301
|
-
return Promise.resolve();
|
|
4302
|
-
}
|
|
4303
|
-
const safeArgs = containsSensitiveArgs ? { tool: toolName, redacted: true } : args;
|
|
4304
|
-
const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
|
|
4305
|
-
const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
|
|
4306
|
-
const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
|
|
4307
|
-
const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
|
|
4308
|
-
Object.entries(riskMetadata).filter(([, v]) => typeof v === "string" && v.length > 0)
|
|
4309
|
-
) : void 0;
|
|
4310
|
-
const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
|
|
4311
|
-
return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
|
|
4312
|
-
method: "POST",
|
|
4313
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
4314
|
-
body: JSON.stringify({
|
|
4315
|
-
toolName,
|
|
4316
|
-
args: safeArgs,
|
|
4317
|
-
checkedBy: safeCheckedBy,
|
|
4318
|
-
...dlpInfo && { dlpPattern, dlpSample },
|
|
4319
|
-
...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
|
|
4320
|
-
// session_id (Claude Code + Gemini CLI) groups all audit rows from one
|
|
4321
|
-
// agent run; transcript_path is the authoritative pointer to the
|
|
4322
|
-
// session log (survives Gemini resume drift). Both optional —
|
|
4323
|
-
// unsupported agents (MCP-mediated) leave them undefined.
|
|
4324
|
-
...meta?.sessionId && { runId: meta.sessionId },
|
|
4325
|
-
...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
|
|
4326
|
-
context: {
|
|
4327
|
-
agent: meta?.agent,
|
|
4328
|
-
mcpServer: meta?.mcpServer,
|
|
4329
|
-
hostname: os8.hostname(),
|
|
4330
|
-
cwd: process.cwd(),
|
|
4331
|
-
platform: os8.platform()
|
|
4332
|
-
}
|
|
4333
|
-
}),
|
|
4334
|
-
signal: AbortSignal.timeout(5e3)
|
|
4335
|
-
}).then(() => {
|
|
4336
|
-
}).catch(() => {
|
|
4337
|
-
});
|
|
4338
|
-
}
|
|
4339
4637
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
|
|
4340
4638
|
const controller = new AbortController();
|
|
4341
4639
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -4623,6 +4921,37 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4623
4921
|
if (taintResult.tainted && taintResult.record) {
|
|
4624
4922
|
const { path: taintedPath, source: taintSource } = taintResult.record;
|
|
4625
4923
|
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
4924
|
+
if (config.policy.egress?.enabled) {
|
|
4925
|
+
const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
4926
|
+
const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
|
|
4927
|
+
const dests = cmd ? extractShellDestinations(cmd) : [];
|
|
4928
|
+
const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
|
|
4929
|
+
if (eg) {
|
|
4930
|
+
if (!isManual)
|
|
4931
|
+
appendLocalAudit(
|
|
4932
|
+
toolName,
|
|
4933
|
+
args,
|
|
4934
|
+
"deny",
|
|
4935
|
+
isObserveMode ? "observe-mode-taint-egress-would-block" : "taint-egress-block",
|
|
4936
|
+
{ ...meta, ruleName: `taint-egress:${eg.host}` },
|
|
4937
|
+
hashAuditArgs
|
|
4938
|
+
);
|
|
4939
|
+
if (isObserveMode) {
|
|
4940
|
+
return {
|
|
4941
|
+
approved: true,
|
|
4942
|
+
checkedBy: "audit",
|
|
4943
|
+
observeWouldBlock: true,
|
|
4944
|
+
blockedByLabel: "\u{1F534} Node9 Taint+Egress (Exfiltration)"
|
|
4945
|
+
};
|
|
4946
|
+
}
|
|
4947
|
+
return {
|
|
4948
|
+
approved: false,
|
|
4949
|
+
reason: `\u{1F534} EXFILTRATION BLOCKED: the tainted file "${taintedPath}" is being sent to untrusted host "${eg.host}". A flagged file leaving to an unrecognized destination is blocked outright.`,
|
|
4950
|
+
blockedBy: "local-config",
|
|
4951
|
+
blockedByLabel: "\u{1F534} Node9 Taint+Egress (Exfiltration Blocked)"
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4626
4955
|
} else if (taintResult.daemonUnavailable) {
|
|
4627
4956
|
taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
|
|
4628
4957
|
}
|
|
@@ -4641,17 +4970,11 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4641
4970
|
args,
|
|
4642
4971
|
"deny",
|
|
4643
4972
|
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
toolName,
|
|
4650
|
-
args,
|
|
4651
|
-
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
4652
|
-
creds,
|
|
4653
|
-
meta,
|
|
4654
|
-
{ pattern: dlpMatch.patternName, redactedSample: dlpMatch.redactedSample },
|
|
4973
|
+
{
|
|
4974
|
+
...meta,
|
|
4975
|
+
dlpPattern: dlpMatch.patternName,
|
|
4976
|
+
dlpSample: dlpMatch.redactedSample
|
|
4977
|
+
},
|
|
4655
4978
|
true
|
|
4656
4979
|
);
|
|
4657
4980
|
if (isWriteTool(toolName) && filePath) {
|
|
@@ -4677,6 +5000,35 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4677
5000
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
4678
5001
|
}
|
|
4679
5002
|
}
|
|
5003
|
+
if (config.policy.dlp.pii === "block" && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
5004
|
+
const piiFound = detectArgsPii(args);
|
|
5005
|
+
if (piiFound.length > 0) {
|
|
5006
|
+
const piiReason = `\u{1F512} PII DETECTED: ${piiFound.join(", ")} found in tool arguments. Remove or tokenize personal data before passing it to a tool.`;
|
|
5007
|
+
if (!isManual)
|
|
5008
|
+
appendLocalAudit(
|
|
5009
|
+
toolName,
|
|
5010
|
+
args,
|
|
5011
|
+
"deny",
|
|
5012
|
+
isObserveMode ? "observe-mode-pii-would-block" : "pii-block",
|
|
5013
|
+
{ ...meta, piiPatterns: piiFound.join(",") },
|
|
5014
|
+
true
|
|
5015
|
+
);
|
|
5016
|
+
if (isObserveMode) {
|
|
5017
|
+
return {
|
|
5018
|
+
approved: true,
|
|
5019
|
+
checkedBy: "audit",
|
|
5020
|
+
observeWouldBlock: true,
|
|
5021
|
+
blockedByLabel: "\u{1F512} Node9 PII (Detected)"
|
|
5022
|
+
};
|
|
5023
|
+
}
|
|
5024
|
+
return {
|
|
5025
|
+
approved: false,
|
|
5026
|
+
reason: piiReason,
|
|
5027
|
+
blockedBy: "local-config",
|
|
5028
|
+
blockedByLabel: "\u{1F512} Node9 PII (Detected)"
|
|
5029
|
+
};
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
4680
5032
|
if (isObserveMode) {
|
|
4681
5033
|
if (!isIgnoredTool2(toolName)) {
|
|
4682
5034
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
@@ -4707,9 +5059,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4707
5059
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
4708
5060
|
if (policyResult.decision === "review") {
|
|
4709
5061
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
4710
|
-
if (approvers.cloud && creds?.apiKey) {
|
|
4711
|
-
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
4712
|
-
}
|
|
4713
5062
|
}
|
|
4714
5063
|
}
|
|
4715
5064
|
return { approved: true, checkedBy: "audit" };
|
|
@@ -4722,8 +5071,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4722
5071
|
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?`;
|
|
4723
5072
|
if (!isManual)
|
|
4724
5073
|
appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
|
|
4725
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4726
|
-
auditLocalAllow(toolName, args, "loop-detected", creds, meta, void 0, true);
|
|
4727
5074
|
return {
|
|
4728
5075
|
approved: false,
|
|
4729
5076
|
reason,
|
|
@@ -4742,15 +5089,15 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4742
5089
|
};
|
|
4743
5090
|
}
|
|
4744
5091
|
if (policyResult.decision === "allow") {
|
|
4745
|
-
if (
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
5092
|
+
if (!isManual)
|
|
5093
|
+
appendLocalAudit(
|
|
5094
|
+
toolName,
|
|
5095
|
+
args,
|
|
5096
|
+
"allow",
|
|
5097
|
+
"local-policy",
|
|
5098
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
5099
|
+
hashAuditArgs
|
|
5100
|
+
);
|
|
4754
5101
|
return { approved: true, checkedBy: "local-policy" };
|
|
4755
5102
|
}
|
|
4756
5103
|
if (policyResult.decision === "block") {
|
|
@@ -4783,23 +5130,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4783
5130
|
{ ...meta, ruleName: policyResult.ruleName },
|
|
4784
5131
|
hashAuditArgs
|
|
4785
5132
|
);
|
|
4786
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4787
|
-
auditLocalAllow(
|
|
4788
|
-
toolName,
|
|
4789
|
-
args,
|
|
4790
|
-
"smart-rule-block-override",
|
|
4791
|
-
creds,
|
|
4792
|
-
meta,
|
|
4793
|
-
void 0,
|
|
4794
|
-
false,
|
|
4795
|
-
{
|
|
4796
|
-
ruleName: policyResult.ruleName,
|
|
4797
|
-
ruleDescription: policyResult.ruleDescription,
|
|
4798
|
-
blockedByLabel: policyResult.blockedByLabel,
|
|
4799
|
-
matchedField: policyResult.matchedField,
|
|
4800
|
-
matchedWord: policyResult.matchedWord
|
|
4801
|
-
}
|
|
4802
|
-
);
|
|
4803
5133
|
const baseLabel = policyResult.blockedByLabel || "Smart Rule";
|
|
4804
5134
|
const OVERRIDE_PREFIX = "\u26A0\uFE0F Override block rule: ";
|
|
4805
5135
|
if (!baseLabel.startsWith(OVERRIDE_PREFIX)) {
|
|
@@ -4824,14 +5154,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4824
5154
|
{ ...meta, ruleName: policyResult.ruleName },
|
|
4825
5155
|
hashAuditArgs
|
|
4826
5156
|
);
|
|
4827
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4828
|
-
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
|
|
4829
|
-
ruleName: policyResult.ruleName,
|
|
4830
|
-
ruleDescription: policyResult.ruleDescription,
|
|
4831
|
-
blockedByLabel: policyResult.blockedByLabel,
|
|
4832
|
-
matchedField: policyResult.matchedField,
|
|
4833
|
-
matchedWord: policyResult.matchedWord
|
|
4834
|
-
});
|
|
4835
5157
|
return {
|
|
4836
5158
|
approved: false,
|
|
4837
5159
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -4860,8 +5182,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4860
5182
|
if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
|
|
4861
5183
|
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
4862
5184
|
if (persistent === "allow") {
|
|
4863
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4864
|
-
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
4865
5185
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
4866
5186
|
return { approved: true, checkedBy: "persistent" };
|
|
4867
5187
|
}
|
|
@@ -4894,8 +5214,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4894
5214
|
}
|
|
4895
5215
|
}
|
|
4896
5216
|
if (!taintWarning && getActiveTrustSession(toolName, args)) {
|
|
4897
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4898
|
-
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
4899
5217
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
4900
5218
|
return { approved: true, checkedBy: "trust" };
|
|
4901
5219
|
}
|
|
@@ -5140,7 +5458,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
5140
5458
|
args,
|
|
5141
5459
|
finalResult.approved ? "allow" : "deny",
|
|
5142
5460
|
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
5143
|
-
|
|
5461
|
+
// cloudRequestId links this row to the BE-origin AuditLog row the
|
|
5462
|
+
// /intercept handshake created — the shipper hands it to the SaaS so
|
|
5463
|
+
// the BE enriches that row instead of inserting a duplicate. Matters
|
|
5464
|
+
// for EVERY racer outcome, not just cloud wins: a native-popup
|
|
5465
|
+
// decision on a cloud-pending request would otherwise count twice.
|
|
5466
|
+
cloudRequestId ? { ...meta, cloudRequestId } : meta,
|
|
5144
5467
|
hashAuditArgs
|
|
5145
5468
|
);
|
|
5146
5469
|
}
|