@node9/proxy 2.31.3 → 2.33.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 +5067 -2626
- package/dist/cli.mjs +4972 -2532
- package/dist/dashboard.mjs +545 -45
- package/dist/index.js +2106 -382
- package/dist/index.mjs +2102 -378
- package/dist/scan-ink.mjs +268 -185
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -58,6 +58,7 @@ import safeRegex3 from "safe-regex2";
|
|
|
58
58
|
import { createHash as createHash2 } from "crypto";
|
|
59
59
|
import { createHash as createHash22 } from "crypto";
|
|
60
60
|
import mvdanSh3 from "mvdan-sh";
|
|
61
|
+
import mvdanSh4 from "mvdan-sh";
|
|
61
62
|
import crypto from "crypto";
|
|
62
63
|
function isShieldVerdict(v) {
|
|
63
64
|
return v === "allow" || v === "review" || v === "block";
|
|
@@ -207,10 +208,10 @@ function compressIpv6(g) {
|
|
|
207
208
|
if (bestLen < 2) return hex.join(":");
|
|
208
209
|
return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
|
|
209
210
|
}
|
|
210
|
-
function normalizeIpLiteral(
|
|
211
|
+
function normalizeIpLiteral(host2) {
|
|
211
212
|
try {
|
|
212
|
-
if (typeof
|
|
213
|
-
let s =
|
|
213
|
+
if (typeof host2 !== "string") return null;
|
|
214
|
+
let s = host2.trim();
|
|
214
215
|
if (!s || s.length > SSRF_MAX_HOST) return null;
|
|
215
216
|
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
216
217
|
const zone = s.indexOf("%");
|
|
@@ -236,11 +237,11 @@ function isStrictGatedTier(tier) {
|
|
|
236
237
|
function ssrfReason(m, asWritten) {
|
|
237
238
|
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
238
239
|
}
|
|
239
|
-
function classifySsrf(
|
|
240
|
+
function classifySsrf(host2) {
|
|
240
241
|
try {
|
|
241
|
-
if (typeof
|
|
242
|
-
const lower2 =
|
|
243
|
-
const ip = normalizeIpLiteral(
|
|
242
|
+
if (typeof host2 !== "string" || !host2) return null;
|
|
243
|
+
const lower2 = host2.trim().toLowerCase().replace(/\.$/, "");
|
|
244
|
+
const ip = normalizeIpLiteral(host2);
|
|
244
245
|
if (ip === null) {
|
|
245
246
|
return METADATA_HOSTNAMES.has(lower2) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
|
|
246
247
|
}
|
|
@@ -442,6 +443,7 @@ function catalogSettingsFromConfig(settings, policy) {
|
|
|
442
443
|
injectionScan: policy.injectionScan,
|
|
443
444
|
skillPinning: policy.skillPinning,
|
|
444
445
|
packageCheck: policy.packageCheck,
|
|
446
|
+
untrustedContent: policy.untrustedContent,
|
|
445
447
|
appliedShields: policy.appliedShields ?? [],
|
|
446
448
|
checks: policy.checks
|
|
447
449
|
};
|
|
@@ -949,11 +951,11 @@ function inlineExecStmt(stmt, pipeFed) {
|
|
|
949
951
|
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
950
952
|
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
951
953
|
}
|
|
952
|
-
let
|
|
954
|
+
let positionals2 = 0;
|
|
953
955
|
let selectedProgram = false;
|
|
954
956
|
for (const a of args) {
|
|
955
957
|
if (a == null) {
|
|
956
|
-
|
|
958
|
+
positionals2++;
|
|
957
959
|
selectedProgram = true;
|
|
958
960
|
continue;
|
|
959
961
|
}
|
|
@@ -964,13 +966,13 @@ function inlineExecStmt(stmt, pipeFed) {
|
|
|
964
966
|
}
|
|
965
967
|
if (a === "-" && !selectedProgram) return true;
|
|
966
968
|
if (!a.startsWith("-")) {
|
|
967
|
-
|
|
969
|
+
positionals2++;
|
|
968
970
|
selectedProgram = true;
|
|
969
971
|
}
|
|
970
972
|
}
|
|
971
973
|
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
972
974
|
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
973
|
-
if (
|
|
975
|
+
if (positionals2 === 0 && (stdinFed || pipeFed)) {
|
|
974
976
|
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
975
977
|
}
|
|
976
978
|
return false;
|
|
@@ -1180,6 +1182,24 @@ function resolveWordLiteral(w) {
|
|
|
1180
1182
|
}
|
|
1181
1183
|
return s;
|
|
1182
1184
|
}
|
|
1185
|
+
function resolveWordPrefix(w) {
|
|
1186
|
+
const walk2 = (parts) => {
|
|
1187
|
+
let s = "";
|
|
1188
|
+
for (const p of parts) {
|
|
1189
|
+
if (p && typeof p === "object" && Array.isArray(p.Parts) && p.Dollar !== void 0) {
|
|
1190
|
+
const inner = walk2(p.Parts);
|
|
1191
|
+
s += inner.prefix;
|
|
1192
|
+
if (inner.dynamic) return { prefix: s, dynamic: true };
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const piece = resolvePart(p, false);
|
|
1196
|
+
if (piece === void 0 || piece === null) return { prefix: s, dynamic: true };
|
|
1197
|
+
s += piece;
|
|
1198
|
+
}
|
|
1199
|
+
return { prefix: s, dynamic: false };
|
|
1200
|
+
};
|
|
1201
|
+
return walk2(w?.Parts || []);
|
|
1202
|
+
}
|
|
1183
1203
|
function resolvePart(p, inQuotes) {
|
|
1184
1204
|
const t = syntax.NodeType(p);
|
|
1185
1205
|
if (t === "Lit") {
|
|
@@ -1227,7 +1247,7 @@ function parseDestHost(token) {
|
|
|
1227
1247
|
}
|
|
1228
1248
|
function destTokensForBinary(binary, args) {
|
|
1229
1249
|
const valueFlags = VALUE_FLAGS[binary] ?? /* @__PURE__ */ new Set();
|
|
1230
|
-
const
|
|
1250
|
+
const positionals2 = [];
|
|
1231
1251
|
const urlFlagValues = [];
|
|
1232
1252
|
for (let i = 0; i < args.length; i++) {
|
|
1233
1253
|
const tok = args[i];
|
|
@@ -1247,21 +1267,21 @@ function destTokensForBinary(binary, args) {
|
|
|
1247
1267
|
if (valueFlags.has(tok)) i++;
|
|
1248
1268
|
continue;
|
|
1249
1269
|
}
|
|
1250
|
-
|
|
1270
|
+
positionals2.push(tok);
|
|
1251
1271
|
}
|
|
1252
1272
|
switch (binary) {
|
|
1253
1273
|
case "curl":
|
|
1254
1274
|
case "wget":
|
|
1255
|
-
return [...urlFlagValues, ...
|
|
1275
|
+
return [...urlFlagValues, ...positionals2];
|
|
1256
1276
|
case "ssh":
|
|
1257
|
-
return
|
|
1277
|
+
return positionals2.slice(0, 1);
|
|
1258
1278
|
case "scp":
|
|
1259
1279
|
case "rsync":
|
|
1260
|
-
return
|
|
1280
|
+
return positionals2.filter((p) => p.includes(":") || p.includes("@"));
|
|
1261
1281
|
case "nc":
|
|
1262
1282
|
case "ncat":
|
|
1263
1283
|
case "netcat":
|
|
1264
|
-
return
|
|
1284
|
+
return positionals2.slice(0, 1);
|
|
1265
1285
|
default:
|
|
1266
1286
|
return [];
|
|
1267
1287
|
}
|
|
@@ -1278,16 +1298,18 @@ function extractShellDestinations(command) {
|
|
|
1278
1298
|
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1279
1299
|
const callArgs = n.Args || [];
|
|
1280
1300
|
if (callArgs.length === 0) return true;
|
|
1281
|
-
const
|
|
1301
|
+
const words = callArgs.map((a) => resolveWordLiteral(a));
|
|
1302
|
+
const at = unwrapCommandHead(words);
|
|
1303
|
+
const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
|
|
1282
1304
|
if (!NET_BINARIES.has(name)) return true;
|
|
1283
|
-
const rest =
|
|
1305
|
+
const rest = words.slice(at + 1);
|
|
1284
1306
|
for (const raw of destTokensForBinary(name, rest)) {
|
|
1285
|
-
const
|
|
1286
|
-
if (!
|
|
1287
|
-
const key = `${name}:${
|
|
1307
|
+
const host2 = parseDestHost(raw);
|
|
1308
|
+
if (!host2) continue;
|
|
1309
|
+
const key = `${name}:${host2}`;
|
|
1288
1310
|
if (seen.has(key)) continue;
|
|
1289
1311
|
seen.add(key);
|
|
1290
|
-
out.push({ host, binary: name, raw });
|
|
1312
|
+
out.push({ host: host2, binary: name, raw });
|
|
1291
1313
|
}
|
|
1292
1314
|
return true;
|
|
1293
1315
|
});
|
|
@@ -2047,9 +2069,9 @@ function matchesPattern(text, patterns) {
|
|
|
2047
2069
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2048
2070
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2049
2071
|
}
|
|
2050
|
-
function getNestedValue(obj,
|
|
2072
|
+
function getNestedValue(obj, path26) {
|
|
2051
2073
|
if (!obj || typeof obj !== "object") return null;
|
|
2052
|
-
const segments2 =
|
|
2074
|
+
const segments2 = path26.split(".");
|
|
2053
2075
|
for (const seg of segments2) {
|
|
2054
2076
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2055
2077
|
}
|
|
@@ -2125,32 +2147,109 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2125
2147
|
function isBashTool(tool) {
|
|
2126
2148
|
return BASH_TOOL_NAMES.has(tool.toLowerCase());
|
|
2127
2149
|
}
|
|
2128
|
-
function
|
|
2150
|
+
function inspectionLayers(policy) {
|
|
2151
|
+
return { trusted: policy.toolInspection, project: policy.projectToolInspection };
|
|
2152
|
+
}
|
|
2153
|
+
function isShellEntry(value) {
|
|
2154
|
+
return typeof value === "object" && value !== null && value.kind === "shell" && typeof value.field === "string" && Object.keys(value).length === 2;
|
|
2155
|
+
}
|
|
2156
|
+
function isInspectionLayers(input) {
|
|
2157
|
+
const trusted = input.trusted;
|
|
2158
|
+
return typeof trusted === "object" && trusted !== null && !isShellEntry(trusted);
|
|
2159
|
+
}
|
|
2160
|
+
function layersOf(input) {
|
|
2161
|
+
return isInspectionLayers(input) ? { trusted: input.trusted, project: input.project ?? {} } : { trusted: input, project: {} };
|
|
2162
|
+
}
|
|
2163
|
+
function entryOf(tool, inspection) {
|
|
2129
2164
|
const pattern = Object.keys(inspection).find((p) => matchesPattern(tool, p));
|
|
2130
2165
|
return pattern === void 0 ? void 0 : inspection[pattern];
|
|
2131
2166
|
}
|
|
2167
|
+
function inspectionEntry(tool, inspection = {}) {
|
|
2168
|
+
const { trusted, project } = layersOf(inspection);
|
|
2169
|
+
return entryOf(tool, trusted) ?? entryOf(tool, project);
|
|
2170
|
+
}
|
|
2171
|
+
function executorName(tool) {
|
|
2172
|
+
const name = tool.toLowerCase();
|
|
2173
|
+
if (!name.startsWith("mcp__")) return name;
|
|
2174
|
+
const at = name.lastIndexOf("__");
|
|
2175
|
+
return at >= "mcp__".length ? name.slice(at + 2) : name;
|
|
2176
|
+
}
|
|
2177
|
+
function entryFields(entry) {
|
|
2178
|
+
if (typeof entry === "object") return [entry.field];
|
|
2179
|
+
return entry === "command" ? ["command"] : null;
|
|
2180
|
+
}
|
|
2132
2181
|
function shellFields(tool, inspection) {
|
|
2133
2182
|
if (isBashTool(tool)) return ["command"];
|
|
2134
|
-
const
|
|
2135
|
-
|
|
2136
|
-
if (
|
|
2137
|
-
const
|
|
2138
|
-
|
|
2183
|
+
const { trusted, project } = layersOf(inspection);
|
|
2184
|
+
const trustedEntry = entryOf(tool, trusted);
|
|
2185
|
+
if (trustedEntry !== void 0) return entryFields(trustedEntry);
|
|
2186
|
+
const builtin = MCP_EXECUTORS.has(executorName(tool)) ? BUILTIN_FIELDS : null;
|
|
2187
|
+
const projectEntry = entryOf(tool, project);
|
|
2188
|
+
if (projectEntry === void 0) return builtin;
|
|
2189
|
+
const added = entryFields(projectEntry);
|
|
2190
|
+
if (!builtin) return added;
|
|
2191
|
+
if (!added) return builtin;
|
|
2192
|
+
return [.../* @__PURE__ */ new Set([...builtin, ...added])];
|
|
2139
2193
|
}
|
|
2140
2194
|
function isShellShapedTool(tool, inspection = {}) {
|
|
2141
2195
|
return shellFields(tool, inspection) !== null;
|
|
2142
2196
|
}
|
|
2197
|
+
function shellWord(word) {
|
|
2198
|
+
if (BARE_WORD.test(word)) return word;
|
|
2199
|
+
return word === "" ? "''" : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
2200
|
+
}
|
|
2201
|
+
function isStringArray(value) {
|
|
2202
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2203
|
+
}
|
|
2204
|
+
function commandLine(args, fieldPath, value, native) {
|
|
2205
|
+
const missing = { reason: "Shell command field is missing or is not a string" };
|
|
2206
|
+
const unreadable = { reason: "Shell command arguments could not be read" };
|
|
2207
|
+
if (typeof value !== "string" && (native || !Array.isArray(value))) return missing;
|
|
2208
|
+
const dot = fieldPath.lastIndexOf(".");
|
|
2209
|
+
const parent = dot === -1 ? args : getNestedValue(args, fieldPath.slice(0, dot));
|
|
2210
|
+
const siblings = ARGV_FIELDS.map((key) => ({
|
|
2211
|
+
key,
|
|
2212
|
+
value: parent && typeof parent === "object" ? parent[key] : void 0
|
|
2213
|
+
})).filter(({ value: value2 }) => value2 !== void 0 && value2 !== null);
|
|
2214
|
+
let argv = siblings[0];
|
|
2215
|
+
if (native) argv = siblings.find(({ value: v }) => isStringArray(v));
|
|
2216
|
+
else if (siblings.length === 2 && JSON.stringify(siblings[0].value) !== JSON.stringify(siblings[1].value))
|
|
2217
|
+
return { reason: "Shell command arguments disagree" };
|
|
2218
|
+
let command;
|
|
2219
|
+
let source = fieldPath;
|
|
2220
|
+
if (Array.isArray(value)) {
|
|
2221
|
+
if (argv || !value.length || !isStringArray(value)) return unreadable;
|
|
2222
|
+
command = value.map(shellWord).join(" ");
|
|
2223
|
+
} else if (!argv) {
|
|
2224
|
+
return { command: value, fieldPath };
|
|
2225
|
+
} else {
|
|
2226
|
+
if (!isStringArray(argv.value)) return unreadable;
|
|
2227
|
+
if (!argv.value.length) return { command: value, fieldPath };
|
|
2228
|
+
command = `${value} ${argv.value.map(shellWord).join(" ")}`;
|
|
2229
|
+
source = `${fieldPath}+${argv.key}`;
|
|
2230
|
+
}
|
|
2231
|
+
if (command.length > MAX_ARGV_CHARACTERS)
|
|
2232
|
+
return { reason: "Shell command arguments exceed the inspectable size" };
|
|
2233
|
+
return { command, fieldPath: source };
|
|
2234
|
+
}
|
|
2143
2235
|
function resolveShellInput(tool, args, inspection = {}) {
|
|
2144
2236
|
const fields = shellFields(tool, inspection);
|
|
2145
2237
|
if (!fields) return { kind: "not-shell" };
|
|
2146
2238
|
const values = fields.map((fieldPath) => ({ fieldPath, value: getNestedValue(args, fieldPath) })).filter(({ value }) => value !== void 0 && value !== null && value !== "");
|
|
2147
|
-
if (!values.length
|
|
2239
|
+
if (!values.length) {
|
|
2148
2240
|
return { kind: "uninspectable", reason: "Shell command field is missing or is not a string" };
|
|
2149
2241
|
}
|
|
2150
|
-
|
|
2242
|
+
const native = isBashTool(tool);
|
|
2243
|
+
const lines = values.map(({ fieldPath, value }) => commandLine(args, fieldPath, value, native));
|
|
2244
|
+
const failed = lines.find((line) => "reason" in line);
|
|
2245
|
+
if (failed) return { kind: "uninspectable", reason: failed.reason };
|
|
2246
|
+
const commands = lines.filter(
|
|
2247
|
+
(line) => "command" in line
|
|
2248
|
+
);
|
|
2249
|
+
if (new Set(commands.map(({ command }) => command)).size !== 1) {
|
|
2151
2250
|
return { kind: "uninspectable", reason: "Multiple shell command fields disagree" };
|
|
2152
2251
|
}
|
|
2153
|
-
return { kind: "shell",
|
|
2252
|
+
return { kind: "shell", ...commands[0] };
|
|
2154
2253
|
}
|
|
2155
2254
|
function shellRuleArgs(tool, args, ruleTool, inspection = {}) {
|
|
2156
2255
|
if (!ruleTool || !matchesPattern("bash", ruleTool) || matchesPattern(tool, ruleTool)) return args;
|
|
@@ -2376,7 +2475,7 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
2376
2475
|
if (!hasScript) hasScript = true;
|
|
2377
2476
|
else files.push(token);
|
|
2378
2477
|
}
|
|
2379
|
-
for (const
|
|
2478
|
+
for (const file2 of files) add(inplace ? "write" : "read", file2, state);
|
|
2380
2479
|
return;
|
|
2381
2480
|
}
|
|
2382
2481
|
if (FS_READ_TOOLS.has(name) && name !== "dd") {
|
|
@@ -2728,6 +2827,17 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
2728
2827
|
if (program) statements(program.Stmts ?? [], initial, 0);
|
|
2729
2828
|
return out;
|
|
2730
2829
|
}
|
|
2830
|
+
function isAgentInstructionPath(absPath, root) {
|
|
2831
|
+
const r = root.replace(/\/$/, "");
|
|
2832
|
+
if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
|
|
2833
|
+
const rel = absPath.slice(r.length + 1);
|
|
2834
|
+
if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
|
|
2835
|
+
return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
2836
|
+
}
|
|
2837
|
+
function isInside(root, p) {
|
|
2838
|
+
const r = root.replace(/\/$/, "") || "/";
|
|
2839
|
+
return p === r || p.startsWith(r === "/" ? "/" : r + "/");
|
|
2840
|
+
}
|
|
2731
2841
|
function isPasswordHashPath(p) {
|
|
2732
2842
|
return PASSWORD_HASH_FILES.includes(p);
|
|
2733
2843
|
}
|
|
@@ -2797,8 +2907,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
|
|
|
2797
2907
|
}
|
|
2798
2908
|
return out;
|
|
2799
2909
|
}
|
|
2800
|
-
function within(
|
|
2801
|
-
return
|
|
2910
|
+
function within(path26, root) {
|
|
2911
|
+
return path26 === root || path26.startsWith(root === "/" ? "/" : root + "/");
|
|
2802
2912
|
}
|
|
2803
2913
|
function hits(access, paths, directories, context) {
|
|
2804
2914
|
const home = context.homeDirs?.[0] ?? "~";
|
|
@@ -2880,20 +2990,25 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
|
|
|
2880
2990
|
}
|
|
2881
2991
|
return best;
|
|
2882
2992
|
}
|
|
2883
|
-
function findMnemonic(text, budget = {
|
|
2884
|
-
const words = [...text.matchAll(/[a-zA-Z]+/g)];
|
|
2993
|
+
function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new Set() }) {
|
|
2885
2994
|
let run = [];
|
|
2995
|
+
let runIndex = [];
|
|
2886
2996
|
let previousEnd = 0;
|
|
2887
2997
|
const rejected = /* @__PURE__ */ new Set();
|
|
2888
2998
|
const inspect = () => {
|
|
2999
|
+
if (run.length < 12) return false;
|
|
3000
|
+
const ascending = new Array(run.length).fill(0);
|
|
3001
|
+
for (let i = 1; i < run.length; i++)
|
|
3002
|
+
ascending[i] = ascending[i - 1] + (runIndex[i] > runIndex[i - 1] ? 1 : 0);
|
|
2889
3003
|
for (let start = 0; start + 12 <= run.length; start++) {
|
|
2890
3004
|
for (const count of lengths) {
|
|
2891
3005
|
if (start + count > run.length) continue;
|
|
3006
|
+
if (ascending[start + count - 1] - ascending[start] === count - 1) continue;
|
|
2892
3007
|
const candidate = run.slice(start, start + count);
|
|
2893
3008
|
const sentence = candidate.join(" ");
|
|
2894
3009
|
if (rejected.has(sentence)) continue;
|
|
2895
|
-
if (++budget.
|
|
2896
|
-
budget.reasons.add("mnemonic-
|
|
3010
|
+
if (++budget.hashes > MAX_MNEMONIC_HASHES) {
|
|
3011
|
+
budget.reasons.add("mnemonic-budget");
|
|
2897
3012
|
return false;
|
|
2898
3013
|
}
|
|
2899
3014
|
if (BIP39_PUBLIC_VECTORS.has(hash(sentence).toString("hex"))) {
|
|
@@ -2911,13 +3026,18 @@ function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ n
|
|
|
2911
3026
|
}
|
|
2912
3027
|
return false;
|
|
2913
3028
|
};
|
|
2914
|
-
for (const word of
|
|
2915
|
-
|
|
3029
|
+
for (const word of text.matchAll(/[a-zA-Z]+/g)) {
|
|
3030
|
+
const index = indices.get(word[0]);
|
|
3031
|
+
if (index === void 0 || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
|
|
2916
3032
|
if (inspect()) return true;
|
|
2917
|
-
if (budget.reasons.has("mnemonic-
|
|
3033
|
+
if (budget.reasons.has("mnemonic-budget")) return false;
|
|
2918
3034
|
run = [];
|
|
3035
|
+
runIndex = [];
|
|
3036
|
+
}
|
|
3037
|
+
if (index !== void 0) {
|
|
3038
|
+
run.push(word[0]);
|
|
3039
|
+
runIndex.push(index);
|
|
2919
3040
|
}
|
|
2920
|
-
if (indices.has(word[0])) run.push(word[0]);
|
|
2921
3041
|
previousEnd = word.index + word[0].length;
|
|
2922
3042
|
}
|
|
2923
3043
|
return inspect();
|
|
@@ -3048,6 +3168,9 @@ function validateCask(raw) {
|
|
|
3048
3168
|
const second = b64urlIndex(s[tsAt + 5]);
|
|
3049
3169
|
return month <= 11 && day <= 30 && hour <= 23 && minute <= 59 && second <= 59;
|
|
3050
3170
|
}
|
|
3171
|
+
function isInvisibleChar(char) {
|
|
3172
|
+
return INVISIBLE_ONE.test(char);
|
|
3173
|
+
}
|
|
3051
3174
|
function stripInvisible(text) {
|
|
3052
3175
|
return text.replace(INVISIBLE_RE, "");
|
|
3053
3176
|
}
|
|
@@ -3234,6 +3357,45 @@ function matchArgumentText(text, fieldPath, budget) {
|
|
|
3234
3357
|
}
|
|
3235
3358
|
return best;
|
|
3236
3359
|
}
|
|
3360
|
+
function matchArgumentBatch(fragments, fieldPath, budget) {
|
|
3361
|
+
if (!fragments.length) return null;
|
|
3362
|
+
const text = fragments.join(BATCH_SEPARATOR);
|
|
3363
|
+
const lower2 = text.toLowerCase();
|
|
3364
|
+
let best = null;
|
|
3365
|
+
for (const pattern of DLP_PATTERNS) {
|
|
3366
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => lower2.includes(kw.toLowerCase())))
|
|
3367
|
+
continue;
|
|
3368
|
+
const flags = pattern.regex.flags.replace(/[gmy]/g, "");
|
|
3369
|
+
if (!new RegExp(pattern.regex.source, flags + "m").test(text)) continue;
|
|
3370
|
+
for (const fragment of fragments) {
|
|
3371
|
+
if (firstAcceptedMatch(pattern, fragment) === null) continue;
|
|
3372
|
+
const severity = pattern.contextBoost && isAssignmentContext(fragment) ? "block" : pattern.severity;
|
|
3373
|
+
if (severity === "block")
|
|
3374
|
+
return {
|
|
3375
|
+
patternName: pattern.name,
|
|
3376
|
+
fieldPath,
|
|
3377
|
+
redactedSample: "[encoded secret]",
|
|
3378
|
+
severity
|
|
3379
|
+
};
|
|
3380
|
+
best ??= {
|
|
3381
|
+
patternName: pattern.name,
|
|
3382
|
+
fieldPath,
|
|
3383
|
+
redactedSample: "[encoded secret]",
|
|
3384
|
+
severity
|
|
3385
|
+
};
|
|
3386
|
+
if (!pattern.contextBoost) break;
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
if (findMnemonic(text, budget)) {
|
|
3390
|
+
return {
|
|
3391
|
+
patternName: "BIP-39 Recovery Phrase",
|
|
3392
|
+
fieldPath,
|
|
3393
|
+
redactedSample: "[recovery phrase]",
|
|
3394
|
+
severity: "block"
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
return best;
|
|
3398
|
+
}
|
|
3237
3399
|
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
3238
3400
|
if (depth > MAX_DEPTH2 || args === null || args === void 0) return null;
|
|
3239
3401
|
if (Array.isArray(args)) {
|
|
@@ -3599,7 +3761,7 @@ function pipInstall(words, from, manager, out) {
|
|
|
3599
3761
|
if (v >= 0 && lower(words[v]) === "install")
|
|
3600
3762
|
pushPy(out, manager, operands(words, v + 1, PIP_VALUE_FLAGS));
|
|
3601
3763
|
}
|
|
3602
|
-
function localContexts(
|
|
3764
|
+
function localContexts(file2) {
|
|
3603
3765
|
const contexts = /* @__PURE__ */ new Map();
|
|
3604
3766
|
const sample = parseShared("cd . && true");
|
|
3605
3767
|
if (typeof sample === "symbol") return contexts;
|
|
@@ -3627,7 +3789,7 @@ function localContexts(file) {
|
|
|
3627
3789
|
contexts.set(cmd.Pos().Offset(), steps);
|
|
3628
3790
|
return null;
|
|
3629
3791
|
}
|
|
3630
|
-
if (
|
|
3792
|
+
if (file2.Stmts?.length === 1) visit(file2.Stmts[0], []);
|
|
3631
3793
|
return contexts;
|
|
3632
3794
|
}
|
|
3633
3795
|
function collect(command, out, depth) {
|
|
@@ -3765,6 +3927,13 @@ function checkToLegacyPolicy(policy, id, v) {
|
|
|
3765
3927
|
return offOr("packageCheck", () => {
|
|
3766
3928
|
obj("packageCheck").onMalicious = v === "review" ? "review" : "block";
|
|
3767
3929
|
});
|
|
3930
|
+
case "behavior.untrusted-content":
|
|
3931
|
+
if (v !== "off" && v !== "log" && v !== "review") return false;
|
|
3932
|
+
return offOr("untrustedContent", () => {
|
|
3933
|
+
const uc = obj("untrustedContent");
|
|
3934
|
+
if (v === "log") uc.afterExposure = "log-only";
|
|
3935
|
+
else if (uc.afterExposure !== "ask-every-time") uc.afterExposure = "ask-once";
|
|
3936
|
+
});
|
|
3768
3937
|
default:
|
|
3769
3938
|
return false;
|
|
3770
3939
|
}
|
|
@@ -3778,38 +3947,38 @@ function configurableValues(id) {
|
|
|
3778
3947
|
if (def.pack || MAP_GOVERNED.has(id)) return def.values;
|
|
3779
3948
|
return KNOB_GOVERNED[id] ?? [];
|
|
3780
3949
|
}
|
|
3781
|
-
function hostMatches(
|
|
3950
|
+
function hostMatches(host2, pattern) {
|
|
3782
3951
|
const p = pattern.trim().toLowerCase();
|
|
3783
3952
|
if (!p) return false;
|
|
3784
3953
|
if (p === "*") return true;
|
|
3785
|
-
const h = canonicalHost(
|
|
3954
|
+
const h = canonicalHost(host2);
|
|
3786
3955
|
if (p.startsWith("*.")) {
|
|
3787
3956
|
const suffix = canonicalHost(p.slice(2));
|
|
3788
3957
|
return h === suffix || h.endsWith("." + suffix);
|
|
3789
3958
|
}
|
|
3790
3959
|
return h === canonicalHost(p);
|
|
3791
3960
|
}
|
|
3792
|
-
function matchesAny(
|
|
3793
|
-
for (const p of patterns ?? []) if (hostMatches(
|
|
3961
|
+
function matchesAny(host2, patterns) {
|
|
3962
|
+
for (const p of patterns ?? []) if (hostMatches(host2, p)) return true;
|
|
3794
3963
|
return false;
|
|
3795
3964
|
}
|
|
3796
|
-
function isUniqueLocalV6(
|
|
3797
|
-
const ip = normalizeIpLiteral(
|
|
3965
|
+
function isUniqueLocalV6(host2) {
|
|
3966
|
+
const ip = normalizeIpLiteral(host2);
|
|
3798
3967
|
if (!ip || !ip.includes(":")) return false;
|
|
3799
3968
|
const g = expandIpv6(ip);
|
|
3800
3969
|
return g !== null && (g[0] & 65024) === 64512;
|
|
3801
3970
|
}
|
|
3802
|
-
function isPrivateHost(
|
|
3803
|
-
const h =
|
|
3971
|
+
function isPrivateHost(host2) {
|
|
3972
|
+
const h = host2.trim().toLowerCase().replace(/\.$/, "");
|
|
3804
3973
|
const m = classifySsrf(h);
|
|
3805
3974
|
if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
|
|
3806
3975
|
if (h === "localhost") return true;
|
|
3807
3976
|
if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
|
|
3808
3977
|
return isUniqueLocalV6(h);
|
|
3809
3978
|
}
|
|
3810
|
-
function canonicalHost(
|
|
3811
|
-
const ip = normalizeIpLiteral(
|
|
3812
|
-
return ip ??
|
|
3979
|
+
function canonicalHost(host2) {
|
|
3980
|
+
const ip = normalizeIpLiteral(host2);
|
|
3981
|
+
return ip ?? host2.trim().toLowerCase().replace(/\.$/, "");
|
|
3813
3982
|
}
|
|
3814
3983
|
function evaluateEgress(dests, policy) {
|
|
3815
3984
|
if (!policy.enabled) return null;
|
|
@@ -4033,9 +4202,9 @@ function bareToolName(toolName) {
|
|
|
4033
4202
|
const parts = toolName.split("__");
|
|
4034
4203
|
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
4035
4204
|
}
|
|
4036
|
-
function valuesAt(args,
|
|
4205
|
+
function valuesAt(args, path26) {
|
|
4037
4206
|
let cursors = [args];
|
|
4038
|
-
for (const rawSegment of
|
|
4207
|
+
for (const rawSegment of path26.split(".")) {
|
|
4039
4208
|
const isArray = rawSegment.endsWith("[]");
|
|
4040
4209
|
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
4041
4210
|
const next = [];
|
|
@@ -4067,10 +4236,10 @@ function extractToolDestinations(toolName, args) {
|
|
|
4067
4236
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4068
4237
|
if (!paths) return [];
|
|
4069
4238
|
const out = [];
|
|
4070
|
-
for (const
|
|
4071
|
-
for (const value of valuesAt(args,
|
|
4072
|
-
const
|
|
4073
|
-
if (
|
|
4239
|
+
for (const path26 of paths) {
|
|
4240
|
+
for (const value of valuesAt(args, path26)) {
|
|
4241
|
+
const host2 = hostOf(value);
|
|
4242
|
+
if (host2) out.push({ host: host2, binary: toolName, raw: value });
|
|
4074
4243
|
}
|
|
4075
4244
|
}
|
|
4076
4245
|
return out;
|
|
@@ -4082,15 +4251,15 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
4082
4251
|
try {
|
|
4083
4252
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4084
4253
|
if (!paths) return null;
|
|
4085
|
-
for (const
|
|
4086
|
-
for (const value of valuesAt(args,
|
|
4087
|
-
const
|
|
4088
|
-
if (!
|
|
4089
|
-
const m = classifySsrf(
|
|
4254
|
+
for (const path26 of paths) {
|
|
4255
|
+
for (const value of valuesAt(args, path26)) {
|
|
4256
|
+
const host2 = hostOf(value);
|
|
4257
|
+
if (!host2) continue;
|
|
4258
|
+
const m = classifySsrf(host2);
|
|
4090
4259
|
if (!m) continue;
|
|
4091
4260
|
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
4092
4261
|
if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
|
|
4093
|
-
return { ...m, argPath:
|
|
4262
|
+
return { ...m, argPath: path26, host: host2, reason: ssrfReason(m, host2) };
|
|
4094
4263
|
}
|
|
4095
4264
|
}
|
|
4096
4265
|
return null;
|
|
@@ -4098,6 +4267,9 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
4098
4267
|
return null;
|
|
4099
4268
|
}
|
|
4100
4269
|
}
|
|
4270
|
+
function newSecretViewBudget() {
|
|
4271
|
+
return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
|
|
4272
|
+
}
|
|
4101
4273
|
function textBytes(buf) {
|
|
4102
4274
|
if (buf.length < 16) return null;
|
|
4103
4275
|
const text = buf.toString("utf8");
|
|
@@ -4116,13 +4288,41 @@ function percentOnce(text) {
|
|
|
4116
4288
|
}
|
|
4117
4289
|
});
|
|
4118
4290
|
}
|
|
4291
|
+
function* wrappedBase64(text) {
|
|
4292
|
+
if (!text.includes("\n")) return;
|
|
4293
|
+
const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
|
|
4294
|
+
let run = [];
|
|
4295
|
+
let width = 0;
|
|
4296
|
+
const flush = function* () {
|
|
4297
|
+
if (run.length >= 2) yield run.join("");
|
|
4298
|
+
run = [];
|
|
4299
|
+
width = 0;
|
|
4300
|
+
};
|
|
4301
|
+
for (const raw of text.split("\n")) {
|
|
4302
|
+
const line = raw.trim();
|
|
4303
|
+
const full = line.length >= 16 && word.test(line) && !line.includes("=");
|
|
4304
|
+
if (full && (!run.length || line.length === width)) {
|
|
4305
|
+
run.push(line);
|
|
4306
|
+
width = line.length;
|
|
4307
|
+
continue;
|
|
4308
|
+
}
|
|
4309
|
+
if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
|
|
4310
|
+
run.push(line);
|
|
4311
|
+
yield* flush();
|
|
4312
|
+
continue;
|
|
4313
|
+
}
|
|
4314
|
+
yield* flush();
|
|
4315
|
+
if (full) {
|
|
4316
|
+
run.push(line);
|
|
4317
|
+
width = line.length;
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
yield* flush();
|
|
4321
|
+
}
|
|
4119
4322
|
function* encodedRegions(text, budget) {
|
|
4120
4323
|
yield [text, false];
|
|
4324
|
+
for (const block of wrappedBase64(text)) yield [block, false];
|
|
4121
4325
|
for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
|
|
4122
|
-
if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
|
|
4123
|
-
budget.reasons.add("decode-candidates");
|
|
4124
|
-
return;
|
|
4125
|
-
}
|
|
4126
4326
|
try {
|
|
4127
4327
|
new URL(match[0]);
|
|
4128
4328
|
} catch {
|
|
@@ -4132,16 +4332,18 @@ function* encodedRegions(text, budget) {
|
|
|
4132
4332
|
/^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i
|
|
4133
4333
|
);
|
|
4134
4334
|
if (!parts) continue;
|
|
4135
|
-
const
|
|
4335
|
+
const host2 = parts[1].slice(parts[1].lastIndexOf("@") + 1).replace(/:\d+$/, "");
|
|
4136
4336
|
const regions = [
|
|
4137
4337
|
parts[1],
|
|
4138
|
-
...
|
|
4338
|
+
...host2.split("."),
|
|
4139
4339
|
...parts[2].split("/"),
|
|
4140
4340
|
...(parts[3] ?? "").split("&"),
|
|
4141
4341
|
parts[4] ?? ""
|
|
4142
4342
|
];
|
|
4143
4343
|
for (const region of regions) if (region) yield [region, false];
|
|
4144
|
-
const labels =
|
|
4344
|
+
const labels = host2.split(".");
|
|
4345
|
+
if (host2.length > SECRET_VIEW_LIMITS.maxHostLength || labels.length > SECRET_VIEW_LIMITS.maxLabels || !labels.every((l) => l.length >= 1 && l.length <= SECRET_VIEW_LIMITS.maxLabelLength))
|
|
4346
|
+
continue;
|
|
4145
4347
|
for (let start = 0; start < labels.length; start++) {
|
|
4146
4348
|
let joined = labels[start];
|
|
4147
4349
|
for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
|
|
@@ -4149,11 +4351,13 @@ function* encodedRegions(text, budget) {
|
|
|
4149
4351
|
if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
|
|
4150
4352
|
break;
|
|
4151
4353
|
joined += labels[end];
|
|
4152
|
-
if (
|
|
4153
|
-
|
|
4354
|
+
if (joined.length < 16) continue;
|
|
4355
|
+
budget.joinedLabelBytes += joined.length;
|
|
4356
|
+
if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
|
|
4357
|
+
budget.reasons.add("decode-labels");
|
|
4154
4358
|
return;
|
|
4155
4359
|
}
|
|
4156
|
-
|
|
4360
|
+
yield [joined, true];
|
|
4157
4361
|
}
|
|
4158
4362
|
}
|
|
4159
4363
|
}
|
|
@@ -4190,10 +4394,6 @@ function* decodedCandidates(text, budget) {
|
|
|
4190
4394
|
if (joined) yield ["dns-labels", region];
|
|
4191
4395
|
for (const [kind, re] of families) {
|
|
4192
4396
|
for (const match of region.matchAll(re)) {
|
|
4193
|
-
if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
|
|
4194
|
-
budget.reasons.add("decode-candidates");
|
|
4195
|
-
return;
|
|
4196
|
-
}
|
|
4197
4397
|
const raw = match[0];
|
|
4198
4398
|
let buf;
|
|
4199
4399
|
if (kind === "base64") {
|
|
@@ -4216,42 +4416,76 @@ function* decodedCandidates(text, budget) {
|
|
|
4216
4416
|
}
|
|
4217
4417
|
}
|
|
4218
4418
|
}
|
|
4219
|
-
function
|
|
4419
|
+
function secretViews(text, budget) {
|
|
4220
4420
|
const seen = /* @__PURE__ */ new Set([text]);
|
|
4221
|
-
const
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
for (const
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4421
|
+
const out = [];
|
|
4422
|
+
let frontier = [{ text, transforms: [] }];
|
|
4423
|
+
for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
|
|
4424
|
+
const next = [];
|
|
4425
|
+
for (const parent of frontier) {
|
|
4426
|
+
let perLeaf = 0;
|
|
4427
|
+
for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
|
|
4428
|
+
if (seen.has(decoded)) continue;
|
|
4429
|
+
seen.add(decoded);
|
|
4430
|
+
const normalizing = kind === "invisibles" || kind === "percent";
|
|
4431
|
+
if (depth >= 2 && !normalizing) {
|
|
4432
|
+
budget.nestedBytes += Buffer.byteLength(decoded);
|
|
4433
|
+
if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
|
|
4434
|
+
budget.reasons.add("decode-nested");
|
|
4435
|
+
return out;
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
const view = { text: decoded, transforms: [...parent.transforms, kind] };
|
|
4439
|
+
out.push(view);
|
|
4440
|
+
next.push(view);
|
|
4235
4441
|
}
|
|
4236
|
-
seen.add(decoded);
|
|
4237
|
-
budget.views++;
|
|
4238
|
-
budget.bytes += bytes;
|
|
4239
|
-
queue.push({ text: decoded, transforms: [...view.transforms, kind] });
|
|
4240
4442
|
}
|
|
4241
|
-
|
|
4443
|
+
frontier = next;
|
|
4444
|
+
if (!frontier.length) return out;
|
|
4445
|
+
}
|
|
4446
|
+
for (const view of frontier) {
|
|
4447
|
+
if (!decodedCandidates(view.text, budget).next().done) {
|
|
4448
|
+
budget.reasons.add("decode-nested");
|
|
4449
|
+
break;
|
|
4450
|
+
}
|
|
4242
4451
|
}
|
|
4452
|
+
return out;
|
|
4453
|
+
}
|
|
4454
|
+
function batches(views) {
|
|
4455
|
+
const out = [];
|
|
4456
|
+
let current = [];
|
|
4457
|
+
let size = 0;
|
|
4458
|
+
for (const { text } of views) {
|
|
4459
|
+
if (current.length && size + text.length > SECRET_VIEW_LIMITS.batchCharacters) {
|
|
4460
|
+
out.push(current);
|
|
4461
|
+
current = [];
|
|
4462
|
+
size = 0;
|
|
4463
|
+
}
|
|
4464
|
+
current.push(text);
|
|
4465
|
+
size += text.length;
|
|
4466
|
+
}
|
|
4467
|
+
if (current.length) out.push(current);
|
|
4468
|
+
return out;
|
|
4243
4469
|
}
|
|
4244
4470
|
function inspectArgumentSecrets(args) {
|
|
4245
|
-
const budget =
|
|
4471
|
+
const budget = newSecretViewBudget();
|
|
4246
4472
|
const ancestors = /* @__PURE__ */ new Set();
|
|
4247
4473
|
let nodes = 0;
|
|
4248
4474
|
let sourceBytes = 0;
|
|
4249
4475
|
let tailBytes = 0;
|
|
4250
4476
|
let best = null;
|
|
4477
|
+
const blocked = () => best?.severity === "block";
|
|
4251
4478
|
const accept = (match) => {
|
|
4252
4479
|
if (match && (!best || best.severity !== "block" && match.severity === "block")) best = match;
|
|
4253
4480
|
};
|
|
4481
|
+
const decodeSegment = (segment, fieldPath) => {
|
|
4482
|
+
for (const batch of batches(secretViews(segment, budget))) {
|
|
4483
|
+
accept(matchArgumentBatch(batch, fieldPath, budget));
|
|
4484
|
+
if (blocked()) return;
|
|
4485
|
+
}
|
|
4486
|
+
};
|
|
4254
4487
|
const visit = (value, depth, fieldPath) => {
|
|
4488
|
+
if (blocked()) return;
|
|
4255
4489
|
if (++nodes > ARGUMENT_SCAN_LIMITS.maxNodes) {
|
|
4256
4490
|
budget.reasons.add("argument-nodes");
|
|
4257
4491
|
return;
|
|
@@ -4293,14 +4527,11 @@ function inspectArgumentSecrets(args) {
|
|
|
4293
4527
|
}
|
|
4294
4528
|
sourceBytes += Math.min(bytes, Math.max(0, remaining));
|
|
4295
4529
|
if (!text) return;
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
if (hit)
|
|
4299
|
-
accept(view.transforms.length ? { ...hit, redactedSample: "[encoded secret]" } : hit);
|
|
4300
|
-
}
|
|
4530
|
+
accept(matchArgumentText(text, fieldPath, budget));
|
|
4531
|
+
let scannedEnd = text.length;
|
|
4301
4532
|
if (!truncated && value.length > window) {
|
|
4302
|
-
budget.reasons.add("decode-window");
|
|
4303
4533
|
for (let start = window - overlap; start < value.length; start += window - overlap) {
|
|
4534
|
+
if (blocked()) return;
|
|
4304
4535
|
const chunk = value.slice(start, start + window);
|
|
4305
4536
|
const chunkBytes = Buffer.byteLength(chunk);
|
|
4306
4537
|
if (tailBytes + chunkBytes > ARGUMENT_SCAN_LIMITS.maxTailBytes) {
|
|
@@ -4309,9 +4540,16 @@ function inspectArgumentSecrets(args) {
|
|
|
4309
4540
|
}
|
|
4310
4541
|
tailBytes += chunkBytes;
|
|
4311
4542
|
accept(matchArgumentText(chunk, fieldPath, budget));
|
|
4543
|
+
scannedEnd = start + chunk.length;
|
|
4312
4544
|
if (start + window >= value.length) break;
|
|
4313
4545
|
}
|
|
4314
4546
|
}
|
|
4547
|
+
for (let start = 0; start < scannedEnd && !blocked(); ) {
|
|
4548
|
+
let end = Math.min(start + window, scannedEnd);
|
|
4549
|
+
while (end < scannedEnd && runChar(value, end - 1) && runChar(value, end)) end++;
|
|
4550
|
+
decodeSegment(value.slice(start, end), fieldPath);
|
|
4551
|
+
start = end;
|
|
4552
|
+
}
|
|
4315
4553
|
const trimmed = text.trim();
|
|
4316
4554
|
if (value.length < ARGUMENT_SCAN_LIMITS.maxJsonCharacters && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
|
|
4317
4555
|
try {
|
|
@@ -4330,7 +4568,7 @@ function inspectArgumentSecrets(args) {
|
|
|
4330
4568
|
function scanArgumentSecrets(args) {
|
|
4331
4569
|
const result = inspectArgumentSecrets(args);
|
|
4332
4570
|
if (result.match) return result.match;
|
|
4333
|
-
return result.
|
|
4571
|
+
return result.coverage === "incomplete" ? {
|
|
4334
4572
|
patternName: "Argument inspection incomplete",
|
|
4335
4573
|
fieldPath: "args",
|
|
4336
4574
|
redactedSample: "[uninspected content]",
|
|
@@ -4358,18 +4596,13 @@ function tokenize2(toolName) {
|
|
|
4358
4596
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
4359
4597
|
}
|
|
4360
4598
|
function extractShellCommand(toolName, args, toolInspection) {
|
|
4361
|
-
const
|
|
4362
|
-
|
|
4363
|
-
if (!matchingPattern) return null;
|
|
4364
|
-
const fieldPath = toolInspection[matchingPattern];
|
|
4599
|
+
const fieldPath = inspectionEntry(toolName, toolInspection);
|
|
4600
|
+
if (fieldPath === void 0) return null;
|
|
4365
4601
|
const value = getNestedValue(args, typeof fieldPath === "string" ? fieldPath : fieldPath.field);
|
|
4366
4602
|
return typeof value === "string" ? value : null;
|
|
4367
4603
|
}
|
|
4368
4604
|
function isSqlTool(toolName, toolInspection) {
|
|
4369
|
-
const
|
|
4370
|
-
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
4371
|
-
if (!matchingPattern) return false;
|
|
4372
|
-
const fieldName = toolInspection[matchingPattern];
|
|
4605
|
+
const fieldName = inspectionEntry(toolName, toolInspection);
|
|
4373
4606
|
return fieldName === "sql" || fieldName === "query";
|
|
4374
4607
|
}
|
|
4375
4608
|
function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
@@ -4377,7 +4610,7 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
|
4377
4610
|
if (!pipeAnalysis.isPipeline) return null;
|
|
4378
4611
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
4379
4612
|
const sinks = pipeAnalysis.sinkTargets;
|
|
4380
|
-
const allTrusted = sinks.length > 0 && sinks.every((
|
|
4613
|
+
const allTrusted = sinks.length > 0 && sinks.every((host2) => isTrustedHost2 ? isTrustedHost2(host2) : false);
|
|
4381
4614
|
if (pipeAnalysis.risk === "critical") {
|
|
4382
4615
|
if (allTrusted) {
|
|
4383
4616
|
return {
|
|
@@ -4436,9 +4669,10 @@ function egressPolicyVerdict(eg) {
|
|
|
4436
4669
|
}
|
|
4437
4670
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
4438
4671
|
let protectedVerdict = null;
|
|
4672
|
+
const layers = inspectionLayers(config.policy);
|
|
4439
4673
|
if (context.agent !== "Terminal") {
|
|
4440
4674
|
try {
|
|
4441
|
-
const accesses = context.accesses ?? collectFileAccesses(toolName, args, context,
|
|
4675
|
+
const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, layers);
|
|
4442
4676
|
protectedVerdict = evaluateProtectedFileAccess(accesses, context, catalogSettings(config));
|
|
4443
4677
|
} catch {
|
|
4444
4678
|
protectedVerdict = incompleteFileVerdict();
|
|
@@ -4473,7 +4707,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4473
4707
|
hooks
|
|
4474
4708
|
);
|
|
4475
4709
|
if (result.decision === "block") return result;
|
|
4476
|
-
if (context.agent !== "Terminal" && resolveShellInput(toolName, args,
|
|
4710
|
+
if (context.agent !== "Terminal" && resolveShellInput(toolName, args, layers).kind === "uninspectable") {
|
|
4477
4711
|
return {
|
|
4478
4712
|
decision: "review",
|
|
4479
4713
|
ruleName: "shell-inspection-incomplete",
|
|
@@ -4489,13 +4723,14 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4489
4723
|
const { agent, cwd, activeEnvironment } = context;
|
|
4490
4724
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
4491
4725
|
const settingsView = catalogSettings(config);
|
|
4726
|
+
const layers = inspectionLayers(config.policy);
|
|
4492
4727
|
const cv = (id) => checkValue(settingsView, id);
|
|
4493
4728
|
let loggedFinding;
|
|
4494
4729
|
const noteLogged = (v) => {
|
|
4495
4730
|
if (v?.logged && !loggedFinding) loggedFinding = v;
|
|
4496
4731
|
};
|
|
4497
4732
|
const allowed = () => loggedFinding ?? { decision: "allow" };
|
|
4498
|
-
const shellInput = resolveShellInput(toolName, args,
|
|
4733
|
+
const shellInput = resolveShellInput(toolName, args, layers);
|
|
4499
4734
|
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
4500
4735
|
{
|
|
4501
4736
|
const dest = ssrfDestinationFloor(toolName, args, {
|
|
@@ -4600,13 +4835,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4600
4835
|
return { id, value: cv(id) };
|
|
4601
4836
|
};
|
|
4602
4837
|
const matches = config.policy.smartRules.filter((rule) => {
|
|
4603
|
-
if (!toolMatchesRule(toolName, rule.tool,
|
|
4838
|
+
if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
|
|
4604
4839
|
if (astSuppressed(rule)) return false;
|
|
4605
4840
|
if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
|
|
4606
|
-
if (!evaluateSmartConditions(
|
|
4607
|
-
shellRuleArgs(toolName, args, rule.tool, config.policy.toolInspection),
|
|
4608
|
-
rule
|
|
4609
|
-
))
|
|
4841
|
+
if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
|
|
4610
4842
|
return false;
|
|
4611
4843
|
const check = ruleCheck(rule);
|
|
4612
4844
|
if (check?.value === "off") return false;
|
|
@@ -4653,7 +4885,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4653
4885
|
let allTokens = [];
|
|
4654
4886
|
let pathTokens = [];
|
|
4655
4887
|
if (pendingToolEgress) return pendingToolEgress;
|
|
4656
|
-
const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args,
|
|
4888
|
+
const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, layers) : null;
|
|
4657
4889
|
if (shellCommand) {
|
|
4658
4890
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
4659
4891
|
allTokens = analyzed.allTokens;
|
|
@@ -4730,7 +4962,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4730
4962
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
4731
4963
|
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
4732
4964
|
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
4733
|
-
allTokens.
|
|
4965
|
+
allTokens = allTokens.concat(sshHosts);
|
|
4734
4966
|
}
|
|
4735
4967
|
if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
|
|
4736
4968
|
const prov = checkProvenance2(firstToken, cwd);
|
|
@@ -4759,7 +4991,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4759
4991
|
};
|
|
4760
4992
|
}
|
|
4761
4993
|
}
|
|
4762
|
-
if (isSqlTool(toolName,
|
|
4994
|
+
if (isSqlTool(toolName, layers)) {
|
|
4763
4995
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
4764
4996
|
}
|
|
4765
4997
|
} else {
|
|
@@ -4767,7 +4999,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4767
4999
|
if (args && typeof args === "object") {
|
|
4768
5000
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
4769
5001
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
4770
|
-
allTokens.
|
|
5002
|
+
allTokens = allTokens.concat(extraTokens);
|
|
4771
5003
|
}
|
|
4772
5004
|
}
|
|
4773
5005
|
const isManual = agent === "Terminal";
|
|
@@ -4854,6 +5086,487 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4854
5086
|
function isIgnoredTool(toolName, config) {
|
|
4855
5087
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
4856
5088
|
}
|
|
5089
|
+
function normalizeHost(raw) {
|
|
5090
|
+
let h = raw.trim().toLowerCase();
|
|
5091
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
5092
|
+
if (!h || h === "localhost") return null;
|
|
5093
|
+
if (/^[0-9a-f:]+$/.test(h) && h.includes(":")) return h;
|
|
5094
|
+
if (!h.includes(".") || !/^[\p{L}\p{N}.-]+$/u.test(h)) return null;
|
|
5095
|
+
return h;
|
|
5096
|
+
}
|
|
5097
|
+
function hostOfUrl(value) {
|
|
5098
|
+
const v = value.replace(/[.,;:!?)\]}>'"`]+$/, "");
|
|
5099
|
+
try {
|
|
5100
|
+
const u = new URL(v.toLowerCase().startsWith("www.") ? `http://${v}` : v);
|
|
5101
|
+
return normalizeHost(u.hostname);
|
|
5102
|
+
} catch {
|
|
5103
|
+
return null;
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
function addIbans(text, out) {
|
|
5107
|
+
for (const m of text.toUpperCase().matchAll(IBAN_RE)) {
|
|
5108
|
+
const compact = m[0].replace(/[ -]/g, "");
|
|
5109
|
+
if (validateIban(compact)) out.add(`iban:${compact}`);
|
|
5110
|
+
else if (/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(compact)) out.add(`account:${compact}`);
|
|
5111
|
+
if (out.size >= MAX_DESTINATIONS) return;
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
function addEmails(text, out) {
|
|
5115
|
+
for (const m of text.matchAll(EMAIL_RE)) {
|
|
5116
|
+
out.add(`email:${m[0].toLowerCase()}`);
|
|
5117
|
+
if (out.size >= MAX_DESTINATIONS) return;
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
function addUrlHosts(text, out) {
|
|
5121
|
+
for (const m of text.matchAll(URL_RE)) {
|
|
5122
|
+
const h = hostOfUrl(m[0]);
|
|
5123
|
+
if (h) out.add(`host:${h}`);
|
|
5124
|
+
if (out.size >= MAX_DESTINATIONS) return;
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
function extractContentDestinations(text) {
|
|
5128
|
+
const out = /* @__PURE__ */ new Set();
|
|
5129
|
+
try {
|
|
5130
|
+
const t = text.length > MAX_SCAN_CHARS ? text.slice(0, MAX_SCAN_CHARS) : text;
|
|
5131
|
+
if (t.includes("@")) addEmails(t, out);
|
|
5132
|
+
if (out.size < MAX_DESTINATIONS) addUrlHosts(t, out);
|
|
5133
|
+
if (out.size < MAX_DESTINATIONS) addIbans(t, out);
|
|
5134
|
+
} catch {
|
|
5135
|
+
}
|
|
5136
|
+
return [...out];
|
|
5137
|
+
}
|
|
5138
|
+
function extractArgDestinations(args) {
|
|
5139
|
+
const out = /* @__PURE__ */ new Set();
|
|
5140
|
+
try {
|
|
5141
|
+
walk(args, void 0, out, 0);
|
|
5142
|
+
} catch {
|
|
5143
|
+
}
|
|
5144
|
+
return [...out];
|
|
5145
|
+
}
|
|
5146
|
+
function walk(value, key, out, depth) {
|
|
5147
|
+
if (out.size >= MAX_DESTINATIONS || depth > 12) return;
|
|
5148
|
+
if (typeof value === "string") {
|
|
5149
|
+
const s = value.length > MAX_SCAN_CHARS ? value.slice(0, MAX_SCAN_CHARS) : value;
|
|
5150
|
+
if (s.includes("@")) addEmails(s, out);
|
|
5151
|
+
addUrlHosts(s, out);
|
|
5152
|
+
addIbans(s, out);
|
|
5153
|
+
if (key && RECIPIENT_KEYS.has(key.toLowerCase()) && !/[\s@]/.test(s)) {
|
|
5154
|
+
const h = normalizeHost(
|
|
5155
|
+
s.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/^\/\//, "").split(/[/?#]/)[0].replace(/:\d+$/, "")
|
|
5156
|
+
);
|
|
5157
|
+
if (h && (h.includes(":") || /\.\p{L}{2,}$/u.test(h))) out.add(`host:${h}`);
|
|
5158
|
+
const acct = s.replace(/[ .-]/g, "").toUpperCase();
|
|
5159
|
+
if (/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(acct) && !validateIban(acct))
|
|
5160
|
+
out.add(`account:${acct}`);
|
|
5161
|
+
}
|
|
5162
|
+
return;
|
|
5163
|
+
}
|
|
5164
|
+
if (Array.isArray(value)) {
|
|
5165
|
+
for (const v of value) walk(v, key, out, depth + 1);
|
|
5166
|
+
return;
|
|
5167
|
+
}
|
|
5168
|
+
if (value && typeof value === "object") {
|
|
5169
|
+
for (const [k, v] of Object.entries(value))
|
|
5170
|
+
walk(v, k, out, depth + 1);
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
function destinationDomain(dest) {
|
|
5174
|
+
if (!dest.startsWith("email:")) return null;
|
|
5175
|
+
const at = dest.lastIndexOf("@");
|
|
5176
|
+
return at > 0 ? dest.slice(at + 1) : null;
|
|
5177
|
+
}
|
|
5178
|
+
function destinationKind(dest) {
|
|
5179
|
+
const i = dest.indexOf(":");
|
|
5180
|
+
const k = i > 0 ? dest.slice(0, i) : "";
|
|
5181
|
+
return k === "email" || k === "host" || k === "iban" || k === "account" ? k : null;
|
|
5182
|
+
}
|
|
5183
|
+
function destinationValue(dest) {
|
|
5184
|
+
const i = dest.indexOf(":");
|
|
5185
|
+
return i > 0 ? dest.slice(i + 1) : dest;
|
|
5186
|
+
}
|
|
5187
|
+
function isLoopbackHost(host2) {
|
|
5188
|
+
let h = host2.trim().toLowerCase();
|
|
5189
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
5190
|
+
return h === "localhost" || h.endsWith(".localhost") || /^127(?:\.\d{1,3}){3}$/.test(h) || h === "::1" || h === "0:0:0:0:0:0:0:1" || h === "0.0.0.0";
|
|
5191
|
+
}
|
|
5192
|
+
function gitRepoPath(url) {
|
|
5193
|
+
const u = url.trim();
|
|
5194
|
+
if (!u) return null;
|
|
5195
|
+
let host2;
|
|
5196
|
+
let p;
|
|
5197
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
5198
|
+
try {
|
|
5199
|
+
const parsed = new URL(u);
|
|
5200
|
+
host2 = parsed.hostname.toLowerCase();
|
|
5201
|
+
p = decodeURIComponent(parsed.pathname);
|
|
5202
|
+
} catch {
|
|
5203
|
+
return null;
|
|
5204
|
+
}
|
|
5205
|
+
} else {
|
|
5206
|
+
const scp = /^(?:[^@/\s]+@)?([A-Za-z0-9.-]+):(?!\/\/)(.+)$/.exec(u);
|
|
5207
|
+
if (!scp || !scp[1].includes(".")) return null;
|
|
5208
|
+
host2 = scp[1].toLowerCase();
|
|
5209
|
+
p = scp[2];
|
|
5210
|
+
}
|
|
5211
|
+
let start = 0;
|
|
5212
|
+
let end = p.length;
|
|
5213
|
+
while (start < end && p[start] === "/") start++;
|
|
5214
|
+
while (end > start && p[end - 1] === "/") end--;
|
|
5215
|
+
const tail = p.slice(start, end).replace(/\.git$/i, "").toLowerCase();
|
|
5216
|
+
if (!host2 || !tail.includes("/")) return null;
|
|
5217
|
+
return `${host2}/${tail}`;
|
|
5218
|
+
}
|
|
5219
|
+
function isDestructiveToolName(bareTool) {
|
|
5220
|
+
return DESTRUCTIVE_VERB_RE.test(bareTool);
|
|
5221
|
+
}
|
|
5222
|
+
function host(token) {
|
|
5223
|
+
if (!token) return null;
|
|
5224
|
+
const h = parseDestHost(token);
|
|
5225
|
+
return h ? `host:${h}` : null;
|
|
5226
|
+
}
|
|
5227
|
+
function ghRepoArg(v, defaultHost) {
|
|
5228
|
+
if (/:\/\/|@/.test(v)) {
|
|
5229
|
+
const r = gitRepoPath(v);
|
|
5230
|
+
if (r) return { host: r.split("/")[0], repo: r };
|
|
5231
|
+
}
|
|
5232
|
+
const parts = v.toLowerCase().replace(/\.git$/, "").split("/").filter(Boolean);
|
|
5233
|
+
if (parts.length === 3) return { host: parts[0], repo: parts.join("/") };
|
|
5234
|
+
if (parts.length === 2) return { host: defaultHost, repo: `${defaultHost}/${parts.join("/")}` };
|
|
5235
|
+
return { host: defaultHost, repo: `${defaultHost}/*` };
|
|
5236
|
+
}
|
|
5237
|
+
function ghApiRepo(endpoint, ghHost) {
|
|
5238
|
+
if (endpoint === null) return `${ghHost}/*`;
|
|
5239
|
+
const e = endpoint.replace(/^https?:\/\/[^/]+\//i, "").replace(/^\/+/, "");
|
|
5240
|
+
const m = /^repos\/([^/]+)\/([^/?#]+)/i.exec(e);
|
|
5241
|
+
if (!m) return `${ghHost}/*`;
|
|
5242
|
+
if (m[1] === "{owner}" && m[2] === "{repo}") return void 0;
|
|
5243
|
+
return `${ghHost}/${m[1].toLowerCase()}/${m[2].toLowerCase()}`;
|
|
5244
|
+
}
|
|
5245
|
+
function sendToGitUrl(url, out) {
|
|
5246
|
+
out.sends = true;
|
|
5247
|
+
const h = typeof url === "string" ? host(url) : null;
|
|
5248
|
+
if (!url || !h) {
|
|
5249
|
+
out.unresolved = true;
|
|
5250
|
+
return;
|
|
5251
|
+
}
|
|
5252
|
+
out.destinations.push(h);
|
|
5253
|
+
const r = gitRepoPath(url);
|
|
5254
|
+
if (r) out.repos.push(r);
|
|
5255
|
+
}
|
|
5256
|
+
function callWords(call) {
|
|
5257
|
+
const raw = call.Args || [];
|
|
5258
|
+
if (raw.length === 0) return null;
|
|
5259
|
+
const words = raw.map((a) => resolveWordLiteral(a));
|
|
5260
|
+
const prefixes = raw.map((a) => resolveWordPrefix(a).prefix);
|
|
5261
|
+
const first = (words[0] || "").toLowerCase().split("/").pop() ?? "";
|
|
5262
|
+
const at = OWN_HEADS.has(first) ? 0 : unwrapCommandHead(words);
|
|
5263
|
+
const head = (words[at] || "").toLowerCase().split("/").pop() ?? "";
|
|
5264
|
+
if (!head) return null;
|
|
5265
|
+
const assigns = call.Assigns || [];
|
|
5266
|
+
const env = [
|
|
5267
|
+
...assigns.map((a) => String(a?.Name?.Value ?? "")),
|
|
5268
|
+
...words.slice(0, at).map((w) => w === null ? "" : /^([A-Za-z_][A-Za-z0-9_]*)=/.exec(w)?.[1] ?? "")
|
|
5269
|
+
].filter(Boolean);
|
|
5270
|
+
return { head, args: words.slice(at + 1), prefixes: prefixes.slice(at + 1), env };
|
|
5271
|
+
}
|
|
5272
|
+
function flagValue(args, names) {
|
|
5273
|
+
for (let i = 0; i < args.length; i++) {
|
|
5274
|
+
const w = args[i];
|
|
5275
|
+
if (w === null) continue;
|
|
5276
|
+
for (const n of names) {
|
|
5277
|
+
if (w === n && typeof args[i + 1] === "string") return args[i + 1];
|
|
5278
|
+
if (w.startsWith(`${n}=`)) return w.slice(n.length + 1);
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
return null;
|
|
5282
|
+
}
|
|
5283
|
+
function positionals(args, valueFlags) {
|
|
5284
|
+
const out = [];
|
|
5285
|
+
for (let i = 0; i < args.length; i++) {
|
|
5286
|
+
const w = args[i];
|
|
5287
|
+
if (w === null) {
|
|
5288
|
+
out.push(null);
|
|
5289
|
+
continue;
|
|
5290
|
+
}
|
|
5291
|
+
if (w === "--") {
|
|
5292
|
+
out.push(...args.slice(i + 1));
|
|
5293
|
+
break;
|
|
5294
|
+
}
|
|
5295
|
+
if (w.startsWith("-")) {
|
|
5296
|
+
if (!w.includes("=") && valueFlags.has(w)) i++;
|
|
5297
|
+
continue;
|
|
5298
|
+
}
|
|
5299
|
+
out.push(w);
|
|
5300
|
+
}
|
|
5301
|
+
return out;
|
|
5302
|
+
}
|
|
5303
|
+
function classifyCall(head, args, prefixes, env, state, out) {
|
|
5304
|
+
const lower2 = args.map((a) => a === null ? null : a.toLowerCase());
|
|
5305
|
+
switch (head) {
|
|
5306
|
+
case "curl":
|
|
5307
|
+
case "wget":
|
|
5308
|
+
case "iwr":
|
|
5309
|
+
case "invoke-webrequest":
|
|
5310
|
+
case "irm":
|
|
5311
|
+
case "invoke-restmethod": {
|
|
5312
|
+
const binary = head === "wget" ? "wget" : "curl";
|
|
5313
|
+
const sendFlags = head === "wget" ? WGET_SEND_FLAGS : head === "curl" ? CURL_SEND_FLAGS : IWR_SEND_FLAGS;
|
|
5314
|
+
let sends = false;
|
|
5315
|
+
for (let i = 0; i < args.length; i++) {
|
|
5316
|
+
const w = args[i];
|
|
5317
|
+
if (w === null) continue;
|
|
5318
|
+
const name = (head === "curl" || head === "wget" ? w : w.toLowerCase()).split("=")[0];
|
|
5319
|
+
if (sendFlags.has(name)) sends = true;
|
|
5320
|
+
const next = lower2[i + 1];
|
|
5321
|
+
if ((name === "-X" || name === "--request" || name === "-method") && next) {
|
|
5322
|
+
if (WRITE_METHODS.has(next)) sends = true;
|
|
5323
|
+
}
|
|
5324
|
+
if (/^(?:--request=|-X)(POST|PUT|PATCH|DELETE)$/i.test(w)) sends = true;
|
|
5325
|
+
if (name === "--method" && next && WRITE_METHODS.has(next)) sends = true;
|
|
5326
|
+
}
|
|
5327
|
+
const withPrefixes = args.map(
|
|
5328
|
+
(a, i) => a === null && /^[a-z][a-z0-9+.-]*:\/\//i.test(prefixes[i] ?? "") ? prefixes[i] : a
|
|
5329
|
+
);
|
|
5330
|
+
const dests = destTokensForBinary(binary, withPrefixes).map((t) => host(t)).filter((h) => h !== null);
|
|
5331
|
+
out.fetches = true;
|
|
5332
|
+
out.fetchKinds.push(head);
|
|
5333
|
+
out.fetchDestinations.push(...dests);
|
|
5334
|
+
if (sends) {
|
|
5335
|
+
out.sends = true;
|
|
5336
|
+
out.destinations.push(...dests);
|
|
5337
|
+
}
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
case "scp":
|
|
5341
|
+
case "rsync": {
|
|
5342
|
+
const pos = positionals(args, VALUE_FLAGS[head] ?? /* @__PURE__ */ new Set()).filter(
|
|
5343
|
+
(p) => p !== null
|
|
5344
|
+
);
|
|
5345
|
+
if (pos.length < 2) return;
|
|
5346
|
+
const remote = (p) => /^[a-z][a-z0-9+.-]*:\/\//i.test(p) || /^[^/]*[^/:]:[^/]/.test(p) || p.includes("@");
|
|
5347
|
+
const target = pos[pos.length - 1];
|
|
5348
|
+
const sources = pos.slice(0, -1);
|
|
5349
|
+
if (remote(target)) {
|
|
5350
|
+
out.sends = true;
|
|
5351
|
+
const h = host(target);
|
|
5352
|
+
if (h) out.destinations.push(h);
|
|
5353
|
+
}
|
|
5354
|
+
if (sources.some(remote)) {
|
|
5355
|
+
out.fetches = true;
|
|
5356
|
+
out.fetchKinds.push(head);
|
|
5357
|
+
for (const src of sources.filter(remote)) {
|
|
5358
|
+
const h = host(src);
|
|
5359
|
+
if (h) out.fetchDestinations.push(h);
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
return;
|
|
5363
|
+
}
|
|
5364
|
+
case "nc":
|
|
5365
|
+
case "ncat":
|
|
5366
|
+
case "netcat":
|
|
5367
|
+
case "ssh": {
|
|
5368
|
+
out.fetches = true;
|
|
5369
|
+
out.sends = true;
|
|
5370
|
+
out.fetchKinds.push(head);
|
|
5371
|
+
for (const t of destTokensForBinary(head, args)) {
|
|
5372
|
+
const h = host(t);
|
|
5373
|
+
if (h) {
|
|
5374
|
+
out.destinations.push(h);
|
|
5375
|
+
out.fetchDestinations.push(h);
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
return;
|
|
5379
|
+
}
|
|
5380
|
+
case "gh": {
|
|
5381
|
+
const pos = positionals(args, GH_HOSTNAME_FLAGS);
|
|
5382
|
+
const group = pos[0] ?? null;
|
|
5383
|
+
const verb = pos[1] ?? null;
|
|
5384
|
+
const hn = flagValue(args, ["--hostname"]);
|
|
5385
|
+
let dest = host(hn ?? "github.com") ?? "host:github.com";
|
|
5386
|
+
const ghHost = dest.slice("host:".length);
|
|
5387
|
+
const envRepo = env.some((n) => n === "GH_REPO" || n === "GH_HOST");
|
|
5388
|
+
if (group === "api") {
|
|
5389
|
+
const method = (flagValue(args, ["-X", "--method"]) ?? "get").toLowerCase();
|
|
5390
|
+
const hasBody = args.some(
|
|
5391
|
+
(w) => w !== null && /^(-f|--raw-field|-F|--field|--input)(=|$)/.test(w)
|
|
5392
|
+
);
|
|
5393
|
+
if (WRITE_METHODS.has(method) || hasBody) {
|
|
5394
|
+
out.sends = true;
|
|
5395
|
+
out.destinations.push(dest);
|
|
5396
|
+
const endpoint = positionals(args, GH_API_VALUE_FLAGS)[1] ?? null;
|
|
5397
|
+
const repo = ghApiRepo(endpoint, ghHost);
|
|
5398
|
+
if (repo !== void 0) out.repos.push(repo);
|
|
5399
|
+
else if (envRepo) out.repos.push(`${ghHost}/*`);
|
|
5400
|
+
} else {
|
|
5401
|
+
out.fetches = true;
|
|
5402
|
+
out.fetchKinds.push("gh api");
|
|
5403
|
+
out.fetchDestinations.push(dest);
|
|
5404
|
+
}
|
|
5405
|
+
return;
|
|
5406
|
+
}
|
|
5407
|
+
if (group === null || verb === null) return;
|
|
5408
|
+
if (GH_WRITE.has(verb)) {
|
|
5409
|
+
out.sends = true;
|
|
5410
|
+
const rIdx = args.findIndex((w) => w === "-R" || w === "--repo");
|
|
5411
|
+
const rEq = args.find((w) => w !== null && /^(-R|--repo)=/.test(w));
|
|
5412
|
+
const rVal = rEq !== void 0 && rEq !== null ? rEq.slice(rEq.indexOf("=") + 1) : rIdx >= 0 ? args[rIdx + 1] : void 0;
|
|
5413
|
+
let repo;
|
|
5414
|
+
if (rIdx >= 0 || rEq !== void 0) {
|
|
5415
|
+
if (typeof rVal === "string") {
|
|
5416
|
+
const named = ghRepoArg(rVal, ghHost);
|
|
5417
|
+
repo = named.repo;
|
|
5418
|
+
if (named.host !== ghHost) dest = `host:${named.host}`;
|
|
5419
|
+
} else repo = `${ghHost}/*`;
|
|
5420
|
+
} else if (group === "gist" || group === "repo" && (verb === "create" || verb === "fork")) {
|
|
5421
|
+
repo = `${ghHost}/*`;
|
|
5422
|
+
} else if (group === "repo" && verb !== "rename") {
|
|
5423
|
+
const operand = positionals(args, GH_REPO_VALUE_FLAGS)[2];
|
|
5424
|
+
if (operand === null) repo = `${ghHost}/*`;
|
|
5425
|
+
else if (operand !== void 0) repo = ghRepoArg(operand, ghHost).repo;
|
|
5426
|
+
else if (envRepo) repo = `${ghHost}/*`;
|
|
5427
|
+
} else if (envRepo) {
|
|
5428
|
+
repo = `${ghHost}/*`;
|
|
5429
|
+
}
|
|
5430
|
+
out.destinations.push(dest);
|
|
5431
|
+
if (repo !== void 0) out.repos.push(repo);
|
|
5432
|
+
} else if (GH_READ.has(verb)) {
|
|
5433
|
+
out.fetches = true;
|
|
5434
|
+
out.fetchKinds.push(`gh ${group}`);
|
|
5435
|
+
out.fetchDestinations.push(dest);
|
|
5436
|
+
}
|
|
5437
|
+
return;
|
|
5438
|
+
}
|
|
5439
|
+
case "git": {
|
|
5440
|
+
const pos = positionals(args, /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree"]));
|
|
5441
|
+
const verb = pos[0];
|
|
5442
|
+
if (verb === null || verb === void 0) return;
|
|
5443
|
+
for (let i = 0; i < args.length; i++) {
|
|
5444
|
+
if (args[i] !== "-c") continue;
|
|
5445
|
+
const v = args[i + 1];
|
|
5446
|
+
if (v === null || v === void 0) {
|
|
5447
|
+
if (verb === "push") out.unresolved = true;
|
|
5448
|
+
continue;
|
|
5449
|
+
}
|
|
5450
|
+
const m = /^remote\.[^=]+\.(?:push)?url=(.*)$/i.exec(v);
|
|
5451
|
+
if (m) sendToGitUrl(m[1], out);
|
|
5452
|
+
}
|
|
5453
|
+
if (verb === "remote") {
|
|
5454
|
+
const rp = positionals(args, /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree", "-t", "-m"]));
|
|
5455
|
+
const sub = rp[1];
|
|
5456
|
+
if (sub === "add" || sub === "set-url") {
|
|
5457
|
+
const url = rp.slice(2).filter((p) => p !== "--push" && p !== "--add")[1];
|
|
5458
|
+
sendToGitUrl(url, out);
|
|
5459
|
+
}
|
|
5460
|
+
return;
|
|
5461
|
+
}
|
|
5462
|
+
if (verb === "config") {
|
|
5463
|
+
const reads = args.some(
|
|
5464
|
+
(w) => w !== null && /^--(get|get-all|get-regexp|unset|unset-all|list|get-urlmatch)$/.test(w)
|
|
5465
|
+
);
|
|
5466
|
+
const k = pos.findIndex((p) => p !== null && /^remote\..+\.(?:push)?url$/i.test(p));
|
|
5467
|
+
if (k >= 0 && k + 1 < pos.length && !reads) sendToGitUrl(pos[k + 1], out);
|
|
5468
|
+
return;
|
|
5469
|
+
}
|
|
5470
|
+
if (GIT_FETCH.has(verb)) {
|
|
5471
|
+
out.fetches = true;
|
|
5472
|
+
out.fetchKinds.push(`git ${verb}`);
|
|
5473
|
+
for (const p of pos.slice(1)) {
|
|
5474
|
+
const h = p !== null && /:\/\/|@/.test(p) ? host(p) : null;
|
|
5475
|
+
if (h) out.fetchDestinations.push(h);
|
|
5476
|
+
}
|
|
5477
|
+
return;
|
|
5478
|
+
}
|
|
5479
|
+
if (verb === "push") {
|
|
5480
|
+
if (pos.length > 1 && pos[1] === null) {
|
|
5481
|
+
out.sends = true;
|
|
5482
|
+
out.unresolved = true;
|
|
5483
|
+
return;
|
|
5484
|
+
}
|
|
5485
|
+
const target = flagValue(args, ["--repo"]) ?? pos.slice(1).find((p) => p !== null && !p.startsWith("-")) ?? null;
|
|
5486
|
+
if (target !== null && (/:\/\/|@/.test(target) || /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+:/.test(target))) {
|
|
5487
|
+
sendToGitUrl(target, out);
|
|
5488
|
+
return;
|
|
5489
|
+
}
|
|
5490
|
+
if (target !== null && /^[./~]|\//.test(target)) return;
|
|
5491
|
+
out.sends = true;
|
|
5492
|
+
const elsewhere = state.cwdChanged || args.some((w) => w !== null && /^(-C|--git-dir|--work-tree)(=|$)/.test(w)) || env.some((n) => n === "GIT_DIR" || n === "GIT_WORK_TREE" || n === "GIT_CONFIG");
|
|
5493
|
+
if (elsewhere) out.unresolved = true;
|
|
5494
|
+
else out.gitRemotes.push(target ?? "");
|
|
5495
|
+
}
|
|
5496
|
+
return;
|
|
5497
|
+
}
|
|
5498
|
+
default: {
|
|
5499
|
+
const pub = PUBLISH[head];
|
|
5500
|
+
if (!pub) return;
|
|
5501
|
+
const pos = positionals(args, new Set(pub.flag ?? []));
|
|
5502
|
+
const verb = pos[0];
|
|
5503
|
+
if (verb === null || verb === void 0 || !pub.verbs.has(verb)) return;
|
|
5504
|
+
if (head === "yarn" && verb === "npm" && pos[1] !== "publish") return;
|
|
5505
|
+
out.sends = true;
|
|
5506
|
+
const named = pub.flag ? flagValue(args, pub.flag) : null;
|
|
5507
|
+
const h = named ? host(named) : null;
|
|
5508
|
+
if (named && !h) return;
|
|
5509
|
+
if (head === "docker") {
|
|
5510
|
+
const image = pos[1];
|
|
5511
|
+
const first = image?.split("/")[0] ?? "";
|
|
5512
|
+
const reg = first.includes(".") || first.includes(":") ? host(first) : null;
|
|
5513
|
+
out.destinations.push(reg ?? "host:docker.io");
|
|
5514
|
+
return;
|
|
5515
|
+
}
|
|
5516
|
+
out.destinations.push(h ?? `host:${pub.registry}`);
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
function walkCalls(command, out, depth, state) {
|
|
5521
|
+
const f = parseShared(command);
|
|
5522
|
+
if (typeof f === "symbol" || !f) return;
|
|
5523
|
+
syntax4.Walk(f, (node) => {
|
|
5524
|
+
if (!node) return false;
|
|
5525
|
+
const type = syntax4.NodeType(node);
|
|
5526
|
+
if (type === "DeclClause") {
|
|
5527
|
+
for (const a of node.Args || []) if (a?.Name?.Value) state.env.add(a.Name.Value);
|
|
5528
|
+
return true;
|
|
5529
|
+
}
|
|
5530
|
+
if (type !== "CallExpr") return true;
|
|
5531
|
+
const call = node;
|
|
5532
|
+
if ((call.Args || []).length === 0) {
|
|
5533
|
+
for (const a of call.Assigns || []) if (a?.Name?.Value) state.env.add(a.Name.Value);
|
|
5534
|
+
return true;
|
|
5535
|
+
}
|
|
5536
|
+
const c = callWords(node);
|
|
5537
|
+
if (!c) return true;
|
|
5538
|
+
if (SHELLS2.has(c.head) && depth < MAX_DEPTH3) {
|
|
5539
|
+
const i = c.args.findIndex((w) => w === "-c" || w === "-lc" || w === "-ec");
|
|
5540
|
+
const payload = i >= 0 ? c.args[i + 1] : null;
|
|
5541
|
+
if (typeof payload === "string") walkCalls(payload, out, depth + 1, state);
|
|
5542
|
+
return true;
|
|
5543
|
+
}
|
|
5544
|
+
if (CWD_CHANGERS.has(c.head)) state.cwdChanged = true;
|
|
5545
|
+
classifyCall(c.head, c.args, c.prefixes, [...c.env, ...state.env], state, out);
|
|
5546
|
+
return true;
|
|
5547
|
+
});
|
|
5548
|
+
}
|
|
5549
|
+
function classifyShellExposure(command) {
|
|
5550
|
+
const out = {
|
|
5551
|
+
...EMPTY,
|
|
5552
|
+
destinations: [],
|
|
5553
|
+
gitRemotes: [],
|
|
5554
|
+
repos: [],
|
|
5555
|
+
fetchDestinations: [],
|
|
5556
|
+
fetchKinds: []
|
|
5557
|
+
};
|
|
5558
|
+
try {
|
|
5559
|
+
walkCalls(command, out, 0, { cwdChanged: false, env: /* @__PURE__ */ new Set() });
|
|
5560
|
+
} catch {
|
|
5561
|
+
return { ...EMPTY };
|
|
5562
|
+
}
|
|
5563
|
+
out.destinations = [...new Set(out.destinations)];
|
|
5564
|
+
out.fetchDestinations = [...new Set(out.fetchDestinations)];
|
|
5565
|
+
out.gitRemotes = [...new Set(out.gitRemotes)];
|
|
5566
|
+
out.repos = [...new Set(out.repos)];
|
|
5567
|
+
out.fetchKinds = [...new Set(out.fetchKinds)];
|
|
5568
|
+
return out;
|
|
5569
|
+
}
|
|
4857
5570
|
function stripTerminalEscapes(s) {
|
|
4858
5571
|
return s.replace(TERMINAL_ESCAPE_RE, "");
|
|
4859
5572
|
}
|
|
@@ -5013,8 +5726,8 @@ function matchCanaryArgs(args, values) {
|
|
|
5013
5726
|
if (values.length === 0) return null;
|
|
5014
5727
|
const needles = prepare(values);
|
|
5015
5728
|
if (needles.length === 0) return null;
|
|
5016
|
-
const
|
|
5017
|
-
if (depth >
|
|
5729
|
+
const walk2 = (v, depth, fieldPath) => {
|
|
5730
|
+
if (depth > MAX_DEPTH4) return null;
|
|
5018
5731
|
if (typeof v === "string") {
|
|
5019
5732
|
const hit = matchPrepared(v, needles);
|
|
5020
5733
|
if (hit) return { id: hit.v.id, view: hit.view, fieldPath, retired: Boolean(hit.v.retired) };
|
|
@@ -5022,31 +5735,31 @@ function matchCanaryArgs(args, values) {
|
|
|
5022
5735
|
const trimmed = v.trim();
|
|
5023
5736
|
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
5024
5737
|
try {
|
|
5025
|
-
return
|
|
5738
|
+
return walk2(JSON.parse(v), depth + 1, fieldPath);
|
|
5026
5739
|
} catch {
|
|
5027
5740
|
}
|
|
5028
5741
|
}
|
|
5029
5742
|
}
|
|
5030
5743
|
return null;
|
|
5031
5744
|
}
|
|
5032
|
-
if (typeof v === "number" && Number.isFinite(v)) return
|
|
5745
|
+
if (typeof v === "number" && Number.isFinite(v)) return walk2(String(v), depth, fieldPath);
|
|
5033
5746
|
if (Array.isArray(v)) {
|
|
5034
5747
|
for (let i = 0; i < v.length; i++) {
|
|
5035
|
-
const h =
|
|
5748
|
+
const h = walk2(v[i], depth + 1, `${fieldPath}[${i}]`);
|
|
5036
5749
|
if (h) return h;
|
|
5037
5750
|
}
|
|
5038
5751
|
return null;
|
|
5039
5752
|
}
|
|
5040
5753
|
if (v && typeof v === "object") {
|
|
5041
5754
|
for (const [k, child] of Object.entries(v)) {
|
|
5042
|
-
const h =
|
|
5755
|
+
const h = walk2(child, depth + 1, fieldPath ? `${fieldPath}.${k}` : k);
|
|
5043
5756
|
if (h) return h;
|
|
5044
5757
|
}
|
|
5045
5758
|
}
|
|
5046
5759
|
return null;
|
|
5047
5760
|
};
|
|
5048
5761
|
try {
|
|
5049
|
-
return
|
|
5762
|
+
return walk2(args, 0, "");
|
|
5050
5763
|
} catch (e) {
|
|
5051
5764
|
console.error(
|
|
5052
5765
|
"[node9 engine] canary args walk failed:",
|
|
@@ -5055,7 +5768,7 @@ function matchCanaryArgs(args, values) {
|
|
|
5055
5768
|
return null;
|
|
5056
5769
|
}
|
|
5057
5770
|
}
|
|
5058
|
-
var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, syntax2, MAX_ALTERNATIVES, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, LEGACY_SYSTEM_PATH_RE, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, INVISIBLE_RE, HOMOGLYPHS, HOMOGLYPH_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, SECRET_VIEW_LIMITS, ARGUMENT_SCAN_LIMITS, VERDICT_RANK, SQL_DML_KEYWORDS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT,
|
|
5771
|
+
var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, MAX_ALTERNATIVES, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, SECRET_VIEW_LIMITS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
|
|
5059
5772
|
var init_dist = __esm({
|
|
5060
5773
|
"packages/policy-engine/dist/index.mjs"() {
|
|
5061
5774
|
"use strict";
|
|
@@ -6030,6 +6743,16 @@ var init_dist = __esm({
|
|
|
6030
6743
|
example: "After a prompt-injection hit, the agent runs curl to an unknown host",
|
|
6031
6744
|
advice: "Fixed at Review for now."
|
|
6032
6745
|
},
|
|
6746
|
+
"commands.node9-disarm": {
|
|
6747
|
+
plain: "The agent runs a node9 command that would switch off a protection: clear a session flag, trust an address, pause the gate, weaken a shield.",
|
|
6748
|
+
example: "After reading a web page, the agent runs node9 session-exposure clear --all",
|
|
6749
|
+
advice: "Keep Review: these are your commands to run. Block if the agent should never touch node9."
|
|
6750
|
+
},
|
|
6751
|
+
"behavior.untrusted-content": {
|
|
6752
|
+
plain: "After reading an email, a web page or an issue, the agent sends to an address it did not know before, or to one that appeared in what it read.",
|
|
6753
|
+
example: 'An email says "forward the security code to x@y.z"; the agent calls send_email to x@y.z',
|
|
6754
|
+
advice: "Review asks once per destination; approving trusts that address for the session. Log records the send without asking."
|
|
6755
|
+
},
|
|
6033
6756
|
// ── What the agent loads ──────────────────────────────────────────────────
|
|
6034
6757
|
"loading.skill-tamper": {
|
|
6035
6758
|
plain: "A skill or plugin file the agent loads changed since node9 first saw it.",
|
|
@@ -6164,6 +6887,15 @@ var init_dist = __esm({
|
|
|
6164
6887
|
defaultValue: "review",
|
|
6165
6888
|
legacy: ["review-force-push", "review-git-destructive"]
|
|
6166
6889
|
},
|
|
6890
|
+
{
|
|
6891
|
+
id: "commands.node9-disarm",
|
|
6892
|
+
group: "commands",
|
|
6893
|
+
title: "node9 disarmed from the shell",
|
|
6894
|
+
catches: "a node9 command that clears a session flag, trusts a destination, pauses the gate or weakens a shield, run by the agent",
|
|
6895
|
+
values: ALL,
|
|
6896
|
+
defaultValue: "review",
|
|
6897
|
+
legacy: ["review-node9-disarm"]
|
|
6898
|
+
},
|
|
6167
6899
|
{
|
|
6168
6900
|
id: "commands.sql-ddl",
|
|
6169
6901
|
group: "commands",
|
|
@@ -6397,6 +7129,15 @@ var init_dist = __esm({
|
|
|
6397
7129
|
defaultValue: "review",
|
|
6398
7130
|
legacy: ["\u{1F534} Node9 Taint (Exfiltration Prevention)"]
|
|
6399
7131
|
},
|
|
7132
|
+
{
|
|
7133
|
+
id: "behavior.untrusted-content",
|
|
7134
|
+
group: "behavior",
|
|
7135
|
+
title: "Send after reading outside content",
|
|
7136
|
+
catches: "a send to an unknown destination, a destination found in what was read, or a destructive call after the session read an email, a web page or an issue",
|
|
7137
|
+
values: ["off", "log", "review"],
|
|
7138
|
+
defaultValue: "review",
|
|
7139
|
+
legacy: ["\u{1F6E1}\uFE0F Node9 Exposure", "exposure"]
|
|
7140
|
+
},
|
|
6400
7141
|
// ── What the agent loads ────────────────────────────────────────────────
|
|
6401
7142
|
{
|
|
6402
7143
|
id: "loading.skill-tamper",
|
|
@@ -6471,6 +7212,11 @@ var init_dist = __esm({
|
|
|
6471
7212
|
if (s.packageCheck?.enabled === void 0) return void 0;
|
|
6472
7213
|
if (!s.packageCheck.enabled) return "off";
|
|
6473
7214
|
return s.packageCheck.onMalicious === "review" ? "review" : "block";
|
|
7215
|
+
},
|
|
7216
|
+
"behavior.untrusted-content": (s) => {
|
|
7217
|
+
if (s.untrustedContent?.enabled === void 0) return void 0;
|
|
7218
|
+
if (!s.untrustedContent.enabled) return "off";
|
|
7219
|
+
return s.untrustedContent.afterExposure === "log-only" ? "log" : "review";
|
|
6474
7220
|
}
|
|
6475
7221
|
};
|
|
6476
7222
|
({ syntax } = mvdanSh);
|
|
@@ -7387,7 +8133,23 @@ var init_dist = __esm({
|
|
|
7387
8133
|
"shell",
|
|
7388
8134
|
"exec_command"
|
|
7389
8135
|
]);
|
|
7390
|
-
MCP_EXECUTORS = /* @__PURE__ */ new Set([
|
|
8136
|
+
MCP_EXECUTORS = /* @__PURE__ */ new Set([
|
|
8137
|
+
...BASH_TOOL_NAMES,
|
|
8138
|
+
"exec",
|
|
8139
|
+
"run_command",
|
|
8140
|
+
"terminal",
|
|
8141
|
+
"execute_command",
|
|
8142
|
+
"execute_shell",
|
|
8143
|
+
"execute_shell_command",
|
|
8144
|
+
"shell_execute",
|
|
8145
|
+
"run_shell",
|
|
8146
|
+
"run_terminal_command",
|
|
8147
|
+
"start_process"
|
|
8148
|
+
]);
|
|
8149
|
+
BUILTIN_FIELDS = ["command", "cmd", "script"];
|
|
8150
|
+
ARGV_FIELDS = ["args", "argv"];
|
|
8151
|
+
BARE_WORD = /^[A-Za-z0-9_./+:@%,-]+$/;
|
|
8152
|
+
MAX_ARGV_CHARACTERS = 262144;
|
|
7391
8153
|
({ syntax: syntax2 } = mvdanSh2);
|
|
7392
8154
|
MAX_ALTERNATIVES = 64;
|
|
7393
8155
|
MAX_DEPTH = 64;
|
|
@@ -7418,6 +8180,22 @@ var init_dist = __esm({
|
|
|
7418
8180
|
SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
|
|
7419
8181
|
PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
|
|
7420
8182
|
STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
|
|
8183
|
+
AGENT_INSTRUCTION_FILES = [
|
|
8184
|
+
".gitlab-ci.yml",
|
|
8185
|
+
"CLAUDE.md",
|
|
8186
|
+
"AGENTS.md",
|
|
8187
|
+
".cursorrules",
|
|
8188
|
+
".mcp.json",
|
|
8189
|
+
".vscode/settings.json",
|
|
8190
|
+
"package.json"
|
|
8191
|
+
];
|
|
8192
|
+
AGENT_INSTRUCTION_DIRS = [
|
|
8193
|
+
".github/workflows",
|
|
8194
|
+
".claude",
|
|
8195
|
+
".cursor",
|
|
8196
|
+
".git/hooks",
|
|
8197
|
+
".husky"
|
|
8198
|
+
];
|
|
7421
8199
|
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
7422
8200
|
RANK = { allow: 0, review: 1, block: 2 };
|
|
7423
8201
|
BIP39_ENGLISH = `abandon
|
|
@@ -9497,6 +10275,7 @@ zoo`.split("\n");
|
|
|
9497
10275
|
indices = new Map(BIP39_ENGLISH.map((word, i) => [word, i]));
|
|
9498
10276
|
lengths = [24, 21, 18, 15, 12];
|
|
9499
10277
|
hash = (value) => createHash2("sha256").update(value).digest();
|
|
10278
|
+
MAX_MNEMONIC_HASHES = 25e3;
|
|
9500
10279
|
IBAN_LENGTH = {
|
|
9501
10280
|
AD: 24,
|
|
9502
10281
|
AE: 23,
|
|
@@ -9604,6 +10383,7 @@ zoo`.split("\n");
|
|
|
9604
10383
|
CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
|
|
9605
10384
|
CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
|
|
9606
10385
|
INVISIBLE_RE = /[͏----︀-️]|[\u{E0000}-\u{E007F}]/gu;
|
|
10386
|
+
INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
|
|
9607
10387
|
HOMOGLYPHS = {
|
|
9608
10388
|
// Cyrillic lowercase
|
|
9609
10389
|
\u0430: "a",
|
|
@@ -10262,6 +11042,7 @@ zoo`.split("\n");
|
|
|
10262
11042
|
MAX_DEPTH2 = 5;
|
|
10263
11043
|
MAX_STRING_BYTES = 1e5;
|
|
10264
11044
|
MAX_JSON_PARSE_BYTES = 1e4;
|
|
11045
|
+
BATCH_SEPARATOR = "\n";
|
|
10265
11046
|
({ syntax: syntax3 } = mvdanSh3);
|
|
10266
11047
|
NPM_INSTALL_VERBS = /* @__PURE__ */ new Set([
|
|
10267
11048
|
"install",
|
|
@@ -10517,7 +11298,25 @@ zoo`.split("\n");
|
|
|
10517
11298
|
name: "onlineFallback",
|
|
10518
11299
|
legacy: ["packageCheck", "onlineFallback"]
|
|
10519
11300
|
},
|
|
10520
|
-
{ checkId: "loading.malicious-package", name: "allow", legacy: ["packageCheck", "allow"] }
|
|
11301
|
+
{ checkId: "loading.malicious-package", name: "allow", legacy: ["packageCheck", "allow"] },
|
|
11302
|
+
// fw#318: the verdict carries off / log (log-only) / review; the tuning
|
|
11303
|
+
// carries whether a review asks once per destination or every time, the
|
|
11304
|
+
// frame around outside content, and the organisation's own domains.
|
|
11305
|
+
{
|
|
11306
|
+
checkId: "behavior.untrusted-content",
|
|
11307
|
+
name: "afterExposure",
|
|
11308
|
+
legacy: ["untrustedContent", "afterExposure"]
|
|
11309
|
+
},
|
|
11310
|
+
{
|
|
11311
|
+
checkId: "behavior.untrusted-content",
|
|
11312
|
+
name: "spotlight",
|
|
11313
|
+
legacy: ["untrustedContent", "spotlight"]
|
|
11314
|
+
},
|
|
11315
|
+
{
|
|
11316
|
+
checkId: "behavior.untrusted-content",
|
|
11317
|
+
name: "orgDomains",
|
|
11318
|
+
legacy: ["untrustedContent", "orgDomains"]
|
|
11319
|
+
}
|
|
10521
11320
|
];
|
|
10522
11321
|
MAP_GOVERNED = /* @__PURE__ */ new Set([
|
|
10523
11322
|
"files.shell-startup",
|
|
@@ -10534,6 +11333,7 @@ zoo`.split("\n");
|
|
|
10534
11333
|
"commands.temp-binary",
|
|
10535
11334
|
"commands.disk-destroy",
|
|
10536
11335
|
"commands.dangerous-word",
|
|
11336
|
+
"commands.node9-disarm",
|
|
10537
11337
|
"data.pipe-chain"
|
|
10538
11338
|
]);
|
|
10539
11339
|
KNOB_GOVERNED = {
|
|
@@ -10545,7 +11345,8 @@ zoo`.split("\n");
|
|
|
10545
11345
|
"behavior.loops": ["off", "block"],
|
|
10546
11346
|
"behavior.prompt-injection": ["off", "log"],
|
|
10547
11347
|
"loading.skill-tamper": ["off", "log", "block"],
|
|
10548
|
-
"loading.malicious-package": ["off", "review", "block"]
|
|
11348
|
+
"loading.malicious-package": ["off", "review", "block"],
|
|
11349
|
+
"behavior.untrusted-content": ["off", "log", "review"]
|
|
10549
11350
|
};
|
|
10550
11351
|
CONFIGURABLE_CHECK_IDS = CHECKS.filter(
|
|
10551
11352
|
(c) => configurableValues(c.id).length > 0
|
|
@@ -10711,16 +11512,24 @@ zoo`.split("\n");
|
|
|
10711
11512
|
["browser_batch", ["actions[].input.url"]]
|
|
10712
11513
|
]);
|
|
10713
11514
|
SECRET_VIEW_LIMITS = {
|
|
11515
|
+
/** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
|
|
10714
11516
|
maxDepth: 4,
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
11517
|
+
/** Nested views per parent view, and nested text per call; past either the call is reviewed. */
|
|
11518
|
+
maxNestedViewsPerLeaf: 64,
|
|
11519
|
+
maxNestedBytes: 1048576,
|
|
11520
|
+
/** Joined hostname-label text per call; past it the call is reviewed. */
|
|
11521
|
+
maxJoinedLabelBytes: 1048576,
|
|
11522
|
+
/** Decoded fragments are matched in batches of about this many characters. */
|
|
11523
|
+
batchCharacters: 65536,
|
|
11524
|
+
/** Only a valid DNS name has its labels joined. */
|
|
11525
|
+
maxHostLength: 253,
|
|
11526
|
+
maxLabelLength: 63,
|
|
11527
|
+
maxLabels: 127
|
|
10719
11528
|
};
|
|
10720
11529
|
ARGUMENT_SCAN_LIMITS = {
|
|
10721
11530
|
maxDepth: 12,
|
|
10722
11531
|
maxNodes: 16384,
|
|
10723
|
-
/**
|
|
11532
|
+
/** Text is matched and decoded in windows of this size. */
|
|
10724
11533
|
maxStringCharacters: 1e5,
|
|
10725
11534
|
/** Windows overlap by this much so a token on a window edge is still seen whole. */
|
|
10726
11535
|
windowOverlap: 4096,
|
|
@@ -10730,25 +11539,176 @@ zoo`.split("\n");
|
|
|
10730
11539
|
maxTailBytes: 4194304,
|
|
10731
11540
|
maxJsonCharacters: 1e4
|
|
10732
11541
|
};
|
|
11542
|
+
RUN_CHAR = /[A-Za-z0-9+/=_%\\.-]/;
|
|
11543
|
+
runChar = (value, index) => {
|
|
11544
|
+
const char = value[index] ?? "";
|
|
11545
|
+
return RUN_CHAR.test(char) || char !== "" && isInvisibleChar(char);
|
|
11546
|
+
};
|
|
10733
11547
|
VERDICT_RANK = {
|
|
10734
11548
|
allow: 0,
|
|
10735
11549
|
review: 1,
|
|
10736
11550
|
block: 2
|
|
10737
11551
|
};
|
|
10738
11552
|
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
11553
|
+
MAX_DESTINATIONS = 200;
|
|
11554
|
+
MAX_SCAN_CHARS = 1e5;
|
|
11555
|
+
EMAIL_RE = /[\p{L}\p{N}._%+-]+@[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?(?:\.[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?)*\.\p{L}{2,}/gu;
|
|
11556
|
+
URL_RE = /\b(?:[a-z][a-z0-9+.-]*:\/\/|www\.)[^\s<>"'`){}]+/gi;
|
|
11557
|
+
IBAN_RE = /\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]{4}){2,7}(?:[ -]?[A-Z0-9]{1,4})?\b/g;
|
|
11558
|
+
RECIPIENT_KEYS = /* @__PURE__ */ new Set([
|
|
11559
|
+
"to",
|
|
11560
|
+
"cc",
|
|
11561
|
+
"bcc",
|
|
11562
|
+
"recipient",
|
|
11563
|
+
"recipients",
|
|
11564
|
+
"email",
|
|
11565
|
+
"participants",
|
|
11566
|
+
"attendees",
|
|
11567
|
+
"destination",
|
|
11568
|
+
"iban",
|
|
11569
|
+
"account",
|
|
11570
|
+
// Where a request goes, when the value is a bare host or a scheme-less URL.
|
|
11571
|
+
"url",
|
|
11572
|
+
"uri",
|
|
11573
|
+
"host",
|
|
11574
|
+
"hostname",
|
|
11575
|
+
"endpoint",
|
|
11576
|
+
"server",
|
|
11577
|
+
"domain",
|
|
11578
|
+
"address",
|
|
11579
|
+
"webhook",
|
|
11580
|
+
"webhook_url",
|
|
11581
|
+
"callback",
|
|
11582
|
+
"callback_url",
|
|
11583
|
+
"target"
|
|
11584
|
+
]);
|
|
11585
|
+
DESTRUCTIVE_VERB_RE = /^(?:delete|remove|drop|destroy|purge|wipe|erase|transfer|pay|send_money|withdraw|change_password|update_password|reset_password|set_permission|grant|revoke|share)(?:_|$)/i;
|
|
11586
|
+
({ syntax: syntax4 } = mvdanSh4);
|
|
11587
|
+
EMPTY = {
|
|
11588
|
+
fetches: false,
|
|
11589
|
+
sends: false,
|
|
11590
|
+
destinations: [],
|
|
11591
|
+
gitRemotes: [],
|
|
11592
|
+
repos: [],
|
|
11593
|
+
unresolved: false,
|
|
11594
|
+
fetchDestinations: [],
|
|
11595
|
+
fetchKinds: []
|
|
11596
|
+
};
|
|
11597
|
+
CURL_SEND_FLAGS = /* @__PURE__ */ new Set([
|
|
11598
|
+
"-d",
|
|
11599
|
+
"--data",
|
|
11600
|
+
"--data-ascii",
|
|
11601
|
+
"--data-binary",
|
|
11602
|
+
"--data-raw",
|
|
11603
|
+
"--data-urlencode",
|
|
11604
|
+
"-F",
|
|
11605
|
+
"--form",
|
|
11606
|
+
"--form-string",
|
|
11607
|
+
"-T",
|
|
11608
|
+
"--upload-file",
|
|
11609
|
+
"--json"
|
|
11610
|
+
]);
|
|
11611
|
+
WGET_SEND_FLAGS = /* @__PURE__ */ new Set(["--post-data", "--post-file", "--body-data", "--body-file"]);
|
|
11612
|
+
IWR_SEND_FLAGS = /* @__PURE__ */ new Set(["-body", "-infile"]);
|
|
11613
|
+
WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
|
|
11614
|
+
GH_READ = /* @__PURE__ */ new Set(["view", "list", "diff", "checks", "status", "download", "clone"]);
|
|
11615
|
+
GH_WRITE = /* @__PURE__ */ new Set([
|
|
11616
|
+
"create",
|
|
11617
|
+
"comment",
|
|
11618
|
+
"edit",
|
|
11619
|
+
"close",
|
|
11620
|
+
"reopen",
|
|
11621
|
+
"merge",
|
|
11622
|
+
"review",
|
|
11623
|
+
"upload",
|
|
11624
|
+
"delete",
|
|
11625
|
+
"transfer",
|
|
11626
|
+
"pin",
|
|
11627
|
+
"lock",
|
|
11628
|
+
"ready",
|
|
11629
|
+
"set",
|
|
11630
|
+
"sync",
|
|
11631
|
+
"fork"
|
|
11632
|
+
]);
|
|
11633
|
+
GH_HOSTNAME_FLAGS = /* @__PURE__ */ new Set(["--hostname", "-h", "-R", "--repo"]);
|
|
11634
|
+
GH_REPO_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
11635
|
+
"--hostname",
|
|
11636
|
+
"-R",
|
|
11637
|
+
"--repo",
|
|
11638
|
+
"-d",
|
|
11639
|
+
"--description",
|
|
11640
|
+
"-h",
|
|
11641
|
+
"--homepage",
|
|
11642
|
+
"--visibility",
|
|
11643
|
+
"--default-branch",
|
|
11644
|
+
"--add-topic",
|
|
11645
|
+
"--remove-topic",
|
|
11646
|
+
"-p",
|
|
11647
|
+
"--template",
|
|
11648
|
+
"-s",
|
|
11649
|
+
"--source",
|
|
11650
|
+
"-b",
|
|
11651
|
+
"--branch",
|
|
11652
|
+
"-l",
|
|
11653
|
+
"--license",
|
|
11654
|
+
"-g",
|
|
11655
|
+
"--gitignore",
|
|
11656
|
+
"-t",
|
|
11657
|
+
"--team",
|
|
11658
|
+
"-r",
|
|
11659
|
+
"--remote",
|
|
11660
|
+
"--org"
|
|
11661
|
+
]);
|
|
11662
|
+
GH_API_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
11663
|
+
"--hostname",
|
|
11664
|
+
"-X",
|
|
11665
|
+
"--method",
|
|
11666
|
+
"-f",
|
|
11667
|
+
"--raw-field",
|
|
11668
|
+
"-F",
|
|
11669
|
+
"--field",
|
|
11670
|
+
"-H",
|
|
11671
|
+
"--header",
|
|
11672
|
+
"--input",
|
|
11673
|
+
"-q",
|
|
11674
|
+
"--jq",
|
|
11675
|
+
"-t",
|
|
11676
|
+
"--template",
|
|
11677
|
+
"--cache",
|
|
11678
|
+
"-p",
|
|
11679
|
+
"--preview"
|
|
11680
|
+
]);
|
|
11681
|
+
GIT_FETCH = /* @__PURE__ */ new Set(["clone", "fetch", "pull", "ls-remote"]);
|
|
11682
|
+
PUBLISH = {
|
|
11683
|
+
npm: { verbs: /* @__PURE__ */ new Set(["publish"]), registry: "registry.npmjs.org", flag: ["--registry"] },
|
|
11684
|
+
pnpm: { verbs: /* @__PURE__ */ new Set(["publish"]), registry: "registry.npmjs.org", flag: ["--registry"] },
|
|
11685
|
+
yarn: {
|
|
11686
|
+
verbs: /* @__PURE__ */ new Set(["publish", "npm"]),
|
|
11687
|
+
registry: "registry.npmjs.org",
|
|
11688
|
+
flag: ["--registry"]
|
|
11689
|
+
},
|
|
11690
|
+
twine: { verbs: /* @__PURE__ */ new Set(["upload"]), registry: "upload.pypi.org", flag: ["--repository-url"] },
|
|
11691
|
+
cargo: { verbs: /* @__PURE__ */ new Set(["publish"]), registry: "crates.io", flag: ["--registry", "--index"] },
|
|
11692
|
+
gem: { verbs: /* @__PURE__ */ new Set(["push"]), registry: "rubygems.org", flag: ["--host"] },
|
|
11693
|
+
docker: { verbs: /* @__PURE__ */ new Set(["push"]), registry: "docker.io" }
|
|
11694
|
+
};
|
|
11695
|
+
SHELLS2 = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
11696
|
+
OWN_HEADS = /* @__PURE__ */ new Set([...Object.keys(PUBLISH), "git", "gh"]);
|
|
11697
|
+
MAX_DEPTH3 = 3;
|
|
11698
|
+
CWD_CHANGERS = /* @__PURE__ */ new Set(["cd", "pushd", "popd"]);
|
|
11699
|
+
TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
11700
|
+
LOOP_MAX_RECORDS = 500;
|
|
11701
|
+
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
11702
|
+
PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
11703
|
+
PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
11704
|
+
PII_CC16_RE = /\b(?:4\d{3}|5[1-5]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
|
|
11705
|
+
PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
|
|
10746
11706
|
PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
|
|
10747
11707
|
REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
|
|
10748
11708
|
MAX_PII_SCAN_BYTES = 1e5;
|
|
10749
11709
|
CANARY_MIN_LENGTH = 16;
|
|
10750
11710
|
MAX_TEXT = 1e5;
|
|
10751
|
-
|
|
11711
|
+
MAX_DEPTH4 = 6;
|
|
10752
11712
|
MAX_JSON_PARSE = 1e4;
|
|
10753
11713
|
URL_DEPTH = 4;
|
|
10754
11714
|
B64_DEPTH = 3;
|
|
@@ -10860,8 +11820,8 @@ function truncateAuditPreview(text) {
|
|
|
10860
11820
|
}
|
|
10861
11821
|
function appendToLog(logPath, entry) {
|
|
10862
11822
|
try {
|
|
10863
|
-
const
|
|
10864
|
-
if (!fs.existsSync(
|
|
11823
|
+
const dir2 = path.dirname(logPath);
|
|
11824
|
+
if (!fs.existsSync(dir2)) fs.mkdirSync(dir2, { recursive: true });
|
|
10865
11825
|
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
10866
11826
|
} catch {
|
|
10867
11827
|
}
|
|
@@ -10928,6 +11888,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
10928
11888
|
taintFromEid: meta.taintFromEid,
|
|
10929
11889
|
...meta.taintSource && { taintSource: meta.taintSource }
|
|
10930
11890
|
} : {};
|
|
11891
|
+
const exposureFields = {
|
|
11892
|
+
...meta?.exposure ? { exposure: meta.exposure } : {},
|
|
11893
|
+
...meta?.exposureFromEid ? { exposureFromEid: meta.exposureFromEid } : {}
|
|
11894
|
+
};
|
|
10931
11895
|
const eid = generateEventId();
|
|
10932
11896
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
10933
11897
|
// eid first: the outbox shipper dedups on it, and a fixed leading field
|
|
@@ -10950,6 +11914,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
10950
11914
|
...loopCountField,
|
|
10951
11915
|
...transcriptPathField,
|
|
10952
11916
|
...taintFields,
|
|
11917
|
+
...exposureFields,
|
|
10953
11918
|
...testRun,
|
|
10954
11919
|
agent: meta?.agent,
|
|
10955
11920
|
mcpServer: meta?.mcpServer,
|
|
@@ -10981,10 +11946,72 @@ var init_audit = __esm({
|
|
|
10981
11946
|
// src/core.ts
|
|
10982
11947
|
init_audit();
|
|
10983
11948
|
|
|
11949
|
+
// src/auth/trusted-recipients.ts
|
|
11950
|
+
init_dist();
|
|
11951
|
+
import fs2 from "fs";
|
|
11952
|
+
import path2 from "path";
|
|
11953
|
+
import os2 from "os";
|
|
11954
|
+
function getTrustedRecipientsPath() {
|
|
11955
|
+
return path2.join(os2.homedir(), ".node9", "trusted-recipients.json");
|
|
11956
|
+
}
|
|
11957
|
+
function readTrustedRecipients() {
|
|
11958
|
+
try {
|
|
11959
|
+
const raw = fs2.readFileSync(getTrustedRecipientsPath(), "utf8");
|
|
11960
|
+
const parsed = JSON.parse(raw);
|
|
11961
|
+
return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
|
|
11962
|
+
} catch {
|
|
11963
|
+
return [];
|
|
11964
|
+
}
|
|
11965
|
+
}
|
|
11966
|
+
function writeTrustedRecipients(entries) {
|
|
11967
|
+
const filePath = getTrustedRecipientsPath();
|
|
11968
|
+
fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
11969
|
+
const tmp = filePath + ".node9-tmp";
|
|
11970
|
+
fs2.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
|
|
11971
|
+
fs2.renameSync(tmp, filePath);
|
|
11972
|
+
}
|
|
11973
|
+
function normalizeRecipient(raw) {
|
|
11974
|
+
const v = raw.trim();
|
|
11975
|
+
if (!v) return null;
|
|
11976
|
+
if (/^(email|host|iban):/i.test(v)) {
|
|
11977
|
+
const i = v.indexOf(":");
|
|
11978
|
+
const kind = v.slice(0, i).toLowerCase();
|
|
11979
|
+
const inner = normalizeRecipient(v.slice(i + 1));
|
|
11980
|
+
return inner && inner.startsWith(`${kind}:`) ? inner : null;
|
|
11981
|
+
}
|
|
11982
|
+
const found = extractArgDestinations({ to: v });
|
|
11983
|
+
return found.length === 1 ? found[0] : null;
|
|
11984
|
+
}
|
|
11985
|
+
function addTrustedRecipient(raw, addedBy = "user") {
|
|
11986
|
+
const value = normalizeRecipient(raw);
|
|
11987
|
+
if (!value) return null;
|
|
11988
|
+
const entries = readTrustedRecipients();
|
|
11989
|
+
if (!entries.some((e) => e.value === value)) {
|
|
11990
|
+
entries.push({ value, addedAt: Date.now(), addedBy });
|
|
11991
|
+
writeTrustedRecipients(entries);
|
|
11992
|
+
}
|
|
11993
|
+
return value;
|
|
11994
|
+
}
|
|
11995
|
+
var cache = null;
|
|
11996
|
+
function mtimeOf() {
|
|
11997
|
+
try {
|
|
11998
|
+
return fs2.statSync(getTrustedRecipientsPath()).mtimeMs;
|
|
11999
|
+
} catch {
|
|
12000
|
+
return 0;
|
|
12001
|
+
}
|
|
12002
|
+
}
|
|
12003
|
+
function isTrustedRecipient(value) {
|
|
12004
|
+
const mtime = mtimeOf();
|
|
12005
|
+
if (!cache || cache.mtime !== mtime) {
|
|
12006
|
+
cache = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
|
|
12007
|
+
}
|
|
12008
|
+
return cache.values.has(value);
|
|
12009
|
+
}
|
|
12010
|
+
|
|
10984
12011
|
// src/config/index.ts
|
|
10985
|
-
import
|
|
10986
|
-
import
|
|
10987
|
-
import
|
|
12012
|
+
import fs5 from "fs";
|
|
12013
|
+
import path5 from "path";
|
|
12014
|
+
import os5 from "os";
|
|
10988
12015
|
|
|
10989
12016
|
// src/config-schema.ts
|
|
10990
12017
|
import { z } from "zod";
|
|
@@ -11154,6 +12181,13 @@ var ConfigFileSchema = z.object({
|
|
|
11154
12181
|
onlineFallback: z.boolean().optional(),
|
|
11155
12182
|
allow: z.array(z.string()).optional()
|
|
11156
12183
|
}).optional(),
|
|
12184
|
+
untrustedContent: z.object({
|
|
12185
|
+
enabled: z.boolean().optional(),
|
|
12186
|
+
spotlight: z.boolean().optional(),
|
|
12187
|
+
afterExposure: z.enum(["ask-once", "ask-every-time", "log-only"]).optional(),
|
|
12188
|
+
orgDomains: z.array(z.string()).optional(),
|
|
12189
|
+
roles: z.record(z.string(), z.record(z.string(), z.enum(["source", "action", "read"]))).optional()
|
|
12190
|
+
}).optional(),
|
|
11157
12191
|
skillPinning: z.object({
|
|
11158
12192
|
enabled: z.boolean().optional(),
|
|
11159
12193
|
mode: z.enum(["warn", "block"]).optional(),
|
|
@@ -11164,8 +12198,8 @@ var ConfigFileSchema = z.object({
|
|
|
11164
12198
|
}).strict();
|
|
11165
12199
|
function formatIssues(issues) {
|
|
11166
12200
|
const lines = issues.map((issue) => {
|
|
11167
|
-
const
|
|
11168
|
-
return ` \u2022 ${
|
|
12201
|
+
const path26 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
12202
|
+
return ` \u2022 ${path26}: ${issue.message}`;
|
|
11169
12203
|
});
|
|
11170
12204
|
return `Invalid config:
|
|
11171
12205
|
${lines.join("\n")}`;
|
|
@@ -11178,9 +12212,9 @@ function prunePaths(root, paths) {
|
|
|
11178
12212
|
const yi = y[y.length - 1];
|
|
11179
12213
|
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
11180
12214
|
});
|
|
11181
|
-
for (const
|
|
12215
|
+
for (const path26 of ordered) {
|
|
11182
12216
|
let cur = root;
|
|
11183
|
-
for (const key of
|
|
12217
|
+
for (const key of path26.slice(0, -1)) {
|
|
11184
12218
|
if (cur === null || typeof cur !== "object") {
|
|
11185
12219
|
cur = void 0;
|
|
11186
12220
|
break;
|
|
@@ -11188,7 +12222,7 @@ function prunePaths(root, paths) {
|
|
|
11188
12222
|
cur = cur[key];
|
|
11189
12223
|
}
|
|
11190
12224
|
if (cur === null || typeof cur !== "object") continue;
|
|
11191
|
-
const last =
|
|
12225
|
+
const last = path26[path26.length - 1];
|
|
11192
12226
|
if (Array.isArray(cur)) {
|
|
11193
12227
|
const i = Number(last);
|
|
11194
12228
|
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
@@ -11224,7 +12258,7 @@ function sanitizeConfig(raw) {
|
|
|
11224
12258
|
return issue.keys.map((k) => [...at, k]);
|
|
11225
12259
|
}
|
|
11226
12260
|
return [at.slice(0, -level || void 0)];
|
|
11227
|
-
}).filter((
|
|
12261
|
+
}).filter((path26) => path26.length > 0);
|
|
11228
12262
|
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
11229
12263
|
}
|
|
11230
12264
|
if (ConfigFileSchema.safeParse(working).success) break;
|
|
@@ -11246,17 +12280,17 @@ function sanitizeConfig(raw) {
|
|
|
11246
12280
|
}
|
|
11247
12281
|
|
|
11248
12282
|
// src/shields.ts
|
|
11249
|
-
import
|
|
11250
|
-
import
|
|
11251
|
-
import
|
|
12283
|
+
import fs3 from "fs";
|
|
12284
|
+
import path3 from "path";
|
|
12285
|
+
import os3 from "os";
|
|
11252
12286
|
init_dist();
|
|
11253
12287
|
init_dist();
|
|
11254
|
-
var USER_SHIELDS_DIR =
|
|
12288
|
+
var USER_SHIELDS_DIR = path3.join(os3.homedir(), ".node9", "shields");
|
|
11255
12289
|
function loadUserShields() {
|
|
11256
12290
|
const result = {};
|
|
11257
12291
|
let entries;
|
|
11258
12292
|
try {
|
|
11259
|
-
entries =
|
|
12293
|
+
entries = fs3.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
|
|
11260
12294
|
} catch (err) {
|
|
11261
12295
|
if (err.code !== "ENOENT") {
|
|
11262
12296
|
process.stderr.write(
|
|
@@ -11266,10 +12300,10 @@ function loadUserShields() {
|
|
|
11266
12300
|
}
|
|
11267
12301
|
return result;
|
|
11268
12302
|
}
|
|
11269
|
-
for (const
|
|
11270
|
-
const filePath =
|
|
12303
|
+
for (const file2 of entries) {
|
|
12304
|
+
const filePath = path3.join(USER_SHIELDS_DIR, file2);
|
|
11271
12305
|
try {
|
|
11272
|
-
const raw = JSON.parse(
|
|
12306
|
+
const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
11273
12307
|
const v = validateShieldDefinition(raw);
|
|
11274
12308
|
if ("error" in v) {
|
|
11275
12309
|
process.stderr.write(`[node9] ${v.error}: ${filePath}
|
|
@@ -11278,7 +12312,7 @@ function loadUserShields() {
|
|
|
11278
12312
|
}
|
|
11279
12313
|
result[v.ok.name] = v.ok;
|
|
11280
12314
|
} catch (err) {
|
|
11281
|
-
process.stderr.write(`[node9] Failed to load user shield ${
|
|
12315
|
+
process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
|
|
11282
12316
|
`);
|
|
11283
12317
|
}
|
|
11284
12318
|
}
|
|
@@ -11300,10 +12334,10 @@ function getShield(name) {
|
|
|
11300
12334
|
const resolved = resolveShieldName(name);
|
|
11301
12335
|
return resolved ? SHIELDS[resolved] : null;
|
|
11302
12336
|
}
|
|
11303
|
-
var SHIELDS_STATE_FILE =
|
|
12337
|
+
var SHIELDS_STATE_FILE = path3.join(os3.homedir(), ".node9", "shields.json");
|
|
11304
12338
|
function readShieldsFile() {
|
|
11305
12339
|
try {
|
|
11306
|
-
const raw =
|
|
12340
|
+
const raw = fs3.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
11307
12341
|
if (!raw.trim()) return { active: [] };
|
|
11308
12342
|
const parsed = JSON.parse(raw);
|
|
11309
12343
|
const active = Array.isArray(parsed.active) ? parsed.active.filter(
|
|
@@ -11515,15 +12549,15 @@ function pathRules(rawPath, verdict, reason) {
|
|
|
11515
12549
|
}
|
|
11516
12550
|
|
|
11517
12551
|
// src/auth/trusted-hosts.ts
|
|
11518
|
-
import
|
|
11519
|
-
import
|
|
11520
|
-
import
|
|
12552
|
+
import fs4 from "fs";
|
|
12553
|
+
import path4 from "path";
|
|
12554
|
+
import os4 from "os";
|
|
11521
12555
|
function getTrustedHostsPath() {
|
|
11522
|
-
return
|
|
12556
|
+
return path4.join(os4.homedir(), ".node9", "trusted-hosts.json");
|
|
11523
12557
|
}
|
|
11524
12558
|
function readTrustedHosts() {
|
|
11525
12559
|
try {
|
|
11526
|
-
const raw =
|
|
12560
|
+
const raw = fs4.readFileSync(getTrustedHostsPath(), "utf8");
|
|
11527
12561
|
const parsed = JSON.parse(raw);
|
|
11528
12562
|
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
11529
12563
|
} catch {
|
|
@@ -11534,7 +12568,7 @@ var _cache = null;
|
|
|
11534
12568
|
var CACHE_TTL_MS = 5e3;
|
|
11535
12569
|
function getFileMtime() {
|
|
11536
12570
|
try {
|
|
11537
|
-
return
|
|
12571
|
+
return fs4.statSync(getTrustedHostsPath()).mtimeMs;
|
|
11538
12572
|
} catch {
|
|
11539
12573
|
return 0;
|
|
11540
12574
|
}
|
|
@@ -11549,11 +12583,11 @@ function getCachedHosts() {
|
|
|
11549
12583
|
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
11550
12584
|
return hosts;
|
|
11551
12585
|
}
|
|
11552
|
-
function
|
|
12586
|
+
function normalizeHost2(raw) {
|
|
11553
12587
|
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
11554
12588
|
}
|
|
11555
|
-
function matchesTrustedHost(
|
|
11556
|
-
const normalized =
|
|
12589
|
+
function matchesTrustedHost(host2, list) {
|
|
12590
|
+
const normalized = normalizeHost2(host2);
|
|
11557
12591
|
return list.some((raw) => {
|
|
11558
12592
|
const entryHost = raw.toLowerCase();
|
|
11559
12593
|
if (entryHost.startsWith("*.")) {
|
|
@@ -11563,9 +12597,9 @@ function matchesTrustedHost(host, list) {
|
|
|
11563
12597
|
return normalized === entryHost;
|
|
11564
12598
|
});
|
|
11565
12599
|
}
|
|
11566
|
-
function isTrustedHost(
|
|
12600
|
+
function isTrustedHost(host2) {
|
|
11567
12601
|
return matchesTrustedHost(
|
|
11568
|
-
|
|
12602
|
+
host2,
|
|
11569
12603
|
getCachedHosts().map((entry) => entry.host)
|
|
11570
12604
|
);
|
|
11571
12605
|
}
|
|
@@ -11683,6 +12717,10 @@ function v2ToLegacy(raw, options = {}) {
|
|
|
11683
12717
|
const [id] = pair.split(".", 1);
|
|
11684
12718
|
if (CHECK_BY_ID.has(id)) warnings.push(`tuning.${pair}: no such setting`);
|
|
11685
12719
|
}
|
|
12720
|
+
if (stated["behavior.untrusted-content"] === "log") {
|
|
12721
|
+
const uc = policy.untrustedContent;
|
|
12722
|
+
if (uc) uc.afterExposure = "log-only";
|
|
12723
|
+
}
|
|
11686
12724
|
const legacy = { version: "1.0" };
|
|
11687
12725
|
if (Object.keys(settings).length) legacy.settings = settings;
|
|
11688
12726
|
if (Object.keys(policy).length) legacy.policy = policy;
|
|
@@ -11700,9 +12738,9 @@ var DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
|
11700
12738
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0"]);
|
|
11701
12739
|
var HOST_ALLOW_ENV = "NODE9_API_HOST_ALLOW";
|
|
11702
12740
|
function defaultHostSuffixes() {
|
|
11703
|
-
const
|
|
11704
|
-
const parent =
|
|
11705
|
-
return parent.includes(".") ? [
|
|
12741
|
+
const host2 = new URL(DEFAULT_API_URL).hostname.toLowerCase();
|
|
12742
|
+
const parent = host2.split(".").slice(1).join(".");
|
|
12743
|
+
return parent.includes(".") ? [host2, parent] : [host2];
|
|
11706
12744
|
}
|
|
11707
12745
|
function allowedSuffixes() {
|
|
11708
12746
|
const extra = (process.env[HOST_ALLOW_ENV] ?? "").split(",").map(
|
|
@@ -11720,11 +12758,11 @@ function validateApiUrl(raw) {
|
|
|
11720
12758
|
}
|
|
11721
12759
|
if (u.username || u.password) return null;
|
|
11722
12760
|
if (u.protocol !== "https:" && u.protocol !== "http:") return null;
|
|
11723
|
-
const
|
|
11724
|
-
if (LOOPBACK.has(
|
|
12761
|
+
const host2 = u.hostname.toLowerCase();
|
|
12762
|
+
if (LOOPBACK.has(host2)) return u;
|
|
11725
12763
|
if (u.protocol !== "https:") return null;
|
|
11726
12764
|
const suffixes = allowedSuffixes();
|
|
11727
|
-
const ok = suffixes.some((s) =>
|
|
12765
|
+
const ok = suffixes.some((s) => host2 === s || host2.endsWith("." + s));
|
|
11728
12766
|
return ok ? u : null;
|
|
11729
12767
|
}
|
|
11730
12768
|
function safeApiUrl(raw, onReject) {
|
|
@@ -11866,6 +12904,29 @@ var DEFAULT_CONFIG = {
|
|
|
11866
12904
|
reason: "SQL DDL destructive statement inside a shell command",
|
|
11867
12905
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
11868
12906
|
},
|
|
12907
|
+
// ── node9 itself ──────────────────────────────────────────────────────
|
|
12908
|
+
// The agent runs as the user, so a `node9 ...` command from Bash can
|
|
12909
|
+
// clear the session's taint or exposure, trust a destination or pause
|
|
12910
|
+
// the gate. Those are the user's own commands; from the agent they are
|
|
12911
|
+
// a review (fw#318 review finding 1).
|
|
12912
|
+
{
|
|
12913
|
+
name: "review-node9-disarm",
|
|
12914
|
+
tool: "bash",
|
|
12915
|
+
conditions: [
|
|
12916
|
+
{
|
|
12917
|
+
field: "command",
|
|
12918
|
+
op: "matches",
|
|
12919
|
+
// Single spaces on purpose: a nested `\\s+` inside the alternation
|
|
12920
|
+
// fails the engine's ReDoS check and the rule would be dropped.
|
|
12921
|
+
value: "(^|&&|\\|\\||;)\\s*(npx )?node9 (session-(exposure|taint) clear|trust (add|recipient add)|pause|shield (disable|set)|egress allow|approver set)\\b",
|
|
12922
|
+
flags: "i"
|
|
12923
|
+
}
|
|
12924
|
+
],
|
|
12925
|
+
conditionMode: "all",
|
|
12926
|
+
verdict: "review",
|
|
12927
|
+
reason: "node9 self-weakening command run by the agent",
|
|
12928
|
+
description: "The AI wants to run a node9 command that clears a session flag, trusts a destination, pauses the gate or weakens a shield. These are your commands to run, not the agent's."
|
|
12929
|
+
},
|
|
11869
12930
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
11870
12931
|
{
|
|
11871
12932
|
name: "review-force-push",
|
|
@@ -11967,6 +13028,14 @@ var DEFAULT_CONFIG = {
|
|
|
11967
13028
|
allow: []
|
|
11968
13029
|
},
|
|
11969
13030
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
13031
|
+
untrustedContent: {
|
|
13032
|
+
enabled: true,
|
|
13033
|
+
spotlight: true,
|
|
13034
|
+
afterExposure: "ask-once",
|
|
13035
|
+
orgDomains: [],
|
|
13036
|
+
roles: {},
|
|
13037
|
+
trustedRecipients: []
|
|
13038
|
+
},
|
|
11970
13039
|
trustedHosts: [],
|
|
11971
13040
|
trustedHostsManaged: false,
|
|
11972
13041
|
appPermissions: {},
|
|
@@ -12039,8 +13108,8 @@ function noteRejectedApiUrl(raw) {
|
|
|
12039
13108
|
if (rejectedApiUrlsSeen.has(key)) return;
|
|
12040
13109
|
rejectedApiUrlsSeen.add(key);
|
|
12041
13110
|
try {
|
|
12042
|
-
|
|
12043
|
-
|
|
13111
|
+
fs5.appendFileSync(
|
|
13112
|
+
path5.join(os5.homedir(), ".node9", "hook-debug.log"),
|
|
12044
13113
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
|
|
12045
13114
|
`
|
|
12046
13115
|
);
|
|
@@ -12057,9 +13126,9 @@ function getCredentials() {
|
|
|
12057
13126
|
};
|
|
12058
13127
|
}
|
|
12059
13128
|
try {
|
|
12060
|
-
const credPath =
|
|
12061
|
-
if (
|
|
12062
|
-
const creds = JSON.parse(
|
|
13129
|
+
const credPath = path5.join(os5.homedir(), ".node9", "credentials.json");
|
|
13130
|
+
if (fs5.existsSync(credPath)) {
|
|
13131
|
+
const creds = JSON.parse(fs5.readFileSync(credPath, "utf-8"));
|
|
12063
13132
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
12064
13133
|
const profile = creds[profileName];
|
|
12065
13134
|
if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
|
|
@@ -12091,7 +13160,7 @@ function readRulesCacheResilient(cacheFile) {
|
|
|
12091
13160
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
12092
13161
|
let content;
|
|
12093
13162
|
try {
|
|
12094
|
-
content =
|
|
13163
|
+
content = fs5.readFileSync(cacheFile, "utf-8");
|
|
12095
13164
|
existed = true;
|
|
12096
13165
|
} catch (err) {
|
|
12097
13166
|
if (err.code === "ENOENT") return {};
|
|
@@ -12106,10 +13175,10 @@ function readRulesCacheResilient(cacheFile) {
|
|
|
12106
13175
|
}
|
|
12107
13176
|
}
|
|
12108
13177
|
if (existed || sawReadError) {
|
|
12109
|
-
const backup =
|
|
13178
|
+
const backup = path5.join(path5.dirname(cacheFile), "rules-cache.last-good.json");
|
|
12110
13179
|
if (backup !== cacheFile) {
|
|
12111
13180
|
try {
|
|
12112
|
-
const raw = JSON.parse(
|
|
13181
|
+
const raw = JSON.parse(fs5.readFileSync(backup, "utf-8"));
|
|
12113
13182
|
logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
|
|
12114
13183
|
lastParsedRulesCache = raw;
|
|
12115
13184
|
return raw;
|
|
@@ -12132,8 +13201,8 @@ function logCacheReadIssue(cacheFile, kind) {
|
|
|
12132
13201
|
if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
|
|
12133
13202
|
cacheReadLastLoggedAt = now;
|
|
12134
13203
|
try {
|
|
12135
|
-
|
|
12136
|
-
|
|
13204
|
+
fs5.appendFileSync(
|
|
13205
|
+
path5.join(os5.homedir(), ".node9", "hook-debug.log"),
|
|
12137
13206
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
|
|
12138
13207
|
`
|
|
12139
13208
|
);
|
|
@@ -12154,8 +13223,8 @@ function catalogSettingsFrom(settings, policy) {
|
|
|
12154
13223
|
}
|
|
12155
13224
|
function getConfig(cwd) {
|
|
12156
13225
|
if (!cwd && cachedConfig) return cachedConfig;
|
|
12157
|
-
const globalPath =
|
|
12158
|
-
const projectPath =
|
|
13226
|
+
const globalPath = path5.join(os5.homedir(), ".node9", "config.json");
|
|
13227
|
+
const projectPath = path5.join(cwd ?? process.cwd(), "node9.config.json");
|
|
12159
13228
|
const globalConfig = tryLoadConfig(globalPath);
|
|
12160
13229
|
const projectConfig = tryLoadConfig(projectPath, { project: true });
|
|
12161
13230
|
const mergedSettings = {
|
|
@@ -12168,6 +13237,7 @@ function getConfig(cwd) {
|
|
|
12168
13237
|
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
12169
13238
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
12170
13239
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
13240
|
+
projectToolInspection: {},
|
|
12171
13241
|
// Shipped rules carry `builtin`: the catalog's per-check value governs
|
|
12172
13242
|
// them and only them (a user rule that reuses a name keeps its verdict).
|
|
12173
13243
|
smartRules: DEFAULT_CONFIG.policy.smartRules.map((r) => ({ ...r, builtin: true })),
|
|
@@ -12191,6 +13261,12 @@ function getConfig(cwd) {
|
|
|
12191
13261
|
...DEFAULT_CONFIG.policy.skillPinning,
|
|
12192
13262
|
roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
|
|
12193
13263
|
},
|
|
13264
|
+
untrustedContent: {
|
|
13265
|
+
...DEFAULT_CONFIG.policy.untrustedContent,
|
|
13266
|
+
orgDomains: [...DEFAULT_CONFIG.policy.untrustedContent.orgDomains],
|
|
13267
|
+
roles: {},
|
|
13268
|
+
trustedRecipients: []
|
|
13269
|
+
},
|
|
12194
13270
|
// Left empty on purpose: the local file is read fresh at policy-eval time
|
|
12195
13271
|
// (getCachedHosts via isTrustedHost), NOT snapshotted into the frozen config
|
|
12196
13272
|
// here. A managed list fills this below and flips trustedHostsManaged.
|
|
@@ -12235,8 +13311,14 @@ function getConfig(cwd) {
|
|
|
12235
13311
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
12236
13312
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
12237
13313
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
12238
|
-
if (p.toolInspection)
|
|
12239
|
-
|
|
13314
|
+
if (p.toolInspection) {
|
|
13315
|
+
if (isProject)
|
|
13316
|
+
mergedPolicy.projectToolInspection = {
|
|
13317
|
+
...mergedPolicy.projectToolInspection,
|
|
13318
|
+
...p.toolInspection
|
|
13319
|
+
};
|
|
13320
|
+
else mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
13321
|
+
}
|
|
12240
13322
|
if (p.smartRules) {
|
|
12241
13323
|
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
12242
13324
|
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
@@ -12350,6 +13432,9 @@ function getConfig(cwd) {
|
|
|
12350
13432
|
}
|
|
12351
13433
|
}
|
|
12352
13434
|
}
|
|
13435
|
+
if (p.untrustedContent && typeof p.untrustedContent === "object") {
|
|
13436
|
+
applyUntrustedContent(mergedPolicy.untrustedContent, p.untrustedContent, false, isProject);
|
|
13437
|
+
}
|
|
12353
13438
|
const envs = source.environments || {};
|
|
12354
13439
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
12355
13440
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -12381,7 +13466,7 @@ function getConfig(cwd) {
|
|
|
12381
13466
|
let cloudMandatesEnforcement = false;
|
|
12382
13467
|
let cloudMandatesAppPerm = false;
|
|
12383
13468
|
{
|
|
12384
|
-
const cacheFile =
|
|
13469
|
+
const cacheFile = path5.join(os5.homedir(), ".node9", "rules-cache.json");
|
|
12385
13470
|
try {
|
|
12386
13471
|
const raw = readRulesCacheResilient(cacheFile);
|
|
12387
13472
|
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
@@ -12549,6 +13634,9 @@ function getConfig(cwd) {
|
|
|
12549
13634
|
allow: Array.isArray(pc.allow) ? pc.allow.filter((a) => typeof a === "string" && a.length > 0) : cur.allow
|
|
12550
13635
|
};
|
|
12551
13636
|
}
|
|
13637
|
+
if (mc.untrustedContent && typeof mc.untrustedContent === "object") {
|
|
13638
|
+
applyUntrustedContent(mergedPolicy.untrustedContent, mc.untrustedContent, true);
|
|
13639
|
+
}
|
|
12552
13640
|
if (mc.loopDetection && typeof mc.loopDetection === "object") {
|
|
12553
13641
|
const l = mc.loopDetection;
|
|
12554
13642
|
const cur = mergedPolicy.loopDetection;
|
|
@@ -12569,18 +13657,18 @@ function getConfig(cwd) {
|
|
|
12569
13657
|
}
|
|
12570
13658
|
if (Array.isArray(mc.jailPaths)) {
|
|
12571
13659
|
for (const jp of mc.jailPaths) {
|
|
12572
|
-
const
|
|
12573
|
-
if (!
|
|
13660
|
+
const path26 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
13661
|
+
if (!path26) continue;
|
|
12574
13662
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
12575
|
-
for (const r of pathRules(
|
|
13663
|
+
for (const r of pathRules(path26, verdict, "org-managed jail")) {
|
|
12576
13664
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
12577
13665
|
}
|
|
12578
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
13666
|
+
mergedPolicy.managedJailPaths.push({ path: path26, verdict });
|
|
12579
13667
|
}
|
|
12580
13668
|
}
|
|
12581
13669
|
if (Array.isArray(mc.trustedHosts)) {
|
|
12582
13670
|
mergedPolicy.trustedHostsManaged = true;
|
|
12583
|
-
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) =>
|
|
13671
|
+
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
|
|
12584
13672
|
}
|
|
12585
13673
|
if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
|
|
12586
13674
|
const coerced = {};
|
|
@@ -12598,7 +13686,7 @@ function getConfig(cwd) {
|
|
|
12598
13686
|
);
|
|
12599
13687
|
}
|
|
12600
13688
|
const on = (v) => !!v && typeof v === "object" && v.enabled === true;
|
|
12601
|
-
cloudMandatesEnforcement = cloudMandatesAppPerm || Array.isArray(mc.jailPaths) && mc.jailPaths.some((jp) => typeof jp?.path === "string" && jp.path.trim() !== "") || on(mc.egress) || on(mc.dlp) || on(mc.injectionScan) || on(mc.skillPinning) || on(mc.loopDetection) || !!mc.commandChecks && typeof mc.commandChecks === "object" && Object.values(mc.commandChecks).some(
|
|
13689
|
+
cloudMandatesEnforcement = cloudMandatesAppPerm || Array.isArray(mc.jailPaths) && mc.jailPaths.some((jp) => typeof jp?.path === "string" && jp.path.trim() !== "") || on(mc.egress) || on(mc.dlp) || on(mc.injectionScan) || on(mc.untrustedContent) || on(mc.skillPinning) || on(mc.loopDetection) || !!mc.commandChecks && typeof mc.commandChecks === "object" && Object.values(mc.commandChecks).some(
|
|
12602
13690
|
(v) => typeof v === "string" && v !== "off"
|
|
12603
13691
|
);
|
|
12604
13692
|
}
|
|
@@ -12771,10 +13859,10 @@ function getConfig(cwd) {
|
|
|
12771
13859
|
}
|
|
12772
13860
|
function tryLoadConfig(filePath, options = {}) {
|
|
12773
13861
|
v2ChecksByPath.delete(filePath);
|
|
12774
|
-
if (!
|
|
13862
|
+
if (!fs5.existsSync(filePath)) return null;
|
|
12775
13863
|
let raw;
|
|
12776
13864
|
try {
|
|
12777
|
-
raw = JSON.parse(
|
|
13865
|
+
raw = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
|
|
12778
13866
|
} catch (err) {
|
|
12779
13867
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12780
13868
|
process.stderr.write(
|
|
@@ -12834,6 +13922,44 @@ ${error.replace("Invalid config:\n", "")}
|
|
|
12834
13922
|
}
|
|
12835
13923
|
return sanitized;
|
|
12836
13924
|
}
|
|
13925
|
+
function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false) {
|
|
13926
|
+
if (!raw || typeof raw !== "object") return;
|
|
13927
|
+
const uc = raw;
|
|
13928
|
+
const AFTER_RANK = { "log-only": 0, "ask-once": 1, "ask-every-time": 2 };
|
|
13929
|
+
if (typeof uc.enabled === "boolean" && !(isProject && uc.enabled === false))
|
|
13930
|
+
cur.enabled = uc.enabled;
|
|
13931
|
+
if (typeof uc.spotlight === "boolean" && !(isProject && uc.spotlight === false))
|
|
13932
|
+
cur.spotlight = uc.spotlight;
|
|
13933
|
+
if ((uc.afterExposure === "ask-once" || uc.afterExposure === "ask-every-time" || uc.afterExposure === "log-only") && !(isProject && AFTER_RANK[uc.afterExposure] < AFTER_RANK[cur.afterExposure]))
|
|
13934
|
+
cur.afterExposure = uc.afterExposure;
|
|
13935
|
+
if (Array.isArray(uc.orgDomains) && !isProject) {
|
|
13936
|
+
const domains = uc.orgDomains.filter((d) => typeof d === "string").map((d) => d.trim().toLowerCase().replace(/^@/, "")).filter((d) => /^[a-z0-9.-]+\.[a-z]{2,}$/.test(d));
|
|
13937
|
+
if (replaceLists) cur.orgDomains = domains;
|
|
13938
|
+
else for (const d of domains) if (!cur.orgDomains.includes(d)) cur.orgDomains.push(d);
|
|
13939
|
+
}
|
|
13940
|
+
if (replaceLists && Array.isArray(uc.trustedRecipients)) {
|
|
13941
|
+
cur.trustedRecipients = [
|
|
13942
|
+
...new Set(
|
|
13943
|
+
uc.trustedRecipients.filter((v) => typeof v === "string").map((v) => normalizeRecipient(v)).filter((v) => v !== null)
|
|
13944
|
+
)
|
|
13945
|
+
].slice(0, 200);
|
|
13946
|
+
}
|
|
13947
|
+
if (uc.roles && typeof uc.roles === "object" && !Array.isArray(uc.roles)) {
|
|
13948
|
+
const next = replaceLists ? {} : cur.roles;
|
|
13949
|
+
for (const [server, tools] of Object.entries(uc.roles)) {
|
|
13950
|
+
if (!tools || typeof tools !== "object" || Array.isArray(tools)) continue;
|
|
13951
|
+
const m = { ...next[server] ?? {} };
|
|
13952
|
+
for (const [tool, role] of Object.entries(tools)) {
|
|
13953
|
+
if (role === "source" || role === "action" || role === "read") {
|
|
13954
|
+
if (isProject && role !== "action") continue;
|
|
13955
|
+
m[tool] = role;
|
|
13956
|
+
}
|
|
13957
|
+
}
|
|
13958
|
+
if (Object.keys(m).length > 0) next[server] = m;
|
|
13959
|
+
}
|
|
13960
|
+
cur.roles = next;
|
|
13961
|
+
}
|
|
13962
|
+
}
|
|
12837
13963
|
|
|
12838
13964
|
// src/utils/regex.ts
|
|
12839
13965
|
init_dist();
|
|
@@ -12841,18 +13967,18 @@ init_dist();
|
|
|
12841
13967
|
// src/dlp.ts
|
|
12842
13968
|
init_dist();
|
|
12843
13969
|
init_dist();
|
|
12844
|
-
import
|
|
12845
|
-
import
|
|
13970
|
+
import fs6 from "fs";
|
|
13971
|
+
import path6 from "path";
|
|
12846
13972
|
function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
|
|
12847
13973
|
if (!filePath) return null;
|
|
12848
13974
|
let resolved;
|
|
12849
13975
|
try {
|
|
12850
|
-
const absolute =
|
|
12851
|
-
resolved =
|
|
13976
|
+
const absolute = path6.resolve(cwd, filePath);
|
|
13977
|
+
resolved = fs6.realpathSync.native(absolute);
|
|
12852
13978
|
} catch (err) {
|
|
12853
13979
|
const code = err.code;
|
|
12854
13980
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
12855
|
-
resolved =
|
|
13981
|
+
resolved = path6.resolve(cwd, filePath);
|
|
12856
13982
|
} else {
|
|
12857
13983
|
return sensitivePathMatch(filePath);
|
|
12858
13984
|
}
|
|
@@ -12862,29 +13988,29 @@ function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
|
|
|
12862
13988
|
|
|
12863
13989
|
// src/policy/file-access.ts
|
|
12864
13990
|
init_dist();
|
|
12865
|
-
import
|
|
12866
|
-
import
|
|
12867
|
-
import
|
|
13991
|
+
import fs7 from "fs";
|
|
13992
|
+
import os6 from "os";
|
|
13993
|
+
import path7 from "path";
|
|
12868
13994
|
function absoluteSpelling(p, cwd, home) {
|
|
12869
13995
|
if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
|
|
12870
|
-
return
|
|
13996
|
+
return path7.isAbsolute(p) ? p : `${cwd}/${p}`;
|
|
12871
13997
|
}
|
|
12872
13998
|
function physical(p, followFinal, links = 0) {
|
|
12873
13999
|
if (links > 40) throw new Error("Too many symbolic links");
|
|
12874
14000
|
if (!followFinal) {
|
|
12875
|
-
const parent = physical(
|
|
12876
|
-
return { path: `${parent.path}/${
|
|
14001
|
+
const parent = physical(path7.dirname(p), true, links);
|
|
14002
|
+
return { path: `${parent.path}/${path7.basename(p)}`, missing: parent.missing };
|
|
12877
14003
|
}
|
|
12878
14004
|
try {
|
|
12879
|
-
return { path:
|
|
14005
|
+
return { path: fs7.realpathSync.native(p), missing: false };
|
|
12880
14006
|
} catch (error) {
|
|
12881
14007
|
const code = error.code;
|
|
12882
14008
|
if (!["ENOENT", "ENOTDIR"].includes(code ?? "")) throw error;
|
|
12883
14009
|
try {
|
|
12884
|
-
if (
|
|
12885
|
-
const target =
|
|
14010
|
+
if (fs7.lstatSync(p).isSymbolicLink()) {
|
|
14011
|
+
const target = fs7.readlinkSync(p);
|
|
12886
14012
|
return physical(
|
|
12887
|
-
|
|
14013
|
+
path7.isAbsolute(target) ? target : `${path7.dirname(p)}/${target}`,
|
|
12888
14014
|
true,
|
|
12889
14015
|
links + 1
|
|
12890
14016
|
);
|
|
@@ -12893,13 +14019,13 @@ function physical(p, followFinal, links = 0) {
|
|
|
12893
14019
|
if (!["ENOENT", "ENOTDIR"].includes(linkError.code ?? ""))
|
|
12894
14020
|
throw linkError;
|
|
12895
14021
|
}
|
|
12896
|
-
const parent =
|
|
14022
|
+
const parent = path7.dirname(p);
|
|
12897
14023
|
if (parent === p) return { path: p, missing: true };
|
|
12898
14024
|
const resolved = physical(parent, true, links);
|
|
12899
|
-
return { path: `${resolved.path}/${
|
|
14025
|
+
return { path: `${resolved.path}/${path7.basename(p)}`, missing: true };
|
|
12900
14026
|
}
|
|
12901
14027
|
}
|
|
12902
|
-
function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home =
|
|
14028
|
+
function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = os6.homedir()) {
|
|
12903
14029
|
const context = { cwd, homeDirs: [home, "/root"] };
|
|
12904
14030
|
const accesses = collectFileAccesses(tool, args, context, inspection);
|
|
12905
14031
|
const handledPaths = /* @__PURE__ */ new Set();
|
|
@@ -12922,14 +14048,14 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
|
|
|
12922
14048
|
const raw = absoluteSpelling(access.path, access.cwd ?? cwd, home);
|
|
12923
14049
|
if (access.directoryTarget) {
|
|
12924
14050
|
try {
|
|
12925
|
-
if (!
|
|
14051
|
+
if (!fs7.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
|
|
12926
14052
|
continue;
|
|
12927
14053
|
} catch {
|
|
12928
14054
|
}
|
|
12929
14055
|
}
|
|
12930
14056
|
if (access.operation === "create") {
|
|
12931
14057
|
try {
|
|
12932
|
-
|
|
14058
|
+
fs7.statSync(raw);
|
|
12933
14059
|
continue;
|
|
12934
14060
|
} catch {
|
|
12935
14061
|
}
|
|
@@ -12978,27 +14104,27 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
|
|
|
12978
14104
|
}
|
|
12979
14105
|
|
|
12980
14106
|
// src/utils/provenance.ts
|
|
12981
|
-
import
|
|
12982
|
-
import
|
|
12983
|
-
import
|
|
14107
|
+
import fs8 from "fs";
|
|
14108
|
+
import path8 from "path";
|
|
14109
|
+
import os7 from "os";
|
|
12984
14110
|
var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
|
|
12985
14111
|
var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
|
|
12986
14112
|
var USER_PREFIXES = [
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
14113
|
+
path8.join(os7.homedir(), "bin"),
|
|
14114
|
+
path8.join(os7.homedir(), ".local", "bin"),
|
|
14115
|
+
path8.join(os7.homedir(), ".cargo", "bin"),
|
|
14116
|
+
path8.join(os7.homedir(), ".npm-global", "bin"),
|
|
14117
|
+
path8.join(os7.homedir(), ".volta", "bin")
|
|
12992
14118
|
];
|
|
12993
14119
|
var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
|
|
12994
14120
|
function findInPath(cmd) {
|
|
12995
|
-
if (
|
|
14121
|
+
if (path8.posix.isAbsolute(cmd)) return cmd;
|
|
12996
14122
|
const pathEnv = process.env.PATH ?? "";
|
|
12997
|
-
for (const
|
|
12998
|
-
if (!
|
|
12999
|
-
const full =
|
|
14123
|
+
for (const dir2 of pathEnv.split(path8.delimiter)) {
|
|
14124
|
+
if (!dir2) continue;
|
|
14125
|
+
const full = path8.join(dir2, cmd);
|
|
13000
14126
|
try {
|
|
13001
|
-
|
|
14127
|
+
fs8.accessSync(full, fs8.constants.X_OK);
|
|
13002
14128
|
return full;
|
|
13003
14129
|
} catch {
|
|
13004
14130
|
}
|
|
@@ -13009,7 +14135,7 @@ function _classifyPath(resolved, cwd) {
|
|
|
13009
14135
|
if (cwd && resolved.startsWith(cwd + "/")) {
|
|
13010
14136
|
return { trustLevel: "user", reason: "binary in project directory" };
|
|
13011
14137
|
}
|
|
13012
|
-
const osTmp =
|
|
14138
|
+
const osTmp = os7.tmpdir();
|
|
13013
14139
|
const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
|
|
13014
14140
|
if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
13015
14141
|
return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
|
|
@@ -13027,7 +14153,7 @@ function _classifyPath(resolved, cwd) {
|
|
|
13027
14153
|
}
|
|
13028
14154
|
function checkProvenance(cmd, cwd) {
|
|
13029
14155
|
const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
|
|
13030
|
-
if (
|
|
14156
|
+
if (path8.posix.isAbsolute(bare)) {
|
|
13031
14157
|
const early = _classifyPath(bare, cwd);
|
|
13032
14158
|
if (early.trustLevel === "suspect") {
|
|
13033
14159
|
return { resolvedPath: bare, ...early };
|
|
@@ -13043,7 +14169,7 @@ function checkProvenance(cmd, cwd) {
|
|
|
13043
14169
|
reason: "binary not found in PATH"
|
|
13044
14170
|
};
|
|
13045
14171
|
}
|
|
13046
|
-
resolved =
|
|
14172
|
+
resolved = fs8.realpathSync(found);
|
|
13047
14173
|
} catch {
|
|
13048
14174
|
return {
|
|
13049
14175
|
resolvedPath: cmd,
|
|
@@ -13052,7 +14178,7 @@ function checkProvenance(cmd, cwd) {
|
|
|
13052
14178
|
};
|
|
13053
14179
|
}
|
|
13054
14180
|
try {
|
|
13055
|
-
const stat =
|
|
14181
|
+
const stat = fs8.statSync(resolved);
|
|
13056
14182
|
if (stat.mode & 2) {
|
|
13057
14183
|
return {
|
|
13058
14184
|
resolvedPath: resolved,
|
|
@@ -13077,7 +14203,7 @@ init_dist();
|
|
|
13077
14203
|
init_dist();
|
|
13078
14204
|
async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
13079
14205
|
const config = opts?.config ?? getConfig(cwd);
|
|
13080
|
-
const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, config.policy
|
|
14206
|
+
const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, inspectionLayers(config.policy));
|
|
13081
14207
|
const activeEnvironment = getActiveEnvironment(config) ?? void 0;
|
|
13082
14208
|
return evaluatePolicy(
|
|
13083
14209
|
config,
|
|
@@ -13097,7 +14223,7 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
|
13097
14223
|
// config; changes arrive via cloud sync). Unmanaged → the local file via
|
|
13098
14224
|
// getCachedHosts (5s TTL + mtime), so a `node9 trust add/remove` still
|
|
13099
14225
|
// reaches a long-lived in-process authorizer (the gateway) within seconds.
|
|
13100
|
-
isTrustedHost: config.policy.trustedHostsManaged ? (
|
|
14226
|
+
isTrustedHost: config.policy.trustedHostsManaged ? (host2) => matchesTrustedHost(host2, config.policy.trustedHosts) : isTrustedHost
|
|
13101
14227
|
}
|
|
13102
14228
|
);
|
|
13103
14229
|
}
|
|
@@ -13106,32 +14232,32 @@ function isIgnoredTool2(toolName) {
|
|
|
13106
14232
|
}
|
|
13107
14233
|
|
|
13108
14234
|
// src/auth/state.ts
|
|
13109
|
-
import
|
|
13110
|
-
import
|
|
13111
|
-
import
|
|
14235
|
+
import fs10 from "fs";
|
|
14236
|
+
import path10 from "path";
|
|
14237
|
+
import os8 from "os";
|
|
13112
14238
|
|
|
13113
14239
|
// src/utils/atomic-write.ts
|
|
13114
|
-
import
|
|
13115
|
-
import
|
|
14240
|
+
import fs9 from "fs";
|
|
14241
|
+
import path9 from "path";
|
|
13116
14242
|
import { randomUUID } from "crypto";
|
|
13117
14243
|
function atomicWriteSync(filePath, data, options) {
|
|
13118
|
-
const
|
|
13119
|
-
if (!
|
|
14244
|
+
const dir2 = path9.dirname(filePath);
|
|
14245
|
+
if (!fs9.existsSync(dir2)) fs9.mkdirSync(dir2, { recursive: true });
|
|
13120
14246
|
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
13121
14247
|
try {
|
|
13122
|
-
|
|
14248
|
+
fs9.writeFileSync(tmpPath, data, options);
|
|
13123
14249
|
} catch (err) {
|
|
13124
14250
|
try {
|
|
13125
|
-
|
|
14251
|
+
fs9.unlinkSync(tmpPath);
|
|
13126
14252
|
} catch {
|
|
13127
14253
|
}
|
|
13128
14254
|
throw err;
|
|
13129
14255
|
}
|
|
13130
14256
|
try {
|
|
13131
|
-
|
|
14257
|
+
fs9.renameSync(tmpPath, filePath);
|
|
13132
14258
|
} catch (err) {
|
|
13133
14259
|
try {
|
|
13134
|
-
|
|
14260
|
+
fs9.unlinkSync(tmpPath);
|
|
13135
14261
|
} catch {
|
|
13136
14262
|
}
|
|
13137
14263
|
throw err;
|
|
@@ -13139,8 +14265,8 @@ function atomicWriteSync(filePath, data, options) {
|
|
|
13139
14265
|
}
|
|
13140
14266
|
|
|
13141
14267
|
// src/auth/state.ts
|
|
13142
|
-
var PAUSED_FILE =
|
|
13143
|
-
var TRUST_FILE =
|
|
14268
|
+
var PAUSED_FILE = path10.join(os8.homedir(), ".node9", "PAUSED");
|
|
14269
|
+
var TRUST_FILE = path10.join(os8.homedir(), ".node9", "trust.json");
|
|
13144
14270
|
function extractCommandPattern(toolName, args) {
|
|
13145
14271
|
const lower2 = toolName.toLowerCase();
|
|
13146
14272
|
if (lower2 !== "bash" && lower2 !== "execute_bash" && lower2 !== "shell") return void 0;
|
|
@@ -13152,11 +14278,11 @@ function extractCommandPattern(toolName, args) {
|
|
|
13152
14278
|
}
|
|
13153
14279
|
function checkPause() {
|
|
13154
14280
|
try {
|
|
13155
|
-
if (!
|
|
13156
|
-
const state = JSON.parse(
|
|
14281
|
+
if (!fs10.existsSync(PAUSED_FILE)) return { paused: false };
|
|
14282
|
+
const state = JSON.parse(fs10.readFileSync(PAUSED_FILE, "utf-8"));
|
|
13157
14283
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
13158
14284
|
try {
|
|
13159
|
-
|
|
14285
|
+
fs10.unlinkSync(PAUSED_FILE);
|
|
13160
14286
|
} catch {
|
|
13161
14287
|
}
|
|
13162
14288
|
return { paused: false };
|
|
@@ -13168,8 +14294,8 @@ function checkPause() {
|
|
|
13168
14294
|
}
|
|
13169
14295
|
function getActiveTrustSession(toolName, args) {
|
|
13170
14296
|
try {
|
|
13171
|
-
if (!
|
|
13172
|
-
const trust = JSON.parse(
|
|
14297
|
+
if (!fs10.existsSync(TRUST_FILE)) return false;
|
|
14298
|
+
const trust = JSON.parse(fs10.readFileSync(TRUST_FILE, "utf-8"));
|
|
13173
14299
|
const now = Date.now();
|
|
13174
14300
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
13175
14301
|
if (active.length !== trust.entries.length) {
|
|
@@ -13192,8 +14318,8 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
13192
14318
|
try {
|
|
13193
14319
|
let trust = { entries: [] };
|
|
13194
14320
|
try {
|
|
13195
|
-
if (
|
|
13196
|
-
trust = JSON.parse(
|
|
14321
|
+
if (fs10.existsSync(TRUST_FILE)) {
|
|
14322
|
+
trust = JSON.parse(fs10.readFileSync(TRUST_FILE, "utf-8"));
|
|
13197
14323
|
}
|
|
13198
14324
|
} catch {
|
|
13199
14325
|
}
|
|
@@ -13215,9 +14341,9 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
13215
14341
|
}
|
|
13216
14342
|
function getPersistentDecision(toolName) {
|
|
13217
14343
|
try {
|
|
13218
|
-
const
|
|
13219
|
-
if (!
|
|
13220
|
-
const decisions = JSON.parse(
|
|
14344
|
+
const file2 = path10.join(os8.homedir(), ".node9", "decisions.json");
|
|
14345
|
+
if (!fs10.existsSync(file2)) return null;
|
|
14346
|
+
const decisions = JSON.parse(fs10.readFileSync(file2, "utf-8"));
|
|
13221
14347
|
const d = decisions[toolName];
|
|
13222
14348
|
if (d === "allow" || d === "deny") return d;
|
|
13223
14349
|
} catch {
|
|
@@ -13226,11 +14352,11 @@ function getPersistentDecision(toolName) {
|
|
|
13226
14352
|
}
|
|
13227
14353
|
|
|
13228
14354
|
// src/auth/daemon.ts
|
|
13229
|
-
import
|
|
14355
|
+
import fs11 from "fs";
|
|
13230
14356
|
import net from "net";
|
|
13231
|
-
import
|
|
13232
|
-
import
|
|
13233
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
14357
|
+
import path11 from "path";
|
|
14358
|
+
import os9 from "os";
|
|
14359
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path11.join(os9.tmpdir(), "node9-activity.sock");
|
|
13234
14360
|
function notifyActivitySocket(data) {
|
|
13235
14361
|
return new Promise((resolve) => {
|
|
13236
14362
|
try {
|
|
@@ -13272,9 +14398,9 @@ var DAEMON_PORT = resolveDaemonPort();
|
|
|
13272
14398
|
var DAEMON_HOST = "127.0.0.1";
|
|
13273
14399
|
function getInternalToken() {
|
|
13274
14400
|
try {
|
|
13275
|
-
const pidFile =
|
|
13276
|
-
if (!
|
|
13277
|
-
const data = JSON.parse(
|
|
14401
|
+
const pidFile = path11.join(os9.homedir(), ".node9", "daemon.pid");
|
|
14402
|
+
if (!fs11.existsSync(pidFile)) return null;
|
|
14403
|
+
const data = JSON.parse(fs11.readFileSync(pidFile, "utf-8"));
|
|
13278
14404
|
process.kill(data.pid, 0);
|
|
13279
14405
|
return data.internalToken ?? null;
|
|
13280
14406
|
} catch {
|
|
@@ -13282,12 +14408,12 @@ function getInternalToken() {
|
|
|
13282
14408
|
}
|
|
13283
14409
|
}
|
|
13284
14410
|
function isDaemonRunning() {
|
|
13285
|
-
const pidFile =
|
|
13286
|
-
if (
|
|
14411
|
+
const pidFile = path11.join(os9.homedir(), ".node9", "daemon.pid");
|
|
14412
|
+
if (fs11.existsSync(pidFile)) {
|
|
13287
14413
|
let pid;
|
|
13288
14414
|
let port;
|
|
13289
14415
|
try {
|
|
13290
|
-
const data = JSON.parse(
|
|
14416
|
+
const data = JSON.parse(fs11.readFileSync(pidFile, "utf-8"));
|
|
13291
14417
|
pid = data.pid;
|
|
13292
14418
|
port = data.port;
|
|
13293
14419
|
} catch {
|
|
@@ -13305,7 +14431,7 @@ function isDaemonRunning() {
|
|
|
13305
14431
|
} catch (err) {
|
|
13306
14432
|
if (err instanceof Error && "code" in err && err.code === "ESRCH") {
|
|
13307
14433
|
try {
|
|
13308
|
-
|
|
14434
|
+
fs11.unlinkSync(pidFile);
|
|
13309
14435
|
} catch {
|
|
13310
14436
|
}
|
|
13311
14437
|
}
|
|
@@ -13327,7 +14453,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
|
13327
14453
|
return false;
|
|
13328
14454
|
}
|
|
13329
14455
|
}
|
|
13330
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
|
|
14456
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
|
|
13331
14457
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
13332
14458
|
const ctrl = new AbortController();
|
|
13333
14459
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -13352,7 +14478,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
13352
14478
|
...recoveryCommand && { recoveryCommand },
|
|
13353
14479
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
13354
14480
|
...viewOnly && { viewOnly: true },
|
|
13355
|
-
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
14481
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true },
|
|
14482
|
+
...sessionId && { sessionId }
|
|
13356
14483
|
}),
|
|
13357
14484
|
signal: ctrl.signal
|
|
13358
14485
|
});
|
|
@@ -13451,7 +14578,7 @@ async function checkSessionTaint(sessionId) {
|
|
|
13451
14578
|
try {
|
|
13452
14579
|
const res = await fetch(`${base2}/session-taint/check`, {
|
|
13453
14580
|
method: "POST",
|
|
13454
|
-
headers:
|
|
14581
|
+
headers: exposureHeaders(),
|
|
13455
14582
|
body: JSON.stringify({ sessionId }),
|
|
13456
14583
|
signal: AbortSignal.timeout(2e3)
|
|
13457
14584
|
});
|
|
@@ -13460,6 +14587,42 @@ async function checkSessionTaint(sessionId) {
|
|
|
13460
14587
|
return { tainted: false, daemonUnavailable: true };
|
|
13461
14588
|
}
|
|
13462
14589
|
}
|
|
14590
|
+
function exposureHeaders() {
|
|
14591
|
+
return {
|
|
14592
|
+
"Content-Type": "application/json",
|
|
14593
|
+
"X-Node9-Internal": getInternalToken() ?? ""
|
|
14594
|
+
};
|
|
14595
|
+
}
|
|
14596
|
+
async function exposurePost(route, body, timeoutMs) {
|
|
14597
|
+
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
14598
|
+
const res = await fetch(`${base2}${route}`, {
|
|
14599
|
+
method: "POST",
|
|
14600
|
+
headers: exposureHeaders(),
|
|
14601
|
+
body: JSON.stringify(body),
|
|
14602
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
14603
|
+
});
|
|
14604
|
+
if (!res.ok) throw new Error(`session-exposure ${route}: HTTP ${res.status}`);
|
|
14605
|
+
return res.json();
|
|
14606
|
+
}
|
|
14607
|
+
async function checkSessionExposure(sessionId) {
|
|
14608
|
+
if (!sessionId || !isDaemonRunning()) return { exposed: false };
|
|
14609
|
+
try {
|
|
14610
|
+
return await exposurePost(
|
|
14611
|
+
"/session-exposure/check",
|
|
14612
|
+
{ sessionId },
|
|
14613
|
+
2e3
|
|
14614
|
+
);
|
|
14615
|
+
} catch {
|
|
14616
|
+
return { exposed: false, daemonUnavailable: true };
|
|
14617
|
+
}
|
|
14618
|
+
}
|
|
14619
|
+
async function approveSessionExposure(sessionId, keys) {
|
|
14620
|
+
if (!sessionId || keys.length === 0 || !isDaemonRunning()) return;
|
|
14621
|
+
try {
|
|
14622
|
+
await exposurePost("/session-exposure/approve", { sessionId, keys }, 1e3);
|
|
14623
|
+
} catch {
|
|
14624
|
+
}
|
|
14625
|
+
}
|
|
13463
14626
|
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
13464
14627
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
13465
14628
|
await fetch(`${base2}/resolve/${id}`, {
|
|
@@ -13472,13 +14635,14 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
13472
14635
|
|
|
13473
14636
|
// src/auth/orchestrator.ts
|
|
13474
14637
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
14638
|
+
import os18 from "os";
|
|
13475
14639
|
|
|
13476
14640
|
// src/ui/native.ts
|
|
13477
14641
|
import { spawn } from "child_process";
|
|
13478
|
-
import
|
|
14642
|
+
import path13 from "path";
|
|
13479
14643
|
|
|
13480
14644
|
// src/context-sniper.ts
|
|
13481
|
-
import
|
|
14645
|
+
import path12 from "path";
|
|
13482
14646
|
function smartTruncate(str, maxLen = 500) {
|
|
13483
14647
|
if (str.length <= maxLen) return str;
|
|
13484
14648
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -13546,7 +14710,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
13546
14710
|
intent = "EDIT";
|
|
13547
14711
|
if (obj.file_path) {
|
|
13548
14712
|
editFilePath = String(obj.file_path);
|
|
13549
|
-
editFileName =
|
|
14713
|
+
editFileName = path12.basename(editFilePath);
|
|
13550
14714
|
}
|
|
13551
14715
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
13552
14716
|
contextSnippet = result.snippet;
|
|
@@ -13591,6 +14755,7 @@ function resolveNativeDecision(opts) {
|
|
|
13591
14755
|
const { code, output, elapsedMs, locked } = opts;
|
|
13592
14756
|
if (locked) return "deny";
|
|
13593
14757
|
const tooFast = elapsedMs < MIN_INTERACTION_MS;
|
|
14758
|
+
if (output.includes("Allow & trust")) return tooFast ? "deny" : "trust_recipients";
|
|
13594
14759
|
if (output.includes("Always Allow")) return tooFast ? "deny" : "always_allow";
|
|
13595
14760
|
if (code === 0) return tooFast ? "deny" : "allow";
|
|
13596
14761
|
return "deny";
|
|
@@ -13613,12 +14778,12 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
13613
14778
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
13614
14779
|
const obj = parsed;
|
|
13615
14780
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
13616
|
-
const
|
|
14781
|
+
const file2 = obj.file_path ? path13.basename(String(obj.file_path)) : "file";
|
|
13617
14782
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
13618
14783
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
13619
14784
|
return {
|
|
13620
14785
|
intent: "EDIT",
|
|
13621
|
-
message: `\u{1F4DD} EDITING: ${
|
|
14786
|
+
message: `\u{1F4DD} EDITING: ${file2}
|
|
13622
14787
|
\u{1F4C2} PATH: ${obj.file_path}
|
|
13623
14788
|
|
|
13624
14789
|
--- REPLACING ---
|
|
@@ -13676,7 +14841,7 @@ ${smartTruncate(str, 500)}`
|
|
|
13676
14841
|
function escapePango(text) {
|
|
13677
14842
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
13678
14843
|
}
|
|
13679
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
14844
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription, exposure = false) {
|
|
13680
14845
|
const lines = [];
|
|
13681
14846
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
13682
14847
|
const safeAgent = safeMessage(agent ?? "AI Agent", 80);
|
|
@@ -13685,17 +14850,19 @@ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
13685
14850
|
if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
|
|
13686
14851
|
lines.push("");
|
|
13687
14852
|
lines.push(formattedArgs);
|
|
13688
|
-
if (allowCount >= 3) {
|
|
14853
|
+
if (allowCount >= 3 && !exposure) {
|
|
13689
14854
|
lines.push("");
|
|
13690
14855
|
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
13691
14856
|
}
|
|
13692
14857
|
if (!locked) {
|
|
13693
14858
|
lines.push("");
|
|
13694
|
-
lines.push(
|
|
14859
|
+
lines.push(
|
|
14860
|
+
exposure ? '\u21B5 Enter = Allow once | \u238B Esc = Block | "Allow & trust" = this address is fine from now on' : '\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again'
|
|
14861
|
+
);
|
|
13695
14862
|
}
|
|
13696
14863
|
return lines.join("\n");
|
|
13697
14864
|
}
|
|
13698
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
14865
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription, exposure = false) {
|
|
13699
14866
|
const lines = [];
|
|
13700
14867
|
if (locked) {
|
|
13701
14868
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -13708,7 +14875,7 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
13708
14875
|
if (ruleDescription) lines.push(`<i>\u2139 ${escapePango(ruleDescription)}</i>`);
|
|
13709
14876
|
lines.push("");
|
|
13710
14877
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
13711
|
-
if (allowCount >= 3) {
|
|
14878
|
+
if (allowCount >= 3 && !exposure) {
|
|
13712
14879
|
lines.push("");
|
|
13713
14880
|
lines.push(
|
|
13714
14881
|
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
@@ -13717,13 +14884,14 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
13717
14884
|
if (!locked) {
|
|
13718
14885
|
lines.push("");
|
|
13719
14886
|
lines.push(
|
|
13720
|
-
'<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
14887
|
+
exposure ? '<small>\u21B5 Enter = <b>Allow once</b> | \u238B Esc = <b>Block</b> | "Allow & trust" = this address is fine from now on</small>' : '<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
13721
14888
|
);
|
|
13722
14889
|
}
|
|
13723
14890
|
return lines.join("\n");
|
|
13724
14891
|
}
|
|
13725
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1, ruleDescription) {
|
|
14892
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1, ruleDescription, exposureDestinations = []) {
|
|
13726
14893
|
if (isTestEnv()) return "deny";
|
|
14894
|
+
const thirdButton = exposureDestinations.length > 0 ? "Allow & trust" : "Always Allow";
|
|
13727
14895
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
13728
14896
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
13729
14897
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
@@ -13734,7 +14902,8 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
13734
14902
|
explainableLabel,
|
|
13735
14903
|
locked,
|
|
13736
14904
|
allowCount,
|
|
13737
|
-
ruleDescription
|
|
14905
|
+
ruleDescription,
|
|
14906
|
+
exposureDestinations.length > 0
|
|
13738
14907
|
);
|
|
13739
14908
|
return new Promise((resolve) => {
|
|
13740
14909
|
let childProcess = null;
|
|
@@ -13754,7 +14923,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
13754
14923
|
}
|
|
13755
14924
|
try {
|
|
13756
14925
|
if (process.platform === "darwin") {
|
|
13757
|
-
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block \u238B", "
|
|
14926
|
+
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block \u238B", "${thirdButton}", "Allow \u21B5"} default button "Allow \u21B5" cancel button "Block \u238B"`;
|
|
13758
14927
|
const script = `on run argv
|
|
13759
14928
|
tell application "System Events"
|
|
13760
14929
|
activate
|
|
@@ -13770,7 +14939,8 @@ end run`;
|
|
|
13770
14939
|
explainableLabel,
|
|
13771
14940
|
locked,
|
|
13772
14941
|
allowCount,
|
|
13773
|
-
ruleDescription
|
|
14942
|
+
ruleDescription,
|
|
14943
|
+
exposureDestinations.length > 0
|
|
13774
14944
|
);
|
|
13775
14945
|
const argsList = [
|
|
13776
14946
|
locked ? "--info" : "--question",
|
|
@@ -13785,7 +14955,7 @@ end run`;
|
|
|
13785
14955
|
];
|
|
13786
14956
|
if (!locked) {
|
|
13787
14957
|
argsList.push("--cancel-label", "Block \u238B");
|
|
13788
|
-
argsList.push("--extra-button",
|
|
14958
|
+
argsList.push("--extra-button", thirdButton);
|
|
13789
14959
|
}
|
|
13790
14960
|
childProcess = spawn("zenity", argsList);
|
|
13791
14961
|
} else if (process.platform === "win32") {
|
|
@@ -13811,24 +14981,24 @@ end run`;
|
|
|
13811
14981
|
}
|
|
13812
14982
|
|
|
13813
14983
|
// src/canary/registry.ts
|
|
13814
|
-
import
|
|
13815
|
-
import
|
|
13816
|
-
import
|
|
14984
|
+
import fs13 from "fs";
|
|
14985
|
+
import os11 from "os";
|
|
14986
|
+
import path15 from "path";
|
|
13817
14987
|
init_dist();
|
|
13818
14988
|
|
|
13819
14989
|
// src/shields/jail.ts
|
|
13820
|
-
import
|
|
13821
|
-
import
|
|
13822
|
-
import
|
|
14990
|
+
import fs12 from "fs";
|
|
14991
|
+
import os10 from "os";
|
|
14992
|
+
import path14 from "path";
|
|
13823
14993
|
init_dist();
|
|
13824
14994
|
var USER_JAIL_SHIELD = "user-jail";
|
|
13825
14995
|
function jailStorePath() {
|
|
13826
|
-
return
|
|
14996
|
+
return path14.join(os10.homedir(), ".node9", "jail-paths.json");
|
|
13827
14997
|
}
|
|
13828
14998
|
function readJailPaths() {
|
|
13829
14999
|
let text;
|
|
13830
15000
|
try {
|
|
13831
|
-
text =
|
|
15001
|
+
text = fs12.readFileSync(jailStorePath(), "utf8");
|
|
13832
15002
|
} catch (err) {
|
|
13833
15003
|
if (err.code === "ENOENT") return [];
|
|
13834
15004
|
throw err;
|
|
@@ -13857,7 +15027,7 @@ function findJailedPathIn(candidate, paths) {
|
|
|
13857
15027
|
|
|
13858
15028
|
// src/canary/registry.ts
|
|
13859
15029
|
function canaryStorePath() {
|
|
13860
|
-
return
|
|
15030
|
+
return path15.join(os11.homedir(), ".node9", "canaries.json");
|
|
13861
15031
|
}
|
|
13862
15032
|
function isRecord(x) {
|
|
13863
15033
|
if (!x || typeof x !== "object") return false;
|
|
@@ -13868,7 +15038,7 @@ function loadCanaries(opts) {
|
|
|
13868
15038
|
const p = canaryStorePath();
|
|
13869
15039
|
let raw;
|
|
13870
15040
|
try {
|
|
13871
|
-
raw =
|
|
15041
|
+
raw = fs13.readFileSync(p, "utf-8");
|
|
13872
15042
|
} catch (e) {
|
|
13873
15043
|
if (e.code === "ENOENT") return [];
|
|
13874
15044
|
throw e;
|
|
@@ -13894,31 +15064,390 @@ function canaryValues() {
|
|
|
13894
15064
|
init_dist();
|
|
13895
15065
|
init_dist();
|
|
13896
15066
|
init_audit();
|
|
15067
|
+
init_dist();
|
|
15068
|
+
|
|
15069
|
+
// src/exposure/known-hosts.ts
|
|
15070
|
+
import fs15 from "fs";
|
|
15071
|
+
import path17 from "path";
|
|
15072
|
+
|
|
15073
|
+
// src/utils/git-remotes.ts
|
|
15074
|
+
init_dist();
|
|
15075
|
+
import fs14 from "fs";
|
|
15076
|
+
import path16 from "path";
|
|
15077
|
+
function readSmallFile(p, max) {
|
|
15078
|
+
let fd;
|
|
15079
|
+
try {
|
|
15080
|
+
fd = fs14.openSync(p, fs14.constants.O_RDONLY | (fs14.constants.O_NOFOLLOW ?? 0));
|
|
15081
|
+
const st = fs14.fstatSync(fd);
|
|
15082
|
+
if (!st.isFile() || st.size > max) return null;
|
|
15083
|
+
const buf = Buffer.alloc(st.size);
|
|
15084
|
+
const n = fs14.readSync(fd, buf, 0, st.size, 0);
|
|
15085
|
+
return buf.subarray(0, n).toString("utf8");
|
|
15086
|
+
} catch {
|
|
15087
|
+
return null;
|
|
15088
|
+
} finally {
|
|
15089
|
+
if (fd !== void 0) fs14.closeSync(fd);
|
|
15090
|
+
}
|
|
15091
|
+
}
|
|
15092
|
+
function gitRemoteHost(url) {
|
|
15093
|
+
const u = url.trim();
|
|
15094
|
+
if (!u) return null;
|
|
15095
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
15096
|
+
try {
|
|
15097
|
+
const h = new URL(u).hostname.toLowerCase();
|
|
15098
|
+
return h || null;
|
|
15099
|
+
} catch {
|
|
15100
|
+
return null;
|
|
15101
|
+
}
|
|
15102
|
+
}
|
|
15103
|
+
const scp = /^(?:[^@/\s]+@)?([A-Za-z0-9.-]+):(?!\/\/)/.exec(u);
|
|
15104
|
+
if (scp && scp[1].includes(".")) return scp[1].toLowerCase();
|
|
15105
|
+
return null;
|
|
15106
|
+
}
|
|
15107
|
+
function findGitConfig(dir2, walkUp = true) {
|
|
15108
|
+
let cur = path16.resolve(dir2);
|
|
15109
|
+
for (let i = 0; i < (walkUp ? 64 : 1); i++) {
|
|
15110
|
+
const dotGit = path16.join(cur, ".git");
|
|
15111
|
+
const direct = readSmallFile(path16.join(dotGit, "config"), 1e6);
|
|
15112
|
+
if (direct !== null) return direct;
|
|
15113
|
+
const pointer = readSmallFile(dotGit, 4096);
|
|
15114
|
+
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
15115
|
+
if (m) {
|
|
15116
|
+
let gitDir = path16.resolve(cur, m[1]);
|
|
15117
|
+
const common = readSmallFile(path16.join(gitDir, "commondir"), 4096);
|
|
15118
|
+
if (common !== null) gitDir = path16.resolve(gitDir, common.trim());
|
|
15119
|
+
return readSmallFile(path16.join(gitDir, "config"), 1e6);
|
|
15120
|
+
}
|
|
15121
|
+
const parent = path16.dirname(cur);
|
|
15122
|
+
if (parent === cur) return null;
|
|
15123
|
+
cur = parent;
|
|
15124
|
+
}
|
|
15125
|
+
return null;
|
|
15126
|
+
}
|
|
15127
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
15128
|
+
function readGitRemotes(dir2, opts = {}) {
|
|
15129
|
+
if (!dir2) return [];
|
|
15130
|
+
const walkUp = opts.walkUp !== false;
|
|
15131
|
+
const key = `${walkUp ? "up" : "at"}:${path16.resolve(dir2)}`;
|
|
15132
|
+
const hit = cache2.get(key);
|
|
15133
|
+
if (hit) return hit;
|
|
15134
|
+
const out = [];
|
|
15135
|
+
try {
|
|
15136
|
+
const cfg = findGitConfig(path16.resolve(dir2), walkUp);
|
|
15137
|
+
if (cfg !== null) {
|
|
15138
|
+
let current = null;
|
|
15139
|
+
for (const line of cfg.split(/\r?\n/)) {
|
|
15140
|
+
const sec = /^\s*\[\s*([^\]\s]+)(?:\s+"([^"]*)")?\s*\]/.exec(line);
|
|
15141
|
+
if (sec) {
|
|
15142
|
+
current = sec[1].toLowerCase() === "remote" && sec[2] ? { name: sec[2], urls: [], hosts: [], repos: [] } : null;
|
|
15143
|
+
if (current) out.push(current);
|
|
15144
|
+
continue;
|
|
15145
|
+
}
|
|
15146
|
+
const url = current && /^\s*(?:push)?url\s*=\s*(.+?)\s*$/i.exec(line);
|
|
15147
|
+
if (url && current) {
|
|
15148
|
+
current.urls.push(url[1]);
|
|
15149
|
+
const h = gitRemoteHost(url[1]);
|
|
15150
|
+
if (h && !current.hosts.includes(`host:${h}`)) current.hosts.push(`host:${h}`);
|
|
15151
|
+
const repo = gitRepoPath(url[1]);
|
|
15152
|
+
if (repo && !current.repos.includes(repo)) current.repos.push(repo);
|
|
15153
|
+
}
|
|
15154
|
+
}
|
|
15155
|
+
}
|
|
15156
|
+
} catch {
|
|
15157
|
+
}
|
|
15158
|
+
cache2.set(key, out);
|
|
15159
|
+
return out;
|
|
15160
|
+
}
|
|
15161
|
+
|
|
15162
|
+
// src/exposure/known-hosts.ts
|
|
15163
|
+
var ECOSYSTEMS = [
|
|
15164
|
+
["package.json", "host:registry.npmjs.org"],
|
|
15165
|
+
["pyproject.toml", "host:upload.pypi.org"],
|
|
15166
|
+
["setup.py", "host:upload.pypi.org"],
|
|
15167
|
+
["Cargo.toml", "host:crates.io"],
|
|
15168
|
+
["Gemfile", "host:rubygems.org"]
|
|
15169
|
+
];
|
|
15170
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
15171
|
+
function manifestRoot(dir2) {
|
|
15172
|
+
let cur = path17.resolve(dir2);
|
|
15173
|
+
for (let i = 0; i < 64; i++) {
|
|
15174
|
+
for (const [m] of ECOSYSTEMS) {
|
|
15175
|
+
try {
|
|
15176
|
+
if (fs15.existsSync(path17.join(cur, m))) return cur;
|
|
15177
|
+
} catch {
|
|
15178
|
+
}
|
|
15179
|
+
}
|
|
15180
|
+
const parent = path17.dirname(cur);
|
|
15181
|
+
if (parent === cur) return null;
|
|
15182
|
+
cur = parent;
|
|
15183
|
+
}
|
|
15184
|
+
return null;
|
|
15185
|
+
}
|
|
15186
|
+
function knownProjectHosts(dir2) {
|
|
15187
|
+
const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
15188
|
+
if (!dir2) return empty;
|
|
15189
|
+
const key = path17.resolve(dir2);
|
|
15190
|
+
const hit = cache3.get(key);
|
|
15191
|
+
if (hit) return hit;
|
|
15192
|
+
const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
15193
|
+
try {
|
|
15194
|
+
for (const r of readGitRemotes(key)) {
|
|
15195
|
+
out.remotes.set(r.name, r.hosts);
|
|
15196
|
+
for (const h of r.hosts) out.hosts.add(h);
|
|
15197
|
+
for (const repo of r.repos) out.repos.add(repo);
|
|
15198
|
+
}
|
|
15199
|
+
const root = manifestRoot(key);
|
|
15200
|
+
if (root !== null) {
|
|
15201
|
+
for (const [m, registry] of ECOSYSTEMS) {
|
|
15202
|
+
if (fs15.existsSync(path17.join(root, m))) out.hosts.add(registry);
|
|
15203
|
+
}
|
|
15204
|
+
try {
|
|
15205
|
+
if (fs15.readdirSync(root).some((f) => f.endsWith(".gemspec")))
|
|
15206
|
+
out.hosts.add("host:rubygems.org");
|
|
15207
|
+
} catch {
|
|
15208
|
+
}
|
|
15209
|
+
}
|
|
15210
|
+
} catch {
|
|
15211
|
+
}
|
|
15212
|
+
cache3.set(key, out);
|
|
15213
|
+
return out;
|
|
15214
|
+
}
|
|
15215
|
+
|
|
15216
|
+
// src/exposure/writes.ts
|
|
15217
|
+
init_dist();
|
|
15218
|
+
import fs16 from "fs";
|
|
15219
|
+
import os12 from "os";
|
|
15220
|
+
import path18 from "path";
|
|
15221
|
+
var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
|
|
15222
|
+
var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
|
|
15223
|
+
function writtenContent(args) {
|
|
15224
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return [];
|
|
15225
|
+
const a = args;
|
|
15226
|
+
const out = [];
|
|
15227
|
+
for (const k of ["content", "new_string", "new_str", "file_text", "contents"]) {
|
|
15228
|
+
if (typeof a[k] === "string") out.push(a[k]);
|
|
15229
|
+
}
|
|
15230
|
+
if (Array.isArray(a.edits)) {
|
|
15231
|
+
for (const e of a.edits) {
|
|
15232
|
+
if (e && typeof e === "object" && typeof e.new_string === "string")
|
|
15233
|
+
out.push(e.new_string);
|
|
15234
|
+
}
|
|
15235
|
+
}
|
|
15236
|
+
return out;
|
|
15237
|
+
}
|
|
15238
|
+
function packageScriptsTouched(args, absPath, shellCommand) {
|
|
15239
|
+
if (shellCommand !== null) return true;
|
|
15240
|
+
const a = args ?? {};
|
|
15241
|
+
const texts = [
|
|
15242
|
+
...writtenContent(args),
|
|
15243
|
+
...typeof a.old_string === "string" ? [a.old_string] : [],
|
|
15244
|
+
...Array.isArray(a.edits) ? a.edits.map((e) => String(e?.old_string ?? "")) : []
|
|
15245
|
+
];
|
|
15246
|
+
if (typeof a.content === "string") {
|
|
15247
|
+
try {
|
|
15248
|
+
const next = JSON.parse(a.content);
|
|
15249
|
+
const prev = JSON.parse(fs16.readFileSync(absPath, "utf8"));
|
|
15250
|
+
return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
|
|
15251
|
+
} catch {
|
|
15252
|
+
return LIFECYCLE_RE.test(a.content);
|
|
15253
|
+
}
|
|
15254
|
+
}
|
|
15255
|
+
return texts.some((t) => LIFECYCLE_RE.test(t));
|
|
15256
|
+
}
|
|
15257
|
+
function judgeWritesAfterExposure(opts) {
|
|
15258
|
+
const holds = [];
|
|
15259
|
+
const home = opts.home ?? os12.homedir();
|
|
15260
|
+
const tmp = normalizeFilePath(os12.tmpdir());
|
|
15261
|
+
const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
|
|
15262
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15263
|
+
for (const access of opts.accesses) {
|
|
15264
|
+
if (!WRITE_OPS.has(access.operation)) continue;
|
|
15265
|
+
const p = access.candidates?.[0] ?? normalizeFilePath(access.path, access.cwd ?? opts.cwd ?? "", home);
|
|
15266
|
+
if (seen.has(p)) continue;
|
|
15267
|
+
seen.add(p);
|
|
15268
|
+
if (p === "/dev" || p.startsWith("/dev/")) continue;
|
|
15269
|
+
if (root !== null && isInside(root, p)) {
|
|
15270
|
+
if (isAgentInstructionPath(p, root) && (path18.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
|
|
15271
|
+
holds.push({ path: p, why: "agent-instruction" });
|
|
15272
|
+
continue;
|
|
15273
|
+
}
|
|
15274
|
+
if (isInside(tmp, p) && !(root !== null && isInside(tmp, root))) continue;
|
|
15275
|
+
holds.push({ path: p, why: "outside-project" });
|
|
15276
|
+
}
|
|
15277
|
+
const texts = opts.shellCommand !== null ? [opts.shellCommand] : writtenContent(opts.args);
|
|
15278
|
+
if (texts.length > 0 && opts.inContent.size > 0) {
|
|
15279
|
+
for (const d of extractContentDestinations(texts.join("\n"))) {
|
|
15280
|
+
if (opts.inContent.has(d) && !opts.isKnown(d)) {
|
|
15281
|
+
const target = [...seen][0] ?? "";
|
|
15282
|
+
if (!holds.some((h) => h.why === "content-destination" && h.destination === d))
|
|
15283
|
+
holds.push({ path: target, why: "content-destination", destination: d });
|
|
15284
|
+
}
|
|
15285
|
+
}
|
|
15286
|
+
}
|
|
15287
|
+
return holds;
|
|
15288
|
+
}
|
|
15289
|
+
|
|
15290
|
+
// src/exposure/marker.ts
|
|
15291
|
+
import crypto3 from "crypto";
|
|
15292
|
+
import fs17 from "fs";
|
|
15293
|
+
import os13 from "os";
|
|
15294
|
+
import path19 from "path";
|
|
15295
|
+
|
|
15296
|
+
// src/daemon/exposure-store.ts
|
|
15297
|
+
var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
15298
|
+
|
|
15299
|
+
// src/exposure/marker.ts
|
|
15300
|
+
function dir() {
|
|
15301
|
+
return path19.join(os13.homedir(), ".node9", "exposure-sessions");
|
|
15302
|
+
}
|
|
15303
|
+
function file(sessionId) {
|
|
15304
|
+
const id = crypto3.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
15305
|
+
return path19.join(dir(), id);
|
|
15306
|
+
}
|
|
15307
|
+
function maybeSessionExposed(sessionId) {
|
|
15308
|
+
if (!sessionId) return false;
|
|
15309
|
+
try {
|
|
15310
|
+
const st = fs17.statSync(file(sessionId));
|
|
15311
|
+
return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
|
|
15312
|
+
} catch (e) {
|
|
15313
|
+
return e.code !== "ENOENT";
|
|
15314
|
+
}
|
|
15315
|
+
}
|
|
15316
|
+
|
|
15317
|
+
// src/exposure/roles.ts
|
|
15318
|
+
import fs19 from "fs";
|
|
15319
|
+
|
|
15320
|
+
// src/daemon/mcp-tools.ts
|
|
15321
|
+
import fs18 from "fs";
|
|
15322
|
+
import path20 from "path";
|
|
15323
|
+
import os14 from "os";
|
|
15324
|
+
function getMcpToolsFile() {
|
|
15325
|
+
return path20.join(os14.homedir(), ".node9", "mcp-tools.json");
|
|
15326
|
+
}
|
|
15327
|
+
function readMcpToolsConfig() {
|
|
15328
|
+
try {
|
|
15329
|
+
const file2 = getMcpToolsFile();
|
|
15330
|
+
if (!fs18.existsSync(file2)) return {};
|
|
15331
|
+
const raw = fs18.readFileSync(file2, "utf-8");
|
|
15332
|
+
return JSON.parse(raw);
|
|
15333
|
+
} catch {
|
|
15334
|
+
return {};
|
|
15335
|
+
}
|
|
15336
|
+
}
|
|
15337
|
+
|
|
15338
|
+
// src/exposure/roles.ts
|
|
15339
|
+
var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
|
|
15340
|
+
var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
|
|
15341
|
+
function foldServerName(name) {
|
|
15342
|
+
return name.replace(/[^A-Za-z0-9_-]/g, "_").toLowerCase();
|
|
15343
|
+
}
|
|
15344
|
+
var inventoryCache = null;
|
|
15345
|
+
var inventoryStamp = null;
|
|
15346
|
+
function inventoryFileStamp() {
|
|
15347
|
+
try {
|
|
15348
|
+
const st = fs19.statSync(getMcpToolsFile());
|
|
15349
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
15350
|
+
} catch {
|
|
15351
|
+
return "";
|
|
15352
|
+
}
|
|
15353
|
+
}
|
|
15354
|
+
function resolveToolAnnotations(server, tool) {
|
|
15355
|
+
const stamp = inventoryFileStamp();
|
|
15356
|
+
if (inventoryCache === null || stamp !== inventoryStamp) {
|
|
15357
|
+
inventoryStamp = stamp;
|
|
15358
|
+
inventoryCache = /* @__PURE__ */ new Map();
|
|
15359
|
+
try {
|
|
15360
|
+
const seen = /* @__PURE__ */ new Map();
|
|
15361
|
+
for (const cfg of Object.values(readMcpToolsConfig())) {
|
|
15362
|
+
if (!cfg?.name) continue;
|
|
15363
|
+
const key = foldServerName(cfg.name);
|
|
15364
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
15365
|
+
const tools = /* @__PURE__ */ new Map();
|
|
15366
|
+
for (const t of cfg.tools ?? []) tools.set(t.name, t.annotations ?? null);
|
|
15367
|
+
inventoryCache.set(key, tools);
|
|
15368
|
+
}
|
|
15369
|
+
for (const [key, n] of seen) if (n > 1) inventoryCache.delete(key);
|
|
15370
|
+
} catch {
|
|
15371
|
+
}
|
|
15372
|
+
}
|
|
15373
|
+
return inventoryCache.get(foldServerName(server))?.get(tool) ?? null;
|
|
15374
|
+
}
|
|
15375
|
+
var BUILTIN_SOURCES = /* @__PURE__ */ new Set([
|
|
15376
|
+
"webfetch",
|
|
15377
|
+
"websearch",
|
|
15378
|
+
"web_fetch",
|
|
15379
|
+
"web_search",
|
|
15380
|
+
"fetch",
|
|
15381
|
+
"navigate",
|
|
15382
|
+
"browser_batch",
|
|
15383
|
+
"get_page_text",
|
|
15384
|
+
"read_page",
|
|
15385
|
+
"find",
|
|
15386
|
+
"read_console_messages",
|
|
15387
|
+
"read_network_requests",
|
|
15388
|
+
// Claude Code reads MCP resources through two built-ins, not through the
|
|
15389
|
+
// server's own tools: an email read as a resource is still an email.
|
|
15390
|
+
"readmcpresourcetool",
|
|
15391
|
+
"listmcpresourcestool"
|
|
15392
|
+
]);
|
|
15393
|
+
var MCP_RE = /^mcp__([^_](?:[^_]|_(?!_))*?)__(.+)$/i;
|
|
15394
|
+
function splitMcpToolName(toolName) {
|
|
15395
|
+
const m = toolName.match(MCP_RE);
|
|
15396
|
+
return m ? { server: m[1], bare: m[2] } : null;
|
|
15397
|
+
}
|
|
15398
|
+
function toolRole(toolName, policy) {
|
|
15399
|
+
const mcp = splitMcpToolName(toolName);
|
|
15400
|
+
const bare = (mcp ? mcp.bare : toolName).toLowerCase();
|
|
15401
|
+
if (mcp) {
|
|
15402
|
+
const configured = policy.roles[mcp.server]?.[mcp.bare] ?? policy.roles[mcp.server]?.[bare];
|
|
15403
|
+
if (configured === "source") return { source: true, action: false };
|
|
15404
|
+
if (configured === "action") return { source: false, action: true };
|
|
15405
|
+
if (configured === "read") return { source: false, action: false };
|
|
15406
|
+
const serverLower = mcp.server.toLowerCase();
|
|
15407
|
+
if (AGENT_INTERNAL_PREFIXES.some((p) => serverLower.startsWith(p)))
|
|
15408
|
+
return { source: false, action: false };
|
|
15409
|
+
if (BUILTIN_SOURCES.has(bare)) return { source: true, action: false };
|
|
15410
|
+
const ann = resolveToolAnnotations(mcp.server, mcp.bare);
|
|
15411
|
+
return { source: true, action: true, destructive: ann?.destructiveHint === true };
|
|
15412
|
+
}
|
|
15413
|
+
return { source: BUILTIN_SOURCES.has(bare), action: false };
|
|
15414
|
+
}
|
|
15415
|
+
function sourceWord(toolName) {
|
|
15416
|
+
const mcp = splitMcpToolName(toolName);
|
|
15417
|
+
const bare = (mcp ? mcp.bare : toolName).toLowerCase();
|
|
15418
|
+
const server = (mcp?.server ?? "").toLowerCase();
|
|
15419
|
+
if (/mail|message|inbox|thread/.test(bare) || /mail|slack|teams|chat/.test(server))
|
|
15420
|
+
return "email";
|
|
15421
|
+
if (/issue|ticket|pull|pr\b|comment/.test(bare) || /github|gitlab|jira|linear/.test(server))
|
|
15422
|
+
return "issue";
|
|
15423
|
+
if (BUILTIN_SOURCES.has(bare) || /browser|web|http|search/.test(server)) return "web page";
|
|
15424
|
+
return "tool result";
|
|
15425
|
+
}
|
|
13897
15426
|
|
|
13898
15427
|
// src/auth/cloud.ts
|
|
13899
15428
|
init_audit();
|
|
13900
|
-
import
|
|
13901
|
-
import
|
|
13902
|
-
import
|
|
15429
|
+
import fs21 from "fs";
|
|
15430
|
+
import os15 from "os";
|
|
15431
|
+
import path21 from "path";
|
|
13903
15432
|
|
|
13904
15433
|
// src/utils/read-capped.ts
|
|
13905
|
-
import
|
|
13906
|
-
function readCapped(
|
|
15434
|
+
import fs20 from "fs";
|
|
15435
|
+
function readCapped(file2, maxBytes) {
|
|
13907
15436
|
if (maxBytes < 0) return null;
|
|
13908
15437
|
let fd;
|
|
13909
15438
|
try {
|
|
13910
|
-
fd =
|
|
15439
|
+
fd = fs20.openSync(file2, "r");
|
|
13911
15440
|
} catch {
|
|
13912
15441
|
return null;
|
|
13913
15442
|
}
|
|
13914
15443
|
try {
|
|
13915
|
-
const st =
|
|
15444
|
+
const st = fs20.fstatSync(fd);
|
|
13916
15445
|
if (!st.isFile()) return null;
|
|
13917
15446
|
const want = Math.min(st.size, maxBytes + 1);
|
|
13918
15447
|
const buf = Buffer.alloc(want);
|
|
13919
15448
|
let read = 0;
|
|
13920
15449
|
while (read < want) {
|
|
13921
|
-
const n =
|
|
15450
|
+
const n = fs20.readSync(fd, buf, read, want - read, read);
|
|
13922
15451
|
if (n <= 0) break;
|
|
13923
15452
|
read += n;
|
|
13924
15453
|
}
|
|
@@ -13930,13 +15459,13 @@ function readCapped(file, maxBytes) {
|
|
|
13930
15459
|
return null;
|
|
13931
15460
|
} finally {
|
|
13932
15461
|
try {
|
|
13933
|
-
|
|
15462
|
+
fs20.closeSync(fd);
|
|
13934
15463
|
} catch {
|
|
13935
15464
|
}
|
|
13936
15465
|
}
|
|
13937
15466
|
}
|
|
13938
|
-
function readCappedText(
|
|
13939
|
-
const r = readCapped(
|
|
15467
|
+
function readCappedText(file2, maxBytes) {
|
|
15468
|
+
const r = readCapped(file2, maxBytes);
|
|
13940
15469
|
return r ? { text: r.bytes.toString("utf8"), truncated: r.truncated } : null;
|
|
13941
15470
|
}
|
|
13942
15471
|
|
|
@@ -13948,7 +15477,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
13948
15477
|
let ciContext;
|
|
13949
15478
|
if (process.env.CI) {
|
|
13950
15479
|
try {
|
|
13951
|
-
const ciContextPath =
|
|
15480
|
+
const ciContextPath = path21.join(os15.homedir(), ".node9", "ci-context.json");
|
|
13952
15481
|
const ci = readCappedText(ciContextPath, 1e4);
|
|
13953
15482
|
if (!ci) throw new Error("ci-context.json unreadable");
|
|
13954
15483
|
if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
|
|
@@ -13985,11 +15514,21 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
13985
15514
|
context: {
|
|
13986
15515
|
agent: meta?.agent,
|
|
13987
15516
|
mcpServer: meta?.mcpServer,
|
|
13988
|
-
hostname:
|
|
15517
|
+
hostname: os15.hostname(),
|
|
13989
15518
|
cwd: process.cwd(),
|
|
13990
|
-
platform:
|
|
15519
|
+
platform: os15.platform()
|
|
15520
|
+
},
|
|
15521
|
+
// The SaaS validates riskMetadata strictly and would drop it whole on
|
|
15522
|
+
// an unknown key, so only fields its schema declares go on the wire.
|
|
15523
|
+
// exposureDestinations ships since the dashboard's 2b contract
|
|
15524
|
+
// (fw#318): "Approve and trust" needs them.
|
|
15525
|
+
...riskMetadata && {
|
|
15526
|
+
riskMetadata: Object.fromEntries(
|
|
15527
|
+
Object.entries(riskMetadata).filter(
|
|
15528
|
+
([k, v]) => !Array.isArray(v) || k === "exposureDestinations"
|
|
15529
|
+
)
|
|
15530
|
+
)
|
|
13991
15531
|
},
|
|
13992
|
-
...riskMetadata && { riskMetadata },
|
|
13993
15532
|
...ciContext && { ciContext },
|
|
13994
15533
|
...agentPolicy && { policy: agentPolicy },
|
|
13995
15534
|
...forceReview && { forceReview: true }
|
|
@@ -14051,14 +15590,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
14051
15590
|
});
|
|
14052
15591
|
clearTimeout(timer);
|
|
14053
15592
|
if (!res.ok) {
|
|
14054
|
-
|
|
15593
|
+
fs21.appendFileSync(
|
|
14055
15594
|
HOOK_DEBUG_LOG,
|
|
14056
15595
|
`[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
|
|
14057
15596
|
`
|
|
14058
15597
|
);
|
|
14059
15598
|
}
|
|
14060
15599
|
} catch (err) {
|
|
14061
|
-
|
|
15600
|
+
fs21.appendFileSync(
|
|
14062
15601
|
HOOK_DEBUG_LOG,
|
|
14063
15602
|
`[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
|
|
14064
15603
|
`
|
|
@@ -14069,16 +15608,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
14069
15608
|
// src/loop-detector.ts
|
|
14070
15609
|
init_dist();
|
|
14071
15610
|
init_dist();
|
|
14072
|
-
import
|
|
14073
|
-
import
|
|
14074
|
-
import
|
|
15611
|
+
import fs22 from "fs";
|
|
15612
|
+
import path22 from "path";
|
|
15613
|
+
import os16 from "os";
|
|
14075
15614
|
function loopStateFile() {
|
|
14076
|
-
return
|
|
15615
|
+
return path22.join(os16.homedir(), ".node9", "loop-state.json");
|
|
14077
15616
|
}
|
|
14078
15617
|
function readState() {
|
|
14079
15618
|
try {
|
|
14080
|
-
if (!
|
|
14081
|
-
const raw =
|
|
15619
|
+
if (!fs22.existsSync(loopStateFile())) return [];
|
|
15620
|
+
const raw = fs22.readFileSync(loopStateFile(), "utf-8");
|
|
14082
15621
|
const parsed = JSON.parse(raw);
|
|
14083
15622
|
if (!Array.isArray(parsed)) return [];
|
|
14084
15623
|
return parsed;
|
|
@@ -14087,11 +15626,11 @@ function readState() {
|
|
|
14087
15626
|
}
|
|
14088
15627
|
}
|
|
14089
15628
|
function writeState(records) {
|
|
14090
|
-
const
|
|
14091
|
-
if (!
|
|
14092
|
-
const tmpPath = `${loopStateFile()}.${
|
|
14093
|
-
|
|
14094
|
-
|
|
15629
|
+
const dir2 = path22.dirname(loopStateFile());
|
|
15630
|
+
if (!fs22.existsSync(dir2)) fs22.mkdirSync(dir2, { recursive: true });
|
|
15631
|
+
const tmpPath = `${loopStateFile()}.${os16.hostname()}.${process.pid}.tmp`;
|
|
15632
|
+
fs22.writeFileSync(tmpPath, JSON.stringify(records));
|
|
15633
|
+
fs22.renameSync(tmpPath, loopStateFile());
|
|
14095
15634
|
}
|
|
14096
15635
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
14097
15636
|
try {
|
|
@@ -14106,20 +15645,20 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
14106
15645
|
// src/supply-chain/check.ts
|
|
14107
15646
|
init_dist();
|
|
14108
15647
|
import picomatch2 from "picomatch";
|
|
14109
|
-
import
|
|
15648
|
+
import path25 from "path";
|
|
14110
15649
|
|
|
14111
15650
|
// src/supply-chain/osv-index.ts
|
|
14112
15651
|
init_dist();
|
|
14113
|
-
import
|
|
14114
|
-
import
|
|
14115
|
-
import
|
|
15652
|
+
import fs23 from "fs";
|
|
15653
|
+
import os17 from "os";
|
|
15654
|
+
import path23 from "path";
|
|
14116
15655
|
import { createHash as createHash3 } from "crypto";
|
|
14117
15656
|
var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
|
|
14118
15657
|
function osvRoot() {
|
|
14119
|
-
return
|
|
15658
|
+
return path23.join(os17.homedir(), ".node9", "osv");
|
|
14120
15659
|
}
|
|
14121
15660
|
function ecoDir(eco) {
|
|
14122
|
-
return
|
|
15661
|
+
return path23.join(osvRoot(), eco);
|
|
14123
15662
|
}
|
|
14124
15663
|
function indexKey(eco, name) {
|
|
14125
15664
|
return eco === "PyPI" ? normalizePyPiName(name) : name.toLowerCase();
|
|
@@ -14128,11 +15667,11 @@ function shardOf(key) {
|
|
|
14128
15667
|
return createHash3("sha1").update(key).digest("hex").slice(0, 2);
|
|
14129
15668
|
}
|
|
14130
15669
|
function shardPath(eco, shard) {
|
|
14131
|
-
return
|
|
15670
|
+
return path23.join(ecoDir(eco), "shards", `${shard}.json`);
|
|
14132
15671
|
}
|
|
14133
15672
|
function readMeta(eco) {
|
|
14134
15673
|
try {
|
|
14135
|
-
const m = JSON.parse(
|
|
15674
|
+
const m = JSON.parse(fs23.readFileSync(path23.join(ecoDir(eco), "meta.json"), "utf8"));
|
|
14136
15675
|
if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
|
|
14137
15676
|
return m;
|
|
14138
15677
|
} catch {
|
|
@@ -14141,7 +15680,7 @@ function readMeta(eco) {
|
|
|
14141
15680
|
}
|
|
14142
15681
|
function readShard(eco, shard) {
|
|
14143
15682
|
try {
|
|
14144
|
-
const s = JSON.parse(
|
|
15683
|
+
const s = JSON.parse(fs23.readFileSync(shardPath(eco, shard), "utf8"));
|
|
14145
15684
|
return s && typeof s === "object" && !Array.isArray(s) ? s : {};
|
|
14146
15685
|
} catch {
|
|
14147
15686
|
return {};
|
|
@@ -14338,19 +15877,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
|
14338
15877
|
}
|
|
14339
15878
|
|
|
14340
15879
|
// src/supply-chain/local-resolve.ts
|
|
14341
|
-
import
|
|
14342
|
-
import
|
|
15880
|
+
import fs24 from "fs";
|
|
15881
|
+
import path24 from "path";
|
|
14343
15882
|
var MAX_LEVELS = 32;
|
|
14344
15883
|
var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
|
|
14345
15884
|
function resolveInstalled(name, cwd) {
|
|
14346
|
-
if (!cwd || !
|
|
15885
|
+
if (!cwd || !path24.isAbsolute(cwd)) return null;
|
|
14347
15886
|
if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
|
|
14348
|
-
let
|
|
15887
|
+
let dir2 = path24.resolve(cwd);
|
|
14349
15888
|
for (let i = 0; i < MAX_LEVELS; i++) {
|
|
14350
|
-
const pkgDir =
|
|
14351
|
-
const manifest =
|
|
15889
|
+
const pkgDir = path24.join(dir2, "node_modules", ...name.split("/"));
|
|
15890
|
+
const manifest = path24.join(pkgDir, "package.json");
|
|
14352
15891
|
try {
|
|
14353
|
-
const raw =
|
|
15892
|
+
const raw = fs24.readFileSync(manifest, "utf8");
|
|
14354
15893
|
const parsed = JSON.parse(raw);
|
|
14355
15894
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
14356
15895
|
return { version: parsed.version, dir: pkgDir };
|
|
@@ -14358,9 +15897,9 @@ function resolveInstalled(name, cwd) {
|
|
|
14358
15897
|
return null;
|
|
14359
15898
|
} catch {
|
|
14360
15899
|
}
|
|
14361
|
-
const parent =
|
|
14362
|
-
if (parent ===
|
|
14363
|
-
|
|
15900
|
+
const parent = path24.dirname(dir2);
|
|
15901
|
+
if (parent === dir2) break;
|
|
15902
|
+
dir2 = parent;
|
|
14364
15903
|
}
|
|
14365
15904
|
return null;
|
|
14366
15905
|
}
|
|
@@ -14455,7 +15994,7 @@ function checkInstalled(p, version, misses) {
|
|
|
14455
15994
|
}
|
|
14456
15995
|
async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
14457
15996
|
if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
|
|
14458
|
-
const localCwd = cwd &&
|
|
15997
|
+
const localCwd = cwd && path25.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => path25.resolve(dir2, step), cwd) : void 0;
|
|
14459
15998
|
const installed = resolveInstalled(p.name, localCwd);
|
|
14460
15999
|
if (installed && (!p.version || p.version === installed.version))
|
|
14461
16000
|
return checkInstalled(p, installed.version, misses);
|
|
@@ -14590,6 +16129,9 @@ function isWriteTool(toolName) {
|
|
|
14590
16129
|
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
14591
16130
|
return WRITE_TOOLS.has(t);
|
|
14592
16131
|
}
|
|
16132
|
+
function isMcpToolName(toolName) {
|
|
16133
|
+
return /^mcp__[^_](?:[^_]|_(?!_))*?__/i.test(toolName);
|
|
16134
|
+
}
|
|
14593
16135
|
function extractFilePaths(toolName, args, shellCommand) {
|
|
14594
16136
|
const paths = [];
|
|
14595
16137
|
if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
|
|
@@ -14707,7 +16249,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
14707
16249
|
let fileAccess;
|
|
14708
16250
|
let fileFinding;
|
|
14709
16251
|
try {
|
|
14710
|
-
fileAccess = prepareFileAccesses(toolName, args, options?.cwd, config.policy
|
|
16252
|
+
fileAccess = prepareFileAccesses(toolName, args, options?.cwd, inspectionLayers(config.policy));
|
|
14711
16253
|
fileFinding = isManual ? null : evaluateProtectedFileAccess(
|
|
14712
16254
|
fileAccess.accesses,
|
|
14713
16255
|
fileAccess,
|
|
@@ -14726,7 +16268,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
14726
16268
|
fileAccess = { cwd: options?.cwd, homeDirs: [], accesses: [], handledPaths: /* @__PURE__ */ new Set() };
|
|
14727
16269
|
fileFinding = isManual ? null : incompleteFileVerdict();
|
|
14728
16270
|
}
|
|
14729
|
-
const shellInput = resolveShellInput(toolName, args, config.policy
|
|
16271
|
+
const shellInput = resolveShellInput(toolName, args, inspectionLayers(config.policy));
|
|
14730
16272
|
const networkShell = shellInput.kind === "shell" && NETWORK_COMMAND_RE.test(shellInput.command);
|
|
14731
16273
|
const shellReview = !isManual && shellInput.kind === "uninspectable";
|
|
14732
16274
|
if (shellReview) {
|
|
@@ -14823,13 +16365,173 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
14823
16365
|
}
|
|
14824
16366
|
}
|
|
14825
16367
|
}
|
|
14826
|
-
|
|
16368
|
+
const mcpHookTool = options?.sessionTaintGated === void 0 && isMcpToolName(toolName);
|
|
16369
|
+
let daemonSaysExposed;
|
|
16370
|
+
if (!taintWarning && meta?.sessionId && (networkShell || isWriteTool(toolName) || options?.sessionTaintGated === true || mcpHookTool)) {
|
|
14827
16371
|
const sessionTaint = await checkSessionTaint(meta.sessionId);
|
|
16372
|
+
if (typeof sessionTaint.exposed === "boolean") daemonSaysExposed = sessionTaint.exposed;
|
|
14828
16373
|
if (sessionTaint.tainted && sessionTaint.record) {
|
|
14829
|
-
const kind = isWriteTool(toolName) ? "write" : networkShell ? "network" : "state-changing";
|
|
16374
|
+
const kind = isWriteTool(toolName) ? "write" : networkShell ? "network" : mcpHookTool ? "MCP" : "state-changing";
|
|
14830
16375
|
taintWarning = `\u26A0\uFE0F node9 flagged this session \u2014 earlier tool output contained ${sessionTaint.record.source}. Approve this ${kind} action before it proceeds.`;
|
|
14831
16376
|
}
|
|
14832
16377
|
}
|
|
16378
|
+
let exposureReview = null;
|
|
16379
|
+
let exposureKeys = [];
|
|
16380
|
+
let exposureDestinations = [];
|
|
16381
|
+
let exposureFromEid;
|
|
16382
|
+
const ucPolicy = config.policy.untrustedContent;
|
|
16383
|
+
if (ucPolicy?.enabled && !taintWarning && meta?.sessionId && !isManual) {
|
|
16384
|
+
const role = toolRole(toolName, ucPolicy);
|
|
16385
|
+
const acts = role.action || options?.sessionTaintGated === true;
|
|
16386
|
+
const shellExp = shellInput.kind === "shell" ? classifyShellExposure(shellInput.command) : null;
|
|
16387
|
+
const bareName = splitMcpToolName(toolName)?.bare;
|
|
16388
|
+
const mcpFileWrite = bareName !== void 0 && fileToolOperation(bareName, args) === "write";
|
|
16389
|
+
const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, inspectionLayers(config.policy)).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
|
|
16390
|
+
const sendsSomething = acts && !mcpFileWrite || !!shellExp?.sends;
|
|
16391
|
+
const mayBeExposed = daemonSaysExposed ?? (maybeSessionExposed(meta.sessionId) || writes.length > 0 && judgeWritesAfterExposure({
|
|
16392
|
+
accesses: writes,
|
|
16393
|
+
args,
|
|
16394
|
+
shellCommand: shellInput.kind === "shell" ? shellInput.command : null,
|
|
16395
|
+
cwd: options?.cwd,
|
|
16396
|
+
inContent: /* @__PURE__ */ new Set(),
|
|
16397
|
+
isKnown: () => true
|
|
16398
|
+
}).length > 0);
|
|
16399
|
+
const judged = sendsSomething || (!!shellExp?.fetches || writes.length > 0) && mayBeExposed;
|
|
16400
|
+
if (judged) {
|
|
16401
|
+
const exposure = await checkSessionExposure(meta.sessionId);
|
|
16402
|
+
if (exposure.exposed && exposure.record) {
|
|
16403
|
+
const rec = exposure.record;
|
|
16404
|
+
const dests = new Set(
|
|
16405
|
+
acts && !shellExp && !mcpFileWrite ? extractArgDestinations(args) : []
|
|
16406
|
+
);
|
|
16407
|
+
const fetchOnly = /* @__PURE__ */ new Set();
|
|
16408
|
+
const projectHosts = shellExp ? knownProjectHosts(options?.cwd) : null;
|
|
16409
|
+
let unknownRemote = false;
|
|
16410
|
+
if (shellExp) {
|
|
16411
|
+
if (shellExp.sends) {
|
|
16412
|
+
for (const d of shellExp.destinations) dests.add(d);
|
|
16413
|
+
for (const name of shellExp.gitRemotes) {
|
|
16414
|
+
const remotes = projectHosts.remotes;
|
|
16415
|
+
const hosts = name === "" ? remotes.get("origin") ?? [...remotes.values()].flat() : remotes.get(name);
|
|
16416
|
+
if (hosts) for (const h of hosts) dests.add(h);
|
|
16417
|
+
else if (projectHosts.remotes.size > 0) unknownRemote = true;
|
|
16418
|
+
}
|
|
16419
|
+
}
|
|
16420
|
+
if (shellExp.fetches) {
|
|
16421
|
+
for (const h of shellExp.fetchDestinations) if (!dests.has(h)) fetchOnly.add(h);
|
|
16422
|
+
}
|
|
16423
|
+
}
|
|
16424
|
+
const inContent = new Set(rec.destinations);
|
|
16425
|
+
const mentioned = new Set(rec.userMentioned);
|
|
16426
|
+
const approved = new Set(rec.approved);
|
|
16427
|
+
const held = [];
|
|
16428
|
+
const logged = [];
|
|
16429
|
+
const writeProjectHosts = projectHosts ?? knownProjectHosts(options?.cwd);
|
|
16430
|
+
const isKnown = (d) => {
|
|
16431
|
+
const domain = destinationDomain(d);
|
|
16432
|
+
const value = destinationValue(d);
|
|
16433
|
+
return domain !== null && ucPolicy.orgDomains.includes(domain) || mentioned.has(d) || isTrustedRecipient(d) || ucPolicy.trustedRecipients.includes(d) || destinationKind(d) === "host" && (isLoopbackHost(value) || isTrustedHost(value) || config.policy.trustedHostsManaged && matchesTrustedHost(value, config.policy.trustedHosts) || config.policy.egress.allow.some((pat) => hostMatches(value, pat)) || writeProjectHosts.hosts.has(d));
|
|
16434
|
+
};
|
|
16435
|
+
const approvedOnce = (key) => ucPolicy.afterExposure === "ask-once" && approved.has(key);
|
|
16436
|
+
const repos = (shellExp?.sends ? shellExp.repos : []).filter(
|
|
16437
|
+
(r) => writeProjectHosts.hosts.has(`host:${r.split("/")[0]}`)
|
|
16438
|
+
);
|
|
16439
|
+
const repoHosts = new Set(repos.map((r) => `host:${r.split("/")[0]}`));
|
|
16440
|
+
for (const r of repos) {
|
|
16441
|
+
if (writeProjectHosts.repos.has(r) || approvedOnce(`${toolName}|repo:${r}`))
|
|
16442
|
+
logged.push(`repo:${r}`);
|
|
16443
|
+
else held.push({ dest: "", why: "repository", repo: r });
|
|
16444
|
+
}
|
|
16445
|
+
for (const d of dests) {
|
|
16446
|
+
if (repoHosts.has(d)) continue;
|
|
16447
|
+
if (approvedOnce(`${toolName}|${d}`) || isKnown(d)) logged.push(d);
|
|
16448
|
+
else if (inContent.has(d)) held.push({ dest: d, why: "appeared in what was read" });
|
|
16449
|
+
else held.push({ dest: d, why: "not known to this session" });
|
|
16450
|
+
}
|
|
16451
|
+
for (const d of fetchOnly) {
|
|
16452
|
+
if (approvedOnce(`${toolName}|${d}`) || isKnown(d) || !inContent.has(d)) logged.push(d);
|
|
16453
|
+
else held.push({ dest: d, why: "appeared in what was read" });
|
|
16454
|
+
}
|
|
16455
|
+
if (shellExp?.sends && (shellExp.unresolved || unknownRemote || shellExp.destinations.length === 0 && shellExp.gitRemotes.length === 0) && !approvedOnce(`${toolName}|unresolved`)) {
|
|
16456
|
+
held.push({ dest: "", why: "unresolved" });
|
|
16457
|
+
}
|
|
16458
|
+
const bare = splitMcpToolName(toolName)?.bare ?? toolName;
|
|
16459
|
+
if (dests.size === 0 && acts && !mcpFileWrite && (role.destructive === true || isDestructiveToolName(bare))) {
|
|
16460
|
+
held.push({ dest: "", why: "destructive" });
|
|
16461
|
+
}
|
|
16462
|
+
const writeHolds = writes.length > 0 ? judgeWritesAfterExposure({
|
|
16463
|
+
accesses: writes,
|
|
16464
|
+
args,
|
|
16465
|
+
shellCommand: shellInput.kind === "shell" ? shellInput.command : null,
|
|
16466
|
+
cwd: options?.cwd,
|
|
16467
|
+
inContent,
|
|
16468
|
+
isKnown
|
|
16469
|
+
}).filter((h) => {
|
|
16470
|
+
const key = `${toolName}|${h.why === "content-destination" ? h.destination : `path:${h.path}`}`;
|
|
16471
|
+
if (ucPolicy.afterExposure === "ask-once" && approved.has(key)) {
|
|
16472
|
+
logged.push(key);
|
|
16473
|
+
return false;
|
|
16474
|
+
}
|
|
16475
|
+
return true;
|
|
16476
|
+
}) : [];
|
|
16477
|
+
const last = rec.sources[rec.sources.length - 1];
|
|
16478
|
+
const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
|
|
16479
|
+
const when = last ? new Date(last.at).toTimeString().slice(0, 5) : "";
|
|
16480
|
+
const read = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
|
|
16481
|
+
if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
|
|
16482
|
+
const noun = (d) => {
|
|
16483
|
+
const k = destinationKind(d);
|
|
16484
|
+
return k === "email" ? "an address" : k === "host" ? "a host" : "an account";
|
|
16485
|
+
};
|
|
16486
|
+
const parts = held.map(
|
|
16487
|
+
(h) => h.dest ? `sends to ${destinationValue(h.dest)}, ${h.why === "appeared in what was read" ? `${noun(h.dest)} that appeared in that ${last ? readWord : "content"}` : `${noun(h.dest)} this session did not know`}` : h.why === "repository" ? h.repo.endsWith("/*") ? `sends to a repository on ${h.repo.slice(0, -2)} it does not name (a gist, a new repository or an API call)` : `sends to ${h.repo}, a repository that is not one of this project's remotes` : h.why === "unresolved" ? "sends to a destination this session cannot see (built at run time, or a remote of another repository)" : `is ${bare}, a destructive call, with no known destination`
|
|
16488
|
+
);
|
|
16489
|
+
const home = os18.homedir();
|
|
16490
|
+
const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
|
|
16491
|
+
for (const w of writeHolds) {
|
|
16492
|
+
parts.push(
|
|
16493
|
+
w.why === "outside-project" ? `writes outside the project, to ${short(w.path)}` : w.why === "agent-instruction" ? `writes to ${short(w.path)}, a file that runs code or instructs the agent` : `writes content carrying ${destinationValue(w.destination ?? "")}, ${noun(w.destination ?? "host:x")} that appeared in that ${readWord}`
|
|
16494
|
+
);
|
|
16495
|
+
}
|
|
16496
|
+
exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read}. This call ${parts.join("; and ")}.`;
|
|
16497
|
+
exposureFromEid = last?.eid;
|
|
16498
|
+
if (exposureFromEid) meta = { ...meta, exposureFromEid };
|
|
16499
|
+
exposureKeys = [
|
|
16500
|
+
...held.filter((h) => h.dest || h.why === "unresolved" || h.why === "repository").map(
|
|
16501
|
+
(h) => h.why === "repository" ? `${toolName}|repo:${h.repo}` : `${toolName}|${h.dest || "unresolved"}`
|
|
16502
|
+
),
|
|
16503
|
+
...writeHolds.map(
|
|
16504
|
+
(w) => `${toolName}|${w.why === "content-destination" ? w.destination : `path:${w.path}`}`
|
|
16505
|
+
)
|
|
16506
|
+
];
|
|
16507
|
+
exposureDestinations = held.filter((h) => h.dest).map((h) => h.dest).slice(0, 10);
|
|
16508
|
+
if (exposureDestinations.length > 0 && options?.deferReview) {
|
|
16509
|
+
exposureReview += ` To trust ${exposureDestinations.length === 1 ? "this address" : "these addresses"} for good, the user runs: ` + exposureDestinations.map((d) => `node9 trust recipient add ${destinationValue(d)}`).join("; ") + ".";
|
|
16510
|
+
}
|
|
16511
|
+
} else if (held.length > 0 || writeHolds.length > 0 || logged.length > 0) {
|
|
16512
|
+
meta = {
|
|
16513
|
+
...meta,
|
|
16514
|
+
exposure: { kind: "logged", destinations: logged.length },
|
|
16515
|
+
...last?.eid ? { exposureFromEid: last.eid } : {}
|
|
16516
|
+
};
|
|
16517
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
16518
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16519
|
+
event: "exposure-logged",
|
|
16520
|
+
tool: toolName,
|
|
16521
|
+
held: held.length + writeHolds.length,
|
|
16522
|
+
known: logged.length,
|
|
16523
|
+
sessionId: meta.sessionId
|
|
16524
|
+
});
|
|
16525
|
+
}
|
|
16526
|
+
} else if (exposure.daemonUnavailable) {
|
|
16527
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
16528
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16529
|
+
event: "exposure-unavailable",
|
|
16530
|
+
tool: toolName
|
|
16531
|
+
});
|
|
16532
|
+
}
|
|
16533
|
+
}
|
|
16534
|
+
}
|
|
14833
16535
|
const canaryVals = safeCanaryValues();
|
|
14834
16536
|
if (canaryVals.length > 0) {
|
|
14835
16537
|
const canaryHit = matchCanaryArgs(args, canaryVals);
|
|
@@ -15070,7 +16772,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
15070
16772
|
}
|
|
15071
16773
|
let appPermReview = null;
|
|
15072
16774
|
let appPermReviewTool = null;
|
|
15073
|
-
const callSpecificReview = () => fileReview || shellReview || dlpReviewFlagged || !!appPermReview || !!taintWarning || !!packageReview;
|
|
16775
|
+
const callSpecificReview = () => fileReview || shellReview || dlpReviewFlagged || !!appPermReview || !!taintWarning || !!exposureReview || !!packageReview;
|
|
15074
16776
|
const standingTrustApplies = () => !callSpecificReview() && !hardBlockDowngraded;
|
|
15075
16777
|
const reviewDemanded = () => callSpecificReview() || localSmartRuleMatched === true || options?.localSmartRuleMatched === true;
|
|
15076
16778
|
if (meta?.serverKey) {
|
|
@@ -15315,6 +17017,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
15315
17017
|
appPermReview ? `${taintWarning}
|
|
15316
17018
|
${appPermReview}` : taintWarning
|
|
15317
17019
|
);
|
|
17020
|
+
} else if (exposureReview) {
|
|
17021
|
+
explainableLabel = EXPOSURE_LABEL;
|
|
17022
|
+
riskMetadata = computeRiskMetadata(args, 6, explainableLabel, void 0, void 0, "exposure");
|
|
17023
|
+
riskMetadata.ruleDescription = (appPermReview ? `${exposureReview}
|
|
17024
|
+
${appPermReview}` : exposureReview).slice(0, 1024);
|
|
17025
|
+
if (exposureDestinations.length > 0) riskMetadata.exposureDestinations = exposureDestinations;
|
|
15318
17026
|
} else if (appPermReview) {
|
|
15319
17027
|
if (dlpReviewFlagged) {
|
|
15320
17028
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review) + \u{1F512} App Permission (Review)";
|
|
@@ -15357,7 +17065,9 @@ ${packageReview}`;
|
|
|
15357
17065
|
// The prompt must say WHY: the taint sentence beats the bare label so
|
|
15358
17066
|
// the dev sees the actual risk context inline. (No appPermReview term:
|
|
15359
17067
|
// deferReview and serverKey never co-occur in production — see above.)
|
|
15360
|
-
reason: taintWarning || packageReview || explainableLabel || "Node9 flagged this action for review.",
|
|
17068
|
+
reason: taintWarning || packageReview || exposureReview || explainableLabel || "Node9 flagged this action for review.",
|
|
17069
|
+
...exposureReview && exposureKeys.length > 0 ? { exposureKeys } : {},
|
|
17070
|
+
...exposureReview && exposureFromEid ? { exposureFromEid } : {},
|
|
15361
17071
|
ruleDescription: policyRuleDescription,
|
|
15362
17072
|
blockedByLabel: explainableLabel
|
|
15363
17073
|
};
|
|
@@ -15451,10 +17161,14 @@ ${packageReview}`;
|
|
|
15451
17161
|
// auto-allows. This process (the long-running gateway — appPermReview
|
|
15452
17162
|
// only ever happens here) stays alive to run its own racers, so the
|
|
15453
17163
|
// daemon just holds the card and waits for the human decision.
|
|
15454
|
-
|
|
17164
|
+
// fw#318 P1.1: the same for every call-specific review (exposure,
|
|
17165
|
+
// session taint, DLP, file review): the re-run carries no session id
|
|
17166
|
+
// and cannot see these reasons, so it must never answer the card.
|
|
17167
|
+
appPermReview || callSpecificReview() ? true : void 0,
|
|
15455
17168
|
void 0,
|
|
15456
17169
|
localSmartRuleMatched || options?.localSmartRuleMatched,
|
|
15457
|
-
options?.socketActivitySent
|
|
17170
|
+
options?.socketActivitySent,
|
|
17171
|
+
meta?.sessionId
|
|
15458
17172
|
);
|
|
15459
17173
|
daemonEntryId = entry.id;
|
|
15460
17174
|
daemonAllowCount = entry.allowCount;
|
|
@@ -15495,8 +17209,15 @@ ${packageReview}`;
|
|
|
15495
17209
|
policyMatchedField,
|
|
15496
17210
|
policyMatchedWord,
|
|
15497
17211
|
daemonAllowCount,
|
|
15498
|
-
riskMetadata?.ruleDescription
|
|
17212
|
+
riskMetadata?.ruleDescription,
|
|
17213
|
+
exposureDestinations
|
|
15499
17214
|
);
|
|
17215
|
+
if (decision === "trust_recipients") {
|
|
17216
|
+
if (config.policySource !== "workspace") {
|
|
17217
|
+
for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
|
|
17218
|
+
}
|
|
17219
|
+
return { approved: true, checkedBy: "daemon", decisionSource: "native" };
|
|
17220
|
+
}
|
|
15500
17221
|
if (decision === "always_allow") {
|
|
15501
17222
|
if (standingTrustApplies()) {
|
|
15502
17223
|
writeTrustSession(toolName, 36e5, args);
|
|
@@ -15597,6 +17318,9 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
15597
17318
|
});
|
|
15598
17319
|
}
|
|
15599
17320
|
});
|
|
17321
|
+
if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
|
|
17322
|
+
await approveSessionExposure(meta.sessionId, exposureKeys);
|
|
17323
|
+
}
|
|
15600
17324
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
15601
17325
|
await resolveNode9SaaS(
|
|
15602
17326
|
cloudRequestId,
|