@node9/proxy 2.16.2 → 2.17.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 +52 -21
- package/dist/cli.mjs +52 -21
- package/dist/index.js +52 -21
- package/dist/index.mjs +52 -21
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2265,18 +2265,18 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
2265
2265
|
return null;
|
|
2266
2266
|
}
|
|
2267
2267
|
function hostMatches(host, pattern) {
|
|
2268
|
-
const
|
|
2269
|
-
const p = pattern.toLowerCase().trim();
|
|
2268
|
+
const p = pattern.trim().toLowerCase();
|
|
2270
2269
|
if (!p) return false;
|
|
2271
2270
|
if (p === "*") return true;
|
|
2271
|
+
const h = canonicalHost(host);
|
|
2272
2272
|
if (p.startsWith("*.")) {
|
|
2273
|
-
const suffix = p.slice(2);
|
|
2273
|
+
const suffix = canonicalHost(p.slice(2));
|
|
2274
2274
|
return h === suffix || h.endsWith("." + suffix);
|
|
2275
2275
|
}
|
|
2276
|
-
return h === p;
|
|
2276
|
+
return h === canonicalHost(p);
|
|
2277
2277
|
}
|
|
2278
2278
|
function matchesAny(host, patterns) {
|
|
2279
|
-
for (const p of patterns) if (hostMatches(host, p)) return true;
|
|
2279
|
+
for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
|
|
2280
2280
|
return false;
|
|
2281
2281
|
}
|
|
2282
2282
|
function isUniqueLocalV6(host) {
|
|
@@ -2286,13 +2286,17 @@ function isUniqueLocalV6(host) {
|
|
|
2286
2286
|
return g !== null && (g[0] & 65024) === 64512;
|
|
2287
2287
|
}
|
|
2288
2288
|
function isPrivateHost(host) {
|
|
2289
|
-
const h = host.trim().toLowerCase();
|
|
2289
|
+
const h = host.trim().toLowerCase().replace(/\.$/, "");
|
|
2290
2290
|
const m = classifySsrf(h);
|
|
2291
2291
|
if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
|
|
2292
2292
|
if (h === "localhost") return true;
|
|
2293
2293
|
if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
|
|
2294
2294
|
return isUniqueLocalV6(h);
|
|
2295
2295
|
}
|
|
2296
|
+
function canonicalHost(host) {
|
|
2297
|
+
const ip = normalizeIpLiteral(host);
|
|
2298
|
+
return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
|
|
2299
|
+
}
|
|
2296
2300
|
function evaluateEgress(dests, policy) {
|
|
2297
2301
|
if (!policy.enabled) return null;
|
|
2298
2302
|
let review = null;
|
|
@@ -2544,6 +2548,22 @@ function hostOf(value) {
|
|
|
2544
2548
|
return null;
|
|
2545
2549
|
}
|
|
2546
2550
|
}
|
|
2551
|
+
function extractToolDestinations(toolName, args) {
|
|
2552
|
+
try {
|
|
2553
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
2554
|
+
if (!paths) return [];
|
|
2555
|
+
const out = [];
|
|
2556
|
+
for (const path78 of paths) {
|
|
2557
|
+
for (const value of valuesAt(args, path78)) {
|
|
2558
|
+
const host = hostOf(value);
|
|
2559
|
+
if (host) out.push({ host, binary: toolName, raw: value });
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return out;
|
|
2563
|
+
} catch {
|
|
2564
|
+
return [];
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2547
2567
|
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
2548
2568
|
try {
|
|
2549
2569
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
@@ -2645,6 +2665,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
|
2645
2665
|
tier: 3
|
|
2646
2666
|
};
|
|
2647
2667
|
}
|
|
2668
|
+
function egressPolicyVerdict(eg) {
|
|
2669
|
+
return {
|
|
2670
|
+
decision: eg.verdict,
|
|
2671
|
+
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
2672
|
+
reason: eg.reason,
|
|
2673
|
+
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
2674
|
+
ruleDescription: eg.reason,
|
|
2675
|
+
tier: eg.verdict === "block" ? 3 : 4
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2648
2678
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
2649
2679
|
const { agent, cwd, activeEnvironment } = context;
|
|
2650
2680
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
@@ -2679,7 +2709,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2679
2709
|
};
|
|
2680
2710
|
}
|
|
2681
2711
|
}
|
|
2682
|
-
|
|
2712
|
+
const pendingToolEgress = config.policy.egress?.enabled ? (() => {
|
|
2713
|
+
const dests = extractToolDestinations(toolName, args);
|
|
2714
|
+
const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
|
|
2715
|
+
return eg ? egressPolicyVerdict(eg) : void 0;
|
|
2716
|
+
})() : void 0;
|
|
2717
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) {
|
|
2718
|
+
return pendingToolEgress ?? { decision: "allow" };
|
|
2719
|
+
}
|
|
2683
2720
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2684
2721
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2685
2722
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
@@ -2768,6 +2805,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2768
2805
|
}
|
|
2769
2806
|
let allTokens = [];
|
|
2770
2807
|
let pathTokens = [];
|
|
2808
|
+
if (pendingToolEgress) return pendingToolEgress;
|
|
2771
2809
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
2772
2810
|
if (shellCommand) {
|
|
2773
2811
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
@@ -2833,16 +2871,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2833
2871
|
const dests = extractShellDestinations(shellCommand);
|
|
2834
2872
|
if (dests.length > 0) {
|
|
2835
2873
|
const eg = evaluateEgress(dests, config.policy.egress);
|
|
2836
|
-
if (eg)
|
|
2837
|
-
return {
|
|
2838
|
-
decision: eg.verdict,
|
|
2839
|
-
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
2840
|
-
reason: eg.reason,
|
|
2841
|
-
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
2842
|
-
ruleDescription: eg.reason,
|
|
2843
|
-
tier: eg.verdict === "block" ? 3 : 4
|
|
2844
|
-
};
|
|
2845
|
-
}
|
|
2874
|
+
if (eg) return egressPolicyVerdict(eg);
|
|
2846
2875
|
}
|
|
2847
2876
|
}
|
|
2848
2877
|
const firstToken = analyzed.actions[0] ?? "";
|
|
@@ -9945,8 +9974,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
9945
9974
|
};
|
|
9946
9975
|
}
|
|
9947
9976
|
}
|
|
9977
|
+
const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
|
|
9978
|
+
const judge = !isIgnoredTool2(toolName) || declaredEgress;
|
|
9948
9979
|
if (isObserveMode) {
|
|
9949
|
-
if (
|
|
9980
|
+
if (judge) {
|
|
9950
9981
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
9951
9982
|
const wouldBlock = policyResult.decision === "block";
|
|
9952
9983
|
if (!isManual)
|
|
@@ -9971,7 +10002,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
9971
10002
|
return { approved: true, checkedBy: "audit" };
|
|
9972
10003
|
}
|
|
9973
10004
|
if (config.settings.mode === "audit") {
|
|
9974
|
-
if (
|
|
10005
|
+
if (judge) {
|
|
9975
10006
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
9976
10007
|
if (policyResult.decision === "review") {
|
|
9977
10008
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
@@ -10010,9 +10041,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
10010
10041
|
appPermReviewTool = bareTool;
|
|
10011
10042
|
}
|
|
10012
10043
|
}
|
|
10013
|
-
if (!taintWarning &&
|
|
10044
|
+
if (!taintWarning && judge) {
|
|
10014
10045
|
const ld = config.policy.loopDetection;
|
|
10015
|
-
if (ld.enabled && !appPermReview) {
|
|
10046
|
+
if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
|
|
10016
10047
|
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
10017
10048
|
if (loopResult.looping) {
|
|
10018
10049
|
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
package/dist/cli.mjs
CHANGED
|
@@ -2276,18 +2276,18 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
2276
2276
|
return null;
|
|
2277
2277
|
}
|
|
2278
2278
|
function hostMatches(host, pattern) {
|
|
2279
|
-
const
|
|
2280
|
-
const p = pattern.toLowerCase().trim();
|
|
2279
|
+
const p = pattern.trim().toLowerCase();
|
|
2281
2280
|
if (!p) return false;
|
|
2282
2281
|
if (p === "*") return true;
|
|
2282
|
+
const h = canonicalHost(host);
|
|
2283
2283
|
if (p.startsWith("*.")) {
|
|
2284
|
-
const suffix = p.slice(2);
|
|
2284
|
+
const suffix = canonicalHost(p.slice(2));
|
|
2285
2285
|
return h === suffix || h.endsWith("." + suffix);
|
|
2286
2286
|
}
|
|
2287
|
-
return h === p;
|
|
2287
|
+
return h === canonicalHost(p);
|
|
2288
2288
|
}
|
|
2289
2289
|
function matchesAny(host, patterns) {
|
|
2290
|
-
for (const p of patterns) if (hostMatches(host, p)) return true;
|
|
2290
|
+
for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
|
|
2291
2291
|
return false;
|
|
2292
2292
|
}
|
|
2293
2293
|
function isUniqueLocalV6(host) {
|
|
@@ -2297,13 +2297,17 @@ function isUniqueLocalV6(host) {
|
|
|
2297
2297
|
return g !== null && (g[0] & 65024) === 64512;
|
|
2298
2298
|
}
|
|
2299
2299
|
function isPrivateHost(host) {
|
|
2300
|
-
const h = host.trim().toLowerCase();
|
|
2300
|
+
const h = host.trim().toLowerCase().replace(/\.$/, "");
|
|
2301
2301
|
const m = classifySsrf(h);
|
|
2302
2302
|
if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
|
|
2303
2303
|
if (h === "localhost") return true;
|
|
2304
2304
|
if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
|
|
2305
2305
|
return isUniqueLocalV6(h);
|
|
2306
2306
|
}
|
|
2307
|
+
function canonicalHost(host) {
|
|
2308
|
+
const ip = normalizeIpLiteral(host);
|
|
2309
|
+
return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
|
|
2310
|
+
}
|
|
2307
2311
|
function evaluateEgress(dests, policy) {
|
|
2308
2312
|
if (!policy.enabled) return null;
|
|
2309
2313
|
let review = null;
|
|
@@ -2555,6 +2559,22 @@ function hostOf(value) {
|
|
|
2555
2559
|
return null;
|
|
2556
2560
|
}
|
|
2557
2561
|
}
|
|
2562
|
+
function extractToolDestinations(toolName, args) {
|
|
2563
|
+
try {
|
|
2564
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
2565
|
+
if (!paths) return [];
|
|
2566
|
+
const out = [];
|
|
2567
|
+
for (const path78 of paths) {
|
|
2568
|
+
for (const value of valuesAt(args, path78)) {
|
|
2569
|
+
const host = hostOf(value);
|
|
2570
|
+
if (host) out.push({ host, binary: toolName, raw: value });
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
return out;
|
|
2574
|
+
} catch {
|
|
2575
|
+
return [];
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2558
2578
|
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
2559
2579
|
try {
|
|
2560
2580
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
@@ -2656,6 +2676,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
|
2656
2676
|
tier: 3
|
|
2657
2677
|
};
|
|
2658
2678
|
}
|
|
2679
|
+
function egressPolicyVerdict(eg) {
|
|
2680
|
+
return {
|
|
2681
|
+
decision: eg.verdict,
|
|
2682
|
+
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
2683
|
+
reason: eg.reason,
|
|
2684
|
+
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
2685
|
+
ruleDescription: eg.reason,
|
|
2686
|
+
tier: eg.verdict === "block" ? 3 : 4
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2659
2689
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
2660
2690
|
const { agent, cwd, activeEnvironment } = context;
|
|
2661
2691
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
@@ -2690,7 +2720,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2690
2720
|
};
|
|
2691
2721
|
}
|
|
2692
2722
|
}
|
|
2693
|
-
|
|
2723
|
+
const pendingToolEgress = config.policy.egress?.enabled ? (() => {
|
|
2724
|
+
const dests = extractToolDestinations(toolName, args);
|
|
2725
|
+
const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
|
|
2726
|
+
return eg ? egressPolicyVerdict(eg) : void 0;
|
|
2727
|
+
})() : void 0;
|
|
2728
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) {
|
|
2729
|
+
return pendingToolEgress ?? { decision: "allow" };
|
|
2730
|
+
}
|
|
2694
2731
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2695
2732
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2696
2733
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
@@ -2779,6 +2816,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2779
2816
|
}
|
|
2780
2817
|
let allTokens = [];
|
|
2781
2818
|
let pathTokens = [];
|
|
2819
|
+
if (pendingToolEgress) return pendingToolEgress;
|
|
2782
2820
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
2783
2821
|
if (shellCommand) {
|
|
2784
2822
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
@@ -2844,16 +2882,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2844
2882
|
const dests = extractShellDestinations(shellCommand);
|
|
2845
2883
|
if (dests.length > 0) {
|
|
2846
2884
|
const eg = evaluateEgress(dests, config.policy.egress);
|
|
2847
|
-
if (eg)
|
|
2848
|
-
return {
|
|
2849
|
-
decision: eg.verdict,
|
|
2850
|
-
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
2851
|
-
reason: eg.reason,
|
|
2852
|
-
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
2853
|
-
ruleDescription: eg.reason,
|
|
2854
|
-
tier: eg.verdict === "block" ? 3 : 4
|
|
2855
|
-
};
|
|
2856
|
-
}
|
|
2885
|
+
if (eg) return egressPolicyVerdict(eg);
|
|
2857
2886
|
}
|
|
2858
2887
|
}
|
|
2859
2888
|
const firstToken = analyzed.actions[0] ?? "";
|
|
@@ -9946,8 +9975,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
9946
9975
|
};
|
|
9947
9976
|
}
|
|
9948
9977
|
}
|
|
9978
|
+
const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
|
|
9979
|
+
const judge = !isIgnoredTool2(toolName) || declaredEgress;
|
|
9949
9980
|
if (isObserveMode) {
|
|
9950
|
-
if (
|
|
9981
|
+
if (judge) {
|
|
9951
9982
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
9952
9983
|
const wouldBlock = policyResult.decision === "block";
|
|
9953
9984
|
if (!isManual)
|
|
@@ -9972,7 +10003,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
9972
10003
|
return { approved: true, checkedBy: "audit" };
|
|
9973
10004
|
}
|
|
9974
10005
|
if (config.settings.mode === "audit") {
|
|
9975
|
-
if (
|
|
10006
|
+
if (judge) {
|
|
9976
10007
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
9977
10008
|
if (policyResult.decision === "review") {
|
|
9978
10009
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
@@ -10011,9 +10042,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
10011
10042
|
appPermReviewTool = bareTool;
|
|
10012
10043
|
}
|
|
10013
10044
|
}
|
|
10014
|
-
if (!taintWarning &&
|
|
10045
|
+
if (!taintWarning && judge) {
|
|
10015
10046
|
const ld = config.policy.loopDetection;
|
|
10016
|
-
if (ld.enabled && !appPermReview) {
|
|
10047
|
+
if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
|
|
10017
10048
|
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
10018
10049
|
if (loopResult.looping) {
|
|
10019
10050
|
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
package/dist/index.js
CHANGED
|
@@ -3675,18 +3675,18 @@ var DEFAULT_EGRESS_ALLOWLIST = [
|
|
|
3675
3675
|
"*.ubuntu.com"
|
|
3676
3676
|
];
|
|
3677
3677
|
function hostMatches(host, pattern) {
|
|
3678
|
-
const
|
|
3679
|
-
const p = pattern.toLowerCase().trim();
|
|
3678
|
+
const p = pattern.trim().toLowerCase();
|
|
3680
3679
|
if (!p) return false;
|
|
3681
3680
|
if (p === "*") return true;
|
|
3681
|
+
const h = canonicalHost(host);
|
|
3682
3682
|
if (p.startsWith("*.")) {
|
|
3683
|
-
const suffix = p.slice(2);
|
|
3683
|
+
const suffix = canonicalHost(p.slice(2));
|
|
3684
3684
|
return h === suffix || h.endsWith("." + suffix);
|
|
3685
3685
|
}
|
|
3686
|
-
return h === p;
|
|
3686
|
+
return h === canonicalHost(p);
|
|
3687
3687
|
}
|
|
3688
3688
|
function matchesAny(host, patterns) {
|
|
3689
|
-
for (const p of patterns) if (hostMatches(host, p)) return true;
|
|
3689
|
+
for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
|
|
3690
3690
|
return false;
|
|
3691
3691
|
}
|
|
3692
3692
|
var PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
|
|
@@ -3697,13 +3697,17 @@ function isUniqueLocalV6(host) {
|
|
|
3697
3697
|
return g !== null && (g[0] & 65024) === 64512;
|
|
3698
3698
|
}
|
|
3699
3699
|
function isPrivateHost(host) {
|
|
3700
|
-
const h = host.trim().toLowerCase();
|
|
3700
|
+
const h = host.trim().toLowerCase().replace(/\.$/, "");
|
|
3701
3701
|
const m = classifySsrf(h);
|
|
3702
3702
|
if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
|
|
3703
3703
|
if (h === "localhost") return true;
|
|
3704
3704
|
if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
|
|
3705
3705
|
return isUniqueLocalV6(h);
|
|
3706
3706
|
}
|
|
3707
|
+
function canonicalHost(host) {
|
|
3708
|
+
const ip = normalizeIpLiteral(host);
|
|
3709
|
+
return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
|
|
3710
|
+
}
|
|
3707
3711
|
function evaluateEgress(dests, policy) {
|
|
3708
3712
|
if (!policy.enabled) return null;
|
|
3709
3713
|
let review = null;
|
|
@@ -4085,6 +4089,22 @@ function hostOf(value) {
|
|
|
4085
4089
|
return null;
|
|
4086
4090
|
}
|
|
4087
4091
|
}
|
|
4092
|
+
function extractToolDestinations(toolName, args) {
|
|
4093
|
+
try {
|
|
4094
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4095
|
+
if (!paths) return [];
|
|
4096
|
+
const out = [];
|
|
4097
|
+
for (const path16 of paths) {
|
|
4098
|
+
for (const value of valuesAt(args, path16)) {
|
|
4099
|
+
const host = hostOf(value);
|
|
4100
|
+
if (host) out.push({ host, binary: toolName, raw: value });
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
return out;
|
|
4104
|
+
} catch {
|
|
4105
|
+
return [];
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4088
4108
|
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
4089
4109
|
try {
|
|
4090
4110
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
@@ -4192,6 +4212,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
|
4192
4212
|
tier: 3
|
|
4193
4213
|
};
|
|
4194
4214
|
}
|
|
4215
|
+
function egressPolicyVerdict(eg) {
|
|
4216
|
+
return {
|
|
4217
|
+
decision: eg.verdict,
|
|
4218
|
+
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
4219
|
+
reason: eg.reason,
|
|
4220
|
+
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
4221
|
+
ruleDescription: eg.reason,
|
|
4222
|
+
tier: eg.verdict === "block" ? 3 : 4
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4195
4225
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
4196
4226
|
const { agent, cwd, activeEnvironment } = context;
|
|
4197
4227
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
@@ -4226,7 +4256,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4226
4256
|
};
|
|
4227
4257
|
}
|
|
4228
4258
|
}
|
|
4229
|
-
|
|
4259
|
+
const pendingToolEgress = config.policy.egress?.enabled ? (() => {
|
|
4260
|
+
const dests = extractToolDestinations(toolName, args);
|
|
4261
|
+
const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
|
|
4262
|
+
return eg ? egressPolicyVerdict(eg) : void 0;
|
|
4263
|
+
})() : void 0;
|
|
4264
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) {
|
|
4265
|
+
return pendingToolEgress ?? { decision: "allow" };
|
|
4266
|
+
}
|
|
4230
4267
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
4231
4268
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
4232
4269
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
@@ -4315,6 +4352,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4315
4352
|
}
|
|
4316
4353
|
let allTokens = [];
|
|
4317
4354
|
let pathTokens = [];
|
|
4355
|
+
if (pendingToolEgress) return pendingToolEgress;
|
|
4318
4356
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
4319
4357
|
if (shellCommand) {
|
|
4320
4358
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
@@ -4380,16 +4418,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4380
4418
|
const dests = extractShellDestinations(shellCommand);
|
|
4381
4419
|
if (dests.length > 0) {
|
|
4382
4420
|
const eg = evaluateEgress(dests, config.policy.egress);
|
|
4383
|
-
if (eg)
|
|
4384
|
-
return {
|
|
4385
|
-
decision: eg.verdict,
|
|
4386
|
-
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
4387
|
-
reason: eg.reason,
|
|
4388
|
-
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
4389
|
-
ruleDescription: eg.reason,
|
|
4390
|
-
tier: eg.verdict === "block" ? 3 : 4
|
|
4391
|
-
};
|
|
4392
|
-
}
|
|
4421
|
+
if (eg) return egressPolicyVerdict(eg);
|
|
4393
4422
|
}
|
|
4394
4423
|
}
|
|
4395
4424
|
const firstToken = analyzed.actions[0] ?? "";
|
|
@@ -8329,8 +8358,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8329
8358
|
};
|
|
8330
8359
|
}
|
|
8331
8360
|
}
|
|
8361
|
+
const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
|
|
8362
|
+
const judge = !isIgnoredTool2(toolName) || declaredEgress;
|
|
8332
8363
|
if (isObserveMode) {
|
|
8333
|
-
if (
|
|
8364
|
+
if (judge) {
|
|
8334
8365
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
8335
8366
|
const wouldBlock = policyResult.decision === "block";
|
|
8336
8367
|
if (!isManual)
|
|
@@ -8355,7 +8386,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8355
8386
|
return { approved: true, checkedBy: "audit" };
|
|
8356
8387
|
}
|
|
8357
8388
|
if (config.settings.mode === "audit") {
|
|
8358
|
-
if (
|
|
8389
|
+
if (judge) {
|
|
8359
8390
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
8360
8391
|
if (policyResult.decision === "review") {
|
|
8361
8392
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
@@ -8394,9 +8425,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8394
8425
|
appPermReviewTool = bareTool;
|
|
8395
8426
|
}
|
|
8396
8427
|
}
|
|
8397
|
-
if (!taintWarning &&
|
|
8428
|
+
if (!taintWarning && judge) {
|
|
8398
8429
|
const ld = config.policy.loopDetection;
|
|
8399
|
-
if (ld.enabled && !appPermReview) {
|
|
8430
|
+
if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
|
|
8400
8431
|
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
8401
8432
|
if (loopResult.looping) {
|
|
8402
8433
|
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
package/dist/index.mjs
CHANGED
|
@@ -3645,18 +3645,18 @@ var DEFAULT_EGRESS_ALLOWLIST = [
|
|
|
3645
3645
|
"*.ubuntu.com"
|
|
3646
3646
|
];
|
|
3647
3647
|
function hostMatches(host, pattern) {
|
|
3648
|
-
const
|
|
3649
|
-
const p = pattern.toLowerCase().trim();
|
|
3648
|
+
const p = pattern.trim().toLowerCase();
|
|
3650
3649
|
if (!p) return false;
|
|
3651
3650
|
if (p === "*") return true;
|
|
3651
|
+
const h = canonicalHost(host);
|
|
3652
3652
|
if (p.startsWith("*.")) {
|
|
3653
|
-
const suffix = p.slice(2);
|
|
3653
|
+
const suffix = canonicalHost(p.slice(2));
|
|
3654
3654
|
return h === suffix || h.endsWith("." + suffix);
|
|
3655
3655
|
}
|
|
3656
|
-
return h === p;
|
|
3656
|
+
return h === canonicalHost(p);
|
|
3657
3657
|
}
|
|
3658
3658
|
function matchesAny(host, patterns) {
|
|
3659
|
-
for (const p of patterns) if (hostMatches(host, p)) return true;
|
|
3659
|
+
for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
|
|
3660
3660
|
return false;
|
|
3661
3661
|
}
|
|
3662
3662
|
var PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
|
|
@@ -3667,13 +3667,17 @@ function isUniqueLocalV6(host) {
|
|
|
3667
3667
|
return g !== null && (g[0] & 65024) === 64512;
|
|
3668
3668
|
}
|
|
3669
3669
|
function isPrivateHost(host) {
|
|
3670
|
-
const h = host.trim().toLowerCase();
|
|
3670
|
+
const h = host.trim().toLowerCase().replace(/\.$/, "");
|
|
3671
3671
|
const m = classifySsrf(h);
|
|
3672
3672
|
if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
|
|
3673
3673
|
if (h === "localhost") return true;
|
|
3674
3674
|
if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
|
|
3675
3675
|
return isUniqueLocalV6(h);
|
|
3676
3676
|
}
|
|
3677
|
+
function canonicalHost(host) {
|
|
3678
|
+
const ip = normalizeIpLiteral(host);
|
|
3679
|
+
return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
|
|
3680
|
+
}
|
|
3677
3681
|
function evaluateEgress(dests, policy) {
|
|
3678
3682
|
if (!policy.enabled) return null;
|
|
3679
3683
|
let review = null;
|
|
@@ -4055,6 +4059,22 @@ function hostOf(value) {
|
|
|
4055
4059
|
return null;
|
|
4056
4060
|
}
|
|
4057
4061
|
}
|
|
4062
|
+
function extractToolDestinations(toolName, args) {
|
|
4063
|
+
try {
|
|
4064
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4065
|
+
if (!paths) return [];
|
|
4066
|
+
const out = [];
|
|
4067
|
+
for (const path16 of paths) {
|
|
4068
|
+
for (const value of valuesAt(args, path16)) {
|
|
4069
|
+
const host = hostOf(value);
|
|
4070
|
+
if (host) out.push({ host, binary: toolName, raw: value });
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
return out;
|
|
4074
|
+
} catch {
|
|
4075
|
+
return [];
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4058
4078
|
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
4059
4079
|
try {
|
|
4060
4080
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
@@ -4162,6 +4182,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
|
4162
4182
|
tier: 3
|
|
4163
4183
|
};
|
|
4164
4184
|
}
|
|
4185
|
+
function egressPolicyVerdict(eg) {
|
|
4186
|
+
return {
|
|
4187
|
+
decision: eg.verdict,
|
|
4188
|
+
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
4189
|
+
reason: eg.reason,
|
|
4190
|
+
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
4191
|
+
ruleDescription: eg.reason,
|
|
4192
|
+
tier: eg.verdict === "block" ? 3 : 4
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4165
4195
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
4166
4196
|
const { agent, cwd, activeEnvironment } = context;
|
|
4167
4197
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
@@ -4196,7 +4226,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4196
4226
|
};
|
|
4197
4227
|
}
|
|
4198
4228
|
}
|
|
4199
|
-
|
|
4229
|
+
const pendingToolEgress = config.policy.egress?.enabled ? (() => {
|
|
4230
|
+
const dests = extractToolDestinations(toolName, args);
|
|
4231
|
+
const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
|
|
4232
|
+
return eg ? egressPolicyVerdict(eg) : void 0;
|
|
4233
|
+
})() : void 0;
|
|
4234
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) {
|
|
4235
|
+
return pendingToolEgress ?? { decision: "allow" };
|
|
4236
|
+
}
|
|
4200
4237
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
4201
4238
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
4202
4239
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
@@ -4285,6 +4322,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4285
4322
|
}
|
|
4286
4323
|
let allTokens = [];
|
|
4287
4324
|
let pathTokens = [];
|
|
4325
|
+
if (pendingToolEgress) return pendingToolEgress;
|
|
4288
4326
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
4289
4327
|
if (shellCommand) {
|
|
4290
4328
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
@@ -4350,16 +4388,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
4350
4388
|
const dests = extractShellDestinations(shellCommand);
|
|
4351
4389
|
if (dests.length > 0) {
|
|
4352
4390
|
const eg = evaluateEgress(dests, config.policy.egress);
|
|
4353
|
-
if (eg)
|
|
4354
|
-
return {
|
|
4355
|
-
decision: eg.verdict,
|
|
4356
|
-
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
4357
|
-
reason: eg.reason,
|
|
4358
|
-
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
4359
|
-
ruleDescription: eg.reason,
|
|
4360
|
-
tier: eg.verdict === "block" ? 3 : 4
|
|
4361
|
-
};
|
|
4362
|
-
}
|
|
4391
|
+
if (eg) return egressPolicyVerdict(eg);
|
|
4363
4392
|
}
|
|
4364
4393
|
}
|
|
4365
4394
|
const firstToken = analyzed.actions[0] ?? "";
|
|
@@ -8299,8 +8328,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8299
8328
|
};
|
|
8300
8329
|
}
|
|
8301
8330
|
}
|
|
8331
|
+
const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
|
|
8332
|
+
const judge = !isIgnoredTool2(toolName) || declaredEgress;
|
|
8302
8333
|
if (isObserveMode) {
|
|
8303
|
-
if (
|
|
8334
|
+
if (judge) {
|
|
8304
8335
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
8305
8336
|
const wouldBlock = policyResult.decision === "block";
|
|
8306
8337
|
if (!isManual)
|
|
@@ -8325,7 +8356,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8325
8356
|
return { approved: true, checkedBy: "audit" };
|
|
8326
8357
|
}
|
|
8327
8358
|
if (config.settings.mode === "audit") {
|
|
8328
|
-
if (
|
|
8359
|
+
if (judge) {
|
|
8329
8360
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
8330
8361
|
if (policyResult.decision === "review") {
|
|
8331
8362
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
@@ -8364,9 +8395,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8364
8395
|
appPermReviewTool = bareTool;
|
|
8365
8396
|
}
|
|
8366
8397
|
}
|
|
8367
|
-
if (!taintWarning &&
|
|
8398
|
+
if (!taintWarning && judge) {
|
|
8368
8399
|
const ld = config.policy.loopDetection;
|
|
8369
|
-
if (ld.enabled && !appPermReview) {
|
|
8400
|
+
if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
|
|
8370
8401
|
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
8371
8402
|
if (loopResult.looping) {
|
|
8372
8403
|
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "IAM for your AI agents. Set what Claude Code, Codex, Gemini, Cursor and any MCP server are allowed to do, review risky actions before they run, and keep every action on the record.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|