@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.js
CHANGED
|
@@ -219,10 +219,10 @@ function compressIpv6(g) {
|
|
|
219
219
|
if (bestLen < 2) return hex.join(":");
|
|
220
220
|
return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
|
|
221
221
|
}
|
|
222
|
-
function normalizeIpLiteral(
|
|
222
|
+
function normalizeIpLiteral(host2) {
|
|
223
223
|
try {
|
|
224
|
-
if (typeof
|
|
225
|
-
let s =
|
|
224
|
+
if (typeof host2 !== "string") return null;
|
|
225
|
+
let s = host2.trim();
|
|
226
226
|
if (!s || s.length > SSRF_MAX_HOST) return null;
|
|
227
227
|
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
228
228
|
const zone = s.indexOf("%");
|
|
@@ -248,11 +248,11 @@ function isStrictGatedTier(tier) {
|
|
|
248
248
|
function ssrfReason(m, asWritten) {
|
|
249
249
|
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
250
250
|
}
|
|
251
|
-
function classifySsrf(
|
|
251
|
+
function classifySsrf(host2) {
|
|
252
252
|
try {
|
|
253
|
-
if (typeof
|
|
254
|
-
const lower2 =
|
|
255
|
-
const ip = normalizeIpLiteral(
|
|
253
|
+
if (typeof host2 !== "string" || !host2) return null;
|
|
254
|
+
const lower2 = host2.trim().toLowerCase().replace(/\.$/, "");
|
|
255
|
+
const ip = normalizeIpLiteral(host2);
|
|
256
256
|
if (ip === null) {
|
|
257
257
|
return METADATA_HOSTNAMES.has(lower2) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
|
|
258
258
|
}
|
|
@@ -454,6 +454,7 @@ function catalogSettingsFromConfig(settings, policy) {
|
|
|
454
454
|
injectionScan: policy.injectionScan,
|
|
455
455
|
skillPinning: policy.skillPinning,
|
|
456
456
|
packageCheck: policy.packageCheck,
|
|
457
|
+
untrustedContent: policy.untrustedContent,
|
|
457
458
|
appliedShields: policy.appliedShields ?? [],
|
|
458
459
|
checks: policy.checks
|
|
459
460
|
};
|
|
@@ -961,11 +962,11 @@ function inlineExecStmt(stmt, pipeFed) {
|
|
|
961
962
|
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
962
963
|
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
963
964
|
}
|
|
964
|
-
let
|
|
965
|
+
let positionals2 = 0;
|
|
965
966
|
let selectedProgram = false;
|
|
966
967
|
for (const a of args) {
|
|
967
968
|
if (a == null) {
|
|
968
|
-
|
|
969
|
+
positionals2++;
|
|
969
970
|
selectedProgram = true;
|
|
970
971
|
continue;
|
|
971
972
|
}
|
|
@@ -976,13 +977,13 @@ function inlineExecStmt(stmt, pipeFed) {
|
|
|
976
977
|
}
|
|
977
978
|
if (a === "-" && !selectedProgram) return true;
|
|
978
979
|
if (!a.startsWith("-")) {
|
|
979
|
-
|
|
980
|
+
positionals2++;
|
|
980
981
|
selectedProgram = true;
|
|
981
982
|
}
|
|
982
983
|
}
|
|
983
984
|
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
984
985
|
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
985
|
-
if (
|
|
986
|
+
if (positionals2 === 0 && (stdinFed || pipeFed)) {
|
|
986
987
|
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
987
988
|
}
|
|
988
989
|
return false;
|
|
@@ -1192,6 +1193,24 @@ function resolveWordLiteral(w) {
|
|
|
1192
1193
|
}
|
|
1193
1194
|
return s;
|
|
1194
1195
|
}
|
|
1196
|
+
function resolveWordPrefix(w) {
|
|
1197
|
+
const walk2 = (parts) => {
|
|
1198
|
+
let s = "";
|
|
1199
|
+
for (const p of parts) {
|
|
1200
|
+
if (p && typeof p === "object" && Array.isArray(p.Parts) && p.Dollar !== void 0) {
|
|
1201
|
+
const inner = walk2(p.Parts);
|
|
1202
|
+
s += inner.prefix;
|
|
1203
|
+
if (inner.dynamic) return { prefix: s, dynamic: true };
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const piece = resolvePart(p, false);
|
|
1207
|
+
if (piece === void 0 || piece === null) return { prefix: s, dynamic: true };
|
|
1208
|
+
s += piece;
|
|
1209
|
+
}
|
|
1210
|
+
return { prefix: s, dynamic: false };
|
|
1211
|
+
};
|
|
1212
|
+
return walk2(w?.Parts || []);
|
|
1213
|
+
}
|
|
1195
1214
|
function resolvePart(p, inQuotes) {
|
|
1196
1215
|
const t = syntax.NodeType(p);
|
|
1197
1216
|
if (t === "Lit") {
|
|
@@ -1239,7 +1258,7 @@ function parseDestHost(token) {
|
|
|
1239
1258
|
}
|
|
1240
1259
|
function destTokensForBinary(binary, args) {
|
|
1241
1260
|
const valueFlags = VALUE_FLAGS[binary] ?? /* @__PURE__ */ new Set();
|
|
1242
|
-
const
|
|
1261
|
+
const positionals2 = [];
|
|
1243
1262
|
const urlFlagValues = [];
|
|
1244
1263
|
for (let i = 0; i < args.length; i++) {
|
|
1245
1264
|
const tok = args[i];
|
|
@@ -1259,21 +1278,21 @@ function destTokensForBinary(binary, args) {
|
|
|
1259
1278
|
if (valueFlags.has(tok)) i++;
|
|
1260
1279
|
continue;
|
|
1261
1280
|
}
|
|
1262
|
-
|
|
1281
|
+
positionals2.push(tok);
|
|
1263
1282
|
}
|
|
1264
1283
|
switch (binary) {
|
|
1265
1284
|
case "curl":
|
|
1266
1285
|
case "wget":
|
|
1267
|
-
return [...urlFlagValues, ...
|
|
1286
|
+
return [...urlFlagValues, ...positionals2];
|
|
1268
1287
|
case "ssh":
|
|
1269
|
-
return
|
|
1288
|
+
return positionals2.slice(0, 1);
|
|
1270
1289
|
case "scp":
|
|
1271
1290
|
case "rsync":
|
|
1272
|
-
return
|
|
1291
|
+
return positionals2.filter((p) => p.includes(":") || p.includes("@"));
|
|
1273
1292
|
case "nc":
|
|
1274
1293
|
case "ncat":
|
|
1275
1294
|
case "netcat":
|
|
1276
|
-
return
|
|
1295
|
+
return positionals2.slice(0, 1);
|
|
1277
1296
|
default:
|
|
1278
1297
|
return [];
|
|
1279
1298
|
}
|
|
@@ -1290,16 +1309,18 @@ function extractShellDestinations(command) {
|
|
|
1290
1309
|
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1291
1310
|
const callArgs = n.Args || [];
|
|
1292
1311
|
if (callArgs.length === 0) return true;
|
|
1293
|
-
const
|
|
1312
|
+
const words = callArgs.map((a) => resolveWordLiteral(a));
|
|
1313
|
+
const at = unwrapCommandHead(words);
|
|
1314
|
+
const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
|
|
1294
1315
|
if (!NET_BINARIES.has(name)) return true;
|
|
1295
|
-
const rest =
|
|
1316
|
+
const rest = words.slice(at + 1);
|
|
1296
1317
|
for (const raw of destTokensForBinary(name, rest)) {
|
|
1297
|
-
const
|
|
1298
|
-
if (!
|
|
1299
|
-
const key = `${name}:${
|
|
1318
|
+
const host2 = parseDestHost(raw);
|
|
1319
|
+
if (!host2) continue;
|
|
1320
|
+
const key = `${name}:${host2}`;
|
|
1300
1321
|
if (seen.has(key)) continue;
|
|
1301
1322
|
seen.add(key);
|
|
1302
|
-
out.push({ host, binary: name, raw });
|
|
1323
|
+
out.push({ host: host2, binary: name, raw });
|
|
1303
1324
|
}
|
|
1304
1325
|
return true;
|
|
1305
1326
|
});
|
|
@@ -2059,9 +2080,9 @@ function matchesPattern(text, patterns) {
|
|
|
2059
2080
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2060
2081
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2061
2082
|
}
|
|
2062
|
-
function getNestedValue(obj,
|
|
2083
|
+
function getNestedValue(obj, path26) {
|
|
2063
2084
|
if (!obj || typeof obj !== "object") return null;
|
|
2064
|
-
const segments2 =
|
|
2085
|
+
const segments2 = path26.split(".");
|
|
2065
2086
|
for (const seg of segments2) {
|
|
2066
2087
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2067
2088
|
}
|
|
@@ -2137,32 +2158,109 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2137
2158
|
function isBashTool(tool) {
|
|
2138
2159
|
return BASH_TOOL_NAMES.has(tool.toLowerCase());
|
|
2139
2160
|
}
|
|
2140
|
-
function
|
|
2161
|
+
function inspectionLayers(policy) {
|
|
2162
|
+
return { trusted: policy.toolInspection, project: policy.projectToolInspection };
|
|
2163
|
+
}
|
|
2164
|
+
function isShellEntry(value) {
|
|
2165
|
+
return typeof value === "object" && value !== null && value.kind === "shell" && typeof value.field === "string" && Object.keys(value).length === 2;
|
|
2166
|
+
}
|
|
2167
|
+
function isInspectionLayers(input) {
|
|
2168
|
+
const trusted = input.trusted;
|
|
2169
|
+
return typeof trusted === "object" && trusted !== null && !isShellEntry(trusted);
|
|
2170
|
+
}
|
|
2171
|
+
function layersOf(input) {
|
|
2172
|
+
return isInspectionLayers(input) ? { trusted: input.trusted, project: input.project ?? {} } : { trusted: input, project: {} };
|
|
2173
|
+
}
|
|
2174
|
+
function entryOf(tool, inspection) {
|
|
2141
2175
|
const pattern = Object.keys(inspection).find((p) => matchesPattern(tool, p));
|
|
2142
2176
|
return pattern === void 0 ? void 0 : inspection[pattern];
|
|
2143
2177
|
}
|
|
2178
|
+
function inspectionEntry(tool, inspection = {}) {
|
|
2179
|
+
const { trusted, project } = layersOf(inspection);
|
|
2180
|
+
return entryOf(tool, trusted) ?? entryOf(tool, project);
|
|
2181
|
+
}
|
|
2182
|
+
function executorName(tool) {
|
|
2183
|
+
const name = tool.toLowerCase();
|
|
2184
|
+
if (!name.startsWith("mcp__")) return name;
|
|
2185
|
+
const at = name.lastIndexOf("__");
|
|
2186
|
+
return at >= "mcp__".length ? name.slice(at + 2) : name;
|
|
2187
|
+
}
|
|
2188
|
+
function entryFields(entry) {
|
|
2189
|
+
if (typeof entry === "object") return [entry.field];
|
|
2190
|
+
return entry === "command" ? ["command"] : null;
|
|
2191
|
+
}
|
|
2144
2192
|
function shellFields(tool, inspection) {
|
|
2145
2193
|
if (isBashTool(tool)) return ["command"];
|
|
2146
|
-
const
|
|
2147
|
-
|
|
2148
|
-
if (
|
|
2149
|
-
const
|
|
2150
|
-
|
|
2194
|
+
const { trusted, project } = layersOf(inspection);
|
|
2195
|
+
const trustedEntry = entryOf(tool, trusted);
|
|
2196
|
+
if (trustedEntry !== void 0) return entryFields(trustedEntry);
|
|
2197
|
+
const builtin = MCP_EXECUTORS.has(executorName(tool)) ? BUILTIN_FIELDS : null;
|
|
2198
|
+
const projectEntry = entryOf(tool, project);
|
|
2199
|
+
if (projectEntry === void 0) return builtin;
|
|
2200
|
+
const added = entryFields(projectEntry);
|
|
2201
|
+
if (!builtin) return added;
|
|
2202
|
+
if (!added) return builtin;
|
|
2203
|
+
return [.../* @__PURE__ */ new Set([...builtin, ...added])];
|
|
2151
2204
|
}
|
|
2152
2205
|
function isShellShapedTool(tool, inspection = {}) {
|
|
2153
2206
|
return shellFields(tool, inspection) !== null;
|
|
2154
2207
|
}
|
|
2208
|
+
function shellWord(word) {
|
|
2209
|
+
if (BARE_WORD.test(word)) return word;
|
|
2210
|
+
return word === "" ? "''" : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
2211
|
+
}
|
|
2212
|
+
function isStringArray(value) {
|
|
2213
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2214
|
+
}
|
|
2215
|
+
function commandLine(args, fieldPath, value, native) {
|
|
2216
|
+
const missing = { reason: "Shell command field is missing or is not a string" };
|
|
2217
|
+
const unreadable = { reason: "Shell command arguments could not be read" };
|
|
2218
|
+
if (typeof value !== "string" && (native || !Array.isArray(value))) return missing;
|
|
2219
|
+
const dot = fieldPath.lastIndexOf(".");
|
|
2220
|
+
const parent = dot === -1 ? args : getNestedValue(args, fieldPath.slice(0, dot));
|
|
2221
|
+
const siblings = ARGV_FIELDS.map((key) => ({
|
|
2222
|
+
key,
|
|
2223
|
+
value: parent && typeof parent === "object" ? parent[key] : void 0
|
|
2224
|
+
})).filter(({ value: value2 }) => value2 !== void 0 && value2 !== null);
|
|
2225
|
+
let argv = siblings[0];
|
|
2226
|
+
if (native) argv = siblings.find(({ value: v }) => isStringArray(v));
|
|
2227
|
+
else if (siblings.length === 2 && JSON.stringify(siblings[0].value) !== JSON.stringify(siblings[1].value))
|
|
2228
|
+
return { reason: "Shell command arguments disagree" };
|
|
2229
|
+
let command;
|
|
2230
|
+
let source = fieldPath;
|
|
2231
|
+
if (Array.isArray(value)) {
|
|
2232
|
+
if (argv || !value.length || !isStringArray(value)) return unreadable;
|
|
2233
|
+
command = value.map(shellWord).join(" ");
|
|
2234
|
+
} else if (!argv) {
|
|
2235
|
+
return { command: value, fieldPath };
|
|
2236
|
+
} else {
|
|
2237
|
+
if (!isStringArray(argv.value)) return unreadable;
|
|
2238
|
+
if (!argv.value.length) return { command: value, fieldPath };
|
|
2239
|
+
command = `${value} ${argv.value.map(shellWord).join(" ")}`;
|
|
2240
|
+
source = `${fieldPath}+${argv.key}`;
|
|
2241
|
+
}
|
|
2242
|
+
if (command.length > MAX_ARGV_CHARACTERS)
|
|
2243
|
+
return { reason: "Shell command arguments exceed the inspectable size" };
|
|
2244
|
+
return { command, fieldPath: source };
|
|
2245
|
+
}
|
|
2155
2246
|
function resolveShellInput(tool, args, inspection = {}) {
|
|
2156
2247
|
const fields = shellFields(tool, inspection);
|
|
2157
2248
|
if (!fields) return { kind: "not-shell" };
|
|
2158
2249
|
const values = fields.map((fieldPath) => ({ fieldPath, value: getNestedValue(args, fieldPath) })).filter(({ value }) => value !== void 0 && value !== null && value !== "");
|
|
2159
|
-
if (!values.length
|
|
2250
|
+
if (!values.length) {
|
|
2160
2251
|
return { kind: "uninspectable", reason: "Shell command field is missing or is not a string" };
|
|
2161
2252
|
}
|
|
2162
|
-
|
|
2253
|
+
const native = isBashTool(tool);
|
|
2254
|
+
const lines = values.map(({ fieldPath, value }) => commandLine(args, fieldPath, value, native));
|
|
2255
|
+
const failed = lines.find((line) => "reason" in line);
|
|
2256
|
+
if (failed) return { kind: "uninspectable", reason: failed.reason };
|
|
2257
|
+
const commands = lines.filter(
|
|
2258
|
+
(line) => "command" in line
|
|
2259
|
+
);
|
|
2260
|
+
if (new Set(commands.map(({ command }) => command)).size !== 1) {
|
|
2163
2261
|
return { kind: "uninspectable", reason: "Multiple shell command fields disagree" };
|
|
2164
2262
|
}
|
|
2165
|
-
return { kind: "shell",
|
|
2263
|
+
return { kind: "shell", ...commands[0] };
|
|
2166
2264
|
}
|
|
2167
2265
|
function shellRuleArgs(tool, args, ruleTool, inspection = {}) {
|
|
2168
2266
|
if (!ruleTool || !matchesPattern("bash", ruleTool) || matchesPattern(tool, ruleTool)) return args;
|
|
@@ -2388,7 +2486,7 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
2388
2486
|
if (!hasScript) hasScript = true;
|
|
2389
2487
|
else files.push(token);
|
|
2390
2488
|
}
|
|
2391
|
-
for (const
|
|
2489
|
+
for (const file2 of files) add(inplace ? "write" : "read", file2, state);
|
|
2392
2490
|
return;
|
|
2393
2491
|
}
|
|
2394
2492
|
if (FS_READ_TOOLS.has(name) && name !== "dd") {
|
|
@@ -2740,6 +2838,17 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
2740
2838
|
if (program) statements(program.Stmts ?? [], initial, 0);
|
|
2741
2839
|
return out;
|
|
2742
2840
|
}
|
|
2841
|
+
function isAgentInstructionPath(absPath, root) {
|
|
2842
|
+
const r = root.replace(/\/$/, "");
|
|
2843
|
+
if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
|
|
2844
|
+
const rel = absPath.slice(r.length + 1);
|
|
2845
|
+
if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
|
|
2846
|
+
return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
2847
|
+
}
|
|
2848
|
+
function isInside(root, p) {
|
|
2849
|
+
const r = root.replace(/\/$/, "") || "/";
|
|
2850
|
+
return p === r || p.startsWith(r === "/" ? "/" : r + "/");
|
|
2851
|
+
}
|
|
2743
2852
|
function isPasswordHashPath(p) {
|
|
2744
2853
|
return PASSWORD_HASH_FILES.includes(p);
|
|
2745
2854
|
}
|
|
@@ -2809,8 +2918,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
|
|
|
2809
2918
|
}
|
|
2810
2919
|
return out;
|
|
2811
2920
|
}
|
|
2812
|
-
function within(
|
|
2813
|
-
return
|
|
2921
|
+
function within(path26, root) {
|
|
2922
|
+
return path26 === root || path26.startsWith(root === "/" ? "/" : root + "/");
|
|
2814
2923
|
}
|
|
2815
2924
|
function hits(access, paths, directories, context) {
|
|
2816
2925
|
const home = context.homeDirs?.[0] ?? "~";
|
|
@@ -2892,20 +3001,25 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
|
|
|
2892
3001
|
}
|
|
2893
3002
|
return best;
|
|
2894
3003
|
}
|
|
2895
|
-
function findMnemonic(text, budget = {
|
|
2896
|
-
const words = [...text.matchAll(/[a-zA-Z]+/g)];
|
|
3004
|
+
function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new Set() }) {
|
|
2897
3005
|
let run = [];
|
|
3006
|
+
let runIndex = [];
|
|
2898
3007
|
let previousEnd = 0;
|
|
2899
3008
|
const rejected = /* @__PURE__ */ new Set();
|
|
2900
3009
|
const inspect = () => {
|
|
3010
|
+
if (run.length < 12) return false;
|
|
3011
|
+
const ascending = new Array(run.length).fill(0);
|
|
3012
|
+
for (let i = 1; i < run.length; i++)
|
|
3013
|
+
ascending[i] = ascending[i - 1] + (runIndex[i] > runIndex[i - 1] ? 1 : 0);
|
|
2901
3014
|
for (let start = 0; start + 12 <= run.length; start++) {
|
|
2902
3015
|
for (const count of lengths) {
|
|
2903
3016
|
if (start + count > run.length) continue;
|
|
3017
|
+
if (ascending[start + count - 1] - ascending[start] === count - 1) continue;
|
|
2904
3018
|
const candidate = run.slice(start, start + count);
|
|
2905
3019
|
const sentence = candidate.join(" ");
|
|
2906
3020
|
if (rejected.has(sentence)) continue;
|
|
2907
|
-
if (++budget.
|
|
2908
|
-
budget.reasons.add("mnemonic-
|
|
3021
|
+
if (++budget.hashes > MAX_MNEMONIC_HASHES) {
|
|
3022
|
+
budget.reasons.add("mnemonic-budget");
|
|
2909
3023
|
return false;
|
|
2910
3024
|
}
|
|
2911
3025
|
if (BIP39_PUBLIC_VECTORS.has(hash(sentence).toString("hex"))) {
|
|
@@ -2923,13 +3037,18 @@ function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ n
|
|
|
2923
3037
|
}
|
|
2924
3038
|
return false;
|
|
2925
3039
|
};
|
|
2926
|
-
for (const word of
|
|
2927
|
-
|
|
3040
|
+
for (const word of text.matchAll(/[a-zA-Z]+/g)) {
|
|
3041
|
+
const index = indices.get(word[0]);
|
|
3042
|
+
if (index === void 0 || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
|
|
2928
3043
|
if (inspect()) return true;
|
|
2929
|
-
if (budget.reasons.has("mnemonic-
|
|
3044
|
+
if (budget.reasons.has("mnemonic-budget")) return false;
|
|
2930
3045
|
run = [];
|
|
3046
|
+
runIndex = [];
|
|
3047
|
+
}
|
|
3048
|
+
if (index !== void 0) {
|
|
3049
|
+
run.push(word[0]);
|
|
3050
|
+
runIndex.push(index);
|
|
2931
3051
|
}
|
|
2932
|
-
if (indices.has(word[0])) run.push(word[0]);
|
|
2933
3052
|
previousEnd = word.index + word[0].length;
|
|
2934
3053
|
}
|
|
2935
3054
|
return inspect();
|
|
@@ -3060,6 +3179,9 @@ function validateCask(raw) {
|
|
|
3060
3179
|
const second = b64urlIndex(s[tsAt + 5]);
|
|
3061
3180
|
return month <= 11 && day <= 30 && hour <= 23 && minute <= 59 && second <= 59;
|
|
3062
3181
|
}
|
|
3182
|
+
function isInvisibleChar(char) {
|
|
3183
|
+
return INVISIBLE_ONE.test(char);
|
|
3184
|
+
}
|
|
3063
3185
|
function stripInvisible(text) {
|
|
3064
3186
|
return text.replace(INVISIBLE_RE, "");
|
|
3065
3187
|
}
|
|
@@ -3246,6 +3368,45 @@ function matchArgumentText(text, fieldPath, budget) {
|
|
|
3246
3368
|
}
|
|
3247
3369
|
return best;
|
|
3248
3370
|
}
|
|
3371
|
+
function matchArgumentBatch(fragments, fieldPath, budget) {
|
|
3372
|
+
if (!fragments.length) return null;
|
|
3373
|
+
const text = fragments.join(BATCH_SEPARATOR);
|
|
3374
|
+
const lower2 = text.toLowerCase();
|
|
3375
|
+
let best = null;
|
|
3376
|
+
for (const pattern of DLP_PATTERNS) {
|
|
3377
|
+
if (pattern.keywords && !pattern.keywords.some((kw) => lower2.includes(kw.toLowerCase())))
|
|
3378
|
+
continue;
|
|
3379
|
+
const flags = pattern.regex.flags.replace(/[gmy]/g, "");
|
|
3380
|
+
if (!new RegExp(pattern.regex.source, flags + "m").test(text)) continue;
|
|
3381
|
+
for (const fragment of fragments) {
|
|
3382
|
+
if (firstAcceptedMatch(pattern, fragment) === null) continue;
|
|
3383
|
+
const severity = pattern.contextBoost && isAssignmentContext(fragment) ? "block" : pattern.severity;
|
|
3384
|
+
if (severity === "block")
|
|
3385
|
+
return {
|
|
3386
|
+
patternName: pattern.name,
|
|
3387
|
+
fieldPath,
|
|
3388
|
+
redactedSample: "[encoded secret]",
|
|
3389
|
+
severity
|
|
3390
|
+
};
|
|
3391
|
+
best ??= {
|
|
3392
|
+
patternName: pattern.name,
|
|
3393
|
+
fieldPath,
|
|
3394
|
+
redactedSample: "[encoded secret]",
|
|
3395
|
+
severity
|
|
3396
|
+
};
|
|
3397
|
+
if (!pattern.contextBoost) break;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
if (findMnemonic(text, budget)) {
|
|
3401
|
+
return {
|
|
3402
|
+
patternName: "BIP-39 Recovery Phrase",
|
|
3403
|
+
fieldPath,
|
|
3404
|
+
redactedSample: "[recovery phrase]",
|
|
3405
|
+
severity: "block"
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
return best;
|
|
3409
|
+
}
|
|
3249
3410
|
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
3250
3411
|
if (depth > MAX_DEPTH2 || args === null || args === void 0) return null;
|
|
3251
3412
|
if (Array.isArray(args)) {
|
|
@@ -3611,7 +3772,7 @@ function pipInstall(words, from, manager, out) {
|
|
|
3611
3772
|
if (v >= 0 && lower(words[v]) === "install")
|
|
3612
3773
|
pushPy(out, manager, operands(words, v + 1, PIP_VALUE_FLAGS));
|
|
3613
3774
|
}
|
|
3614
|
-
function localContexts(
|
|
3775
|
+
function localContexts(file2) {
|
|
3615
3776
|
const contexts = /* @__PURE__ */ new Map();
|
|
3616
3777
|
const sample = parseShared("cd . && true");
|
|
3617
3778
|
if (typeof sample === "symbol") return contexts;
|
|
@@ -3639,7 +3800,7 @@ function localContexts(file) {
|
|
|
3639
3800
|
contexts.set(cmd.Pos().Offset(), steps);
|
|
3640
3801
|
return null;
|
|
3641
3802
|
}
|
|
3642
|
-
if (
|
|
3803
|
+
if (file2.Stmts?.length === 1) visit(file2.Stmts[0], []);
|
|
3643
3804
|
return contexts;
|
|
3644
3805
|
}
|
|
3645
3806
|
function collect(command, out, depth) {
|
|
@@ -3777,6 +3938,13 @@ function checkToLegacyPolicy(policy, id, v) {
|
|
|
3777
3938
|
return offOr("packageCheck", () => {
|
|
3778
3939
|
obj("packageCheck").onMalicious = v === "review" ? "review" : "block";
|
|
3779
3940
|
});
|
|
3941
|
+
case "behavior.untrusted-content":
|
|
3942
|
+
if (v !== "off" && v !== "log" && v !== "review") return false;
|
|
3943
|
+
return offOr("untrustedContent", () => {
|
|
3944
|
+
const uc = obj("untrustedContent");
|
|
3945
|
+
if (v === "log") uc.afterExposure = "log-only";
|
|
3946
|
+
else if (uc.afterExposure !== "ask-every-time") uc.afterExposure = "ask-once";
|
|
3947
|
+
});
|
|
3780
3948
|
default:
|
|
3781
3949
|
return false;
|
|
3782
3950
|
}
|
|
@@ -3790,38 +3958,38 @@ function configurableValues(id) {
|
|
|
3790
3958
|
if (def.pack || MAP_GOVERNED.has(id)) return def.values;
|
|
3791
3959
|
return KNOB_GOVERNED[id] ?? [];
|
|
3792
3960
|
}
|
|
3793
|
-
function hostMatches(
|
|
3961
|
+
function hostMatches(host2, pattern) {
|
|
3794
3962
|
const p = pattern.trim().toLowerCase();
|
|
3795
3963
|
if (!p) return false;
|
|
3796
3964
|
if (p === "*") return true;
|
|
3797
|
-
const h = canonicalHost(
|
|
3965
|
+
const h = canonicalHost(host2);
|
|
3798
3966
|
if (p.startsWith("*.")) {
|
|
3799
3967
|
const suffix = canonicalHost(p.slice(2));
|
|
3800
3968
|
return h === suffix || h.endsWith("." + suffix);
|
|
3801
3969
|
}
|
|
3802
3970
|
return h === canonicalHost(p);
|
|
3803
3971
|
}
|
|
3804
|
-
function matchesAny(
|
|
3805
|
-
for (const p of patterns ?? []) if (hostMatches(
|
|
3972
|
+
function matchesAny(host2, patterns) {
|
|
3973
|
+
for (const p of patterns ?? []) if (hostMatches(host2, p)) return true;
|
|
3806
3974
|
return false;
|
|
3807
3975
|
}
|
|
3808
|
-
function isUniqueLocalV6(
|
|
3809
|
-
const ip = normalizeIpLiteral(
|
|
3976
|
+
function isUniqueLocalV6(host2) {
|
|
3977
|
+
const ip = normalizeIpLiteral(host2);
|
|
3810
3978
|
if (!ip || !ip.includes(":")) return false;
|
|
3811
3979
|
const g = expandIpv6(ip);
|
|
3812
3980
|
return g !== null && (g[0] & 65024) === 64512;
|
|
3813
3981
|
}
|
|
3814
|
-
function isPrivateHost(
|
|
3815
|
-
const h =
|
|
3982
|
+
function isPrivateHost(host2) {
|
|
3983
|
+
const h = host2.trim().toLowerCase().replace(/\.$/, "");
|
|
3816
3984
|
const m = classifySsrf(h);
|
|
3817
3985
|
if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
|
|
3818
3986
|
if (h === "localhost") return true;
|
|
3819
3987
|
if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
|
|
3820
3988
|
return isUniqueLocalV6(h);
|
|
3821
3989
|
}
|
|
3822
|
-
function canonicalHost(
|
|
3823
|
-
const ip = normalizeIpLiteral(
|
|
3824
|
-
return ip ??
|
|
3990
|
+
function canonicalHost(host2) {
|
|
3991
|
+
const ip = normalizeIpLiteral(host2);
|
|
3992
|
+
return ip ?? host2.trim().toLowerCase().replace(/\.$/, "");
|
|
3825
3993
|
}
|
|
3826
3994
|
function evaluateEgress(dests, policy) {
|
|
3827
3995
|
if (!policy.enabled) return null;
|
|
@@ -4045,9 +4213,9 @@ function bareToolName(toolName) {
|
|
|
4045
4213
|
const parts = toolName.split("__");
|
|
4046
4214
|
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
4047
4215
|
}
|
|
4048
|
-
function valuesAt(args,
|
|
4216
|
+
function valuesAt(args, path26) {
|
|
4049
4217
|
let cursors = [args];
|
|
4050
|
-
for (const rawSegment of
|
|
4218
|
+
for (const rawSegment of path26.split(".")) {
|
|
4051
4219
|
const isArray = rawSegment.endsWith("[]");
|
|
4052
4220
|
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
4053
4221
|
const next = [];
|
|
@@ -4079,10 +4247,10 @@ function extractToolDestinations(toolName, args) {
|
|
|
4079
4247
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4080
4248
|
if (!paths) return [];
|
|
4081
4249
|
const out = [];
|
|
4082
|
-
for (const
|
|
4083
|
-
for (const value of valuesAt(args,
|
|
4084
|
-
const
|
|
4085
|
-
if (
|
|
4250
|
+
for (const path26 of paths) {
|
|
4251
|
+
for (const value of valuesAt(args, path26)) {
|
|
4252
|
+
const host2 = hostOf(value);
|
|
4253
|
+
if (host2) out.push({ host: host2, binary: toolName, raw: value });
|
|
4086
4254
|
}
|
|
4087
4255
|
}
|
|
4088
4256
|
return out;
|
|
@@ -4094,15 +4262,15 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
4094
4262
|
try {
|
|
4095
4263
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4096
4264
|
if (!paths) return null;
|
|
4097
|
-
for (const
|
|
4098
|
-
for (const value of valuesAt(args,
|
|
4099
|
-
const
|
|
4100
|
-
if (!
|
|
4101
|
-
const m = classifySsrf(
|
|
4265
|
+
for (const path26 of paths) {
|
|
4266
|
+
for (const value of valuesAt(args, path26)) {
|
|
4267
|
+
const host2 = hostOf(value);
|
|
4268
|
+
if (!host2) continue;
|
|
4269
|
+
const m = classifySsrf(host2);
|
|
4102
4270
|
if (!m) continue;
|
|
4103
4271
|
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
4104
4272
|
if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
|
|
4105
|
-
return { ...m, argPath:
|
|
4273
|
+
return { ...m, argPath: path26, host: host2, reason: ssrfReason(m, host2) };
|
|
4106
4274
|
}
|
|
4107
4275
|
}
|
|
4108
4276
|
return null;
|
|
@@ -4110,6 +4278,9 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
4110
4278
|
return null;
|
|
4111
4279
|
}
|
|
4112
4280
|
}
|
|
4281
|
+
function newSecretViewBudget() {
|
|
4282
|
+
return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
|
|
4283
|
+
}
|
|
4113
4284
|
function textBytes(buf) {
|
|
4114
4285
|
if (buf.length < 16) return null;
|
|
4115
4286
|
const text = buf.toString("utf8");
|
|
@@ -4128,13 +4299,41 @@ function percentOnce(text) {
|
|
|
4128
4299
|
}
|
|
4129
4300
|
});
|
|
4130
4301
|
}
|
|
4302
|
+
function* wrappedBase64(text) {
|
|
4303
|
+
if (!text.includes("\n")) return;
|
|
4304
|
+
const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
|
|
4305
|
+
let run = [];
|
|
4306
|
+
let width = 0;
|
|
4307
|
+
const flush = function* () {
|
|
4308
|
+
if (run.length >= 2) yield run.join("");
|
|
4309
|
+
run = [];
|
|
4310
|
+
width = 0;
|
|
4311
|
+
};
|
|
4312
|
+
for (const raw of text.split("\n")) {
|
|
4313
|
+
const line = raw.trim();
|
|
4314
|
+
const full = line.length >= 16 && word.test(line) && !line.includes("=");
|
|
4315
|
+
if (full && (!run.length || line.length === width)) {
|
|
4316
|
+
run.push(line);
|
|
4317
|
+
width = line.length;
|
|
4318
|
+
continue;
|
|
4319
|
+
}
|
|
4320
|
+
if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
|
|
4321
|
+
run.push(line);
|
|
4322
|
+
yield* flush();
|
|
4323
|
+
continue;
|
|
4324
|
+
}
|
|
4325
|
+
yield* flush();
|
|
4326
|
+
if (full) {
|
|
4327
|
+
run.push(line);
|
|
4328
|
+
width = line.length;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
yield* flush();
|
|
4332
|
+
}
|
|
4131
4333
|
function* encodedRegions(text, budget) {
|
|
4132
4334
|
yield [text, false];
|
|
4335
|
+
for (const block of wrappedBase64(text)) yield [block, false];
|
|
4133
4336
|
for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
|
|
4134
|
-
if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
|
|
4135
|
-
budget.reasons.add("decode-candidates");
|
|
4136
|
-
return;
|
|
4137
|
-
}
|
|
4138
4337
|
try {
|
|
4139
4338
|
new URL(match[0]);
|
|
4140
4339
|
} catch {
|
|
@@ -4144,16 +4343,18 @@ function* encodedRegions(text, budget) {
|
|
|
4144
4343
|
/^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i
|
|
4145
4344
|
);
|
|
4146
4345
|
if (!parts) continue;
|
|
4147
|
-
const
|
|
4346
|
+
const host2 = parts[1].slice(parts[1].lastIndexOf("@") + 1).replace(/:\d+$/, "");
|
|
4148
4347
|
const regions = [
|
|
4149
4348
|
parts[1],
|
|
4150
|
-
...
|
|
4349
|
+
...host2.split("."),
|
|
4151
4350
|
...parts[2].split("/"),
|
|
4152
4351
|
...(parts[3] ?? "").split("&"),
|
|
4153
4352
|
parts[4] ?? ""
|
|
4154
4353
|
];
|
|
4155
4354
|
for (const region of regions) if (region) yield [region, false];
|
|
4156
|
-
const labels =
|
|
4355
|
+
const labels = host2.split(".");
|
|
4356
|
+
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))
|
|
4357
|
+
continue;
|
|
4157
4358
|
for (let start = 0; start < labels.length; start++) {
|
|
4158
4359
|
let joined = labels[start];
|
|
4159
4360
|
for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
|
|
@@ -4161,11 +4362,13 @@ function* encodedRegions(text, budget) {
|
|
|
4161
4362
|
if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
|
|
4162
4363
|
break;
|
|
4163
4364
|
joined += labels[end];
|
|
4164
|
-
if (
|
|
4165
|
-
|
|
4365
|
+
if (joined.length < 16) continue;
|
|
4366
|
+
budget.joinedLabelBytes += joined.length;
|
|
4367
|
+
if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
|
|
4368
|
+
budget.reasons.add("decode-labels");
|
|
4166
4369
|
return;
|
|
4167
4370
|
}
|
|
4168
|
-
|
|
4371
|
+
yield [joined, true];
|
|
4169
4372
|
}
|
|
4170
4373
|
}
|
|
4171
4374
|
}
|
|
@@ -4202,10 +4405,6 @@ function* decodedCandidates(text, budget) {
|
|
|
4202
4405
|
if (joined) yield ["dns-labels", region];
|
|
4203
4406
|
for (const [kind, re] of families) {
|
|
4204
4407
|
for (const match of region.matchAll(re)) {
|
|
4205
|
-
if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
|
|
4206
|
-
budget.reasons.add("decode-candidates");
|
|
4207
|
-
return;
|
|
4208
|
-
}
|
|
4209
4408
|
const raw = match[0];
|
|
4210
4409
|
let buf;
|
|
4211
4410
|
if (kind === "base64") {
|
|
@@ -4228,42 +4427,76 @@ function* decodedCandidates(text, budget) {
|
|
|
4228
4427
|
}
|
|
4229
4428
|
}
|
|
4230
4429
|
}
|
|
4231
|
-
function
|
|
4430
|
+
function secretViews(text, budget) {
|
|
4232
4431
|
const seen = /* @__PURE__ */ new Set([text]);
|
|
4233
|
-
const
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
for (const
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4432
|
+
const out = [];
|
|
4433
|
+
let frontier = [{ text, transforms: [] }];
|
|
4434
|
+
for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
|
|
4435
|
+
const next = [];
|
|
4436
|
+
for (const parent of frontier) {
|
|
4437
|
+
let perLeaf = 0;
|
|
4438
|
+
for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
|
|
4439
|
+
if (seen.has(decoded)) continue;
|
|
4440
|
+
seen.add(decoded);
|
|
4441
|
+
const normalizing = kind === "invisibles" || kind === "percent";
|
|
4442
|
+
if (depth >= 2 && !normalizing) {
|
|
4443
|
+
budget.nestedBytes += Buffer.byteLength(decoded);
|
|
4444
|
+
if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
|
|
4445
|
+
budget.reasons.add("decode-nested");
|
|
4446
|
+
return out;
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
const view = { text: decoded, transforms: [...parent.transforms, kind] };
|
|
4450
|
+
out.push(view);
|
|
4451
|
+
next.push(view);
|
|
4247
4452
|
}
|
|
4248
|
-
seen.add(decoded);
|
|
4249
|
-
budget.views++;
|
|
4250
|
-
budget.bytes += bytes;
|
|
4251
|
-
queue.push({ text: decoded, transforms: [...view.transforms, kind] });
|
|
4252
4453
|
}
|
|
4253
|
-
|
|
4454
|
+
frontier = next;
|
|
4455
|
+
if (!frontier.length) return out;
|
|
4456
|
+
}
|
|
4457
|
+
for (const view of frontier) {
|
|
4458
|
+
if (!decodedCandidates(view.text, budget).next().done) {
|
|
4459
|
+
budget.reasons.add("decode-nested");
|
|
4460
|
+
break;
|
|
4461
|
+
}
|
|
4254
4462
|
}
|
|
4463
|
+
return out;
|
|
4464
|
+
}
|
|
4465
|
+
function batches(views) {
|
|
4466
|
+
const out = [];
|
|
4467
|
+
let current = [];
|
|
4468
|
+
let size = 0;
|
|
4469
|
+
for (const { text } of views) {
|
|
4470
|
+
if (current.length && size + text.length > SECRET_VIEW_LIMITS.batchCharacters) {
|
|
4471
|
+
out.push(current);
|
|
4472
|
+
current = [];
|
|
4473
|
+
size = 0;
|
|
4474
|
+
}
|
|
4475
|
+
current.push(text);
|
|
4476
|
+
size += text.length;
|
|
4477
|
+
}
|
|
4478
|
+
if (current.length) out.push(current);
|
|
4479
|
+
return out;
|
|
4255
4480
|
}
|
|
4256
4481
|
function inspectArgumentSecrets(args) {
|
|
4257
|
-
const budget =
|
|
4482
|
+
const budget = newSecretViewBudget();
|
|
4258
4483
|
const ancestors = /* @__PURE__ */ new Set();
|
|
4259
4484
|
let nodes = 0;
|
|
4260
4485
|
let sourceBytes = 0;
|
|
4261
4486
|
let tailBytes = 0;
|
|
4262
4487
|
let best = null;
|
|
4488
|
+
const blocked = () => best?.severity === "block";
|
|
4263
4489
|
const accept = (match) => {
|
|
4264
4490
|
if (match && (!best || best.severity !== "block" && match.severity === "block")) best = match;
|
|
4265
4491
|
};
|
|
4492
|
+
const decodeSegment = (segment, fieldPath) => {
|
|
4493
|
+
for (const batch of batches(secretViews(segment, budget))) {
|
|
4494
|
+
accept(matchArgumentBatch(batch, fieldPath, budget));
|
|
4495
|
+
if (blocked()) return;
|
|
4496
|
+
}
|
|
4497
|
+
};
|
|
4266
4498
|
const visit = (value, depth, fieldPath) => {
|
|
4499
|
+
if (blocked()) return;
|
|
4267
4500
|
if (++nodes > ARGUMENT_SCAN_LIMITS.maxNodes) {
|
|
4268
4501
|
budget.reasons.add("argument-nodes");
|
|
4269
4502
|
return;
|
|
@@ -4305,14 +4538,11 @@ function inspectArgumentSecrets(args) {
|
|
|
4305
4538
|
}
|
|
4306
4539
|
sourceBytes += Math.min(bytes, Math.max(0, remaining));
|
|
4307
4540
|
if (!text) return;
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
if (hit)
|
|
4311
|
-
accept(view.transforms.length ? { ...hit, redactedSample: "[encoded secret]" } : hit);
|
|
4312
|
-
}
|
|
4541
|
+
accept(matchArgumentText(text, fieldPath, budget));
|
|
4542
|
+
let scannedEnd = text.length;
|
|
4313
4543
|
if (!truncated && value.length > window) {
|
|
4314
|
-
budget.reasons.add("decode-window");
|
|
4315
4544
|
for (let start = window - overlap; start < value.length; start += window - overlap) {
|
|
4545
|
+
if (blocked()) return;
|
|
4316
4546
|
const chunk = value.slice(start, start + window);
|
|
4317
4547
|
const chunkBytes = Buffer.byteLength(chunk);
|
|
4318
4548
|
if (tailBytes + chunkBytes > ARGUMENT_SCAN_LIMITS.maxTailBytes) {
|
|
@@ -4321,9 +4551,16 @@ function inspectArgumentSecrets(args) {
|
|
|
4321
4551
|
}
|
|
4322
4552
|
tailBytes += chunkBytes;
|
|
4323
4553
|
accept(matchArgumentText(chunk, fieldPath, budget));
|
|
4554
|
+
scannedEnd = start + chunk.length;
|
|
4324
4555
|
if (start + window >= value.length) break;
|
|
4325
4556
|
}
|
|
4326
4557
|
}
|
|
4558
|
+
for (let start = 0; start < scannedEnd && !blocked(); ) {
|
|
4559
|
+
let end = Math.min(start + window, scannedEnd);
|
|
4560
|
+
while (end < scannedEnd && runChar(value, end - 1) && runChar(value, end)) end++;
|
|
4561
|
+
decodeSegment(value.slice(start, end), fieldPath);
|
|
4562
|
+
start = end;
|
|
4563
|
+
}
|
|
4327
4564
|
const trimmed = text.trim();
|
|
4328
4565
|
if (value.length < ARGUMENT_SCAN_LIMITS.maxJsonCharacters && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
|
|
4329
4566
|
try {
|
|
@@ -4342,7 +4579,7 @@ function inspectArgumentSecrets(args) {
|
|
|
4342
4579
|
function scanArgumentSecrets(args) {
|
|
4343
4580
|
const result = inspectArgumentSecrets(args);
|
|
4344
4581
|
if (result.match) return result.match;
|
|
4345
|
-
return result.
|
|
4582
|
+
return result.coverage === "incomplete" ? {
|
|
4346
4583
|
patternName: "Argument inspection incomplete",
|
|
4347
4584
|
fieldPath: "args",
|
|
4348
4585
|
redactedSample: "[uninspected content]",
|
|
@@ -4370,18 +4607,13 @@ function tokenize2(toolName) {
|
|
|
4370
4607
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
4371
4608
|
}
|
|
4372
4609
|
function extractShellCommand(toolName, args, toolInspection) {
|
|
4373
|
-
const
|
|
4374
|
-
|
|
4375
|
-
if (!matchingPattern) return null;
|
|
4376
|
-
const fieldPath = toolInspection[matchingPattern];
|
|
4610
|
+
const fieldPath = inspectionEntry(toolName, toolInspection);
|
|
4611
|
+
if (fieldPath === void 0) return null;
|
|
4377
4612
|
const value = getNestedValue(args, typeof fieldPath === "string" ? fieldPath : fieldPath.field);
|
|
4378
4613
|
return typeof value === "string" ? value : null;
|
|
4379
4614
|
}
|
|
4380
4615
|
function isSqlTool(toolName, toolInspection) {
|
|
4381
|
-
const
|
|
4382
|
-
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
4383
|
-
if (!matchingPattern) return false;
|
|
4384
|
-
const fieldName = toolInspection[matchingPattern];
|
|
4616
|
+
const fieldName = inspectionEntry(toolName, toolInspection);
|
|
4385
4617
|
return fieldName === "sql" || fieldName === "query";
|
|
4386
4618
|
}
|
|
4387
4619
|
function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
@@ -4389,7 +4621,7 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
|
4389
4621
|
if (!pipeAnalysis.isPipeline) return null;
|
|
4390
4622
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
4391
4623
|
const sinks = pipeAnalysis.sinkTargets;
|
|
4392
|
-
const allTrusted = sinks.length > 0 && sinks.every((
|
|
4624
|
+
const allTrusted = sinks.length > 0 && sinks.every((host2) => isTrustedHost2 ? isTrustedHost2(host2) : false);
|
|
4393
4625
|
if (pipeAnalysis.risk === "critical") {
|
|
4394
4626
|
if (allTrusted) {
|
|
4395
4627
|
return {
|
|
@@ -4448,9 +4680,10 @@ function egressPolicyVerdict(eg) {
|
|
|
4448
4680
|
}
|
|
4449
4681
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
4450
4682
|
let protectedVerdict = null;
|
|
4683
|
+
const layers = inspectionLayers(config.policy);
|
|
4451
4684
|
if (context.agent !== "Terminal") {
|
|
4452
4685
|
try {
|
|
4453
|
-
const accesses = context.accesses ?? collectFileAccesses(toolName, args, context,
|
|
4686
|
+
const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, layers);
|
|
4454
4687
|
protectedVerdict = evaluateProtectedFileAccess(accesses, context, catalogSettings(config));
|
|
4455
4688
|
} catch {
|
|
4456
4689
|
protectedVerdict = incompleteFileVerdict();
|
|
@@ -4485,7 +4718,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4485
4718
|
hooks
|
|
4486
4719
|
);
|
|
4487
4720
|
if (result.decision === "block") return result;
|
|
4488
|
-
if (context.agent !== "Terminal" && resolveShellInput(toolName, args,
|
|
4721
|
+
if (context.agent !== "Terminal" && resolveShellInput(toolName, args, layers).kind === "uninspectable") {
|
|
4489
4722
|
return {
|
|
4490
4723
|
decision: "review",
|
|
4491
4724
|
ruleName: "shell-inspection-incomplete",
|
|
@@ -4501,13 +4734,14 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4501
4734
|
const { agent, cwd, activeEnvironment } = context;
|
|
4502
4735
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
4503
4736
|
const settingsView = catalogSettings(config);
|
|
4737
|
+
const layers = inspectionLayers(config.policy);
|
|
4504
4738
|
const cv = (id) => checkValue(settingsView, id);
|
|
4505
4739
|
let loggedFinding;
|
|
4506
4740
|
const noteLogged = (v) => {
|
|
4507
4741
|
if (v?.logged && !loggedFinding) loggedFinding = v;
|
|
4508
4742
|
};
|
|
4509
4743
|
const allowed = () => loggedFinding ?? { decision: "allow" };
|
|
4510
|
-
const shellInput = resolveShellInput(toolName, args,
|
|
4744
|
+
const shellInput = resolveShellInput(toolName, args, layers);
|
|
4511
4745
|
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
4512
4746
|
{
|
|
4513
4747
|
const dest = ssrfDestinationFloor(toolName, args, {
|
|
@@ -4612,13 +4846,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4612
4846
|
return { id, value: cv(id) };
|
|
4613
4847
|
};
|
|
4614
4848
|
const matches = config.policy.smartRules.filter((rule) => {
|
|
4615
|
-
if (!toolMatchesRule(toolName, rule.tool,
|
|
4849
|
+
if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
|
|
4616
4850
|
if (astSuppressed(rule)) return false;
|
|
4617
4851
|
if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
|
|
4618
|
-
if (!evaluateSmartConditions(
|
|
4619
|
-
shellRuleArgs(toolName, args, rule.tool, config.policy.toolInspection),
|
|
4620
|
-
rule
|
|
4621
|
-
))
|
|
4852
|
+
if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
|
|
4622
4853
|
return false;
|
|
4623
4854
|
const check = ruleCheck(rule);
|
|
4624
4855
|
if (check?.value === "off") return false;
|
|
@@ -4665,7 +4896,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4665
4896
|
let allTokens = [];
|
|
4666
4897
|
let pathTokens = [];
|
|
4667
4898
|
if (pendingToolEgress) return pendingToolEgress;
|
|
4668
|
-
const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args,
|
|
4899
|
+
const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, layers) : null;
|
|
4669
4900
|
if (shellCommand) {
|
|
4670
4901
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
4671
4902
|
allTokens = analyzed.allTokens;
|
|
@@ -4742,7 +4973,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4742
4973
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
4743
4974
|
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
4744
4975
|
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
4745
|
-
allTokens.
|
|
4976
|
+
allTokens = allTokens.concat(sshHosts);
|
|
4746
4977
|
}
|
|
4747
4978
|
if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
|
|
4748
4979
|
const prov = checkProvenance2(firstToken, cwd);
|
|
@@ -4771,7 +5002,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4771
5002
|
};
|
|
4772
5003
|
}
|
|
4773
5004
|
}
|
|
4774
|
-
if (isSqlTool(toolName,
|
|
5005
|
+
if (isSqlTool(toolName, layers)) {
|
|
4775
5006
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
4776
5007
|
}
|
|
4777
5008
|
} else {
|
|
@@ -4779,7 +5010,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4779
5010
|
if (args && typeof args === "object") {
|
|
4780
5011
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
4781
5012
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
4782
|
-
allTokens.
|
|
5013
|
+
allTokens = allTokens.concat(extraTokens);
|
|
4783
5014
|
}
|
|
4784
5015
|
}
|
|
4785
5016
|
const isManual = agent === "Terminal";
|
|
@@ -4866,6 +5097,487 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
4866
5097
|
function isIgnoredTool(toolName, config) {
|
|
4867
5098
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
4868
5099
|
}
|
|
5100
|
+
function normalizeHost(raw) {
|
|
5101
|
+
let h = raw.trim().toLowerCase();
|
|
5102
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
5103
|
+
if (!h || h === "localhost") return null;
|
|
5104
|
+
if (/^[0-9a-f:]+$/.test(h) && h.includes(":")) return h;
|
|
5105
|
+
if (!h.includes(".") || !/^[\p{L}\p{N}.-]+$/u.test(h)) return null;
|
|
5106
|
+
return h;
|
|
5107
|
+
}
|
|
5108
|
+
function hostOfUrl(value) {
|
|
5109
|
+
const v = value.replace(/[.,;:!?)\]}>'"`]+$/, "");
|
|
5110
|
+
try {
|
|
5111
|
+
const u = new URL(v.toLowerCase().startsWith("www.") ? `http://${v}` : v);
|
|
5112
|
+
return normalizeHost(u.hostname);
|
|
5113
|
+
} catch {
|
|
5114
|
+
return null;
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
function addIbans(text, out) {
|
|
5118
|
+
for (const m of text.toUpperCase().matchAll(IBAN_RE)) {
|
|
5119
|
+
const compact = m[0].replace(/[ -]/g, "");
|
|
5120
|
+
if (validateIban(compact)) out.add(`iban:${compact}`);
|
|
5121
|
+
else if (/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(compact)) out.add(`account:${compact}`);
|
|
5122
|
+
if (out.size >= MAX_DESTINATIONS) return;
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
function addEmails(text, out) {
|
|
5126
|
+
for (const m of text.matchAll(EMAIL_RE)) {
|
|
5127
|
+
out.add(`email:${m[0].toLowerCase()}`);
|
|
5128
|
+
if (out.size >= MAX_DESTINATIONS) return;
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
function addUrlHosts(text, out) {
|
|
5132
|
+
for (const m of text.matchAll(URL_RE)) {
|
|
5133
|
+
const h = hostOfUrl(m[0]);
|
|
5134
|
+
if (h) out.add(`host:${h}`);
|
|
5135
|
+
if (out.size >= MAX_DESTINATIONS) return;
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
function extractContentDestinations(text) {
|
|
5139
|
+
const out = /* @__PURE__ */ new Set();
|
|
5140
|
+
try {
|
|
5141
|
+
const t = text.length > MAX_SCAN_CHARS ? text.slice(0, MAX_SCAN_CHARS) : text;
|
|
5142
|
+
if (t.includes("@")) addEmails(t, out);
|
|
5143
|
+
if (out.size < MAX_DESTINATIONS) addUrlHosts(t, out);
|
|
5144
|
+
if (out.size < MAX_DESTINATIONS) addIbans(t, out);
|
|
5145
|
+
} catch {
|
|
5146
|
+
}
|
|
5147
|
+
return [...out];
|
|
5148
|
+
}
|
|
5149
|
+
function extractArgDestinations(args) {
|
|
5150
|
+
const out = /* @__PURE__ */ new Set();
|
|
5151
|
+
try {
|
|
5152
|
+
walk(args, void 0, out, 0);
|
|
5153
|
+
} catch {
|
|
5154
|
+
}
|
|
5155
|
+
return [...out];
|
|
5156
|
+
}
|
|
5157
|
+
function walk(value, key, out, depth) {
|
|
5158
|
+
if (out.size >= MAX_DESTINATIONS || depth > 12) return;
|
|
5159
|
+
if (typeof value === "string") {
|
|
5160
|
+
const s = value.length > MAX_SCAN_CHARS ? value.slice(0, MAX_SCAN_CHARS) : value;
|
|
5161
|
+
if (s.includes("@")) addEmails(s, out);
|
|
5162
|
+
addUrlHosts(s, out);
|
|
5163
|
+
addIbans(s, out);
|
|
5164
|
+
if (key && RECIPIENT_KEYS.has(key.toLowerCase()) && !/[\s@]/.test(s)) {
|
|
5165
|
+
const h = normalizeHost(
|
|
5166
|
+
s.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/^\/\//, "").split(/[/?#]/)[0].replace(/:\d+$/, "")
|
|
5167
|
+
);
|
|
5168
|
+
if (h && (h.includes(":") || /\.\p{L}{2,}$/u.test(h))) out.add(`host:${h}`);
|
|
5169
|
+
const acct = s.replace(/[ .-]/g, "").toUpperCase();
|
|
5170
|
+
if (/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(acct) && !validateIban(acct))
|
|
5171
|
+
out.add(`account:${acct}`);
|
|
5172
|
+
}
|
|
5173
|
+
return;
|
|
5174
|
+
}
|
|
5175
|
+
if (Array.isArray(value)) {
|
|
5176
|
+
for (const v of value) walk(v, key, out, depth + 1);
|
|
5177
|
+
return;
|
|
5178
|
+
}
|
|
5179
|
+
if (value && typeof value === "object") {
|
|
5180
|
+
for (const [k, v] of Object.entries(value))
|
|
5181
|
+
walk(v, k, out, depth + 1);
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
function destinationDomain(dest) {
|
|
5185
|
+
if (!dest.startsWith("email:")) return null;
|
|
5186
|
+
const at = dest.lastIndexOf("@");
|
|
5187
|
+
return at > 0 ? dest.slice(at + 1) : null;
|
|
5188
|
+
}
|
|
5189
|
+
function destinationKind(dest) {
|
|
5190
|
+
const i = dest.indexOf(":");
|
|
5191
|
+
const k = i > 0 ? dest.slice(0, i) : "";
|
|
5192
|
+
return k === "email" || k === "host" || k === "iban" || k === "account" ? k : null;
|
|
5193
|
+
}
|
|
5194
|
+
function destinationValue(dest) {
|
|
5195
|
+
const i = dest.indexOf(":");
|
|
5196
|
+
return i > 0 ? dest.slice(i + 1) : dest;
|
|
5197
|
+
}
|
|
5198
|
+
function isLoopbackHost(host2) {
|
|
5199
|
+
let h = host2.trim().toLowerCase();
|
|
5200
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
5201
|
+
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";
|
|
5202
|
+
}
|
|
5203
|
+
function gitRepoPath(url) {
|
|
5204
|
+
const u = url.trim();
|
|
5205
|
+
if (!u) return null;
|
|
5206
|
+
let host2;
|
|
5207
|
+
let p;
|
|
5208
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
5209
|
+
try {
|
|
5210
|
+
const parsed = new URL(u);
|
|
5211
|
+
host2 = parsed.hostname.toLowerCase();
|
|
5212
|
+
p = decodeURIComponent(parsed.pathname);
|
|
5213
|
+
} catch {
|
|
5214
|
+
return null;
|
|
5215
|
+
}
|
|
5216
|
+
} else {
|
|
5217
|
+
const scp = /^(?:[^@/\s]+@)?([A-Za-z0-9.-]+):(?!\/\/)(.+)$/.exec(u);
|
|
5218
|
+
if (!scp || !scp[1].includes(".")) return null;
|
|
5219
|
+
host2 = scp[1].toLowerCase();
|
|
5220
|
+
p = scp[2];
|
|
5221
|
+
}
|
|
5222
|
+
let start = 0;
|
|
5223
|
+
let end = p.length;
|
|
5224
|
+
while (start < end && p[start] === "/") start++;
|
|
5225
|
+
while (end > start && p[end - 1] === "/") end--;
|
|
5226
|
+
const tail = p.slice(start, end).replace(/\.git$/i, "").toLowerCase();
|
|
5227
|
+
if (!host2 || !tail.includes("/")) return null;
|
|
5228
|
+
return `${host2}/${tail}`;
|
|
5229
|
+
}
|
|
5230
|
+
function isDestructiveToolName(bareTool) {
|
|
5231
|
+
return DESTRUCTIVE_VERB_RE.test(bareTool);
|
|
5232
|
+
}
|
|
5233
|
+
function host(token) {
|
|
5234
|
+
if (!token) return null;
|
|
5235
|
+
const h = parseDestHost(token);
|
|
5236
|
+
return h ? `host:${h}` : null;
|
|
5237
|
+
}
|
|
5238
|
+
function ghRepoArg(v, defaultHost) {
|
|
5239
|
+
if (/:\/\/|@/.test(v)) {
|
|
5240
|
+
const r = gitRepoPath(v);
|
|
5241
|
+
if (r) return { host: r.split("/")[0], repo: r };
|
|
5242
|
+
}
|
|
5243
|
+
const parts = v.toLowerCase().replace(/\.git$/, "").split("/").filter(Boolean);
|
|
5244
|
+
if (parts.length === 3) return { host: parts[0], repo: parts.join("/") };
|
|
5245
|
+
if (parts.length === 2) return { host: defaultHost, repo: `${defaultHost}/${parts.join("/")}` };
|
|
5246
|
+
return { host: defaultHost, repo: `${defaultHost}/*` };
|
|
5247
|
+
}
|
|
5248
|
+
function ghApiRepo(endpoint, ghHost) {
|
|
5249
|
+
if (endpoint === null) return `${ghHost}/*`;
|
|
5250
|
+
const e = endpoint.replace(/^https?:\/\/[^/]+\//i, "").replace(/^\/+/, "");
|
|
5251
|
+
const m = /^repos\/([^/]+)\/([^/?#]+)/i.exec(e);
|
|
5252
|
+
if (!m) return `${ghHost}/*`;
|
|
5253
|
+
if (m[1] === "{owner}" && m[2] === "{repo}") return void 0;
|
|
5254
|
+
return `${ghHost}/${m[1].toLowerCase()}/${m[2].toLowerCase()}`;
|
|
5255
|
+
}
|
|
5256
|
+
function sendToGitUrl(url, out) {
|
|
5257
|
+
out.sends = true;
|
|
5258
|
+
const h = typeof url === "string" ? host(url) : null;
|
|
5259
|
+
if (!url || !h) {
|
|
5260
|
+
out.unresolved = true;
|
|
5261
|
+
return;
|
|
5262
|
+
}
|
|
5263
|
+
out.destinations.push(h);
|
|
5264
|
+
const r = gitRepoPath(url);
|
|
5265
|
+
if (r) out.repos.push(r);
|
|
5266
|
+
}
|
|
5267
|
+
function callWords(call) {
|
|
5268
|
+
const raw = call.Args || [];
|
|
5269
|
+
if (raw.length === 0) return null;
|
|
5270
|
+
const words = raw.map((a) => resolveWordLiteral(a));
|
|
5271
|
+
const prefixes = raw.map((a) => resolveWordPrefix(a).prefix);
|
|
5272
|
+
const first = (words[0] || "").toLowerCase().split("/").pop() ?? "";
|
|
5273
|
+
const at = OWN_HEADS.has(first) ? 0 : unwrapCommandHead(words);
|
|
5274
|
+
const head = (words[at] || "").toLowerCase().split("/").pop() ?? "";
|
|
5275
|
+
if (!head) return null;
|
|
5276
|
+
const assigns = call.Assigns || [];
|
|
5277
|
+
const env = [
|
|
5278
|
+
...assigns.map((a) => String(a?.Name?.Value ?? "")),
|
|
5279
|
+
...words.slice(0, at).map((w) => w === null ? "" : /^([A-Za-z_][A-Za-z0-9_]*)=/.exec(w)?.[1] ?? "")
|
|
5280
|
+
].filter(Boolean);
|
|
5281
|
+
return { head, args: words.slice(at + 1), prefixes: prefixes.slice(at + 1), env };
|
|
5282
|
+
}
|
|
5283
|
+
function flagValue(args, names) {
|
|
5284
|
+
for (let i = 0; i < args.length; i++) {
|
|
5285
|
+
const w = args[i];
|
|
5286
|
+
if (w === null) continue;
|
|
5287
|
+
for (const n of names) {
|
|
5288
|
+
if (w === n && typeof args[i + 1] === "string") return args[i + 1];
|
|
5289
|
+
if (w.startsWith(`${n}=`)) return w.slice(n.length + 1);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
return null;
|
|
5293
|
+
}
|
|
5294
|
+
function positionals(args, valueFlags) {
|
|
5295
|
+
const out = [];
|
|
5296
|
+
for (let i = 0; i < args.length; i++) {
|
|
5297
|
+
const w = args[i];
|
|
5298
|
+
if (w === null) {
|
|
5299
|
+
out.push(null);
|
|
5300
|
+
continue;
|
|
5301
|
+
}
|
|
5302
|
+
if (w === "--") {
|
|
5303
|
+
out.push(...args.slice(i + 1));
|
|
5304
|
+
break;
|
|
5305
|
+
}
|
|
5306
|
+
if (w.startsWith("-")) {
|
|
5307
|
+
if (!w.includes("=") && valueFlags.has(w)) i++;
|
|
5308
|
+
continue;
|
|
5309
|
+
}
|
|
5310
|
+
out.push(w);
|
|
5311
|
+
}
|
|
5312
|
+
return out;
|
|
5313
|
+
}
|
|
5314
|
+
function classifyCall(head, args, prefixes, env, state, out) {
|
|
5315
|
+
const lower2 = args.map((a) => a === null ? null : a.toLowerCase());
|
|
5316
|
+
switch (head) {
|
|
5317
|
+
case "curl":
|
|
5318
|
+
case "wget":
|
|
5319
|
+
case "iwr":
|
|
5320
|
+
case "invoke-webrequest":
|
|
5321
|
+
case "irm":
|
|
5322
|
+
case "invoke-restmethod": {
|
|
5323
|
+
const binary = head === "wget" ? "wget" : "curl";
|
|
5324
|
+
const sendFlags = head === "wget" ? WGET_SEND_FLAGS : head === "curl" ? CURL_SEND_FLAGS : IWR_SEND_FLAGS;
|
|
5325
|
+
let sends = false;
|
|
5326
|
+
for (let i = 0; i < args.length; i++) {
|
|
5327
|
+
const w = args[i];
|
|
5328
|
+
if (w === null) continue;
|
|
5329
|
+
const name = (head === "curl" || head === "wget" ? w : w.toLowerCase()).split("=")[0];
|
|
5330
|
+
if (sendFlags.has(name)) sends = true;
|
|
5331
|
+
const next = lower2[i + 1];
|
|
5332
|
+
if ((name === "-X" || name === "--request" || name === "-method") && next) {
|
|
5333
|
+
if (WRITE_METHODS.has(next)) sends = true;
|
|
5334
|
+
}
|
|
5335
|
+
if (/^(?:--request=|-X)(POST|PUT|PATCH|DELETE)$/i.test(w)) sends = true;
|
|
5336
|
+
if (name === "--method" && next && WRITE_METHODS.has(next)) sends = true;
|
|
5337
|
+
}
|
|
5338
|
+
const withPrefixes = args.map(
|
|
5339
|
+
(a, i) => a === null && /^[a-z][a-z0-9+.-]*:\/\//i.test(prefixes[i] ?? "") ? prefixes[i] : a
|
|
5340
|
+
);
|
|
5341
|
+
const dests = destTokensForBinary(binary, withPrefixes).map((t) => host(t)).filter((h) => h !== null);
|
|
5342
|
+
out.fetches = true;
|
|
5343
|
+
out.fetchKinds.push(head);
|
|
5344
|
+
out.fetchDestinations.push(...dests);
|
|
5345
|
+
if (sends) {
|
|
5346
|
+
out.sends = true;
|
|
5347
|
+
out.destinations.push(...dests);
|
|
5348
|
+
}
|
|
5349
|
+
return;
|
|
5350
|
+
}
|
|
5351
|
+
case "scp":
|
|
5352
|
+
case "rsync": {
|
|
5353
|
+
const pos = positionals(args, VALUE_FLAGS[head] ?? /* @__PURE__ */ new Set()).filter(
|
|
5354
|
+
(p) => p !== null
|
|
5355
|
+
);
|
|
5356
|
+
if (pos.length < 2) return;
|
|
5357
|
+
const remote = (p) => /^[a-z][a-z0-9+.-]*:\/\//i.test(p) || /^[^/]*[^/:]:[^/]/.test(p) || p.includes("@");
|
|
5358
|
+
const target = pos[pos.length - 1];
|
|
5359
|
+
const sources = pos.slice(0, -1);
|
|
5360
|
+
if (remote(target)) {
|
|
5361
|
+
out.sends = true;
|
|
5362
|
+
const h = host(target);
|
|
5363
|
+
if (h) out.destinations.push(h);
|
|
5364
|
+
}
|
|
5365
|
+
if (sources.some(remote)) {
|
|
5366
|
+
out.fetches = true;
|
|
5367
|
+
out.fetchKinds.push(head);
|
|
5368
|
+
for (const src of sources.filter(remote)) {
|
|
5369
|
+
const h = host(src);
|
|
5370
|
+
if (h) out.fetchDestinations.push(h);
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
return;
|
|
5374
|
+
}
|
|
5375
|
+
case "nc":
|
|
5376
|
+
case "ncat":
|
|
5377
|
+
case "netcat":
|
|
5378
|
+
case "ssh": {
|
|
5379
|
+
out.fetches = true;
|
|
5380
|
+
out.sends = true;
|
|
5381
|
+
out.fetchKinds.push(head);
|
|
5382
|
+
for (const t of destTokensForBinary(head, args)) {
|
|
5383
|
+
const h = host(t);
|
|
5384
|
+
if (h) {
|
|
5385
|
+
out.destinations.push(h);
|
|
5386
|
+
out.fetchDestinations.push(h);
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
return;
|
|
5390
|
+
}
|
|
5391
|
+
case "gh": {
|
|
5392
|
+
const pos = positionals(args, GH_HOSTNAME_FLAGS);
|
|
5393
|
+
const group = pos[0] ?? null;
|
|
5394
|
+
const verb = pos[1] ?? null;
|
|
5395
|
+
const hn = flagValue(args, ["--hostname"]);
|
|
5396
|
+
let dest = host(hn ?? "github.com") ?? "host:github.com";
|
|
5397
|
+
const ghHost = dest.slice("host:".length);
|
|
5398
|
+
const envRepo = env.some((n) => n === "GH_REPO" || n === "GH_HOST");
|
|
5399
|
+
if (group === "api") {
|
|
5400
|
+
const method = (flagValue(args, ["-X", "--method"]) ?? "get").toLowerCase();
|
|
5401
|
+
const hasBody = args.some(
|
|
5402
|
+
(w) => w !== null && /^(-f|--raw-field|-F|--field|--input)(=|$)/.test(w)
|
|
5403
|
+
);
|
|
5404
|
+
if (WRITE_METHODS.has(method) || hasBody) {
|
|
5405
|
+
out.sends = true;
|
|
5406
|
+
out.destinations.push(dest);
|
|
5407
|
+
const endpoint = positionals(args, GH_API_VALUE_FLAGS)[1] ?? null;
|
|
5408
|
+
const repo = ghApiRepo(endpoint, ghHost);
|
|
5409
|
+
if (repo !== void 0) out.repos.push(repo);
|
|
5410
|
+
else if (envRepo) out.repos.push(`${ghHost}/*`);
|
|
5411
|
+
} else {
|
|
5412
|
+
out.fetches = true;
|
|
5413
|
+
out.fetchKinds.push("gh api");
|
|
5414
|
+
out.fetchDestinations.push(dest);
|
|
5415
|
+
}
|
|
5416
|
+
return;
|
|
5417
|
+
}
|
|
5418
|
+
if (group === null || verb === null) return;
|
|
5419
|
+
if (GH_WRITE.has(verb)) {
|
|
5420
|
+
out.sends = true;
|
|
5421
|
+
const rIdx = args.findIndex((w) => w === "-R" || w === "--repo");
|
|
5422
|
+
const rEq = args.find((w) => w !== null && /^(-R|--repo)=/.test(w));
|
|
5423
|
+
const rVal = rEq !== void 0 && rEq !== null ? rEq.slice(rEq.indexOf("=") + 1) : rIdx >= 0 ? args[rIdx + 1] : void 0;
|
|
5424
|
+
let repo;
|
|
5425
|
+
if (rIdx >= 0 || rEq !== void 0) {
|
|
5426
|
+
if (typeof rVal === "string") {
|
|
5427
|
+
const named = ghRepoArg(rVal, ghHost);
|
|
5428
|
+
repo = named.repo;
|
|
5429
|
+
if (named.host !== ghHost) dest = `host:${named.host}`;
|
|
5430
|
+
} else repo = `${ghHost}/*`;
|
|
5431
|
+
} else if (group === "gist" || group === "repo" && (verb === "create" || verb === "fork")) {
|
|
5432
|
+
repo = `${ghHost}/*`;
|
|
5433
|
+
} else if (group === "repo" && verb !== "rename") {
|
|
5434
|
+
const operand = positionals(args, GH_REPO_VALUE_FLAGS)[2];
|
|
5435
|
+
if (operand === null) repo = `${ghHost}/*`;
|
|
5436
|
+
else if (operand !== void 0) repo = ghRepoArg(operand, ghHost).repo;
|
|
5437
|
+
else if (envRepo) repo = `${ghHost}/*`;
|
|
5438
|
+
} else if (envRepo) {
|
|
5439
|
+
repo = `${ghHost}/*`;
|
|
5440
|
+
}
|
|
5441
|
+
out.destinations.push(dest);
|
|
5442
|
+
if (repo !== void 0) out.repos.push(repo);
|
|
5443
|
+
} else if (GH_READ.has(verb)) {
|
|
5444
|
+
out.fetches = true;
|
|
5445
|
+
out.fetchKinds.push(`gh ${group}`);
|
|
5446
|
+
out.fetchDestinations.push(dest);
|
|
5447
|
+
}
|
|
5448
|
+
return;
|
|
5449
|
+
}
|
|
5450
|
+
case "git": {
|
|
5451
|
+
const pos = positionals(args, /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree"]));
|
|
5452
|
+
const verb = pos[0];
|
|
5453
|
+
if (verb === null || verb === void 0) return;
|
|
5454
|
+
for (let i = 0; i < args.length; i++) {
|
|
5455
|
+
if (args[i] !== "-c") continue;
|
|
5456
|
+
const v = args[i + 1];
|
|
5457
|
+
if (v === null || v === void 0) {
|
|
5458
|
+
if (verb === "push") out.unresolved = true;
|
|
5459
|
+
continue;
|
|
5460
|
+
}
|
|
5461
|
+
const m = /^remote\.[^=]+\.(?:push)?url=(.*)$/i.exec(v);
|
|
5462
|
+
if (m) sendToGitUrl(m[1], out);
|
|
5463
|
+
}
|
|
5464
|
+
if (verb === "remote") {
|
|
5465
|
+
const rp = positionals(args, /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree", "-t", "-m"]));
|
|
5466
|
+
const sub = rp[1];
|
|
5467
|
+
if (sub === "add" || sub === "set-url") {
|
|
5468
|
+
const url = rp.slice(2).filter((p) => p !== "--push" && p !== "--add")[1];
|
|
5469
|
+
sendToGitUrl(url, out);
|
|
5470
|
+
}
|
|
5471
|
+
return;
|
|
5472
|
+
}
|
|
5473
|
+
if (verb === "config") {
|
|
5474
|
+
const reads = args.some(
|
|
5475
|
+
(w) => w !== null && /^--(get|get-all|get-regexp|unset|unset-all|list|get-urlmatch)$/.test(w)
|
|
5476
|
+
);
|
|
5477
|
+
const k = pos.findIndex((p) => p !== null && /^remote\..+\.(?:push)?url$/i.test(p));
|
|
5478
|
+
if (k >= 0 && k + 1 < pos.length && !reads) sendToGitUrl(pos[k + 1], out);
|
|
5479
|
+
return;
|
|
5480
|
+
}
|
|
5481
|
+
if (GIT_FETCH.has(verb)) {
|
|
5482
|
+
out.fetches = true;
|
|
5483
|
+
out.fetchKinds.push(`git ${verb}`);
|
|
5484
|
+
for (const p of pos.slice(1)) {
|
|
5485
|
+
const h = p !== null && /:\/\/|@/.test(p) ? host(p) : null;
|
|
5486
|
+
if (h) out.fetchDestinations.push(h);
|
|
5487
|
+
}
|
|
5488
|
+
return;
|
|
5489
|
+
}
|
|
5490
|
+
if (verb === "push") {
|
|
5491
|
+
if (pos.length > 1 && pos[1] === null) {
|
|
5492
|
+
out.sends = true;
|
|
5493
|
+
out.unresolved = true;
|
|
5494
|
+
return;
|
|
5495
|
+
}
|
|
5496
|
+
const target = flagValue(args, ["--repo"]) ?? pos.slice(1).find((p) => p !== null && !p.startsWith("-")) ?? null;
|
|
5497
|
+
if (target !== null && (/:\/\/|@/.test(target) || /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+:/.test(target))) {
|
|
5498
|
+
sendToGitUrl(target, out);
|
|
5499
|
+
return;
|
|
5500
|
+
}
|
|
5501
|
+
if (target !== null && /^[./~]|\//.test(target)) return;
|
|
5502
|
+
out.sends = true;
|
|
5503
|
+
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");
|
|
5504
|
+
if (elsewhere) out.unresolved = true;
|
|
5505
|
+
else out.gitRemotes.push(target ?? "");
|
|
5506
|
+
}
|
|
5507
|
+
return;
|
|
5508
|
+
}
|
|
5509
|
+
default: {
|
|
5510
|
+
const pub = PUBLISH[head];
|
|
5511
|
+
if (!pub) return;
|
|
5512
|
+
const pos = positionals(args, new Set(pub.flag ?? []));
|
|
5513
|
+
const verb = pos[0];
|
|
5514
|
+
if (verb === null || verb === void 0 || !pub.verbs.has(verb)) return;
|
|
5515
|
+
if (head === "yarn" && verb === "npm" && pos[1] !== "publish") return;
|
|
5516
|
+
out.sends = true;
|
|
5517
|
+
const named = pub.flag ? flagValue(args, pub.flag) : null;
|
|
5518
|
+
const h = named ? host(named) : null;
|
|
5519
|
+
if (named && !h) return;
|
|
5520
|
+
if (head === "docker") {
|
|
5521
|
+
const image = pos[1];
|
|
5522
|
+
const first = image?.split("/")[0] ?? "";
|
|
5523
|
+
const reg = first.includes(".") || first.includes(":") ? host(first) : null;
|
|
5524
|
+
out.destinations.push(reg ?? "host:docker.io");
|
|
5525
|
+
return;
|
|
5526
|
+
}
|
|
5527
|
+
out.destinations.push(h ?? `host:${pub.registry}`);
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
function walkCalls(command, out, depth, state) {
|
|
5532
|
+
const f = parseShared(command);
|
|
5533
|
+
if (typeof f === "symbol" || !f) return;
|
|
5534
|
+
syntax4.Walk(f, (node) => {
|
|
5535
|
+
if (!node) return false;
|
|
5536
|
+
const type = syntax4.NodeType(node);
|
|
5537
|
+
if (type === "DeclClause") {
|
|
5538
|
+
for (const a of node.Args || []) if (a?.Name?.Value) state.env.add(a.Name.Value);
|
|
5539
|
+
return true;
|
|
5540
|
+
}
|
|
5541
|
+
if (type !== "CallExpr") return true;
|
|
5542
|
+
const call = node;
|
|
5543
|
+
if ((call.Args || []).length === 0) {
|
|
5544
|
+
for (const a of call.Assigns || []) if (a?.Name?.Value) state.env.add(a.Name.Value);
|
|
5545
|
+
return true;
|
|
5546
|
+
}
|
|
5547
|
+
const c = callWords(node);
|
|
5548
|
+
if (!c) return true;
|
|
5549
|
+
if (SHELLS2.has(c.head) && depth < MAX_DEPTH3) {
|
|
5550
|
+
const i = c.args.findIndex((w) => w === "-c" || w === "-lc" || w === "-ec");
|
|
5551
|
+
const payload = i >= 0 ? c.args[i + 1] : null;
|
|
5552
|
+
if (typeof payload === "string") walkCalls(payload, out, depth + 1, state);
|
|
5553
|
+
return true;
|
|
5554
|
+
}
|
|
5555
|
+
if (CWD_CHANGERS.has(c.head)) state.cwdChanged = true;
|
|
5556
|
+
classifyCall(c.head, c.args, c.prefixes, [...c.env, ...state.env], state, out);
|
|
5557
|
+
return true;
|
|
5558
|
+
});
|
|
5559
|
+
}
|
|
5560
|
+
function classifyShellExposure(command) {
|
|
5561
|
+
const out = {
|
|
5562
|
+
...EMPTY,
|
|
5563
|
+
destinations: [],
|
|
5564
|
+
gitRemotes: [],
|
|
5565
|
+
repos: [],
|
|
5566
|
+
fetchDestinations: [],
|
|
5567
|
+
fetchKinds: []
|
|
5568
|
+
};
|
|
5569
|
+
try {
|
|
5570
|
+
walkCalls(command, out, 0, { cwdChanged: false, env: /* @__PURE__ */ new Set() });
|
|
5571
|
+
} catch {
|
|
5572
|
+
return { ...EMPTY };
|
|
5573
|
+
}
|
|
5574
|
+
out.destinations = [...new Set(out.destinations)];
|
|
5575
|
+
out.fetchDestinations = [...new Set(out.fetchDestinations)];
|
|
5576
|
+
out.gitRemotes = [...new Set(out.gitRemotes)];
|
|
5577
|
+
out.repos = [...new Set(out.repos)];
|
|
5578
|
+
out.fetchKinds = [...new Set(out.fetchKinds)];
|
|
5579
|
+
return out;
|
|
5580
|
+
}
|
|
4869
5581
|
function stripTerminalEscapes(s) {
|
|
4870
5582
|
return s.replace(TERMINAL_ESCAPE_RE, "");
|
|
4871
5583
|
}
|
|
@@ -5025,8 +5737,8 @@ function matchCanaryArgs(args, values) {
|
|
|
5025
5737
|
if (values.length === 0) return null;
|
|
5026
5738
|
const needles = prepare(values);
|
|
5027
5739
|
if (needles.length === 0) return null;
|
|
5028
|
-
const
|
|
5029
|
-
if (depth >
|
|
5740
|
+
const walk2 = (v, depth, fieldPath) => {
|
|
5741
|
+
if (depth > MAX_DEPTH4) return null;
|
|
5030
5742
|
if (typeof v === "string") {
|
|
5031
5743
|
const hit = matchPrepared(v, needles);
|
|
5032
5744
|
if (hit) return { id: hit.v.id, view: hit.view, fieldPath, retired: Boolean(hit.v.retired) };
|
|
@@ -5034,31 +5746,31 @@ function matchCanaryArgs(args, values) {
|
|
|
5034
5746
|
const trimmed = v.trim();
|
|
5035
5747
|
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
5036
5748
|
try {
|
|
5037
|
-
return
|
|
5749
|
+
return walk2(JSON.parse(v), depth + 1, fieldPath);
|
|
5038
5750
|
} catch {
|
|
5039
5751
|
}
|
|
5040
5752
|
}
|
|
5041
5753
|
}
|
|
5042
5754
|
return null;
|
|
5043
5755
|
}
|
|
5044
|
-
if (typeof v === "number" && Number.isFinite(v)) return
|
|
5756
|
+
if (typeof v === "number" && Number.isFinite(v)) return walk2(String(v), depth, fieldPath);
|
|
5045
5757
|
if (Array.isArray(v)) {
|
|
5046
5758
|
for (let i = 0; i < v.length; i++) {
|
|
5047
|
-
const h =
|
|
5759
|
+
const h = walk2(v[i], depth + 1, `${fieldPath}[${i}]`);
|
|
5048
5760
|
if (h) return h;
|
|
5049
5761
|
}
|
|
5050
5762
|
return null;
|
|
5051
5763
|
}
|
|
5052
5764
|
if (v && typeof v === "object") {
|
|
5053
5765
|
for (const [k, child] of Object.entries(v)) {
|
|
5054
|
-
const h =
|
|
5766
|
+
const h = walk2(child, depth + 1, fieldPath ? `${fieldPath}.${k}` : k);
|
|
5055
5767
|
if (h) return h;
|
|
5056
5768
|
}
|
|
5057
5769
|
}
|
|
5058
5770
|
return null;
|
|
5059
5771
|
};
|
|
5060
5772
|
try {
|
|
5061
|
-
return
|
|
5773
|
+
return walk2(args, 0, "");
|
|
5062
5774
|
} catch (e) {
|
|
5063
5775
|
console.error(
|
|
5064
5776
|
"[node9 engine] canary args walk failed:",
|
|
@@ -5067,7 +5779,7 @@ function matchCanaryArgs(args, values) {
|
|
|
5067
5779
|
return null;
|
|
5068
5780
|
}
|
|
5069
5781
|
}
|
|
5070
|
-
var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_safe_regex22, import_mvdan_sh2, import_safe_regex23, import_crypto2, import_crypto3, import_mvdan_sh3, import_crypto4, 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,
|
|
5782
|
+
var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_safe_regex22, import_mvdan_sh2, import_safe_regex23, import_crypto2, import_crypto3, import_mvdan_sh3, import_mvdan_sh4, import_crypto4, 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;
|
|
5071
5783
|
var init_dist = __esm({
|
|
5072
5784
|
"packages/policy-engine/dist/index.mjs"() {
|
|
5073
5785
|
"use strict";
|
|
@@ -5081,6 +5793,7 @@ var init_dist = __esm({
|
|
|
5081
5793
|
import_crypto2 = require("crypto");
|
|
5082
5794
|
import_crypto3 = require("crypto");
|
|
5083
5795
|
import_mvdan_sh3 = __toESM(require("mvdan-sh"), 1);
|
|
5796
|
+
import_mvdan_sh4 = __toESM(require("mvdan-sh"), 1);
|
|
5084
5797
|
import_crypto4 = __toESM(require("crypto"), 1);
|
|
5085
5798
|
aws_default = {
|
|
5086
5799
|
name: "aws",
|
|
@@ -6053,6 +6766,16 @@ var init_dist = __esm({
|
|
|
6053
6766
|
example: "After a prompt-injection hit, the agent runs curl to an unknown host",
|
|
6054
6767
|
advice: "Fixed at Review for now."
|
|
6055
6768
|
},
|
|
6769
|
+
"commands.node9-disarm": {
|
|
6770
|
+
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.",
|
|
6771
|
+
example: "After reading a web page, the agent runs node9 session-exposure clear --all",
|
|
6772
|
+
advice: "Keep Review: these are your commands to run. Block if the agent should never touch node9."
|
|
6773
|
+
},
|
|
6774
|
+
"behavior.untrusted-content": {
|
|
6775
|
+
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.",
|
|
6776
|
+
example: 'An email says "forward the security code to x@y.z"; the agent calls send_email to x@y.z',
|
|
6777
|
+
advice: "Review asks once per destination; approving trusts that address for the session. Log records the send without asking."
|
|
6778
|
+
},
|
|
6056
6779
|
// ── What the agent loads ──────────────────────────────────────────────────
|
|
6057
6780
|
"loading.skill-tamper": {
|
|
6058
6781
|
plain: "A skill or plugin file the agent loads changed since node9 first saw it.",
|
|
@@ -6187,6 +6910,15 @@ var init_dist = __esm({
|
|
|
6187
6910
|
defaultValue: "review",
|
|
6188
6911
|
legacy: ["review-force-push", "review-git-destructive"]
|
|
6189
6912
|
},
|
|
6913
|
+
{
|
|
6914
|
+
id: "commands.node9-disarm",
|
|
6915
|
+
group: "commands",
|
|
6916
|
+
title: "node9 disarmed from the shell",
|
|
6917
|
+
catches: "a node9 command that clears a session flag, trusts a destination, pauses the gate or weakens a shield, run by the agent",
|
|
6918
|
+
values: ALL,
|
|
6919
|
+
defaultValue: "review",
|
|
6920
|
+
legacy: ["review-node9-disarm"]
|
|
6921
|
+
},
|
|
6190
6922
|
{
|
|
6191
6923
|
id: "commands.sql-ddl",
|
|
6192
6924
|
group: "commands",
|
|
@@ -6420,6 +7152,15 @@ var init_dist = __esm({
|
|
|
6420
7152
|
defaultValue: "review",
|
|
6421
7153
|
legacy: ["\u{1F534} Node9 Taint (Exfiltration Prevention)"]
|
|
6422
7154
|
},
|
|
7155
|
+
{
|
|
7156
|
+
id: "behavior.untrusted-content",
|
|
7157
|
+
group: "behavior",
|
|
7158
|
+
title: "Send after reading outside content",
|
|
7159
|
+
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",
|
|
7160
|
+
values: ["off", "log", "review"],
|
|
7161
|
+
defaultValue: "review",
|
|
7162
|
+
legacy: ["\u{1F6E1}\uFE0F Node9 Exposure", "exposure"]
|
|
7163
|
+
},
|
|
6423
7164
|
// ── What the agent loads ────────────────────────────────────────────────
|
|
6424
7165
|
{
|
|
6425
7166
|
id: "loading.skill-tamper",
|
|
@@ -6494,6 +7235,11 @@ var init_dist = __esm({
|
|
|
6494
7235
|
if (s.packageCheck?.enabled === void 0) return void 0;
|
|
6495
7236
|
if (!s.packageCheck.enabled) return "off";
|
|
6496
7237
|
return s.packageCheck.onMalicious === "review" ? "review" : "block";
|
|
7238
|
+
},
|
|
7239
|
+
"behavior.untrusted-content": (s) => {
|
|
7240
|
+
if (s.untrustedContent?.enabled === void 0) return void 0;
|
|
7241
|
+
if (!s.untrustedContent.enabled) return "off";
|
|
7242
|
+
return s.untrustedContent.afterExposure === "log-only" ? "log" : "review";
|
|
6497
7243
|
}
|
|
6498
7244
|
};
|
|
6499
7245
|
({ syntax } = import_mvdan_sh.default);
|
|
@@ -7410,7 +8156,23 @@ var init_dist = __esm({
|
|
|
7410
8156
|
"shell",
|
|
7411
8157
|
"exec_command"
|
|
7412
8158
|
]);
|
|
7413
|
-
MCP_EXECUTORS = /* @__PURE__ */ new Set([
|
|
8159
|
+
MCP_EXECUTORS = /* @__PURE__ */ new Set([
|
|
8160
|
+
...BASH_TOOL_NAMES,
|
|
8161
|
+
"exec",
|
|
8162
|
+
"run_command",
|
|
8163
|
+
"terminal",
|
|
8164
|
+
"execute_command",
|
|
8165
|
+
"execute_shell",
|
|
8166
|
+
"execute_shell_command",
|
|
8167
|
+
"shell_execute",
|
|
8168
|
+
"run_shell",
|
|
8169
|
+
"run_terminal_command",
|
|
8170
|
+
"start_process"
|
|
8171
|
+
]);
|
|
8172
|
+
BUILTIN_FIELDS = ["command", "cmd", "script"];
|
|
8173
|
+
ARGV_FIELDS = ["args", "argv"];
|
|
8174
|
+
BARE_WORD = /^[A-Za-z0-9_./+:@%,-]+$/;
|
|
8175
|
+
MAX_ARGV_CHARACTERS = 262144;
|
|
7414
8176
|
({ syntax: syntax2 } = import_mvdan_sh2.default);
|
|
7415
8177
|
MAX_ALTERNATIVES = 64;
|
|
7416
8178
|
MAX_DEPTH = 64;
|
|
@@ -7441,6 +8203,22 @@ var init_dist = __esm({
|
|
|
7441
8203
|
SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
|
|
7442
8204
|
PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
|
|
7443
8205
|
STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
|
|
8206
|
+
AGENT_INSTRUCTION_FILES = [
|
|
8207
|
+
".gitlab-ci.yml",
|
|
8208
|
+
"CLAUDE.md",
|
|
8209
|
+
"AGENTS.md",
|
|
8210
|
+
".cursorrules",
|
|
8211
|
+
".mcp.json",
|
|
8212
|
+
".vscode/settings.json",
|
|
8213
|
+
"package.json"
|
|
8214
|
+
];
|
|
8215
|
+
AGENT_INSTRUCTION_DIRS = [
|
|
8216
|
+
".github/workflows",
|
|
8217
|
+
".claude",
|
|
8218
|
+
".cursor",
|
|
8219
|
+
".git/hooks",
|
|
8220
|
+
".husky"
|
|
8221
|
+
];
|
|
7444
8222
|
LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
|
|
7445
8223
|
RANK = { allow: 0, review: 1, block: 2 };
|
|
7446
8224
|
BIP39_ENGLISH = `abandon
|
|
@@ -9520,6 +10298,7 @@ zoo`.split("\n");
|
|
|
9520
10298
|
indices = new Map(BIP39_ENGLISH.map((word, i) => [word, i]));
|
|
9521
10299
|
lengths = [24, 21, 18, 15, 12];
|
|
9522
10300
|
hash = (value) => (0, import_crypto2.createHash)("sha256").update(value).digest();
|
|
10301
|
+
MAX_MNEMONIC_HASHES = 25e3;
|
|
9523
10302
|
IBAN_LENGTH = {
|
|
9524
10303
|
AD: 24,
|
|
9525
10304
|
AE: 23,
|
|
@@ -9627,6 +10406,7 @@ zoo`.split("\n");
|
|
|
9627
10406
|
CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
|
|
9628
10407
|
CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
|
|
9629
10408
|
INVISIBLE_RE = /[͏----︀-️]|[\u{E0000}-\u{E007F}]/gu;
|
|
10409
|
+
INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
|
|
9630
10410
|
HOMOGLYPHS = {
|
|
9631
10411
|
// Cyrillic lowercase
|
|
9632
10412
|
\u0430: "a",
|
|
@@ -10285,6 +11065,7 @@ zoo`.split("\n");
|
|
|
10285
11065
|
MAX_DEPTH2 = 5;
|
|
10286
11066
|
MAX_STRING_BYTES = 1e5;
|
|
10287
11067
|
MAX_JSON_PARSE_BYTES = 1e4;
|
|
11068
|
+
BATCH_SEPARATOR = "\n";
|
|
10288
11069
|
({ syntax: syntax3 } = import_mvdan_sh3.default);
|
|
10289
11070
|
NPM_INSTALL_VERBS = /* @__PURE__ */ new Set([
|
|
10290
11071
|
"install",
|
|
@@ -10540,7 +11321,25 @@ zoo`.split("\n");
|
|
|
10540
11321
|
name: "onlineFallback",
|
|
10541
11322
|
legacy: ["packageCheck", "onlineFallback"]
|
|
10542
11323
|
},
|
|
10543
|
-
{ checkId: "loading.malicious-package", name: "allow", legacy: ["packageCheck", "allow"] }
|
|
11324
|
+
{ checkId: "loading.malicious-package", name: "allow", legacy: ["packageCheck", "allow"] },
|
|
11325
|
+
// fw#318: the verdict carries off / log (log-only) / review; the tuning
|
|
11326
|
+
// carries whether a review asks once per destination or every time, the
|
|
11327
|
+
// frame around outside content, and the organisation's own domains.
|
|
11328
|
+
{
|
|
11329
|
+
checkId: "behavior.untrusted-content",
|
|
11330
|
+
name: "afterExposure",
|
|
11331
|
+
legacy: ["untrustedContent", "afterExposure"]
|
|
11332
|
+
},
|
|
11333
|
+
{
|
|
11334
|
+
checkId: "behavior.untrusted-content",
|
|
11335
|
+
name: "spotlight",
|
|
11336
|
+
legacy: ["untrustedContent", "spotlight"]
|
|
11337
|
+
},
|
|
11338
|
+
{
|
|
11339
|
+
checkId: "behavior.untrusted-content",
|
|
11340
|
+
name: "orgDomains",
|
|
11341
|
+
legacy: ["untrustedContent", "orgDomains"]
|
|
11342
|
+
}
|
|
10544
11343
|
];
|
|
10545
11344
|
MAP_GOVERNED = /* @__PURE__ */ new Set([
|
|
10546
11345
|
"files.shell-startup",
|
|
@@ -10557,6 +11356,7 @@ zoo`.split("\n");
|
|
|
10557
11356
|
"commands.temp-binary",
|
|
10558
11357
|
"commands.disk-destroy",
|
|
10559
11358
|
"commands.dangerous-word",
|
|
11359
|
+
"commands.node9-disarm",
|
|
10560
11360
|
"data.pipe-chain"
|
|
10561
11361
|
]);
|
|
10562
11362
|
KNOB_GOVERNED = {
|
|
@@ -10568,7 +11368,8 @@ zoo`.split("\n");
|
|
|
10568
11368
|
"behavior.loops": ["off", "block"],
|
|
10569
11369
|
"behavior.prompt-injection": ["off", "log"],
|
|
10570
11370
|
"loading.skill-tamper": ["off", "log", "block"],
|
|
10571
|
-
"loading.malicious-package": ["off", "review", "block"]
|
|
11371
|
+
"loading.malicious-package": ["off", "review", "block"],
|
|
11372
|
+
"behavior.untrusted-content": ["off", "log", "review"]
|
|
10572
11373
|
};
|
|
10573
11374
|
CONFIGURABLE_CHECK_IDS = CHECKS.filter(
|
|
10574
11375
|
(c) => configurableValues(c.id).length > 0
|
|
@@ -10734,16 +11535,24 @@ zoo`.split("\n");
|
|
|
10734
11535
|
["browser_batch", ["actions[].input.url"]]
|
|
10735
11536
|
]);
|
|
10736
11537
|
SECRET_VIEW_LIMITS = {
|
|
11538
|
+
/** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
|
|
10737
11539
|
maxDepth: 4,
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
11540
|
+
/** Nested views per parent view, and nested text per call; past either the call is reviewed. */
|
|
11541
|
+
maxNestedViewsPerLeaf: 64,
|
|
11542
|
+
maxNestedBytes: 1048576,
|
|
11543
|
+
/** Joined hostname-label text per call; past it the call is reviewed. */
|
|
11544
|
+
maxJoinedLabelBytes: 1048576,
|
|
11545
|
+
/** Decoded fragments are matched in batches of about this many characters. */
|
|
11546
|
+
batchCharacters: 65536,
|
|
11547
|
+
/** Only a valid DNS name has its labels joined. */
|
|
11548
|
+
maxHostLength: 253,
|
|
11549
|
+
maxLabelLength: 63,
|
|
11550
|
+
maxLabels: 127
|
|
10742
11551
|
};
|
|
10743
11552
|
ARGUMENT_SCAN_LIMITS = {
|
|
10744
11553
|
maxDepth: 12,
|
|
10745
11554
|
maxNodes: 16384,
|
|
10746
|
-
/**
|
|
11555
|
+
/** Text is matched and decoded in windows of this size. */
|
|
10747
11556
|
maxStringCharacters: 1e5,
|
|
10748
11557
|
/** Windows overlap by this much so a token on a window edge is still seen whole. */
|
|
10749
11558
|
windowOverlap: 4096,
|
|
@@ -10753,25 +11562,176 @@ zoo`.split("\n");
|
|
|
10753
11562
|
maxTailBytes: 4194304,
|
|
10754
11563
|
maxJsonCharacters: 1e4
|
|
10755
11564
|
};
|
|
11565
|
+
RUN_CHAR = /[A-Za-z0-9+/=_%\\.-]/;
|
|
11566
|
+
runChar = (value, index) => {
|
|
11567
|
+
const char = value[index] ?? "";
|
|
11568
|
+
return RUN_CHAR.test(char) || char !== "" && isInvisibleChar(char);
|
|
11569
|
+
};
|
|
10756
11570
|
VERDICT_RANK = {
|
|
10757
11571
|
allow: 0,
|
|
10758
11572
|
review: 1,
|
|
10759
11573
|
block: 2
|
|
10760
11574
|
};
|
|
10761
11575
|
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
11576
|
+
MAX_DESTINATIONS = 200;
|
|
11577
|
+
MAX_SCAN_CHARS = 1e5;
|
|
11578
|
+
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;
|
|
11579
|
+
URL_RE = /\b(?:[a-z][a-z0-9+.-]*:\/\/|www\.)[^\s<>"'`){}]+/gi;
|
|
11580
|
+
IBAN_RE = /\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]{4}){2,7}(?:[ -]?[A-Z0-9]{1,4})?\b/g;
|
|
11581
|
+
RECIPIENT_KEYS = /* @__PURE__ */ new Set([
|
|
11582
|
+
"to",
|
|
11583
|
+
"cc",
|
|
11584
|
+
"bcc",
|
|
11585
|
+
"recipient",
|
|
11586
|
+
"recipients",
|
|
11587
|
+
"email",
|
|
11588
|
+
"participants",
|
|
11589
|
+
"attendees",
|
|
11590
|
+
"destination",
|
|
11591
|
+
"iban",
|
|
11592
|
+
"account",
|
|
11593
|
+
// Where a request goes, when the value is a bare host or a scheme-less URL.
|
|
11594
|
+
"url",
|
|
11595
|
+
"uri",
|
|
11596
|
+
"host",
|
|
11597
|
+
"hostname",
|
|
11598
|
+
"endpoint",
|
|
11599
|
+
"server",
|
|
11600
|
+
"domain",
|
|
11601
|
+
"address",
|
|
11602
|
+
"webhook",
|
|
11603
|
+
"webhook_url",
|
|
11604
|
+
"callback",
|
|
11605
|
+
"callback_url",
|
|
11606
|
+
"target"
|
|
11607
|
+
]);
|
|
11608
|
+
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;
|
|
11609
|
+
({ syntax: syntax4 } = import_mvdan_sh4.default);
|
|
11610
|
+
EMPTY = {
|
|
11611
|
+
fetches: false,
|
|
11612
|
+
sends: false,
|
|
11613
|
+
destinations: [],
|
|
11614
|
+
gitRemotes: [],
|
|
11615
|
+
repos: [],
|
|
11616
|
+
unresolved: false,
|
|
11617
|
+
fetchDestinations: [],
|
|
11618
|
+
fetchKinds: []
|
|
11619
|
+
};
|
|
11620
|
+
CURL_SEND_FLAGS = /* @__PURE__ */ new Set([
|
|
11621
|
+
"-d",
|
|
11622
|
+
"--data",
|
|
11623
|
+
"--data-ascii",
|
|
11624
|
+
"--data-binary",
|
|
11625
|
+
"--data-raw",
|
|
11626
|
+
"--data-urlencode",
|
|
11627
|
+
"-F",
|
|
11628
|
+
"--form",
|
|
11629
|
+
"--form-string",
|
|
11630
|
+
"-T",
|
|
11631
|
+
"--upload-file",
|
|
11632
|
+
"--json"
|
|
11633
|
+
]);
|
|
11634
|
+
WGET_SEND_FLAGS = /* @__PURE__ */ new Set(["--post-data", "--post-file", "--body-data", "--body-file"]);
|
|
11635
|
+
IWR_SEND_FLAGS = /* @__PURE__ */ new Set(["-body", "-infile"]);
|
|
11636
|
+
WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
|
|
11637
|
+
GH_READ = /* @__PURE__ */ new Set(["view", "list", "diff", "checks", "status", "download", "clone"]);
|
|
11638
|
+
GH_WRITE = /* @__PURE__ */ new Set([
|
|
11639
|
+
"create",
|
|
11640
|
+
"comment",
|
|
11641
|
+
"edit",
|
|
11642
|
+
"close",
|
|
11643
|
+
"reopen",
|
|
11644
|
+
"merge",
|
|
11645
|
+
"review",
|
|
11646
|
+
"upload",
|
|
11647
|
+
"delete",
|
|
11648
|
+
"transfer",
|
|
11649
|
+
"pin",
|
|
11650
|
+
"lock",
|
|
11651
|
+
"ready",
|
|
11652
|
+
"set",
|
|
11653
|
+
"sync",
|
|
11654
|
+
"fork"
|
|
11655
|
+
]);
|
|
11656
|
+
GH_HOSTNAME_FLAGS = /* @__PURE__ */ new Set(["--hostname", "-h", "-R", "--repo"]);
|
|
11657
|
+
GH_REPO_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
11658
|
+
"--hostname",
|
|
11659
|
+
"-R",
|
|
11660
|
+
"--repo",
|
|
11661
|
+
"-d",
|
|
11662
|
+
"--description",
|
|
11663
|
+
"-h",
|
|
11664
|
+
"--homepage",
|
|
11665
|
+
"--visibility",
|
|
11666
|
+
"--default-branch",
|
|
11667
|
+
"--add-topic",
|
|
11668
|
+
"--remove-topic",
|
|
11669
|
+
"-p",
|
|
11670
|
+
"--template",
|
|
11671
|
+
"-s",
|
|
11672
|
+
"--source",
|
|
11673
|
+
"-b",
|
|
11674
|
+
"--branch",
|
|
11675
|
+
"-l",
|
|
11676
|
+
"--license",
|
|
11677
|
+
"-g",
|
|
11678
|
+
"--gitignore",
|
|
11679
|
+
"-t",
|
|
11680
|
+
"--team",
|
|
11681
|
+
"-r",
|
|
11682
|
+
"--remote",
|
|
11683
|
+
"--org"
|
|
11684
|
+
]);
|
|
11685
|
+
GH_API_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
11686
|
+
"--hostname",
|
|
11687
|
+
"-X",
|
|
11688
|
+
"--method",
|
|
11689
|
+
"-f",
|
|
11690
|
+
"--raw-field",
|
|
11691
|
+
"-F",
|
|
11692
|
+
"--field",
|
|
11693
|
+
"-H",
|
|
11694
|
+
"--header",
|
|
11695
|
+
"--input",
|
|
11696
|
+
"-q",
|
|
11697
|
+
"--jq",
|
|
11698
|
+
"-t",
|
|
11699
|
+
"--template",
|
|
11700
|
+
"--cache",
|
|
11701
|
+
"-p",
|
|
11702
|
+
"--preview"
|
|
11703
|
+
]);
|
|
11704
|
+
GIT_FETCH = /* @__PURE__ */ new Set(["clone", "fetch", "pull", "ls-remote"]);
|
|
11705
|
+
PUBLISH = {
|
|
11706
|
+
npm: { verbs: /* @__PURE__ */ new Set(["publish"]), registry: "registry.npmjs.org", flag: ["--registry"] },
|
|
11707
|
+
pnpm: { verbs: /* @__PURE__ */ new Set(["publish"]), registry: "registry.npmjs.org", flag: ["--registry"] },
|
|
11708
|
+
yarn: {
|
|
11709
|
+
verbs: /* @__PURE__ */ new Set(["publish", "npm"]),
|
|
11710
|
+
registry: "registry.npmjs.org",
|
|
11711
|
+
flag: ["--registry"]
|
|
11712
|
+
},
|
|
11713
|
+
twine: { verbs: /* @__PURE__ */ new Set(["upload"]), registry: "upload.pypi.org", flag: ["--repository-url"] },
|
|
11714
|
+
cargo: { verbs: /* @__PURE__ */ new Set(["publish"]), registry: "crates.io", flag: ["--registry", "--index"] },
|
|
11715
|
+
gem: { verbs: /* @__PURE__ */ new Set(["push"]), registry: "rubygems.org", flag: ["--host"] },
|
|
11716
|
+
docker: { verbs: /* @__PURE__ */ new Set(["push"]), registry: "docker.io" }
|
|
11717
|
+
};
|
|
11718
|
+
SHELLS2 = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
11719
|
+
OWN_HEADS = /* @__PURE__ */ new Set([...Object.keys(PUBLISH), "git", "gh"]);
|
|
11720
|
+
MAX_DEPTH3 = 3;
|
|
11721
|
+
CWD_CHANGERS = /* @__PURE__ */ new Set(["cd", "pushd", "popd"]);
|
|
11722
|
+
TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
11723
|
+
LOOP_MAX_RECORDS = 500;
|
|
11724
|
+
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
11725
|
+
PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
11726
|
+
PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
11727
|
+
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/;
|
|
11728
|
+
PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
|
|
10769
11729
|
PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
|
|
10770
11730
|
REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
|
|
10771
11731
|
MAX_PII_SCAN_BYTES = 1e5;
|
|
10772
11732
|
CANARY_MIN_LENGTH = 16;
|
|
10773
11733
|
MAX_TEXT = 1e5;
|
|
10774
|
-
|
|
11734
|
+
MAX_DEPTH4 = 6;
|
|
10775
11735
|
MAX_JSON_PARSE = 1e4;
|
|
10776
11736
|
URL_DEPTH = 4;
|
|
10777
11737
|
B64_DEPTH = 3;
|
|
@@ -10879,8 +11839,8 @@ function truncateAuditPreview(text) {
|
|
|
10879
11839
|
}
|
|
10880
11840
|
function appendToLog(logPath, entry) {
|
|
10881
11841
|
try {
|
|
10882
|
-
const
|
|
10883
|
-
if (!import_fs.default.existsSync(
|
|
11842
|
+
const dir2 = import_path.default.dirname(logPath);
|
|
11843
|
+
if (!import_fs.default.existsSync(dir2)) import_fs.default.mkdirSync(dir2, { recursive: true });
|
|
10884
11844
|
import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
10885
11845
|
} catch {
|
|
10886
11846
|
}
|
|
@@ -10947,6 +11907,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
10947
11907
|
taintFromEid: meta.taintFromEid,
|
|
10948
11908
|
...meta.taintSource && { taintSource: meta.taintSource }
|
|
10949
11909
|
} : {};
|
|
11910
|
+
const exposureFields = {
|
|
11911
|
+
...meta?.exposure ? { exposure: meta.exposure } : {},
|
|
11912
|
+
...meta?.exposureFromEid ? { exposureFromEid: meta.exposureFromEid } : {}
|
|
11913
|
+
};
|
|
10950
11914
|
const eid = generateEventId();
|
|
10951
11915
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
10952
11916
|
// eid first: the outbox shipper dedups on it, and a fixed leading field
|
|
@@ -10969,6 +11933,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
10969
11933
|
...loopCountField,
|
|
10970
11934
|
...transcriptPathField,
|
|
10971
11935
|
...taintFields,
|
|
11936
|
+
...exposureFields,
|
|
10972
11937
|
...testRun,
|
|
10973
11938
|
agent: meta?.agent,
|
|
10974
11939
|
mcpServer: meta?.mcpServer,
|
|
@@ -11011,10 +11976,72 @@ module.exports = __toCommonJS(src_exports);
|
|
|
11011
11976
|
// src/core.ts
|
|
11012
11977
|
init_audit();
|
|
11013
11978
|
|
|
11979
|
+
// src/auth/trusted-recipients.ts
|
|
11980
|
+
var import_fs2 = __toESM(require("fs"));
|
|
11981
|
+
var import_path2 = __toESM(require("path"));
|
|
11982
|
+
var import_os2 = __toESM(require("os"));
|
|
11983
|
+
init_dist();
|
|
11984
|
+
function getTrustedRecipientsPath() {
|
|
11985
|
+
return import_path2.default.join(import_os2.default.homedir(), ".node9", "trusted-recipients.json");
|
|
11986
|
+
}
|
|
11987
|
+
function readTrustedRecipients() {
|
|
11988
|
+
try {
|
|
11989
|
+
const raw = import_fs2.default.readFileSync(getTrustedRecipientsPath(), "utf8");
|
|
11990
|
+
const parsed = JSON.parse(raw);
|
|
11991
|
+
return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
|
|
11992
|
+
} catch {
|
|
11993
|
+
return [];
|
|
11994
|
+
}
|
|
11995
|
+
}
|
|
11996
|
+
function writeTrustedRecipients(entries) {
|
|
11997
|
+
const filePath = getTrustedRecipientsPath();
|
|
11998
|
+
import_fs2.default.mkdirSync(import_path2.default.dirname(filePath), { recursive: true });
|
|
11999
|
+
const tmp = filePath + ".node9-tmp";
|
|
12000
|
+
import_fs2.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
|
|
12001
|
+
import_fs2.default.renameSync(tmp, filePath);
|
|
12002
|
+
}
|
|
12003
|
+
function normalizeRecipient(raw) {
|
|
12004
|
+
const v = raw.trim();
|
|
12005
|
+
if (!v) return null;
|
|
12006
|
+
if (/^(email|host|iban):/i.test(v)) {
|
|
12007
|
+
const i = v.indexOf(":");
|
|
12008
|
+
const kind = v.slice(0, i).toLowerCase();
|
|
12009
|
+
const inner = normalizeRecipient(v.slice(i + 1));
|
|
12010
|
+
return inner && inner.startsWith(`${kind}:`) ? inner : null;
|
|
12011
|
+
}
|
|
12012
|
+
const found = extractArgDestinations({ to: v });
|
|
12013
|
+
return found.length === 1 ? found[0] : null;
|
|
12014
|
+
}
|
|
12015
|
+
function addTrustedRecipient(raw, addedBy = "user") {
|
|
12016
|
+
const value = normalizeRecipient(raw);
|
|
12017
|
+
if (!value) return null;
|
|
12018
|
+
const entries = readTrustedRecipients();
|
|
12019
|
+
if (!entries.some((e) => e.value === value)) {
|
|
12020
|
+
entries.push({ value, addedAt: Date.now(), addedBy });
|
|
12021
|
+
writeTrustedRecipients(entries);
|
|
12022
|
+
}
|
|
12023
|
+
return value;
|
|
12024
|
+
}
|
|
12025
|
+
var cache = null;
|
|
12026
|
+
function mtimeOf() {
|
|
12027
|
+
try {
|
|
12028
|
+
return import_fs2.default.statSync(getTrustedRecipientsPath()).mtimeMs;
|
|
12029
|
+
} catch {
|
|
12030
|
+
return 0;
|
|
12031
|
+
}
|
|
12032
|
+
}
|
|
12033
|
+
function isTrustedRecipient(value) {
|
|
12034
|
+
const mtime = mtimeOf();
|
|
12035
|
+
if (!cache || cache.mtime !== mtime) {
|
|
12036
|
+
cache = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
|
|
12037
|
+
}
|
|
12038
|
+
return cache.values.has(value);
|
|
12039
|
+
}
|
|
12040
|
+
|
|
11014
12041
|
// src/config/index.ts
|
|
11015
|
-
var
|
|
11016
|
-
var
|
|
11017
|
-
var
|
|
12042
|
+
var import_fs5 = __toESM(require("fs"));
|
|
12043
|
+
var import_path5 = __toESM(require("path"));
|
|
12044
|
+
var import_os5 = __toESM(require("os"));
|
|
11018
12045
|
|
|
11019
12046
|
// src/config-schema.ts
|
|
11020
12047
|
var import_zod = require("zod");
|
|
@@ -11184,6 +12211,13 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
11184
12211
|
onlineFallback: import_zod.z.boolean().optional(),
|
|
11185
12212
|
allow: import_zod.z.array(import_zod.z.string()).optional()
|
|
11186
12213
|
}).optional(),
|
|
12214
|
+
untrustedContent: import_zod.z.object({
|
|
12215
|
+
enabled: import_zod.z.boolean().optional(),
|
|
12216
|
+
spotlight: import_zod.z.boolean().optional(),
|
|
12217
|
+
afterExposure: import_zod.z.enum(["ask-once", "ask-every-time", "log-only"]).optional(),
|
|
12218
|
+
orgDomains: import_zod.z.array(import_zod.z.string()).optional(),
|
|
12219
|
+
roles: import_zod.z.record(import_zod.z.string(), import_zod.z.record(import_zod.z.string(), import_zod.z.enum(["source", "action", "read"]))).optional()
|
|
12220
|
+
}).optional(),
|
|
11187
12221
|
skillPinning: import_zod.z.object({
|
|
11188
12222
|
enabled: import_zod.z.boolean().optional(),
|
|
11189
12223
|
mode: import_zod.z.enum(["warn", "block"]).optional(),
|
|
@@ -11194,8 +12228,8 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
11194
12228
|
}).strict();
|
|
11195
12229
|
function formatIssues(issues) {
|
|
11196
12230
|
const lines = issues.map((issue) => {
|
|
11197
|
-
const
|
|
11198
|
-
return ` \u2022 ${
|
|
12231
|
+
const path26 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
12232
|
+
return ` \u2022 ${path26}: ${issue.message}`;
|
|
11199
12233
|
});
|
|
11200
12234
|
return `Invalid config:
|
|
11201
12235
|
${lines.join("\n")}`;
|
|
@@ -11208,9 +12242,9 @@ function prunePaths(root, paths) {
|
|
|
11208
12242
|
const yi = y[y.length - 1];
|
|
11209
12243
|
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
11210
12244
|
});
|
|
11211
|
-
for (const
|
|
12245
|
+
for (const path26 of ordered) {
|
|
11212
12246
|
let cur = root;
|
|
11213
|
-
for (const key of
|
|
12247
|
+
for (const key of path26.slice(0, -1)) {
|
|
11214
12248
|
if (cur === null || typeof cur !== "object") {
|
|
11215
12249
|
cur = void 0;
|
|
11216
12250
|
break;
|
|
@@ -11218,7 +12252,7 @@ function prunePaths(root, paths) {
|
|
|
11218
12252
|
cur = cur[key];
|
|
11219
12253
|
}
|
|
11220
12254
|
if (cur === null || typeof cur !== "object") continue;
|
|
11221
|
-
const last =
|
|
12255
|
+
const last = path26[path26.length - 1];
|
|
11222
12256
|
if (Array.isArray(cur)) {
|
|
11223
12257
|
const i = Number(last);
|
|
11224
12258
|
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
@@ -11254,7 +12288,7 @@ function sanitizeConfig(raw) {
|
|
|
11254
12288
|
return issue.keys.map((k) => [...at, k]);
|
|
11255
12289
|
}
|
|
11256
12290
|
return [at.slice(0, -level || void 0)];
|
|
11257
|
-
}).filter((
|
|
12291
|
+
}).filter((path26) => path26.length > 0);
|
|
11258
12292
|
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
11259
12293
|
}
|
|
11260
12294
|
if (ConfigFileSchema.safeParse(working).success) break;
|
|
@@ -11276,17 +12310,17 @@ function sanitizeConfig(raw) {
|
|
|
11276
12310
|
}
|
|
11277
12311
|
|
|
11278
12312
|
// src/shields.ts
|
|
11279
|
-
var
|
|
11280
|
-
var
|
|
11281
|
-
var
|
|
12313
|
+
var import_fs3 = __toESM(require("fs"));
|
|
12314
|
+
var import_path3 = __toESM(require("path"));
|
|
12315
|
+
var import_os3 = __toESM(require("os"));
|
|
11282
12316
|
init_dist();
|
|
11283
12317
|
init_dist();
|
|
11284
|
-
var USER_SHIELDS_DIR =
|
|
12318
|
+
var USER_SHIELDS_DIR = import_path3.default.join(import_os3.default.homedir(), ".node9", "shields");
|
|
11285
12319
|
function loadUserShields() {
|
|
11286
12320
|
const result = {};
|
|
11287
12321
|
let entries;
|
|
11288
12322
|
try {
|
|
11289
|
-
entries =
|
|
12323
|
+
entries = import_fs3.default.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
|
|
11290
12324
|
} catch (err) {
|
|
11291
12325
|
if (err.code !== "ENOENT") {
|
|
11292
12326
|
process.stderr.write(
|
|
@@ -11296,10 +12330,10 @@ function loadUserShields() {
|
|
|
11296
12330
|
}
|
|
11297
12331
|
return result;
|
|
11298
12332
|
}
|
|
11299
|
-
for (const
|
|
11300
|
-
const filePath =
|
|
12333
|
+
for (const file2 of entries) {
|
|
12334
|
+
const filePath = import_path3.default.join(USER_SHIELDS_DIR, file2);
|
|
11301
12335
|
try {
|
|
11302
|
-
const raw = JSON.parse(
|
|
12336
|
+
const raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
11303
12337
|
const v = validateShieldDefinition(raw);
|
|
11304
12338
|
if ("error" in v) {
|
|
11305
12339
|
process.stderr.write(`[node9] ${v.error}: ${filePath}
|
|
@@ -11308,7 +12342,7 @@ function loadUserShields() {
|
|
|
11308
12342
|
}
|
|
11309
12343
|
result[v.ok.name] = v.ok;
|
|
11310
12344
|
} catch (err) {
|
|
11311
|
-
process.stderr.write(`[node9] Failed to load user shield ${
|
|
12345
|
+
process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
|
|
11312
12346
|
`);
|
|
11313
12347
|
}
|
|
11314
12348
|
}
|
|
@@ -11330,10 +12364,10 @@ function getShield(name) {
|
|
|
11330
12364
|
const resolved = resolveShieldName(name);
|
|
11331
12365
|
return resolved ? SHIELDS[resolved] : null;
|
|
11332
12366
|
}
|
|
11333
|
-
var SHIELDS_STATE_FILE =
|
|
12367
|
+
var SHIELDS_STATE_FILE = import_path3.default.join(import_os3.default.homedir(), ".node9", "shields.json");
|
|
11334
12368
|
function readShieldsFile() {
|
|
11335
12369
|
try {
|
|
11336
|
-
const raw =
|
|
12370
|
+
const raw = import_fs3.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
11337
12371
|
if (!raw.trim()) return { active: [] };
|
|
11338
12372
|
const parsed = JSON.parse(raw);
|
|
11339
12373
|
const active = Array.isArray(parsed.active) ? parsed.active.filter(
|
|
@@ -11545,15 +12579,15 @@ function pathRules(rawPath, verdict, reason) {
|
|
|
11545
12579
|
}
|
|
11546
12580
|
|
|
11547
12581
|
// src/auth/trusted-hosts.ts
|
|
11548
|
-
var
|
|
11549
|
-
var
|
|
11550
|
-
var
|
|
12582
|
+
var import_fs4 = __toESM(require("fs"));
|
|
12583
|
+
var import_path4 = __toESM(require("path"));
|
|
12584
|
+
var import_os4 = __toESM(require("os"));
|
|
11551
12585
|
function getTrustedHostsPath() {
|
|
11552
|
-
return
|
|
12586
|
+
return import_path4.default.join(import_os4.default.homedir(), ".node9", "trusted-hosts.json");
|
|
11553
12587
|
}
|
|
11554
12588
|
function readTrustedHosts() {
|
|
11555
12589
|
try {
|
|
11556
|
-
const raw =
|
|
12590
|
+
const raw = import_fs4.default.readFileSync(getTrustedHostsPath(), "utf8");
|
|
11557
12591
|
const parsed = JSON.parse(raw);
|
|
11558
12592
|
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
11559
12593
|
} catch {
|
|
@@ -11564,7 +12598,7 @@ var _cache = null;
|
|
|
11564
12598
|
var CACHE_TTL_MS = 5e3;
|
|
11565
12599
|
function getFileMtime() {
|
|
11566
12600
|
try {
|
|
11567
|
-
return
|
|
12601
|
+
return import_fs4.default.statSync(getTrustedHostsPath()).mtimeMs;
|
|
11568
12602
|
} catch {
|
|
11569
12603
|
return 0;
|
|
11570
12604
|
}
|
|
@@ -11579,11 +12613,11 @@ function getCachedHosts() {
|
|
|
11579
12613
|
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
11580
12614
|
return hosts;
|
|
11581
12615
|
}
|
|
11582
|
-
function
|
|
12616
|
+
function normalizeHost2(raw) {
|
|
11583
12617
|
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
11584
12618
|
}
|
|
11585
|
-
function matchesTrustedHost(
|
|
11586
|
-
const normalized =
|
|
12619
|
+
function matchesTrustedHost(host2, list) {
|
|
12620
|
+
const normalized = normalizeHost2(host2);
|
|
11587
12621
|
return list.some((raw) => {
|
|
11588
12622
|
const entryHost = raw.toLowerCase();
|
|
11589
12623
|
if (entryHost.startsWith("*.")) {
|
|
@@ -11593,9 +12627,9 @@ function matchesTrustedHost(host, list) {
|
|
|
11593
12627
|
return normalized === entryHost;
|
|
11594
12628
|
});
|
|
11595
12629
|
}
|
|
11596
|
-
function isTrustedHost(
|
|
12630
|
+
function isTrustedHost(host2) {
|
|
11597
12631
|
return matchesTrustedHost(
|
|
11598
|
-
|
|
12632
|
+
host2,
|
|
11599
12633
|
getCachedHosts().map((entry) => entry.host)
|
|
11600
12634
|
);
|
|
11601
12635
|
}
|
|
@@ -11713,6 +12747,10 @@ function v2ToLegacy(raw, options = {}) {
|
|
|
11713
12747
|
const [id] = pair.split(".", 1);
|
|
11714
12748
|
if (CHECK_BY_ID.has(id)) warnings.push(`tuning.${pair}: no such setting`);
|
|
11715
12749
|
}
|
|
12750
|
+
if (stated["behavior.untrusted-content"] === "log") {
|
|
12751
|
+
const uc = policy.untrustedContent;
|
|
12752
|
+
if (uc) uc.afterExposure = "log-only";
|
|
12753
|
+
}
|
|
11716
12754
|
const legacy = { version: "1.0" };
|
|
11717
12755
|
if (Object.keys(settings).length) legacy.settings = settings;
|
|
11718
12756
|
if (Object.keys(policy).length) legacy.policy = policy;
|
|
@@ -11730,9 +12768,9 @@ var DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
|
11730
12768
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0"]);
|
|
11731
12769
|
var HOST_ALLOW_ENV = "NODE9_API_HOST_ALLOW";
|
|
11732
12770
|
function defaultHostSuffixes() {
|
|
11733
|
-
const
|
|
11734
|
-
const parent =
|
|
11735
|
-
return parent.includes(".") ? [
|
|
12771
|
+
const host2 = new URL(DEFAULT_API_URL).hostname.toLowerCase();
|
|
12772
|
+
const parent = host2.split(".").slice(1).join(".");
|
|
12773
|
+
return parent.includes(".") ? [host2, parent] : [host2];
|
|
11736
12774
|
}
|
|
11737
12775
|
function allowedSuffixes() {
|
|
11738
12776
|
const extra = (process.env[HOST_ALLOW_ENV] ?? "").split(",").map(
|
|
@@ -11750,11 +12788,11 @@ function validateApiUrl(raw) {
|
|
|
11750
12788
|
}
|
|
11751
12789
|
if (u.username || u.password) return null;
|
|
11752
12790
|
if (u.protocol !== "https:" && u.protocol !== "http:") return null;
|
|
11753
|
-
const
|
|
11754
|
-
if (LOOPBACK.has(
|
|
12791
|
+
const host2 = u.hostname.toLowerCase();
|
|
12792
|
+
if (LOOPBACK.has(host2)) return u;
|
|
11755
12793
|
if (u.protocol !== "https:") return null;
|
|
11756
12794
|
const suffixes = allowedSuffixes();
|
|
11757
|
-
const ok = suffixes.some((s) =>
|
|
12795
|
+
const ok = suffixes.some((s) => host2 === s || host2.endsWith("." + s));
|
|
11758
12796
|
return ok ? u : null;
|
|
11759
12797
|
}
|
|
11760
12798
|
function safeApiUrl(raw, onReject) {
|
|
@@ -11896,6 +12934,29 @@ var DEFAULT_CONFIG = {
|
|
|
11896
12934
|
reason: "SQL DDL destructive statement inside a shell command",
|
|
11897
12935
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
11898
12936
|
},
|
|
12937
|
+
// ── node9 itself ──────────────────────────────────────────────────────
|
|
12938
|
+
// The agent runs as the user, so a `node9 ...` command from Bash can
|
|
12939
|
+
// clear the session's taint or exposure, trust a destination or pause
|
|
12940
|
+
// the gate. Those are the user's own commands; from the agent they are
|
|
12941
|
+
// a review (fw#318 review finding 1).
|
|
12942
|
+
{
|
|
12943
|
+
name: "review-node9-disarm",
|
|
12944
|
+
tool: "bash",
|
|
12945
|
+
conditions: [
|
|
12946
|
+
{
|
|
12947
|
+
field: "command",
|
|
12948
|
+
op: "matches",
|
|
12949
|
+
// Single spaces on purpose: a nested `\\s+` inside the alternation
|
|
12950
|
+
// fails the engine's ReDoS check and the rule would be dropped.
|
|
12951
|
+
value: "(^|&&|\\|\\||;)\\s*(npx )?node9 (session-(exposure|taint) clear|trust (add|recipient add)|pause|shield (disable|set)|egress allow|approver set)\\b",
|
|
12952
|
+
flags: "i"
|
|
12953
|
+
}
|
|
12954
|
+
],
|
|
12955
|
+
conditionMode: "all",
|
|
12956
|
+
verdict: "review",
|
|
12957
|
+
reason: "node9 self-weakening command run by the agent",
|
|
12958
|
+
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."
|
|
12959
|
+
},
|
|
11899
12960
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
11900
12961
|
{
|
|
11901
12962
|
name: "review-force-push",
|
|
@@ -11997,6 +13058,14 @@ var DEFAULT_CONFIG = {
|
|
|
11997
13058
|
allow: []
|
|
11998
13059
|
},
|
|
11999
13060
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
13061
|
+
untrustedContent: {
|
|
13062
|
+
enabled: true,
|
|
13063
|
+
spotlight: true,
|
|
13064
|
+
afterExposure: "ask-once",
|
|
13065
|
+
orgDomains: [],
|
|
13066
|
+
roles: {},
|
|
13067
|
+
trustedRecipients: []
|
|
13068
|
+
},
|
|
12000
13069
|
trustedHosts: [],
|
|
12001
13070
|
trustedHostsManaged: false,
|
|
12002
13071
|
appPermissions: {},
|
|
@@ -12069,8 +13138,8 @@ function noteRejectedApiUrl(raw) {
|
|
|
12069
13138
|
if (rejectedApiUrlsSeen.has(key)) return;
|
|
12070
13139
|
rejectedApiUrlsSeen.add(key);
|
|
12071
13140
|
try {
|
|
12072
|
-
|
|
12073
|
-
|
|
13141
|
+
import_fs5.default.appendFileSync(
|
|
13142
|
+
import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log"),
|
|
12074
13143
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
|
|
12075
13144
|
`
|
|
12076
13145
|
);
|
|
@@ -12087,9 +13156,9 @@ function getCredentials() {
|
|
|
12087
13156
|
};
|
|
12088
13157
|
}
|
|
12089
13158
|
try {
|
|
12090
|
-
const credPath =
|
|
12091
|
-
if (
|
|
12092
|
-
const creds = JSON.parse(
|
|
13159
|
+
const credPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
|
|
13160
|
+
if (import_fs5.default.existsSync(credPath)) {
|
|
13161
|
+
const creds = JSON.parse(import_fs5.default.readFileSync(credPath, "utf-8"));
|
|
12093
13162
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
12094
13163
|
const profile = creds[profileName];
|
|
12095
13164
|
if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
|
|
@@ -12121,7 +13190,7 @@ function readRulesCacheResilient(cacheFile) {
|
|
|
12121
13190
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
12122
13191
|
let content;
|
|
12123
13192
|
try {
|
|
12124
|
-
content =
|
|
13193
|
+
content = import_fs5.default.readFileSync(cacheFile, "utf-8");
|
|
12125
13194
|
existed = true;
|
|
12126
13195
|
} catch (err) {
|
|
12127
13196
|
if (err.code === "ENOENT") return {};
|
|
@@ -12136,10 +13205,10 @@ function readRulesCacheResilient(cacheFile) {
|
|
|
12136
13205
|
}
|
|
12137
13206
|
}
|
|
12138
13207
|
if (existed || sawReadError) {
|
|
12139
|
-
const backup =
|
|
13208
|
+
const backup = import_path5.default.join(import_path5.default.dirname(cacheFile), "rules-cache.last-good.json");
|
|
12140
13209
|
if (backup !== cacheFile) {
|
|
12141
13210
|
try {
|
|
12142
|
-
const raw = JSON.parse(
|
|
13211
|
+
const raw = JSON.parse(import_fs5.default.readFileSync(backup, "utf-8"));
|
|
12143
13212
|
logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
|
|
12144
13213
|
lastParsedRulesCache = raw;
|
|
12145
13214
|
return raw;
|
|
@@ -12162,8 +13231,8 @@ function logCacheReadIssue(cacheFile, kind) {
|
|
|
12162
13231
|
if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
|
|
12163
13232
|
cacheReadLastLoggedAt = now;
|
|
12164
13233
|
try {
|
|
12165
|
-
|
|
12166
|
-
|
|
13234
|
+
import_fs5.default.appendFileSync(
|
|
13235
|
+
import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log"),
|
|
12167
13236
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
|
|
12168
13237
|
`
|
|
12169
13238
|
);
|
|
@@ -12184,8 +13253,8 @@ function catalogSettingsFrom(settings, policy) {
|
|
|
12184
13253
|
}
|
|
12185
13254
|
function getConfig(cwd) {
|
|
12186
13255
|
if (!cwd && cachedConfig) return cachedConfig;
|
|
12187
|
-
const globalPath =
|
|
12188
|
-
const projectPath =
|
|
13256
|
+
const globalPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
13257
|
+
const projectPath = import_path5.default.join(cwd ?? process.cwd(), "node9.config.json");
|
|
12189
13258
|
const globalConfig = tryLoadConfig(globalPath);
|
|
12190
13259
|
const projectConfig = tryLoadConfig(projectPath, { project: true });
|
|
12191
13260
|
const mergedSettings = {
|
|
@@ -12198,6 +13267,7 @@ function getConfig(cwd) {
|
|
|
12198
13267
|
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
12199
13268
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
12200
13269
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
13270
|
+
projectToolInspection: {},
|
|
12201
13271
|
// Shipped rules carry `builtin`: the catalog's per-check value governs
|
|
12202
13272
|
// them and only them (a user rule that reuses a name keeps its verdict).
|
|
12203
13273
|
smartRules: DEFAULT_CONFIG.policy.smartRules.map((r) => ({ ...r, builtin: true })),
|
|
@@ -12221,6 +13291,12 @@ function getConfig(cwd) {
|
|
|
12221
13291
|
...DEFAULT_CONFIG.policy.skillPinning,
|
|
12222
13292
|
roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
|
|
12223
13293
|
},
|
|
13294
|
+
untrustedContent: {
|
|
13295
|
+
...DEFAULT_CONFIG.policy.untrustedContent,
|
|
13296
|
+
orgDomains: [...DEFAULT_CONFIG.policy.untrustedContent.orgDomains],
|
|
13297
|
+
roles: {},
|
|
13298
|
+
trustedRecipients: []
|
|
13299
|
+
},
|
|
12224
13300
|
// Left empty on purpose: the local file is read fresh at policy-eval time
|
|
12225
13301
|
// (getCachedHosts via isTrustedHost), NOT snapshotted into the frozen config
|
|
12226
13302
|
// here. A managed list fills this below and flips trustedHostsManaged.
|
|
@@ -12265,8 +13341,14 @@ function getConfig(cwd) {
|
|
|
12265
13341
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
12266
13342
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
12267
13343
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
12268
|
-
if (p.toolInspection)
|
|
12269
|
-
|
|
13344
|
+
if (p.toolInspection) {
|
|
13345
|
+
if (isProject)
|
|
13346
|
+
mergedPolicy.projectToolInspection = {
|
|
13347
|
+
...mergedPolicy.projectToolInspection,
|
|
13348
|
+
...p.toolInspection
|
|
13349
|
+
};
|
|
13350
|
+
else mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
13351
|
+
}
|
|
12270
13352
|
if (p.smartRules) {
|
|
12271
13353
|
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
12272
13354
|
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
@@ -12380,6 +13462,9 @@ function getConfig(cwd) {
|
|
|
12380
13462
|
}
|
|
12381
13463
|
}
|
|
12382
13464
|
}
|
|
13465
|
+
if (p.untrustedContent && typeof p.untrustedContent === "object") {
|
|
13466
|
+
applyUntrustedContent(mergedPolicy.untrustedContent, p.untrustedContent, false, isProject);
|
|
13467
|
+
}
|
|
12383
13468
|
const envs = source.environments || {};
|
|
12384
13469
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
12385
13470
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -12411,7 +13496,7 @@ function getConfig(cwd) {
|
|
|
12411
13496
|
let cloudMandatesEnforcement = false;
|
|
12412
13497
|
let cloudMandatesAppPerm = false;
|
|
12413
13498
|
{
|
|
12414
|
-
const cacheFile =
|
|
13499
|
+
const cacheFile = import_path5.default.join(import_os5.default.homedir(), ".node9", "rules-cache.json");
|
|
12415
13500
|
try {
|
|
12416
13501
|
const raw = readRulesCacheResilient(cacheFile);
|
|
12417
13502
|
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
@@ -12579,6 +13664,9 @@ function getConfig(cwd) {
|
|
|
12579
13664
|
allow: Array.isArray(pc.allow) ? pc.allow.filter((a) => typeof a === "string" && a.length > 0) : cur.allow
|
|
12580
13665
|
};
|
|
12581
13666
|
}
|
|
13667
|
+
if (mc.untrustedContent && typeof mc.untrustedContent === "object") {
|
|
13668
|
+
applyUntrustedContent(mergedPolicy.untrustedContent, mc.untrustedContent, true);
|
|
13669
|
+
}
|
|
12582
13670
|
if (mc.loopDetection && typeof mc.loopDetection === "object") {
|
|
12583
13671
|
const l = mc.loopDetection;
|
|
12584
13672
|
const cur = mergedPolicy.loopDetection;
|
|
@@ -12599,18 +13687,18 @@ function getConfig(cwd) {
|
|
|
12599
13687
|
}
|
|
12600
13688
|
if (Array.isArray(mc.jailPaths)) {
|
|
12601
13689
|
for (const jp of mc.jailPaths) {
|
|
12602
|
-
const
|
|
12603
|
-
if (!
|
|
13690
|
+
const path26 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
13691
|
+
if (!path26) continue;
|
|
12604
13692
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
12605
|
-
for (const r of pathRules(
|
|
13693
|
+
for (const r of pathRules(path26, verdict, "org-managed jail")) {
|
|
12606
13694
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
12607
13695
|
}
|
|
12608
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
13696
|
+
mergedPolicy.managedJailPaths.push({ path: path26, verdict });
|
|
12609
13697
|
}
|
|
12610
13698
|
}
|
|
12611
13699
|
if (Array.isArray(mc.trustedHosts)) {
|
|
12612
13700
|
mergedPolicy.trustedHostsManaged = true;
|
|
12613
|
-
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) =>
|
|
13701
|
+
mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
|
|
12614
13702
|
}
|
|
12615
13703
|
if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
|
|
12616
13704
|
const coerced = {};
|
|
@@ -12628,7 +13716,7 @@ function getConfig(cwd) {
|
|
|
12628
13716
|
);
|
|
12629
13717
|
}
|
|
12630
13718
|
const on = (v) => !!v && typeof v === "object" && v.enabled === true;
|
|
12631
|
-
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(
|
|
13719
|
+
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(
|
|
12632
13720
|
(v) => typeof v === "string" && v !== "off"
|
|
12633
13721
|
);
|
|
12634
13722
|
}
|
|
@@ -12801,10 +13889,10 @@ function getConfig(cwd) {
|
|
|
12801
13889
|
}
|
|
12802
13890
|
function tryLoadConfig(filePath, options = {}) {
|
|
12803
13891
|
v2ChecksByPath.delete(filePath);
|
|
12804
|
-
if (!
|
|
13892
|
+
if (!import_fs5.default.existsSync(filePath)) return null;
|
|
12805
13893
|
let raw;
|
|
12806
13894
|
try {
|
|
12807
|
-
raw = JSON.parse(
|
|
13895
|
+
raw = JSON.parse(import_fs5.default.readFileSync(filePath, "utf-8"));
|
|
12808
13896
|
} catch (err) {
|
|
12809
13897
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12810
13898
|
process.stderr.write(
|
|
@@ -12864,25 +13952,63 @@ ${error.replace("Invalid config:\n", "")}
|
|
|
12864
13952
|
}
|
|
12865
13953
|
return sanitized;
|
|
12866
13954
|
}
|
|
13955
|
+
function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false) {
|
|
13956
|
+
if (!raw || typeof raw !== "object") return;
|
|
13957
|
+
const uc = raw;
|
|
13958
|
+
const AFTER_RANK = { "log-only": 0, "ask-once": 1, "ask-every-time": 2 };
|
|
13959
|
+
if (typeof uc.enabled === "boolean" && !(isProject && uc.enabled === false))
|
|
13960
|
+
cur.enabled = uc.enabled;
|
|
13961
|
+
if (typeof uc.spotlight === "boolean" && !(isProject && uc.spotlight === false))
|
|
13962
|
+
cur.spotlight = uc.spotlight;
|
|
13963
|
+
if ((uc.afterExposure === "ask-once" || uc.afterExposure === "ask-every-time" || uc.afterExposure === "log-only") && !(isProject && AFTER_RANK[uc.afterExposure] < AFTER_RANK[cur.afterExposure]))
|
|
13964
|
+
cur.afterExposure = uc.afterExposure;
|
|
13965
|
+
if (Array.isArray(uc.orgDomains) && !isProject) {
|
|
13966
|
+
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));
|
|
13967
|
+
if (replaceLists) cur.orgDomains = domains;
|
|
13968
|
+
else for (const d of domains) if (!cur.orgDomains.includes(d)) cur.orgDomains.push(d);
|
|
13969
|
+
}
|
|
13970
|
+
if (replaceLists && Array.isArray(uc.trustedRecipients)) {
|
|
13971
|
+
cur.trustedRecipients = [
|
|
13972
|
+
...new Set(
|
|
13973
|
+
uc.trustedRecipients.filter((v) => typeof v === "string").map((v) => normalizeRecipient(v)).filter((v) => v !== null)
|
|
13974
|
+
)
|
|
13975
|
+
].slice(0, 200);
|
|
13976
|
+
}
|
|
13977
|
+
if (uc.roles && typeof uc.roles === "object" && !Array.isArray(uc.roles)) {
|
|
13978
|
+
const next = replaceLists ? {} : cur.roles;
|
|
13979
|
+
for (const [server, tools] of Object.entries(uc.roles)) {
|
|
13980
|
+
if (!tools || typeof tools !== "object" || Array.isArray(tools)) continue;
|
|
13981
|
+
const m = { ...next[server] ?? {} };
|
|
13982
|
+
for (const [tool, role] of Object.entries(tools)) {
|
|
13983
|
+
if (role === "source" || role === "action" || role === "read") {
|
|
13984
|
+
if (isProject && role !== "action") continue;
|
|
13985
|
+
m[tool] = role;
|
|
13986
|
+
}
|
|
13987
|
+
}
|
|
13988
|
+
if (Object.keys(m).length > 0) next[server] = m;
|
|
13989
|
+
}
|
|
13990
|
+
cur.roles = next;
|
|
13991
|
+
}
|
|
13992
|
+
}
|
|
12867
13993
|
|
|
12868
13994
|
// src/utils/regex.ts
|
|
12869
13995
|
init_dist();
|
|
12870
13996
|
|
|
12871
13997
|
// src/dlp.ts
|
|
12872
|
-
var
|
|
12873
|
-
var
|
|
13998
|
+
var import_fs6 = __toESM(require("fs"));
|
|
13999
|
+
var import_path6 = __toESM(require("path"));
|
|
12874
14000
|
init_dist();
|
|
12875
14001
|
init_dist();
|
|
12876
14002
|
function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
|
|
12877
14003
|
if (!filePath) return null;
|
|
12878
14004
|
let resolved;
|
|
12879
14005
|
try {
|
|
12880
|
-
const absolute =
|
|
12881
|
-
resolved =
|
|
14006
|
+
const absolute = import_path6.default.resolve(cwd, filePath);
|
|
14007
|
+
resolved = import_fs6.default.realpathSync.native(absolute);
|
|
12882
14008
|
} catch (err) {
|
|
12883
14009
|
const code = err.code;
|
|
12884
14010
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
12885
|
-
resolved =
|
|
14011
|
+
resolved = import_path6.default.resolve(cwd, filePath);
|
|
12886
14012
|
} else {
|
|
12887
14013
|
return sensitivePathMatch(filePath);
|
|
12888
14014
|
}
|
|
@@ -12891,30 +14017,30 @@ function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
|
|
|
12891
14017
|
}
|
|
12892
14018
|
|
|
12893
14019
|
// src/policy/file-access.ts
|
|
12894
|
-
var
|
|
12895
|
-
var
|
|
12896
|
-
var
|
|
14020
|
+
var import_fs7 = __toESM(require("fs"));
|
|
14021
|
+
var import_os6 = __toESM(require("os"));
|
|
14022
|
+
var import_path7 = __toESM(require("path"));
|
|
12897
14023
|
init_dist();
|
|
12898
14024
|
function absoluteSpelling(p, cwd, home) {
|
|
12899
14025
|
if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
|
|
12900
|
-
return
|
|
14026
|
+
return import_path7.default.isAbsolute(p) ? p : `${cwd}/${p}`;
|
|
12901
14027
|
}
|
|
12902
14028
|
function physical(p, followFinal, links = 0) {
|
|
12903
14029
|
if (links > 40) throw new Error("Too many symbolic links");
|
|
12904
14030
|
if (!followFinal) {
|
|
12905
|
-
const parent = physical(
|
|
12906
|
-
return { path: `${parent.path}/${
|
|
14031
|
+
const parent = physical(import_path7.default.dirname(p), true, links);
|
|
14032
|
+
return { path: `${parent.path}/${import_path7.default.basename(p)}`, missing: parent.missing };
|
|
12907
14033
|
}
|
|
12908
14034
|
try {
|
|
12909
|
-
return { path:
|
|
14035
|
+
return { path: import_fs7.default.realpathSync.native(p), missing: false };
|
|
12910
14036
|
} catch (error) {
|
|
12911
14037
|
const code = error.code;
|
|
12912
14038
|
if (!["ENOENT", "ENOTDIR"].includes(code ?? "")) throw error;
|
|
12913
14039
|
try {
|
|
12914
|
-
if (
|
|
12915
|
-
const target =
|
|
14040
|
+
if (import_fs7.default.lstatSync(p).isSymbolicLink()) {
|
|
14041
|
+
const target = import_fs7.default.readlinkSync(p);
|
|
12916
14042
|
return physical(
|
|
12917
|
-
|
|
14043
|
+
import_path7.default.isAbsolute(target) ? target : `${import_path7.default.dirname(p)}/${target}`,
|
|
12918
14044
|
true,
|
|
12919
14045
|
links + 1
|
|
12920
14046
|
);
|
|
@@ -12923,13 +14049,13 @@ function physical(p, followFinal, links = 0) {
|
|
|
12923
14049
|
if (!["ENOENT", "ENOTDIR"].includes(linkError.code ?? ""))
|
|
12924
14050
|
throw linkError;
|
|
12925
14051
|
}
|
|
12926
|
-
const parent =
|
|
14052
|
+
const parent = import_path7.default.dirname(p);
|
|
12927
14053
|
if (parent === p) return { path: p, missing: true };
|
|
12928
14054
|
const resolved = physical(parent, true, links);
|
|
12929
|
-
return { path: `${resolved.path}/${
|
|
14055
|
+
return { path: `${resolved.path}/${import_path7.default.basename(p)}`, missing: true };
|
|
12930
14056
|
}
|
|
12931
14057
|
}
|
|
12932
|
-
function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home =
|
|
14058
|
+
function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = import_os6.default.homedir()) {
|
|
12933
14059
|
const context = { cwd, homeDirs: [home, "/root"] };
|
|
12934
14060
|
const accesses = collectFileAccesses(tool, args, context, inspection);
|
|
12935
14061
|
const handledPaths = /* @__PURE__ */ new Set();
|
|
@@ -12952,14 +14078,14 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
|
|
|
12952
14078
|
const raw = absoluteSpelling(access.path, access.cwd ?? cwd, home);
|
|
12953
14079
|
if (access.directoryTarget) {
|
|
12954
14080
|
try {
|
|
12955
|
-
if (!
|
|
14081
|
+
if (!import_fs7.default.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
|
|
12956
14082
|
continue;
|
|
12957
14083
|
} catch {
|
|
12958
14084
|
}
|
|
12959
14085
|
}
|
|
12960
14086
|
if (access.operation === "create") {
|
|
12961
14087
|
try {
|
|
12962
|
-
|
|
14088
|
+
import_fs7.default.statSync(raw);
|
|
12963
14089
|
continue;
|
|
12964
14090
|
} catch {
|
|
12965
14091
|
}
|
|
@@ -13008,27 +14134,27 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
|
|
|
13008
14134
|
}
|
|
13009
14135
|
|
|
13010
14136
|
// src/utils/provenance.ts
|
|
13011
|
-
var
|
|
13012
|
-
var
|
|
13013
|
-
var
|
|
14137
|
+
var import_fs8 = __toESM(require("fs"));
|
|
14138
|
+
var import_path8 = __toESM(require("path"));
|
|
14139
|
+
var import_os7 = __toESM(require("os"));
|
|
13014
14140
|
var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
|
|
13015
14141
|
var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
|
|
13016
14142
|
var USER_PREFIXES = [
|
|
13017
|
-
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
|
|
14143
|
+
import_path8.default.join(import_os7.default.homedir(), "bin"),
|
|
14144
|
+
import_path8.default.join(import_os7.default.homedir(), ".local", "bin"),
|
|
14145
|
+
import_path8.default.join(import_os7.default.homedir(), ".cargo", "bin"),
|
|
14146
|
+
import_path8.default.join(import_os7.default.homedir(), ".npm-global", "bin"),
|
|
14147
|
+
import_path8.default.join(import_os7.default.homedir(), ".volta", "bin")
|
|
13022
14148
|
];
|
|
13023
14149
|
var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
|
|
13024
14150
|
function findInPath(cmd) {
|
|
13025
|
-
if (
|
|
14151
|
+
if (import_path8.default.posix.isAbsolute(cmd)) return cmd;
|
|
13026
14152
|
const pathEnv = process.env.PATH ?? "";
|
|
13027
|
-
for (const
|
|
13028
|
-
if (!
|
|
13029
|
-
const full =
|
|
14153
|
+
for (const dir2 of pathEnv.split(import_path8.default.delimiter)) {
|
|
14154
|
+
if (!dir2) continue;
|
|
14155
|
+
const full = import_path8.default.join(dir2, cmd);
|
|
13030
14156
|
try {
|
|
13031
|
-
|
|
14157
|
+
import_fs8.default.accessSync(full, import_fs8.default.constants.X_OK);
|
|
13032
14158
|
return full;
|
|
13033
14159
|
} catch {
|
|
13034
14160
|
}
|
|
@@ -13039,7 +14165,7 @@ function _classifyPath(resolved, cwd) {
|
|
|
13039
14165
|
if (cwd && resolved.startsWith(cwd + "/")) {
|
|
13040
14166
|
return { trustLevel: "user", reason: "binary in project directory" };
|
|
13041
14167
|
}
|
|
13042
|
-
const osTmp =
|
|
14168
|
+
const osTmp = import_os7.default.tmpdir();
|
|
13043
14169
|
const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
|
|
13044
14170
|
if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
|
|
13045
14171
|
return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
|
|
@@ -13057,7 +14183,7 @@ function _classifyPath(resolved, cwd) {
|
|
|
13057
14183
|
}
|
|
13058
14184
|
function checkProvenance(cmd, cwd) {
|
|
13059
14185
|
const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
|
|
13060
|
-
if (
|
|
14186
|
+
if (import_path8.default.posix.isAbsolute(bare)) {
|
|
13061
14187
|
const early = _classifyPath(bare, cwd);
|
|
13062
14188
|
if (early.trustLevel === "suspect") {
|
|
13063
14189
|
return { resolvedPath: bare, ...early };
|
|
@@ -13073,7 +14199,7 @@ function checkProvenance(cmd, cwd) {
|
|
|
13073
14199
|
reason: "binary not found in PATH"
|
|
13074
14200
|
};
|
|
13075
14201
|
}
|
|
13076
|
-
resolved =
|
|
14202
|
+
resolved = import_fs8.default.realpathSync(found);
|
|
13077
14203
|
} catch {
|
|
13078
14204
|
return {
|
|
13079
14205
|
resolvedPath: cmd,
|
|
@@ -13082,7 +14208,7 @@ function checkProvenance(cmd, cwd) {
|
|
|
13082
14208
|
};
|
|
13083
14209
|
}
|
|
13084
14210
|
try {
|
|
13085
|
-
const stat =
|
|
14211
|
+
const stat = import_fs8.default.statSync(resolved);
|
|
13086
14212
|
if (stat.mode & 2) {
|
|
13087
14213
|
return {
|
|
13088
14214
|
resolvedPath: resolved,
|
|
@@ -13107,7 +14233,7 @@ init_dist();
|
|
|
13107
14233
|
init_dist();
|
|
13108
14234
|
async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
13109
14235
|
const config = opts?.config ?? getConfig(cwd);
|
|
13110
|
-
const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, config.policy
|
|
14236
|
+
const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, inspectionLayers(config.policy));
|
|
13111
14237
|
const activeEnvironment = getActiveEnvironment(config) ?? void 0;
|
|
13112
14238
|
return evaluatePolicy(
|
|
13113
14239
|
config,
|
|
@@ -13127,7 +14253,7 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
|
13127
14253
|
// config; changes arrive via cloud sync). Unmanaged → the local file via
|
|
13128
14254
|
// getCachedHosts (5s TTL + mtime), so a `node9 trust add/remove` still
|
|
13129
14255
|
// reaches a long-lived in-process authorizer (the gateway) within seconds.
|
|
13130
|
-
isTrustedHost: config.policy.trustedHostsManaged ? (
|
|
14256
|
+
isTrustedHost: config.policy.trustedHostsManaged ? (host2) => matchesTrustedHost(host2, config.policy.trustedHosts) : isTrustedHost
|
|
13131
14257
|
}
|
|
13132
14258
|
);
|
|
13133
14259
|
}
|
|
@@ -13136,32 +14262,32 @@ function isIgnoredTool2(toolName) {
|
|
|
13136
14262
|
}
|
|
13137
14263
|
|
|
13138
14264
|
// src/auth/state.ts
|
|
13139
|
-
var
|
|
13140
|
-
var
|
|
13141
|
-
var
|
|
14265
|
+
var import_fs10 = __toESM(require("fs"));
|
|
14266
|
+
var import_path10 = __toESM(require("path"));
|
|
14267
|
+
var import_os8 = __toESM(require("os"));
|
|
13142
14268
|
|
|
13143
14269
|
// src/utils/atomic-write.ts
|
|
13144
|
-
var
|
|
13145
|
-
var
|
|
14270
|
+
var import_fs9 = __toESM(require("fs"));
|
|
14271
|
+
var import_path9 = __toESM(require("path"));
|
|
13146
14272
|
var import_crypto6 = require("crypto");
|
|
13147
14273
|
function atomicWriteSync(filePath, data, options) {
|
|
13148
|
-
const
|
|
13149
|
-
if (!
|
|
14274
|
+
const dir2 = import_path9.default.dirname(filePath);
|
|
14275
|
+
if (!import_fs9.default.existsSync(dir2)) import_fs9.default.mkdirSync(dir2, { recursive: true });
|
|
13150
14276
|
const tmpPath = `${filePath}.${(0, import_crypto6.randomUUID)()}.tmp`;
|
|
13151
14277
|
try {
|
|
13152
|
-
|
|
14278
|
+
import_fs9.default.writeFileSync(tmpPath, data, options);
|
|
13153
14279
|
} catch (err) {
|
|
13154
14280
|
try {
|
|
13155
|
-
|
|
14281
|
+
import_fs9.default.unlinkSync(tmpPath);
|
|
13156
14282
|
} catch {
|
|
13157
14283
|
}
|
|
13158
14284
|
throw err;
|
|
13159
14285
|
}
|
|
13160
14286
|
try {
|
|
13161
|
-
|
|
14287
|
+
import_fs9.default.renameSync(tmpPath, filePath);
|
|
13162
14288
|
} catch (err) {
|
|
13163
14289
|
try {
|
|
13164
|
-
|
|
14290
|
+
import_fs9.default.unlinkSync(tmpPath);
|
|
13165
14291
|
} catch {
|
|
13166
14292
|
}
|
|
13167
14293
|
throw err;
|
|
@@ -13169,8 +14295,8 @@ function atomicWriteSync(filePath, data, options) {
|
|
|
13169
14295
|
}
|
|
13170
14296
|
|
|
13171
14297
|
// src/auth/state.ts
|
|
13172
|
-
var PAUSED_FILE =
|
|
13173
|
-
var TRUST_FILE =
|
|
14298
|
+
var PAUSED_FILE = import_path10.default.join(import_os8.default.homedir(), ".node9", "PAUSED");
|
|
14299
|
+
var TRUST_FILE = import_path10.default.join(import_os8.default.homedir(), ".node9", "trust.json");
|
|
13174
14300
|
function extractCommandPattern(toolName, args) {
|
|
13175
14301
|
const lower2 = toolName.toLowerCase();
|
|
13176
14302
|
if (lower2 !== "bash" && lower2 !== "execute_bash" && lower2 !== "shell") return void 0;
|
|
@@ -13182,11 +14308,11 @@ function extractCommandPattern(toolName, args) {
|
|
|
13182
14308
|
}
|
|
13183
14309
|
function checkPause() {
|
|
13184
14310
|
try {
|
|
13185
|
-
if (!
|
|
13186
|
-
const state = JSON.parse(
|
|
14311
|
+
if (!import_fs10.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
14312
|
+
const state = JSON.parse(import_fs10.default.readFileSync(PAUSED_FILE, "utf-8"));
|
|
13187
14313
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
13188
14314
|
try {
|
|
13189
|
-
|
|
14315
|
+
import_fs10.default.unlinkSync(PAUSED_FILE);
|
|
13190
14316
|
} catch {
|
|
13191
14317
|
}
|
|
13192
14318
|
return { paused: false };
|
|
@@ -13198,8 +14324,8 @@ function checkPause() {
|
|
|
13198
14324
|
}
|
|
13199
14325
|
function getActiveTrustSession(toolName, args) {
|
|
13200
14326
|
try {
|
|
13201
|
-
if (!
|
|
13202
|
-
const trust = JSON.parse(
|
|
14327
|
+
if (!import_fs10.default.existsSync(TRUST_FILE)) return false;
|
|
14328
|
+
const trust = JSON.parse(import_fs10.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
13203
14329
|
const now = Date.now();
|
|
13204
14330
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
13205
14331
|
if (active.length !== trust.entries.length) {
|
|
@@ -13222,8 +14348,8 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
13222
14348
|
try {
|
|
13223
14349
|
let trust = { entries: [] };
|
|
13224
14350
|
try {
|
|
13225
|
-
if (
|
|
13226
|
-
trust = JSON.parse(
|
|
14351
|
+
if (import_fs10.default.existsSync(TRUST_FILE)) {
|
|
14352
|
+
trust = JSON.parse(import_fs10.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
13227
14353
|
}
|
|
13228
14354
|
} catch {
|
|
13229
14355
|
}
|
|
@@ -13245,9 +14371,9 @@ function writeTrustSession(toolName, durationMs, args) {
|
|
|
13245
14371
|
}
|
|
13246
14372
|
function getPersistentDecision(toolName) {
|
|
13247
14373
|
try {
|
|
13248
|
-
const
|
|
13249
|
-
if (!
|
|
13250
|
-
const decisions = JSON.parse(
|
|
14374
|
+
const file2 = import_path10.default.join(import_os8.default.homedir(), ".node9", "decisions.json");
|
|
14375
|
+
if (!import_fs10.default.existsSync(file2)) return null;
|
|
14376
|
+
const decisions = JSON.parse(import_fs10.default.readFileSync(file2, "utf-8"));
|
|
13251
14377
|
const d = decisions[toolName];
|
|
13252
14378
|
if (d === "allow" || d === "deny") return d;
|
|
13253
14379
|
} catch {
|
|
@@ -13256,11 +14382,11 @@ function getPersistentDecision(toolName) {
|
|
|
13256
14382
|
}
|
|
13257
14383
|
|
|
13258
14384
|
// src/auth/daemon.ts
|
|
13259
|
-
var
|
|
14385
|
+
var import_fs11 = __toESM(require("fs"));
|
|
13260
14386
|
var import_net = __toESM(require("net"));
|
|
13261
|
-
var
|
|
13262
|
-
var
|
|
13263
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
14387
|
+
var import_path11 = __toESM(require("path"));
|
|
14388
|
+
var import_os9 = __toESM(require("os"));
|
|
14389
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path11.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
|
|
13264
14390
|
function notifyActivitySocket(data) {
|
|
13265
14391
|
return new Promise((resolve) => {
|
|
13266
14392
|
try {
|
|
@@ -13302,9 +14428,9 @@ var DAEMON_PORT = resolveDaemonPort();
|
|
|
13302
14428
|
var DAEMON_HOST = "127.0.0.1";
|
|
13303
14429
|
function getInternalToken() {
|
|
13304
14430
|
try {
|
|
13305
|
-
const pidFile =
|
|
13306
|
-
if (!
|
|
13307
|
-
const data = JSON.parse(
|
|
14431
|
+
const pidFile = import_path11.default.join(import_os9.default.homedir(), ".node9", "daemon.pid");
|
|
14432
|
+
if (!import_fs11.default.existsSync(pidFile)) return null;
|
|
14433
|
+
const data = JSON.parse(import_fs11.default.readFileSync(pidFile, "utf-8"));
|
|
13308
14434
|
process.kill(data.pid, 0);
|
|
13309
14435
|
return data.internalToken ?? null;
|
|
13310
14436
|
} catch {
|
|
@@ -13312,12 +14438,12 @@ function getInternalToken() {
|
|
|
13312
14438
|
}
|
|
13313
14439
|
}
|
|
13314
14440
|
function isDaemonRunning() {
|
|
13315
|
-
const pidFile =
|
|
13316
|
-
if (
|
|
14441
|
+
const pidFile = import_path11.default.join(import_os9.default.homedir(), ".node9", "daemon.pid");
|
|
14442
|
+
if (import_fs11.default.existsSync(pidFile)) {
|
|
13317
14443
|
let pid;
|
|
13318
14444
|
let port;
|
|
13319
14445
|
try {
|
|
13320
|
-
const data = JSON.parse(
|
|
14446
|
+
const data = JSON.parse(import_fs11.default.readFileSync(pidFile, "utf-8"));
|
|
13321
14447
|
pid = data.pid;
|
|
13322
14448
|
port = data.port;
|
|
13323
14449
|
} catch {
|
|
@@ -13335,7 +14461,7 @@ function isDaemonRunning() {
|
|
|
13335
14461
|
} catch (err) {
|
|
13336
14462
|
if (err instanceof Error && "code" in err && err.code === "ESRCH") {
|
|
13337
14463
|
try {
|
|
13338
|
-
|
|
14464
|
+
import_fs11.default.unlinkSync(pidFile);
|
|
13339
14465
|
} catch {
|
|
13340
14466
|
}
|
|
13341
14467
|
}
|
|
@@ -13357,7 +14483,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
|
13357
14483
|
return false;
|
|
13358
14484
|
}
|
|
13359
14485
|
}
|
|
13360
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent) {
|
|
14486
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
|
|
13361
14487
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
13362
14488
|
const ctrl = new AbortController();
|
|
13363
14489
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -13382,7 +14508,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
13382
14508
|
...recoveryCommand && { recoveryCommand },
|
|
13383
14509
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
13384
14510
|
...viewOnly && { viewOnly: true },
|
|
13385
|
-
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
14511
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true },
|
|
14512
|
+
...sessionId && { sessionId }
|
|
13386
14513
|
}),
|
|
13387
14514
|
signal: ctrl.signal
|
|
13388
14515
|
});
|
|
@@ -13481,7 +14608,7 @@ async function checkSessionTaint(sessionId) {
|
|
|
13481
14608
|
try {
|
|
13482
14609
|
const res = await fetch(`${base2}/session-taint/check`, {
|
|
13483
14610
|
method: "POST",
|
|
13484
|
-
headers:
|
|
14611
|
+
headers: exposureHeaders(),
|
|
13485
14612
|
body: JSON.stringify({ sessionId }),
|
|
13486
14613
|
signal: AbortSignal.timeout(2e3)
|
|
13487
14614
|
});
|
|
@@ -13490,6 +14617,42 @@ async function checkSessionTaint(sessionId) {
|
|
|
13490
14617
|
return { tainted: false, daemonUnavailable: true };
|
|
13491
14618
|
}
|
|
13492
14619
|
}
|
|
14620
|
+
function exposureHeaders() {
|
|
14621
|
+
return {
|
|
14622
|
+
"Content-Type": "application/json",
|
|
14623
|
+
"X-Node9-Internal": getInternalToken() ?? ""
|
|
14624
|
+
};
|
|
14625
|
+
}
|
|
14626
|
+
async function exposurePost(route, body, timeoutMs) {
|
|
14627
|
+
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
14628
|
+
const res = await fetch(`${base2}${route}`, {
|
|
14629
|
+
method: "POST",
|
|
14630
|
+
headers: exposureHeaders(),
|
|
14631
|
+
body: JSON.stringify(body),
|
|
14632
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
14633
|
+
});
|
|
14634
|
+
if (!res.ok) throw new Error(`session-exposure ${route}: HTTP ${res.status}`);
|
|
14635
|
+
return res.json();
|
|
14636
|
+
}
|
|
14637
|
+
async function checkSessionExposure(sessionId) {
|
|
14638
|
+
if (!sessionId || !isDaemonRunning()) return { exposed: false };
|
|
14639
|
+
try {
|
|
14640
|
+
return await exposurePost(
|
|
14641
|
+
"/session-exposure/check",
|
|
14642
|
+
{ sessionId },
|
|
14643
|
+
2e3
|
|
14644
|
+
);
|
|
14645
|
+
} catch {
|
|
14646
|
+
return { exposed: false, daemonUnavailable: true };
|
|
14647
|
+
}
|
|
14648
|
+
}
|
|
14649
|
+
async function approveSessionExposure(sessionId, keys) {
|
|
14650
|
+
if (!sessionId || keys.length === 0 || !isDaemonRunning()) return;
|
|
14651
|
+
try {
|
|
14652
|
+
await exposurePost("/session-exposure/approve", { sessionId, keys }, 1e3);
|
|
14653
|
+
} catch {
|
|
14654
|
+
}
|
|
14655
|
+
}
|
|
13493
14656
|
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
13494
14657
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
13495
14658
|
await fetch(`${base2}/resolve/${id}`, {
|
|
@@ -13501,14 +14664,15 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
13501
14664
|
}
|
|
13502
14665
|
|
|
13503
14666
|
// src/auth/orchestrator.ts
|
|
13504
|
-
var
|
|
14667
|
+
var import_crypto9 = require("crypto");
|
|
14668
|
+
var import_os18 = __toESM(require("os"));
|
|
13505
14669
|
|
|
13506
14670
|
// src/ui/native.ts
|
|
13507
14671
|
var import_child_process = require("child_process");
|
|
13508
|
-
var
|
|
14672
|
+
var import_path13 = __toESM(require("path"));
|
|
13509
14673
|
|
|
13510
14674
|
// src/context-sniper.ts
|
|
13511
|
-
var
|
|
14675
|
+
var import_path12 = __toESM(require("path"));
|
|
13512
14676
|
function smartTruncate(str, maxLen = 500) {
|
|
13513
14677
|
if (str.length <= maxLen) return str;
|
|
13514
14678
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -13576,7 +14740,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
13576
14740
|
intent = "EDIT";
|
|
13577
14741
|
if (obj.file_path) {
|
|
13578
14742
|
editFilePath = String(obj.file_path);
|
|
13579
|
-
editFileName =
|
|
14743
|
+
editFileName = import_path12.default.basename(editFilePath);
|
|
13580
14744
|
}
|
|
13581
14745
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
13582
14746
|
contextSnippet = result.snippet;
|
|
@@ -13621,6 +14785,7 @@ function resolveNativeDecision(opts) {
|
|
|
13621
14785
|
const { code, output, elapsedMs, locked } = opts;
|
|
13622
14786
|
if (locked) return "deny";
|
|
13623
14787
|
const tooFast = elapsedMs < MIN_INTERACTION_MS;
|
|
14788
|
+
if (output.includes("Allow & trust")) return tooFast ? "deny" : "trust_recipients";
|
|
13624
14789
|
if (output.includes("Always Allow")) return tooFast ? "deny" : "always_allow";
|
|
13625
14790
|
if (code === 0) return tooFast ? "deny" : "allow";
|
|
13626
14791
|
return "deny";
|
|
@@ -13643,12 +14808,12 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
13643
14808
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
13644
14809
|
const obj = parsed;
|
|
13645
14810
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
13646
|
-
const
|
|
14811
|
+
const file2 = obj.file_path ? import_path13.default.basename(String(obj.file_path)) : "file";
|
|
13647
14812
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
13648
14813
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
13649
14814
|
return {
|
|
13650
14815
|
intent: "EDIT",
|
|
13651
|
-
message: `\u{1F4DD} EDITING: ${
|
|
14816
|
+
message: `\u{1F4DD} EDITING: ${file2}
|
|
13652
14817
|
\u{1F4C2} PATH: ${obj.file_path}
|
|
13653
14818
|
|
|
13654
14819
|
--- REPLACING ---
|
|
@@ -13706,7 +14871,7 @@ ${smartTruncate(str, 500)}`
|
|
|
13706
14871
|
function escapePango(text) {
|
|
13707
14872
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
13708
14873
|
}
|
|
13709
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
14874
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription, exposure = false) {
|
|
13710
14875
|
const lines = [];
|
|
13711
14876
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
13712
14877
|
const safeAgent = safeMessage(agent ?? "AI Agent", 80);
|
|
@@ -13715,17 +14880,19 @@ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
13715
14880
|
if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
|
|
13716
14881
|
lines.push("");
|
|
13717
14882
|
lines.push(formattedArgs);
|
|
13718
|
-
if (allowCount >= 3) {
|
|
14883
|
+
if (allowCount >= 3 && !exposure) {
|
|
13719
14884
|
lines.push("");
|
|
13720
14885
|
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
13721
14886
|
}
|
|
13722
14887
|
if (!locked) {
|
|
13723
14888
|
lines.push("");
|
|
13724
|
-
lines.push(
|
|
14889
|
+
lines.push(
|
|
14890
|
+
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'
|
|
14891
|
+
);
|
|
13725
14892
|
}
|
|
13726
14893
|
return lines.join("\n");
|
|
13727
14894
|
}
|
|
13728
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
14895
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription, exposure = false) {
|
|
13729
14896
|
const lines = [];
|
|
13730
14897
|
if (locked) {
|
|
13731
14898
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -13738,7 +14905,7 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
13738
14905
|
if (ruleDescription) lines.push(`<i>\u2139 ${escapePango(ruleDescription)}</i>`);
|
|
13739
14906
|
lines.push("");
|
|
13740
14907
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
13741
|
-
if (allowCount >= 3) {
|
|
14908
|
+
if (allowCount >= 3 && !exposure) {
|
|
13742
14909
|
lines.push("");
|
|
13743
14910
|
lines.push(
|
|
13744
14911
|
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
@@ -13747,13 +14914,14 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
13747
14914
|
if (!locked) {
|
|
13748
14915
|
lines.push("");
|
|
13749
14916
|
lines.push(
|
|
13750
|
-
'<small>\u21B5 Enter = <b>Allow \u21B5</b> | \u238B Esc = <b>Block \u238B</b> | "Always Allow" = never ask again</small>'
|
|
14917
|
+
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>'
|
|
13751
14918
|
);
|
|
13752
14919
|
}
|
|
13753
14920
|
return lines.join("\n");
|
|
13754
14921
|
}
|
|
13755
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1, ruleDescription) {
|
|
14922
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1, ruleDescription, exposureDestinations = []) {
|
|
13756
14923
|
if (isTestEnv()) return "deny";
|
|
14924
|
+
const thirdButton = exposureDestinations.length > 0 ? "Allow & trust" : "Always Allow";
|
|
13757
14925
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
13758
14926
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
13759
14927
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
@@ -13764,7 +14932,8 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
13764
14932
|
explainableLabel,
|
|
13765
14933
|
locked,
|
|
13766
14934
|
allowCount,
|
|
13767
|
-
ruleDescription
|
|
14935
|
+
ruleDescription,
|
|
14936
|
+
exposureDestinations.length > 0
|
|
13768
14937
|
);
|
|
13769
14938
|
return new Promise((resolve) => {
|
|
13770
14939
|
let childProcess = null;
|
|
@@ -13784,7 +14953,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
13784
14953
|
}
|
|
13785
14954
|
try {
|
|
13786
14955
|
if (process.platform === "darwin") {
|
|
13787
|
-
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block \u238B", "
|
|
14956
|
+
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block \u238B", "${thirdButton}", "Allow \u21B5"} default button "Allow \u21B5" cancel button "Block \u238B"`;
|
|
13788
14957
|
const script = `on run argv
|
|
13789
14958
|
tell application "System Events"
|
|
13790
14959
|
activate
|
|
@@ -13800,7 +14969,8 @@ end run`;
|
|
|
13800
14969
|
explainableLabel,
|
|
13801
14970
|
locked,
|
|
13802
14971
|
allowCount,
|
|
13803
|
-
ruleDescription
|
|
14972
|
+
ruleDescription,
|
|
14973
|
+
exposureDestinations.length > 0
|
|
13804
14974
|
);
|
|
13805
14975
|
const argsList = [
|
|
13806
14976
|
locked ? "--info" : "--question",
|
|
@@ -13815,7 +14985,7 @@ end run`;
|
|
|
13815
14985
|
];
|
|
13816
14986
|
if (!locked) {
|
|
13817
14987
|
argsList.push("--cancel-label", "Block \u238B");
|
|
13818
|
-
argsList.push("--extra-button",
|
|
14988
|
+
argsList.push("--extra-button", thirdButton);
|
|
13819
14989
|
}
|
|
13820
14990
|
childProcess = (0, import_child_process.spawn)("zenity", argsList);
|
|
13821
14991
|
} else if (process.platform === "win32") {
|
|
@@ -13841,24 +15011,24 @@ end run`;
|
|
|
13841
15011
|
}
|
|
13842
15012
|
|
|
13843
15013
|
// src/canary/registry.ts
|
|
13844
|
-
var
|
|
13845
|
-
var
|
|
13846
|
-
var
|
|
15014
|
+
var import_fs13 = __toESM(require("fs"));
|
|
15015
|
+
var import_os11 = __toESM(require("os"));
|
|
15016
|
+
var import_path15 = __toESM(require("path"));
|
|
13847
15017
|
init_dist();
|
|
13848
15018
|
|
|
13849
15019
|
// src/shields/jail.ts
|
|
13850
|
-
var
|
|
13851
|
-
var
|
|
13852
|
-
var
|
|
15020
|
+
var import_fs12 = __toESM(require("fs"));
|
|
15021
|
+
var import_os10 = __toESM(require("os"));
|
|
15022
|
+
var import_path14 = __toESM(require("path"));
|
|
13853
15023
|
init_dist();
|
|
13854
15024
|
var USER_JAIL_SHIELD = "user-jail";
|
|
13855
15025
|
function jailStorePath() {
|
|
13856
|
-
return
|
|
15026
|
+
return import_path14.default.join(import_os10.default.homedir(), ".node9", "jail-paths.json");
|
|
13857
15027
|
}
|
|
13858
15028
|
function readJailPaths() {
|
|
13859
15029
|
let text;
|
|
13860
15030
|
try {
|
|
13861
|
-
text =
|
|
15031
|
+
text = import_fs12.default.readFileSync(jailStorePath(), "utf8");
|
|
13862
15032
|
} catch (err) {
|
|
13863
15033
|
if (err.code === "ENOENT") return [];
|
|
13864
15034
|
throw err;
|
|
@@ -13887,7 +15057,7 @@ function findJailedPathIn(candidate, paths) {
|
|
|
13887
15057
|
|
|
13888
15058
|
// src/canary/registry.ts
|
|
13889
15059
|
function canaryStorePath() {
|
|
13890
|
-
return
|
|
15060
|
+
return import_path15.default.join(import_os11.default.homedir(), ".node9", "canaries.json");
|
|
13891
15061
|
}
|
|
13892
15062
|
function isRecord(x) {
|
|
13893
15063
|
if (!x || typeof x !== "object") return false;
|
|
@@ -13898,7 +15068,7 @@ function loadCanaries(opts) {
|
|
|
13898
15068
|
const p = canaryStorePath();
|
|
13899
15069
|
let raw;
|
|
13900
15070
|
try {
|
|
13901
|
-
raw =
|
|
15071
|
+
raw = import_fs13.default.readFileSync(p, "utf-8");
|
|
13902
15072
|
} catch (e) {
|
|
13903
15073
|
if (e.code === "ENOENT") return [];
|
|
13904
15074
|
throw e;
|
|
@@ -13924,31 +15094,390 @@ function canaryValues() {
|
|
|
13924
15094
|
init_dist();
|
|
13925
15095
|
init_dist();
|
|
13926
15096
|
init_audit();
|
|
15097
|
+
init_dist();
|
|
13927
15098
|
|
|
13928
|
-
// src/
|
|
15099
|
+
// src/exposure/known-hosts.ts
|
|
15100
|
+
var import_fs15 = __toESM(require("fs"));
|
|
15101
|
+
var import_path17 = __toESM(require("path"));
|
|
15102
|
+
|
|
15103
|
+
// src/utils/git-remotes.ts
|
|
13929
15104
|
var import_fs14 = __toESM(require("fs"));
|
|
13930
|
-
var
|
|
13931
|
-
|
|
15105
|
+
var import_path16 = __toESM(require("path"));
|
|
15106
|
+
init_dist();
|
|
15107
|
+
function readSmallFile(p, max) {
|
|
15108
|
+
let fd;
|
|
15109
|
+
try {
|
|
15110
|
+
fd = import_fs14.default.openSync(p, import_fs14.default.constants.O_RDONLY | (import_fs14.default.constants.O_NOFOLLOW ?? 0));
|
|
15111
|
+
const st = import_fs14.default.fstatSync(fd);
|
|
15112
|
+
if (!st.isFile() || st.size > max) return null;
|
|
15113
|
+
const buf = Buffer.alloc(st.size);
|
|
15114
|
+
const n = import_fs14.default.readSync(fd, buf, 0, st.size, 0);
|
|
15115
|
+
return buf.subarray(0, n).toString("utf8");
|
|
15116
|
+
} catch {
|
|
15117
|
+
return null;
|
|
15118
|
+
} finally {
|
|
15119
|
+
if (fd !== void 0) import_fs14.default.closeSync(fd);
|
|
15120
|
+
}
|
|
15121
|
+
}
|
|
15122
|
+
function gitRemoteHost(url) {
|
|
15123
|
+
const u = url.trim();
|
|
15124
|
+
if (!u) return null;
|
|
15125
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
15126
|
+
try {
|
|
15127
|
+
const h = new URL(u).hostname.toLowerCase();
|
|
15128
|
+
return h || null;
|
|
15129
|
+
} catch {
|
|
15130
|
+
return null;
|
|
15131
|
+
}
|
|
15132
|
+
}
|
|
15133
|
+
const scp = /^(?:[^@/\s]+@)?([A-Za-z0-9.-]+):(?!\/\/)/.exec(u);
|
|
15134
|
+
if (scp && scp[1].includes(".")) return scp[1].toLowerCase();
|
|
15135
|
+
return null;
|
|
15136
|
+
}
|
|
15137
|
+
function findGitConfig(dir2, walkUp = true) {
|
|
15138
|
+
let cur = import_path16.default.resolve(dir2);
|
|
15139
|
+
for (let i = 0; i < (walkUp ? 64 : 1); i++) {
|
|
15140
|
+
const dotGit = import_path16.default.join(cur, ".git");
|
|
15141
|
+
const direct = readSmallFile(import_path16.default.join(dotGit, "config"), 1e6);
|
|
15142
|
+
if (direct !== null) return direct;
|
|
15143
|
+
const pointer = readSmallFile(dotGit, 4096);
|
|
15144
|
+
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
15145
|
+
if (m) {
|
|
15146
|
+
let gitDir = import_path16.default.resolve(cur, m[1]);
|
|
15147
|
+
const common = readSmallFile(import_path16.default.join(gitDir, "commondir"), 4096);
|
|
15148
|
+
if (common !== null) gitDir = import_path16.default.resolve(gitDir, common.trim());
|
|
15149
|
+
return readSmallFile(import_path16.default.join(gitDir, "config"), 1e6);
|
|
15150
|
+
}
|
|
15151
|
+
const parent = import_path16.default.dirname(cur);
|
|
15152
|
+
if (parent === cur) return null;
|
|
15153
|
+
cur = parent;
|
|
15154
|
+
}
|
|
15155
|
+
return null;
|
|
15156
|
+
}
|
|
15157
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
15158
|
+
function readGitRemotes(dir2, opts = {}) {
|
|
15159
|
+
if (!dir2) return [];
|
|
15160
|
+
const walkUp = opts.walkUp !== false;
|
|
15161
|
+
const key = `${walkUp ? "up" : "at"}:${import_path16.default.resolve(dir2)}`;
|
|
15162
|
+
const hit = cache2.get(key);
|
|
15163
|
+
if (hit) return hit;
|
|
15164
|
+
const out = [];
|
|
15165
|
+
try {
|
|
15166
|
+
const cfg = findGitConfig(import_path16.default.resolve(dir2), walkUp);
|
|
15167
|
+
if (cfg !== null) {
|
|
15168
|
+
let current = null;
|
|
15169
|
+
for (const line of cfg.split(/\r?\n/)) {
|
|
15170
|
+
const sec = /^\s*\[\s*([^\]\s]+)(?:\s+"([^"]*)")?\s*\]/.exec(line);
|
|
15171
|
+
if (sec) {
|
|
15172
|
+
current = sec[1].toLowerCase() === "remote" && sec[2] ? { name: sec[2], urls: [], hosts: [], repos: [] } : null;
|
|
15173
|
+
if (current) out.push(current);
|
|
15174
|
+
continue;
|
|
15175
|
+
}
|
|
15176
|
+
const url = current && /^\s*(?:push)?url\s*=\s*(.+?)\s*$/i.exec(line);
|
|
15177
|
+
if (url && current) {
|
|
15178
|
+
current.urls.push(url[1]);
|
|
15179
|
+
const h = gitRemoteHost(url[1]);
|
|
15180
|
+
if (h && !current.hosts.includes(`host:${h}`)) current.hosts.push(`host:${h}`);
|
|
15181
|
+
const repo = gitRepoPath(url[1]);
|
|
15182
|
+
if (repo && !current.repos.includes(repo)) current.repos.push(repo);
|
|
15183
|
+
}
|
|
15184
|
+
}
|
|
15185
|
+
}
|
|
15186
|
+
} catch {
|
|
15187
|
+
}
|
|
15188
|
+
cache2.set(key, out);
|
|
15189
|
+
return out;
|
|
15190
|
+
}
|
|
15191
|
+
|
|
15192
|
+
// src/exposure/known-hosts.ts
|
|
15193
|
+
var ECOSYSTEMS = [
|
|
15194
|
+
["package.json", "host:registry.npmjs.org"],
|
|
15195
|
+
["pyproject.toml", "host:upload.pypi.org"],
|
|
15196
|
+
["setup.py", "host:upload.pypi.org"],
|
|
15197
|
+
["Cargo.toml", "host:crates.io"],
|
|
15198
|
+
["Gemfile", "host:rubygems.org"]
|
|
15199
|
+
];
|
|
15200
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
15201
|
+
function manifestRoot(dir2) {
|
|
15202
|
+
let cur = import_path17.default.resolve(dir2);
|
|
15203
|
+
for (let i = 0; i < 64; i++) {
|
|
15204
|
+
for (const [m] of ECOSYSTEMS) {
|
|
15205
|
+
try {
|
|
15206
|
+
if (import_fs15.default.existsSync(import_path17.default.join(cur, m))) return cur;
|
|
15207
|
+
} catch {
|
|
15208
|
+
}
|
|
15209
|
+
}
|
|
15210
|
+
const parent = import_path17.default.dirname(cur);
|
|
15211
|
+
if (parent === cur) return null;
|
|
15212
|
+
cur = parent;
|
|
15213
|
+
}
|
|
15214
|
+
return null;
|
|
15215
|
+
}
|
|
15216
|
+
function knownProjectHosts(dir2) {
|
|
15217
|
+
const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
15218
|
+
if (!dir2) return empty;
|
|
15219
|
+
const key = import_path17.default.resolve(dir2);
|
|
15220
|
+
const hit = cache3.get(key);
|
|
15221
|
+
if (hit) return hit;
|
|
15222
|
+
const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
15223
|
+
try {
|
|
15224
|
+
for (const r of readGitRemotes(key)) {
|
|
15225
|
+
out.remotes.set(r.name, r.hosts);
|
|
15226
|
+
for (const h of r.hosts) out.hosts.add(h);
|
|
15227
|
+
for (const repo of r.repos) out.repos.add(repo);
|
|
15228
|
+
}
|
|
15229
|
+
const root = manifestRoot(key);
|
|
15230
|
+
if (root !== null) {
|
|
15231
|
+
for (const [m, registry] of ECOSYSTEMS) {
|
|
15232
|
+
if (import_fs15.default.existsSync(import_path17.default.join(root, m))) out.hosts.add(registry);
|
|
15233
|
+
}
|
|
15234
|
+
try {
|
|
15235
|
+
if (import_fs15.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
|
|
15236
|
+
out.hosts.add("host:rubygems.org");
|
|
15237
|
+
} catch {
|
|
15238
|
+
}
|
|
15239
|
+
}
|
|
15240
|
+
} catch {
|
|
15241
|
+
}
|
|
15242
|
+
cache3.set(key, out);
|
|
15243
|
+
return out;
|
|
15244
|
+
}
|
|
15245
|
+
|
|
15246
|
+
// src/exposure/writes.ts
|
|
15247
|
+
var import_fs16 = __toESM(require("fs"));
|
|
15248
|
+
var import_os12 = __toESM(require("os"));
|
|
15249
|
+
var import_path18 = __toESM(require("path"));
|
|
15250
|
+
init_dist();
|
|
15251
|
+
var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
|
|
15252
|
+
var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
|
|
15253
|
+
function writtenContent(args) {
|
|
15254
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return [];
|
|
15255
|
+
const a = args;
|
|
15256
|
+
const out = [];
|
|
15257
|
+
for (const k of ["content", "new_string", "new_str", "file_text", "contents"]) {
|
|
15258
|
+
if (typeof a[k] === "string") out.push(a[k]);
|
|
15259
|
+
}
|
|
15260
|
+
if (Array.isArray(a.edits)) {
|
|
15261
|
+
for (const e of a.edits) {
|
|
15262
|
+
if (e && typeof e === "object" && typeof e.new_string === "string")
|
|
15263
|
+
out.push(e.new_string);
|
|
15264
|
+
}
|
|
15265
|
+
}
|
|
15266
|
+
return out;
|
|
15267
|
+
}
|
|
15268
|
+
function packageScriptsTouched(args, absPath, shellCommand) {
|
|
15269
|
+
if (shellCommand !== null) return true;
|
|
15270
|
+
const a = args ?? {};
|
|
15271
|
+
const texts = [
|
|
15272
|
+
...writtenContent(args),
|
|
15273
|
+
...typeof a.old_string === "string" ? [a.old_string] : [],
|
|
15274
|
+
...Array.isArray(a.edits) ? a.edits.map((e) => String(e?.old_string ?? "")) : []
|
|
15275
|
+
];
|
|
15276
|
+
if (typeof a.content === "string") {
|
|
15277
|
+
try {
|
|
15278
|
+
const next = JSON.parse(a.content);
|
|
15279
|
+
const prev = JSON.parse(import_fs16.default.readFileSync(absPath, "utf8"));
|
|
15280
|
+
return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
|
|
15281
|
+
} catch {
|
|
15282
|
+
return LIFECYCLE_RE.test(a.content);
|
|
15283
|
+
}
|
|
15284
|
+
}
|
|
15285
|
+
return texts.some((t) => LIFECYCLE_RE.test(t));
|
|
15286
|
+
}
|
|
15287
|
+
function judgeWritesAfterExposure(opts) {
|
|
15288
|
+
const holds = [];
|
|
15289
|
+
const home = opts.home ?? import_os12.default.homedir();
|
|
15290
|
+
const tmp = normalizeFilePath(import_os12.default.tmpdir());
|
|
15291
|
+
const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
|
|
15292
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15293
|
+
for (const access of opts.accesses) {
|
|
15294
|
+
if (!WRITE_OPS.has(access.operation)) continue;
|
|
15295
|
+
const p = access.candidates?.[0] ?? normalizeFilePath(access.path, access.cwd ?? opts.cwd ?? "", home);
|
|
15296
|
+
if (seen.has(p)) continue;
|
|
15297
|
+
seen.add(p);
|
|
15298
|
+
if (p === "/dev" || p.startsWith("/dev/")) continue;
|
|
15299
|
+
if (root !== null && isInside(root, p)) {
|
|
15300
|
+
if (isAgentInstructionPath(p, root) && (import_path18.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
|
|
15301
|
+
holds.push({ path: p, why: "agent-instruction" });
|
|
15302
|
+
continue;
|
|
15303
|
+
}
|
|
15304
|
+
if (isInside(tmp, p) && !(root !== null && isInside(tmp, root))) continue;
|
|
15305
|
+
holds.push({ path: p, why: "outside-project" });
|
|
15306
|
+
}
|
|
15307
|
+
const texts = opts.shellCommand !== null ? [opts.shellCommand] : writtenContent(opts.args);
|
|
15308
|
+
if (texts.length > 0 && opts.inContent.size > 0) {
|
|
15309
|
+
for (const d of extractContentDestinations(texts.join("\n"))) {
|
|
15310
|
+
if (opts.inContent.has(d) && !opts.isKnown(d)) {
|
|
15311
|
+
const target = [...seen][0] ?? "";
|
|
15312
|
+
if (!holds.some((h) => h.why === "content-destination" && h.destination === d))
|
|
15313
|
+
holds.push({ path: target, why: "content-destination", destination: d });
|
|
15314
|
+
}
|
|
15315
|
+
}
|
|
15316
|
+
}
|
|
15317
|
+
return holds;
|
|
15318
|
+
}
|
|
15319
|
+
|
|
15320
|
+
// src/exposure/marker.ts
|
|
15321
|
+
var import_crypto7 = __toESM(require("crypto"));
|
|
15322
|
+
var import_fs17 = __toESM(require("fs"));
|
|
15323
|
+
var import_os13 = __toESM(require("os"));
|
|
15324
|
+
var import_path19 = __toESM(require("path"));
|
|
15325
|
+
|
|
15326
|
+
// src/daemon/exposure-store.ts
|
|
15327
|
+
var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
15328
|
+
|
|
15329
|
+
// src/exposure/marker.ts
|
|
15330
|
+
function dir() {
|
|
15331
|
+
return import_path19.default.join(import_os13.default.homedir(), ".node9", "exposure-sessions");
|
|
15332
|
+
}
|
|
15333
|
+
function file(sessionId) {
|
|
15334
|
+
const id = import_crypto7.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
15335
|
+
return import_path19.default.join(dir(), id);
|
|
15336
|
+
}
|
|
15337
|
+
function maybeSessionExposed(sessionId) {
|
|
15338
|
+
if (!sessionId) return false;
|
|
15339
|
+
try {
|
|
15340
|
+
const st = import_fs17.default.statSync(file(sessionId));
|
|
15341
|
+
return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
|
|
15342
|
+
} catch (e) {
|
|
15343
|
+
return e.code !== "ENOENT";
|
|
15344
|
+
}
|
|
15345
|
+
}
|
|
15346
|
+
|
|
15347
|
+
// src/exposure/roles.ts
|
|
15348
|
+
var import_fs19 = __toESM(require("fs"));
|
|
15349
|
+
|
|
15350
|
+
// src/daemon/mcp-tools.ts
|
|
15351
|
+
var import_fs18 = __toESM(require("fs"));
|
|
15352
|
+
var import_path20 = __toESM(require("path"));
|
|
15353
|
+
var import_os14 = __toESM(require("os"));
|
|
15354
|
+
function getMcpToolsFile() {
|
|
15355
|
+
return import_path20.default.join(import_os14.default.homedir(), ".node9", "mcp-tools.json");
|
|
15356
|
+
}
|
|
15357
|
+
function readMcpToolsConfig() {
|
|
15358
|
+
try {
|
|
15359
|
+
const file2 = getMcpToolsFile();
|
|
15360
|
+
if (!import_fs18.default.existsSync(file2)) return {};
|
|
15361
|
+
const raw = import_fs18.default.readFileSync(file2, "utf-8");
|
|
15362
|
+
return JSON.parse(raw);
|
|
15363
|
+
} catch {
|
|
15364
|
+
return {};
|
|
15365
|
+
}
|
|
15366
|
+
}
|
|
15367
|
+
|
|
15368
|
+
// src/exposure/roles.ts
|
|
15369
|
+
var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
|
|
15370
|
+
var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
|
|
15371
|
+
function foldServerName(name) {
|
|
15372
|
+
return name.replace(/[^A-Za-z0-9_-]/g, "_").toLowerCase();
|
|
15373
|
+
}
|
|
15374
|
+
var inventoryCache = null;
|
|
15375
|
+
var inventoryStamp = null;
|
|
15376
|
+
function inventoryFileStamp() {
|
|
15377
|
+
try {
|
|
15378
|
+
const st = import_fs19.default.statSync(getMcpToolsFile());
|
|
15379
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
15380
|
+
} catch {
|
|
15381
|
+
return "";
|
|
15382
|
+
}
|
|
15383
|
+
}
|
|
15384
|
+
function resolveToolAnnotations(server, tool) {
|
|
15385
|
+
const stamp = inventoryFileStamp();
|
|
15386
|
+
if (inventoryCache === null || stamp !== inventoryStamp) {
|
|
15387
|
+
inventoryStamp = stamp;
|
|
15388
|
+
inventoryCache = /* @__PURE__ */ new Map();
|
|
15389
|
+
try {
|
|
15390
|
+
const seen = /* @__PURE__ */ new Map();
|
|
15391
|
+
for (const cfg of Object.values(readMcpToolsConfig())) {
|
|
15392
|
+
if (!cfg?.name) continue;
|
|
15393
|
+
const key = foldServerName(cfg.name);
|
|
15394
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
15395
|
+
const tools = /* @__PURE__ */ new Map();
|
|
15396
|
+
for (const t of cfg.tools ?? []) tools.set(t.name, t.annotations ?? null);
|
|
15397
|
+
inventoryCache.set(key, tools);
|
|
15398
|
+
}
|
|
15399
|
+
for (const [key, n] of seen) if (n > 1) inventoryCache.delete(key);
|
|
15400
|
+
} catch {
|
|
15401
|
+
}
|
|
15402
|
+
}
|
|
15403
|
+
return inventoryCache.get(foldServerName(server))?.get(tool) ?? null;
|
|
15404
|
+
}
|
|
15405
|
+
var BUILTIN_SOURCES = /* @__PURE__ */ new Set([
|
|
15406
|
+
"webfetch",
|
|
15407
|
+
"websearch",
|
|
15408
|
+
"web_fetch",
|
|
15409
|
+
"web_search",
|
|
15410
|
+
"fetch",
|
|
15411
|
+
"navigate",
|
|
15412
|
+
"browser_batch",
|
|
15413
|
+
"get_page_text",
|
|
15414
|
+
"read_page",
|
|
15415
|
+
"find",
|
|
15416
|
+
"read_console_messages",
|
|
15417
|
+
"read_network_requests",
|
|
15418
|
+
// Claude Code reads MCP resources through two built-ins, not through the
|
|
15419
|
+
// server's own tools: an email read as a resource is still an email.
|
|
15420
|
+
"readmcpresourcetool",
|
|
15421
|
+
"listmcpresourcestool"
|
|
15422
|
+
]);
|
|
15423
|
+
var MCP_RE = /^mcp__([^_](?:[^_]|_(?!_))*?)__(.+)$/i;
|
|
15424
|
+
function splitMcpToolName(toolName) {
|
|
15425
|
+
const m = toolName.match(MCP_RE);
|
|
15426
|
+
return m ? { server: m[1], bare: m[2] } : null;
|
|
15427
|
+
}
|
|
15428
|
+
function toolRole(toolName, policy) {
|
|
15429
|
+
const mcp = splitMcpToolName(toolName);
|
|
15430
|
+
const bare = (mcp ? mcp.bare : toolName).toLowerCase();
|
|
15431
|
+
if (mcp) {
|
|
15432
|
+
const configured = policy.roles[mcp.server]?.[mcp.bare] ?? policy.roles[mcp.server]?.[bare];
|
|
15433
|
+
if (configured === "source") return { source: true, action: false };
|
|
15434
|
+
if (configured === "action") return { source: false, action: true };
|
|
15435
|
+
if (configured === "read") return { source: false, action: false };
|
|
15436
|
+
const serverLower = mcp.server.toLowerCase();
|
|
15437
|
+
if (AGENT_INTERNAL_PREFIXES.some((p) => serverLower.startsWith(p)))
|
|
15438
|
+
return { source: false, action: false };
|
|
15439
|
+
if (BUILTIN_SOURCES.has(bare)) return { source: true, action: false };
|
|
15440
|
+
const ann = resolveToolAnnotations(mcp.server, mcp.bare);
|
|
15441
|
+
return { source: true, action: true, destructive: ann?.destructiveHint === true };
|
|
15442
|
+
}
|
|
15443
|
+
return { source: BUILTIN_SOURCES.has(bare), action: false };
|
|
15444
|
+
}
|
|
15445
|
+
function sourceWord(toolName) {
|
|
15446
|
+
const mcp = splitMcpToolName(toolName);
|
|
15447
|
+
const bare = (mcp ? mcp.bare : toolName).toLowerCase();
|
|
15448
|
+
const server = (mcp?.server ?? "").toLowerCase();
|
|
15449
|
+
if (/mail|message|inbox|thread/.test(bare) || /mail|slack|teams|chat/.test(server))
|
|
15450
|
+
return "email";
|
|
15451
|
+
if (/issue|ticket|pull|pr\b|comment/.test(bare) || /github|gitlab|jira|linear/.test(server))
|
|
15452
|
+
return "issue";
|
|
15453
|
+
if (BUILTIN_SOURCES.has(bare) || /browser|web|http|search/.test(server)) return "web page";
|
|
15454
|
+
return "tool result";
|
|
15455
|
+
}
|
|
15456
|
+
|
|
15457
|
+
// src/auth/cloud.ts
|
|
15458
|
+
var import_fs21 = __toESM(require("fs"));
|
|
15459
|
+
var import_os15 = __toESM(require("os"));
|
|
15460
|
+
var import_path21 = __toESM(require("path"));
|
|
13932
15461
|
init_audit();
|
|
13933
15462
|
|
|
13934
15463
|
// src/utils/read-capped.ts
|
|
13935
|
-
var
|
|
13936
|
-
function readCapped(
|
|
15464
|
+
var import_fs20 = __toESM(require("fs"));
|
|
15465
|
+
function readCapped(file2, maxBytes) {
|
|
13937
15466
|
if (maxBytes < 0) return null;
|
|
13938
15467
|
let fd;
|
|
13939
15468
|
try {
|
|
13940
|
-
fd =
|
|
15469
|
+
fd = import_fs20.default.openSync(file2, "r");
|
|
13941
15470
|
} catch {
|
|
13942
15471
|
return null;
|
|
13943
15472
|
}
|
|
13944
15473
|
try {
|
|
13945
|
-
const st =
|
|
15474
|
+
const st = import_fs20.default.fstatSync(fd);
|
|
13946
15475
|
if (!st.isFile()) return null;
|
|
13947
15476
|
const want = Math.min(st.size, maxBytes + 1);
|
|
13948
15477
|
const buf = Buffer.alloc(want);
|
|
13949
15478
|
let read = 0;
|
|
13950
15479
|
while (read < want) {
|
|
13951
|
-
const n =
|
|
15480
|
+
const n = import_fs20.default.readSync(fd, buf, read, want - read, read);
|
|
13952
15481
|
if (n <= 0) break;
|
|
13953
15482
|
read += n;
|
|
13954
15483
|
}
|
|
@@ -13960,13 +15489,13 @@ function readCapped(file, maxBytes) {
|
|
|
13960
15489
|
return null;
|
|
13961
15490
|
} finally {
|
|
13962
15491
|
try {
|
|
13963
|
-
|
|
15492
|
+
import_fs20.default.closeSync(fd);
|
|
13964
15493
|
} catch {
|
|
13965
15494
|
}
|
|
13966
15495
|
}
|
|
13967
15496
|
}
|
|
13968
|
-
function readCappedText(
|
|
13969
|
-
const r = readCapped(
|
|
15497
|
+
function readCappedText(file2, maxBytes) {
|
|
15498
|
+
const r = readCapped(file2, maxBytes);
|
|
13970
15499
|
return r ? { text: r.bytes.toString("utf8"), truncated: r.truncated } : null;
|
|
13971
15500
|
}
|
|
13972
15501
|
|
|
@@ -13978,7 +15507,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
13978
15507
|
let ciContext;
|
|
13979
15508
|
if (process.env.CI) {
|
|
13980
15509
|
try {
|
|
13981
|
-
const ciContextPath =
|
|
15510
|
+
const ciContextPath = import_path21.default.join(import_os15.default.homedir(), ".node9", "ci-context.json");
|
|
13982
15511
|
const ci = readCappedText(ciContextPath, 1e4);
|
|
13983
15512
|
if (!ci) throw new Error("ci-context.json unreadable");
|
|
13984
15513
|
if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
|
|
@@ -14015,11 +15544,21 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
14015
15544
|
context: {
|
|
14016
15545
|
agent: meta?.agent,
|
|
14017
15546
|
mcpServer: meta?.mcpServer,
|
|
14018
|
-
hostname:
|
|
15547
|
+
hostname: import_os15.default.hostname(),
|
|
14019
15548
|
cwd: process.cwd(),
|
|
14020
|
-
platform:
|
|
15549
|
+
platform: import_os15.default.platform()
|
|
15550
|
+
},
|
|
15551
|
+
// The SaaS validates riskMetadata strictly and would drop it whole on
|
|
15552
|
+
// an unknown key, so only fields its schema declares go on the wire.
|
|
15553
|
+
// exposureDestinations ships since the dashboard's 2b contract
|
|
15554
|
+
// (fw#318): "Approve and trust" needs them.
|
|
15555
|
+
...riskMetadata && {
|
|
15556
|
+
riskMetadata: Object.fromEntries(
|
|
15557
|
+
Object.entries(riskMetadata).filter(
|
|
15558
|
+
([k, v]) => !Array.isArray(v) || k === "exposureDestinations"
|
|
15559
|
+
)
|
|
15560
|
+
)
|
|
14021
15561
|
},
|
|
14022
|
-
...riskMetadata && { riskMetadata },
|
|
14023
15562
|
...ciContext && { ciContext },
|
|
14024
15563
|
...agentPolicy && { policy: agentPolicy },
|
|
14025
15564
|
...forceReview && { forceReview: true }
|
|
@@ -14081,14 +15620,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
14081
15620
|
});
|
|
14082
15621
|
clearTimeout(timer);
|
|
14083
15622
|
if (!res.ok) {
|
|
14084
|
-
|
|
15623
|
+
import_fs21.default.appendFileSync(
|
|
14085
15624
|
HOOK_DEBUG_LOG,
|
|
14086
15625
|
`[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
|
|
14087
15626
|
`
|
|
14088
15627
|
);
|
|
14089
15628
|
}
|
|
14090
15629
|
} catch (err) {
|
|
14091
|
-
|
|
15630
|
+
import_fs21.default.appendFileSync(
|
|
14092
15631
|
HOOK_DEBUG_LOG,
|
|
14093
15632
|
`[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
|
|
14094
15633
|
`
|
|
@@ -14097,18 +15636,18 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
14097
15636
|
}
|
|
14098
15637
|
|
|
14099
15638
|
// src/loop-detector.ts
|
|
14100
|
-
var
|
|
14101
|
-
var
|
|
14102
|
-
var
|
|
15639
|
+
var import_fs22 = __toESM(require("fs"));
|
|
15640
|
+
var import_path22 = __toESM(require("path"));
|
|
15641
|
+
var import_os16 = __toESM(require("os"));
|
|
14103
15642
|
init_dist();
|
|
14104
15643
|
init_dist();
|
|
14105
15644
|
function loopStateFile() {
|
|
14106
|
-
return
|
|
15645
|
+
return import_path22.default.join(import_os16.default.homedir(), ".node9", "loop-state.json");
|
|
14107
15646
|
}
|
|
14108
15647
|
function readState() {
|
|
14109
15648
|
try {
|
|
14110
|
-
if (!
|
|
14111
|
-
const raw =
|
|
15649
|
+
if (!import_fs22.default.existsSync(loopStateFile())) return [];
|
|
15650
|
+
const raw = import_fs22.default.readFileSync(loopStateFile(), "utf-8");
|
|
14112
15651
|
const parsed = JSON.parse(raw);
|
|
14113
15652
|
if (!Array.isArray(parsed)) return [];
|
|
14114
15653
|
return parsed;
|
|
@@ -14117,11 +15656,11 @@ function readState() {
|
|
|
14117
15656
|
}
|
|
14118
15657
|
}
|
|
14119
15658
|
function writeState(records) {
|
|
14120
|
-
const
|
|
14121
|
-
if (!
|
|
14122
|
-
const tmpPath = `${loopStateFile()}.${
|
|
14123
|
-
|
|
14124
|
-
|
|
15659
|
+
const dir2 = import_path22.default.dirname(loopStateFile());
|
|
15660
|
+
if (!import_fs22.default.existsSync(dir2)) import_fs22.default.mkdirSync(dir2, { recursive: true });
|
|
15661
|
+
const tmpPath = `${loopStateFile()}.${import_os16.default.hostname()}.${process.pid}.tmp`;
|
|
15662
|
+
import_fs22.default.writeFileSync(tmpPath, JSON.stringify(records));
|
|
15663
|
+
import_fs22.default.renameSync(tmpPath, loopStateFile());
|
|
14125
15664
|
}
|
|
14126
15665
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
14127
15666
|
try {
|
|
@@ -14135,34 +15674,34 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
14135
15674
|
|
|
14136
15675
|
// src/supply-chain/check.ts
|
|
14137
15676
|
var import_picomatch3 = __toESM(require("picomatch"));
|
|
14138
|
-
var
|
|
15677
|
+
var import_path25 = __toESM(require("path"));
|
|
14139
15678
|
init_dist();
|
|
14140
15679
|
|
|
14141
15680
|
// src/supply-chain/osv-index.ts
|
|
14142
|
-
var
|
|
14143
|
-
var
|
|
14144
|
-
var
|
|
14145
|
-
var
|
|
15681
|
+
var import_fs23 = __toESM(require("fs"));
|
|
15682
|
+
var import_os17 = __toESM(require("os"));
|
|
15683
|
+
var import_path23 = __toESM(require("path"));
|
|
15684
|
+
var import_crypto8 = require("crypto");
|
|
14146
15685
|
init_dist();
|
|
14147
15686
|
var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
|
|
14148
15687
|
function osvRoot() {
|
|
14149
|
-
return
|
|
15688
|
+
return import_path23.default.join(import_os17.default.homedir(), ".node9", "osv");
|
|
14150
15689
|
}
|
|
14151
15690
|
function ecoDir(eco) {
|
|
14152
|
-
return
|
|
15691
|
+
return import_path23.default.join(osvRoot(), eco);
|
|
14153
15692
|
}
|
|
14154
15693
|
function indexKey(eco, name) {
|
|
14155
15694
|
return eco === "PyPI" ? normalizePyPiName(name) : name.toLowerCase();
|
|
14156
15695
|
}
|
|
14157
15696
|
function shardOf(key) {
|
|
14158
|
-
return (0,
|
|
15697
|
+
return (0, import_crypto8.createHash)("sha1").update(key).digest("hex").slice(0, 2);
|
|
14159
15698
|
}
|
|
14160
15699
|
function shardPath(eco, shard) {
|
|
14161
|
-
return
|
|
15700
|
+
return import_path23.default.join(ecoDir(eco), "shards", `${shard}.json`);
|
|
14162
15701
|
}
|
|
14163
15702
|
function readMeta(eco) {
|
|
14164
15703
|
try {
|
|
14165
|
-
const m = JSON.parse(
|
|
15704
|
+
const m = JSON.parse(import_fs23.default.readFileSync(import_path23.default.join(ecoDir(eco), "meta.json"), "utf8"));
|
|
14166
15705
|
if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
|
|
14167
15706
|
return m;
|
|
14168
15707
|
} catch {
|
|
@@ -14171,7 +15710,7 @@ function readMeta(eco) {
|
|
|
14171
15710
|
}
|
|
14172
15711
|
function readShard(eco, shard) {
|
|
14173
15712
|
try {
|
|
14174
|
-
const s = JSON.parse(
|
|
15713
|
+
const s = JSON.parse(import_fs23.default.readFileSync(shardPath(eco, shard), "utf8"));
|
|
14175
15714
|
return s && typeof s === "object" && !Array.isArray(s) ? s : {};
|
|
14176
15715
|
} catch {
|
|
14177
15716
|
return {};
|
|
@@ -14368,19 +15907,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
|
14368
15907
|
}
|
|
14369
15908
|
|
|
14370
15909
|
// src/supply-chain/local-resolve.ts
|
|
14371
|
-
var
|
|
14372
|
-
var
|
|
15910
|
+
var import_fs24 = __toESM(require("fs"));
|
|
15911
|
+
var import_path24 = __toESM(require("path"));
|
|
14373
15912
|
var MAX_LEVELS = 32;
|
|
14374
15913
|
var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
|
|
14375
15914
|
function resolveInstalled(name, cwd) {
|
|
14376
|
-
if (!cwd || !
|
|
15915
|
+
if (!cwd || !import_path24.default.isAbsolute(cwd)) return null;
|
|
14377
15916
|
if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
|
|
14378
|
-
let
|
|
15917
|
+
let dir2 = import_path24.default.resolve(cwd);
|
|
14379
15918
|
for (let i = 0; i < MAX_LEVELS; i++) {
|
|
14380
|
-
const pkgDir =
|
|
14381
|
-
const manifest =
|
|
15919
|
+
const pkgDir = import_path24.default.join(dir2, "node_modules", ...name.split("/"));
|
|
15920
|
+
const manifest = import_path24.default.join(pkgDir, "package.json");
|
|
14382
15921
|
try {
|
|
14383
|
-
const raw =
|
|
15922
|
+
const raw = import_fs24.default.readFileSync(manifest, "utf8");
|
|
14384
15923
|
const parsed = JSON.parse(raw);
|
|
14385
15924
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
14386
15925
|
return { version: parsed.version, dir: pkgDir };
|
|
@@ -14388,9 +15927,9 @@ function resolveInstalled(name, cwd) {
|
|
|
14388
15927
|
return null;
|
|
14389
15928
|
} catch {
|
|
14390
15929
|
}
|
|
14391
|
-
const parent =
|
|
14392
|
-
if (parent ===
|
|
14393
|
-
|
|
15930
|
+
const parent = import_path24.default.dirname(dir2);
|
|
15931
|
+
if (parent === dir2) break;
|
|
15932
|
+
dir2 = parent;
|
|
14394
15933
|
}
|
|
14395
15934
|
return null;
|
|
14396
15935
|
}
|
|
@@ -14485,7 +16024,7 @@ function checkInstalled(p, version, misses) {
|
|
|
14485
16024
|
}
|
|
14486
16025
|
async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
14487
16026
|
if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
|
|
14488
|
-
const localCwd = cwd &&
|
|
16027
|
+
const localCwd = cwd && import_path25.default.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => import_path25.default.resolve(dir2, step), cwd) : void 0;
|
|
14489
16028
|
const installed = resolveInstalled(p.name, localCwd);
|
|
14490
16029
|
if (installed && (!p.version || p.version === installed.version))
|
|
14491
16030
|
return checkInstalled(p, installed.version, misses);
|
|
@@ -14620,6 +16159,9 @@ function isWriteTool(toolName) {
|
|
|
14620
16159
|
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
14621
16160
|
return WRITE_TOOLS.has(t);
|
|
14622
16161
|
}
|
|
16162
|
+
function isMcpToolName(toolName) {
|
|
16163
|
+
return /^mcp__[^_](?:[^_]|_(?!_))*?__/i.test(toolName);
|
|
16164
|
+
}
|
|
14623
16165
|
function extractFilePaths(toolName, args, shellCommand) {
|
|
14624
16166
|
const paths = [];
|
|
14625
16167
|
if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
|
|
@@ -14657,7 +16199,7 @@ function mayDowngradeHardBlock(opts) {
|
|
|
14657
16199
|
}
|
|
14658
16200
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
14659
16201
|
if (!options?.calledFromDaemon) {
|
|
14660
|
-
const actId = (0,
|
|
16202
|
+
const actId = (0, import_crypto9.randomUUID)();
|
|
14661
16203
|
const actTs = Date.now();
|
|
14662
16204
|
const sanitizedAgent = meta?.agent ? safeMessage(meta.agent, 80) : void 0;
|
|
14663
16205
|
const sanitizedMcpServer = meta?.mcpServer ? safeMessage(meta.mcpServer, 40) : void 0;
|
|
@@ -14737,7 +16279,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
14737
16279
|
let fileAccess;
|
|
14738
16280
|
let fileFinding;
|
|
14739
16281
|
try {
|
|
14740
|
-
fileAccess = prepareFileAccesses(toolName, args, options?.cwd, config.policy
|
|
16282
|
+
fileAccess = prepareFileAccesses(toolName, args, options?.cwd, inspectionLayers(config.policy));
|
|
14741
16283
|
fileFinding = isManual ? null : evaluateProtectedFileAccess(
|
|
14742
16284
|
fileAccess.accesses,
|
|
14743
16285
|
fileAccess,
|
|
@@ -14756,7 +16298,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
14756
16298
|
fileAccess = { cwd: options?.cwd, homeDirs: [], accesses: [], handledPaths: /* @__PURE__ */ new Set() };
|
|
14757
16299
|
fileFinding = isManual ? null : incompleteFileVerdict();
|
|
14758
16300
|
}
|
|
14759
|
-
const shellInput = resolveShellInput(toolName, args, config.policy
|
|
16301
|
+
const shellInput = resolveShellInput(toolName, args, inspectionLayers(config.policy));
|
|
14760
16302
|
const networkShell = shellInput.kind === "shell" && NETWORK_COMMAND_RE.test(shellInput.command);
|
|
14761
16303
|
const shellReview = !isManual && shellInput.kind === "uninspectable";
|
|
14762
16304
|
if (shellReview) {
|
|
@@ -14853,13 +16395,173 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
14853
16395
|
}
|
|
14854
16396
|
}
|
|
14855
16397
|
}
|
|
14856
|
-
|
|
16398
|
+
const mcpHookTool = options?.sessionTaintGated === void 0 && isMcpToolName(toolName);
|
|
16399
|
+
let daemonSaysExposed;
|
|
16400
|
+
if (!taintWarning && meta?.sessionId && (networkShell || isWriteTool(toolName) || options?.sessionTaintGated === true || mcpHookTool)) {
|
|
14857
16401
|
const sessionTaint = await checkSessionTaint(meta.sessionId);
|
|
16402
|
+
if (typeof sessionTaint.exposed === "boolean") daemonSaysExposed = sessionTaint.exposed;
|
|
14858
16403
|
if (sessionTaint.tainted && sessionTaint.record) {
|
|
14859
|
-
const kind = isWriteTool(toolName) ? "write" : networkShell ? "network" : "state-changing";
|
|
16404
|
+
const kind = isWriteTool(toolName) ? "write" : networkShell ? "network" : mcpHookTool ? "MCP" : "state-changing";
|
|
14860
16405
|
taintWarning = `\u26A0\uFE0F node9 flagged this session \u2014 earlier tool output contained ${sessionTaint.record.source}. Approve this ${kind} action before it proceeds.`;
|
|
14861
16406
|
}
|
|
14862
16407
|
}
|
|
16408
|
+
let exposureReview = null;
|
|
16409
|
+
let exposureKeys = [];
|
|
16410
|
+
let exposureDestinations = [];
|
|
16411
|
+
let exposureFromEid;
|
|
16412
|
+
const ucPolicy = config.policy.untrustedContent;
|
|
16413
|
+
if (ucPolicy?.enabled && !taintWarning && meta?.sessionId && !isManual) {
|
|
16414
|
+
const role = toolRole(toolName, ucPolicy);
|
|
16415
|
+
const acts = role.action || options?.sessionTaintGated === true;
|
|
16416
|
+
const shellExp = shellInput.kind === "shell" ? classifyShellExposure(shellInput.command) : null;
|
|
16417
|
+
const bareName = splitMcpToolName(toolName)?.bare;
|
|
16418
|
+
const mcpFileWrite = bareName !== void 0 && fileToolOperation(bareName, args) === "write";
|
|
16419
|
+
const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, inspectionLayers(config.policy)).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
|
|
16420
|
+
const sendsSomething = acts && !mcpFileWrite || !!shellExp?.sends;
|
|
16421
|
+
const mayBeExposed = daemonSaysExposed ?? (maybeSessionExposed(meta.sessionId) || writes.length > 0 && judgeWritesAfterExposure({
|
|
16422
|
+
accesses: writes,
|
|
16423
|
+
args,
|
|
16424
|
+
shellCommand: shellInput.kind === "shell" ? shellInput.command : null,
|
|
16425
|
+
cwd: options?.cwd,
|
|
16426
|
+
inContent: /* @__PURE__ */ new Set(),
|
|
16427
|
+
isKnown: () => true
|
|
16428
|
+
}).length > 0);
|
|
16429
|
+
const judged = sendsSomething || (!!shellExp?.fetches || writes.length > 0) && mayBeExposed;
|
|
16430
|
+
if (judged) {
|
|
16431
|
+
const exposure = await checkSessionExposure(meta.sessionId);
|
|
16432
|
+
if (exposure.exposed && exposure.record) {
|
|
16433
|
+
const rec = exposure.record;
|
|
16434
|
+
const dests = new Set(
|
|
16435
|
+
acts && !shellExp && !mcpFileWrite ? extractArgDestinations(args) : []
|
|
16436
|
+
);
|
|
16437
|
+
const fetchOnly = /* @__PURE__ */ new Set();
|
|
16438
|
+
const projectHosts = shellExp ? knownProjectHosts(options?.cwd) : null;
|
|
16439
|
+
let unknownRemote = false;
|
|
16440
|
+
if (shellExp) {
|
|
16441
|
+
if (shellExp.sends) {
|
|
16442
|
+
for (const d of shellExp.destinations) dests.add(d);
|
|
16443
|
+
for (const name of shellExp.gitRemotes) {
|
|
16444
|
+
const remotes = projectHosts.remotes;
|
|
16445
|
+
const hosts = name === "" ? remotes.get("origin") ?? [...remotes.values()].flat() : remotes.get(name);
|
|
16446
|
+
if (hosts) for (const h of hosts) dests.add(h);
|
|
16447
|
+
else if (projectHosts.remotes.size > 0) unknownRemote = true;
|
|
16448
|
+
}
|
|
16449
|
+
}
|
|
16450
|
+
if (shellExp.fetches) {
|
|
16451
|
+
for (const h of shellExp.fetchDestinations) if (!dests.has(h)) fetchOnly.add(h);
|
|
16452
|
+
}
|
|
16453
|
+
}
|
|
16454
|
+
const inContent = new Set(rec.destinations);
|
|
16455
|
+
const mentioned = new Set(rec.userMentioned);
|
|
16456
|
+
const approved = new Set(rec.approved);
|
|
16457
|
+
const held = [];
|
|
16458
|
+
const logged = [];
|
|
16459
|
+
const writeProjectHosts = projectHosts ?? knownProjectHosts(options?.cwd);
|
|
16460
|
+
const isKnown = (d) => {
|
|
16461
|
+
const domain = destinationDomain(d);
|
|
16462
|
+
const value = destinationValue(d);
|
|
16463
|
+
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));
|
|
16464
|
+
};
|
|
16465
|
+
const approvedOnce = (key) => ucPolicy.afterExposure === "ask-once" && approved.has(key);
|
|
16466
|
+
const repos = (shellExp?.sends ? shellExp.repos : []).filter(
|
|
16467
|
+
(r) => writeProjectHosts.hosts.has(`host:${r.split("/")[0]}`)
|
|
16468
|
+
);
|
|
16469
|
+
const repoHosts = new Set(repos.map((r) => `host:${r.split("/")[0]}`));
|
|
16470
|
+
for (const r of repos) {
|
|
16471
|
+
if (writeProjectHosts.repos.has(r) || approvedOnce(`${toolName}|repo:${r}`))
|
|
16472
|
+
logged.push(`repo:${r}`);
|
|
16473
|
+
else held.push({ dest: "", why: "repository", repo: r });
|
|
16474
|
+
}
|
|
16475
|
+
for (const d of dests) {
|
|
16476
|
+
if (repoHosts.has(d)) continue;
|
|
16477
|
+
if (approvedOnce(`${toolName}|${d}`) || isKnown(d)) logged.push(d);
|
|
16478
|
+
else if (inContent.has(d)) held.push({ dest: d, why: "appeared in what was read" });
|
|
16479
|
+
else held.push({ dest: d, why: "not known to this session" });
|
|
16480
|
+
}
|
|
16481
|
+
for (const d of fetchOnly) {
|
|
16482
|
+
if (approvedOnce(`${toolName}|${d}`) || isKnown(d) || !inContent.has(d)) logged.push(d);
|
|
16483
|
+
else held.push({ dest: d, why: "appeared in what was read" });
|
|
16484
|
+
}
|
|
16485
|
+
if (shellExp?.sends && (shellExp.unresolved || unknownRemote || shellExp.destinations.length === 0 && shellExp.gitRemotes.length === 0) && !approvedOnce(`${toolName}|unresolved`)) {
|
|
16486
|
+
held.push({ dest: "", why: "unresolved" });
|
|
16487
|
+
}
|
|
16488
|
+
const bare = splitMcpToolName(toolName)?.bare ?? toolName;
|
|
16489
|
+
if (dests.size === 0 && acts && !mcpFileWrite && (role.destructive === true || isDestructiveToolName(bare))) {
|
|
16490
|
+
held.push({ dest: "", why: "destructive" });
|
|
16491
|
+
}
|
|
16492
|
+
const writeHolds = writes.length > 0 ? judgeWritesAfterExposure({
|
|
16493
|
+
accesses: writes,
|
|
16494
|
+
args,
|
|
16495
|
+
shellCommand: shellInput.kind === "shell" ? shellInput.command : null,
|
|
16496
|
+
cwd: options?.cwd,
|
|
16497
|
+
inContent,
|
|
16498
|
+
isKnown
|
|
16499
|
+
}).filter((h) => {
|
|
16500
|
+
const key = `${toolName}|${h.why === "content-destination" ? h.destination : `path:${h.path}`}`;
|
|
16501
|
+
if (ucPolicy.afterExposure === "ask-once" && approved.has(key)) {
|
|
16502
|
+
logged.push(key);
|
|
16503
|
+
return false;
|
|
16504
|
+
}
|
|
16505
|
+
return true;
|
|
16506
|
+
}) : [];
|
|
16507
|
+
const last = rec.sources[rec.sources.length - 1];
|
|
16508
|
+
const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
|
|
16509
|
+
const when = last ? new Date(last.at).toTimeString().slice(0, 5) : "";
|
|
16510
|
+
const read = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
|
|
16511
|
+
if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
|
|
16512
|
+
const noun = (d) => {
|
|
16513
|
+
const k = destinationKind(d);
|
|
16514
|
+
return k === "email" ? "an address" : k === "host" ? "a host" : "an account";
|
|
16515
|
+
};
|
|
16516
|
+
const parts = held.map(
|
|
16517
|
+
(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`
|
|
16518
|
+
);
|
|
16519
|
+
const home = import_os18.default.homedir();
|
|
16520
|
+
const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
|
|
16521
|
+
for (const w of writeHolds) {
|
|
16522
|
+
parts.push(
|
|
16523
|
+
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}`
|
|
16524
|
+
);
|
|
16525
|
+
}
|
|
16526
|
+
exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read}. This call ${parts.join("; and ")}.`;
|
|
16527
|
+
exposureFromEid = last?.eid;
|
|
16528
|
+
if (exposureFromEid) meta = { ...meta, exposureFromEid };
|
|
16529
|
+
exposureKeys = [
|
|
16530
|
+
...held.filter((h) => h.dest || h.why === "unresolved" || h.why === "repository").map(
|
|
16531
|
+
(h) => h.why === "repository" ? `${toolName}|repo:${h.repo}` : `${toolName}|${h.dest || "unresolved"}`
|
|
16532
|
+
),
|
|
16533
|
+
...writeHolds.map(
|
|
16534
|
+
(w) => `${toolName}|${w.why === "content-destination" ? w.destination : `path:${w.path}`}`
|
|
16535
|
+
)
|
|
16536
|
+
];
|
|
16537
|
+
exposureDestinations = held.filter((h) => h.dest).map((h) => h.dest).slice(0, 10);
|
|
16538
|
+
if (exposureDestinations.length > 0 && options?.deferReview) {
|
|
16539
|
+
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("; ") + ".";
|
|
16540
|
+
}
|
|
16541
|
+
} else if (held.length > 0 || writeHolds.length > 0 || logged.length > 0) {
|
|
16542
|
+
meta = {
|
|
16543
|
+
...meta,
|
|
16544
|
+
exposure: { kind: "logged", destinations: logged.length },
|
|
16545
|
+
...last?.eid ? { exposureFromEid: last.eid } : {}
|
|
16546
|
+
};
|
|
16547
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
16548
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16549
|
+
event: "exposure-logged",
|
|
16550
|
+
tool: toolName,
|
|
16551
|
+
held: held.length + writeHolds.length,
|
|
16552
|
+
known: logged.length,
|
|
16553
|
+
sessionId: meta.sessionId
|
|
16554
|
+
});
|
|
16555
|
+
}
|
|
16556
|
+
} else if (exposure.daemonUnavailable) {
|
|
16557
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
16558
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16559
|
+
event: "exposure-unavailable",
|
|
16560
|
+
tool: toolName
|
|
16561
|
+
});
|
|
16562
|
+
}
|
|
16563
|
+
}
|
|
16564
|
+
}
|
|
14863
16565
|
const canaryVals = safeCanaryValues();
|
|
14864
16566
|
if (canaryVals.length > 0) {
|
|
14865
16567
|
const canaryHit = matchCanaryArgs(args, canaryVals);
|
|
@@ -15100,7 +16802,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
15100
16802
|
}
|
|
15101
16803
|
let appPermReview = null;
|
|
15102
16804
|
let appPermReviewTool = null;
|
|
15103
|
-
const callSpecificReview = () => fileReview || shellReview || dlpReviewFlagged || !!appPermReview || !!taintWarning || !!packageReview;
|
|
16805
|
+
const callSpecificReview = () => fileReview || shellReview || dlpReviewFlagged || !!appPermReview || !!taintWarning || !!exposureReview || !!packageReview;
|
|
15104
16806
|
const standingTrustApplies = () => !callSpecificReview() && !hardBlockDowngraded;
|
|
15105
16807
|
const reviewDemanded = () => callSpecificReview() || localSmartRuleMatched === true || options?.localSmartRuleMatched === true;
|
|
15106
16808
|
if (meta?.serverKey) {
|
|
@@ -15345,6 +17047,12 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
15345
17047
|
appPermReview ? `${taintWarning}
|
|
15346
17048
|
${appPermReview}` : taintWarning
|
|
15347
17049
|
);
|
|
17050
|
+
} else if (exposureReview) {
|
|
17051
|
+
explainableLabel = EXPOSURE_LABEL;
|
|
17052
|
+
riskMetadata = computeRiskMetadata(args, 6, explainableLabel, void 0, void 0, "exposure");
|
|
17053
|
+
riskMetadata.ruleDescription = (appPermReview ? `${exposureReview}
|
|
17054
|
+
${appPermReview}` : exposureReview).slice(0, 1024);
|
|
17055
|
+
if (exposureDestinations.length > 0) riskMetadata.exposureDestinations = exposureDestinations;
|
|
15348
17056
|
} else if (appPermReview) {
|
|
15349
17057
|
if (dlpReviewFlagged) {
|
|
15350
17058
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review) + \u{1F512} App Permission (Review)";
|
|
@@ -15387,7 +17095,9 @@ ${packageReview}`;
|
|
|
15387
17095
|
// The prompt must say WHY: the taint sentence beats the bare label so
|
|
15388
17096
|
// the dev sees the actual risk context inline. (No appPermReview term:
|
|
15389
17097
|
// deferReview and serverKey never co-occur in production — see above.)
|
|
15390
|
-
reason: taintWarning || packageReview || explainableLabel || "Node9 flagged this action for review.",
|
|
17098
|
+
reason: taintWarning || packageReview || exposureReview || explainableLabel || "Node9 flagged this action for review.",
|
|
17099
|
+
...exposureReview && exposureKeys.length > 0 ? { exposureKeys } : {},
|
|
17100
|
+
...exposureReview && exposureFromEid ? { exposureFromEid } : {},
|
|
15391
17101
|
ruleDescription: policyRuleDescription,
|
|
15392
17102
|
blockedByLabel: explainableLabel
|
|
15393
17103
|
};
|
|
@@ -15481,10 +17191,14 @@ ${packageReview}`;
|
|
|
15481
17191
|
// auto-allows. This process (the long-running gateway — appPermReview
|
|
15482
17192
|
// only ever happens here) stays alive to run its own racers, so the
|
|
15483
17193
|
// daemon just holds the card and waits for the human decision.
|
|
15484
|
-
|
|
17194
|
+
// fw#318 P1.1: the same for every call-specific review (exposure,
|
|
17195
|
+
// session taint, DLP, file review): the re-run carries no session id
|
|
17196
|
+
// and cannot see these reasons, so it must never answer the card.
|
|
17197
|
+
appPermReview || callSpecificReview() ? true : void 0,
|
|
15485
17198
|
void 0,
|
|
15486
17199
|
localSmartRuleMatched || options?.localSmartRuleMatched,
|
|
15487
|
-
options?.socketActivitySent
|
|
17200
|
+
options?.socketActivitySent,
|
|
17201
|
+
meta?.sessionId
|
|
15488
17202
|
);
|
|
15489
17203
|
daemonEntryId = entry.id;
|
|
15490
17204
|
daemonAllowCount = entry.allowCount;
|
|
@@ -15525,8 +17239,15 @@ ${packageReview}`;
|
|
|
15525
17239
|
policyMatchedField,
|
|
15526
17240
|
policyMatchedWord,
|
|
15527
17241
|
daemonAllowCount,
|
|
15528
|
-
riskMetadata?.ruleDescription
|
|
17242
|
+
riskMetadata?.ruleDescription,
|
|
17243
|
+
exposureDestinations
|
|
15529
17244
|
);
|
|
17245
|
+
if (decision === "trust_recipients") {
|
|
17246
|
+
if (config.policySource !== "workspace") {
|
|
17247
|
+
for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
|
|
17248
|
+
}
|
|
17249
|
+
return { approved: true, checkedBy: "daemon", decisionSource: "native" };
|
|
17250
|
+
}
|
|
15530
17251
|
if (decision === "always_allow") {
|
|
15531
17252
|
if (standingTrustApplies()) {
|
|
15532
17253
|
writeTrustSession(toolName, 36e5, args);
|
|
@@ -15627,6 +17348,9 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
15627
17348
|
});
|
|
15628
17349
|
}
|
|
15629
17350
|
});
|
|
17351
|
+
if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
|
|
17352
|
+
await approveSessionExposure(meta.sessionId, exposureKeys);
|
|
17353
|
+
}
|
|
15630
17354
|
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
15631
17355
|
await resolveNode9SaaS(
|
|
15632
17356
|
cloudRequestId,
|