@node9/policy-engine 2.9.3 → 2.10.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/index.d.mts CHANGED
@@ -1254,6 +1254,13 @@ declare const SSRF_MAX_HOST = 253;
1254
1254
  * id are stripped here: the caller does NOT strip them (measured).
1255
1255
  */
1256
1256
  declare function normalizeIpLiteral(host: string): string | null;
1257
+ /** Is this tier reachable by default, waiting on the `ssrfStrict` knob?
1258
+ * Exported so the non-shell path asks the SAME question rather than
1259
+ * approximating it with `overridable`, which is a different property. */
1260
+ declare function isStrictGatedTier(tier: SsrfTier): boolean;
1261
+ /** The sentence a user reads when the floor stops them. One builder, so the
1262
+ * shell path and the non-shell path cannot word the same block differently. */
1263
+ declare function ssrfReason(m: SsrfMatch, asWritten: string): string;
1257
1264
  /**
1258
1265
  * Classify a destination host. Returns null when it is not a protected address,
1259
1266
  * which means the ordinary egress policy decides. Never throws.
@@ -1288,7 +1295,36 @@ declare function ssrfFloor(tokens: ReadonlyArray<{
1288
1295
  binary: string;
1289
1296
  }>, opts?: SsrfFloorOptions): SsrfVerdict | null;
1290
1297
 
1298
+ /**
1299
+ * Tools whose named argument really is a destination the agent is about to
1300
+ * reach. Keyed on the BARE tool name (an `mcp__server__` prefix is stripped
1301
+ * before matching), because the browser tools arrive over MCP.
1302
+ *
1303
+ * Derived from real history, not imagination: walking 240,058 audit entries
1304
+ * produced exactly these argument paths. A tool is here because its network
1305
+ * semantics are declared by its name, not because it happens to carry a key
1306
+ * called `url` (founder call, 2026-09-08: ignore those).
1307
+ *
1308
+ * `[]` in a path means "every element of this array".
1309
+ */
1310
+ declare const DESTINATION_ARGS: ReadonlyMap<string, readonly string[]>;
1311
+ interface DestinationHit extends SsrfMatch {
1312
+ /** The path that carried it, for the message and the audit row. */
1313
+ argPath: string;
1314
+ host: string;
1315
+ reason: string;
1316
+ }
1317
+ /**
1318
+ * Judge a non-shell tool call. Returns the first protected destination, or
1319
+ * null. Never throws: this runs on the hook path, where an exception would
1320
+ * break every tool call on the machine.
1321
+ */
1322
+ declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
1323
+ ssrfStrict?: boolean;
1324
+ ssrfAllow?: readonly string[];
1325
+ }): DestinationHit | null;
1326
+
1291
1327
  /** Engine version stamped on audit entries for future drift detection. */
1292
1328
  declare const ENGINE_VERSION = "1.4.0";
1293
1329
 
1294
- export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfFloor, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
1330
+ export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTINATION_ARGS, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.d.ts CHANGED
@@ -1254,6 +1254,13 @@ declare const SSRF_MAX_HOST = 253;
1254
1254
  * id are stripped here: the caller does NOT strip them (measured).
1255
1255
  */
1256
1256
  declare function normalizeIpLiteral(host: string): string | null;
1257
+ /** Is this tier reachable by default, waiting on the `ssrfStrict` knob?
1258
+ * Exported so the non-shell path asks the SAME question rather than
1259
+ * approximating it with `overridable`, which is a different property. */
1260
+ declare function isStrictGatedTier(tier: SsrfTier): boolean;
1261
+ /** The sentence a user reads when the floor stops them. One builder, so the
1262
+ * shell path and the non-shell path cannot word the same block differently. */
1263
+ declare function ssrfReason(m: SsrfMatch, asWritten: string): string;
1257
1264
  /**
1258
1265
  * Classify a destination host. Returns null when it is not a protected address,
1259
1266
  * which means the ordinary egress policy decides. Never throws.
@@ -1288,7 +1295,36 @@ declare function ssrfFloor(tokens: ReadonlyArray<{
1288
1295
  binary: string;
1289
1296
  }>, opts?: SsrfFloorOptions): SsrfVerdict | null;
1290
1297
 
1298
+ /**
1299
+ * Tools whose named argument really is a destination the agent is about to
1300
+ * reach. Keyed on the BARE tool name (an `mcp__server__` prefix is stripped
1301
+ * before matching), because the browser tools arrive over MCP.
1302
+ *
1303
+ * Derived from real history, not imagination: walking 240,058 audit entries
1304
+ * produced exactly these argument paths. A tool is here because its network
1305
+ * semantics are declared by its name, not because it happens to carry a key
1306
+ * called `url` (founder call, 2026-09-08: ignore those).
1307
+ *
1308
+ * `[]` in a path means "every element of this array".
1309
+ */
1310
+ declare const DESTINATION_ARGS: ReadonlyMap<string, readonly string[]>;
1311
+ interface DestinationHit extends SsrfMatch {
1312
+ /** The path that carried it, for the message and the audit row. */
1313
+ argPath: string;
1314
+ host: string;
1315
+ reason: string;
1316
+ }
1317
+ /**
1318
+ * Judge a non-shell tool call. Returns the first protected destination, or
1319
+ * null. Never throws: this runs on the hook path, where an exception would
1320
+ * break every tool call on the machine.
1321
+ */
1322
+ declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
1323
+ ssrfStrict?: boolean;
1324
+ ssrfAllow?: readonly string[];
1325
+ }): DestinationHit | null;
1326
+
1291
1327
  /** Engine version stamped on audit entries for future drift detection. */
1292
1328
  declare const ENGINE_VERSION = "1.4.0";
1293
1329
 
1294
- export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfFloor, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
1330
+ export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTINATION_ARGS, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ __export(src_exports, {
38
38
  CANONICAL_EXTRACTOR_VERSION: () => CANONICAL_EXTRACTOR_VERSION,
39
39
  COST_PER_LOOP_ITER_USD: () => COST_PER_LOOP_ITER_USD,
40
40
  DEFAULT_EGRESS_ALLOWLIST: () => DEFAULT_EGRESS_ALLOWLIST,
41
+ DESTINATION_ARGS: () => DESTINATION_ARGS,
41
42
  DESTRUCTIVE_OP_RE: () => DESTRUCTIVE_OP_RE,
42
43
  DLP_PATTERNS: () => DLP_PATTERNS,
43
44
  DLP_SCAN_LIMITS: () => DLP_SCAN_LIMITS,
@@ -93,6 +94,7 @@ __export(src_exports, {
93
94
  isProtectedHomePath: () => isProtectedHomePath,
94
95
  isShellShapedTool: () => isShellShapedTool,
95
96
  isShieldVerdict: () => isShieldVerdict,
97
+ isStrictGatedTier: () => isStrictGatedTier,
96
98
  matchCanary: () => matchCanary,
97
99
  matchCanaryArgs: () => matchCanaryArgs,
98
100
  matchSensitivePath: () => matchSensitivePath,
@@ -109,7 +111,9 @@ __export(src_exports, {
109
111
  scanInjection: () => scanInjection,
110
112
  scanText: () => scanText,
111
113
  sensitivePathMatch: () => sensitivePathMatch,
114
+ ssrfDestinationFloor: () => ssrfDestinationFloor,
112
115
  ssrfFloor: () => ssrfFloor,
116
+ ssrfReason: () => ssrfReason,
113
117
  summarizeBlast: () => summarizeBlast,
114
118
  summarizeScan: () => summarizeScan,
115
119
  toScanFinding: () => toScanFinding,
@@ -2875,6 +2879,12 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
2875
2879
  "100.100.100.200"
2876
2880
  ]);
2877
2881
  var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
2882
+ function isStrictGatedTier(tier) {
2883
+ return STRICT_TIERS.has(tier);
2884
+ }
2885
+ function ssrfReason(m, asWritten) {
2886
+ return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2887
+ }
2878
2888
  var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
2879
2889
  var v4Octets = (a) => {
2880
2890
  const p = a.split(".");
@@ -2933,18 +2943,76 @@ function ssrfFloor(tokens, opts = {}) {
2933
2943
  for (const { token, binary } of tokens) {
2934
2944
  const m = classifySsrf(token);
2935
2945
  if (!m) continue;
2936
- if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
2946
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2937
2947
  if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
2938
- return {
2939
- ...m,
2940
- host: token,
2941
- binary,
2942
- reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
2943
- };
2948
+ return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2944
2949
  }
2945
2950
  return null;
2946
2951
  }
2947
2952
 
2953
+ // src/egress/destinations.ts
2954
+ var DESTINATION_ARGS = /* @__PURE__ */ new Map([
2955
+ ["webfetch", ["url"]],
2956
+ ["fetch", ["url", "uri"]],
2957
+ ["navigate", ["url"]],
2958
+ ["preview_start", ["url"]],
2959
+ ["browser_batch", ["actions[].input.url"]]
2960
+ ]);
2961
+ function bareToolName(toolName) {
2962
+ const parts = toolName.split("__");
2963
+ return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
2964
+ }
2965
+ function valuesAt(args, path) {
2966
+ let cursors = [args];
2967
+ for (const rawSegment of path.split(".")) {
2968
+ const isArray = rawSegment.endsWith("[]");
2969
+ const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
2970
+ const next = [];
2971
+ for (const cursor of cursors) {
2972
+ if (cursor === null || typeof cursor !== "object") continue;
2973
+ const child = cursor[key];
2974
+ if (isArray) {
2975
+ if (Array.isArray(child)) next.push(...child);
2976
+ } else if (child !== void 0) {
2977
+ next.push(child);
2978
+ }
2979
+ }
2980
+ cursors = next;
2981
+ if (cursors.length === 0) return [];
2982
+ }
2983
+ return cursors.filter((v) => typeof v === "string");
2984
+ }
2985
+ function hostOf(value) {
2986
+ try {
2987
+ const u = new URL(value);
2988
+ const h = u.hostname;
2989
+ return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
2990
+ } catch {
2991
+ return null;
2992
+ }
2993
+ }
2994
+ function ssrfDestinationFloor(toolName, args, opts = {}) {
2995
+ try {
2996
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
2997
+ if (!paths) return null;
2998
+ const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
2999
+ for (const path of paths) {
3000
+ for (const value of valuesAt(args, path)) {
3001
+ const host = hostOf(value);
3002
+ if (!host) continue;
3003
+ const m = classifySsrf(host);
3004
+ if (!m) continue;
3005
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3006
+ if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3007
+ return { ...m, argPath: path, host, reason: ssrfReason(m, host) };
3008
+ }
3009
+ }
3010
+ return null;
3011
+ } catch {
3012
+ return null;
3013
+ }
3014
+ }
3015
+
2948
3016
  // src/policy/index.ts
2949
3017
  function resolveCheck(v) {
2950
3018
  return v === "off" || v === "block" ? v : "review";
@@ -3058,6 +3126,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3058
3126
  };
3059
3127
  }
3060
3128
  }
3129
+ {
3130
+ const dest = ssrfDestinationFloor(toolName, args, {
3131
+ ssrfAllow: config.policy.egress?.ssrfAllow,
3132
+ ssrfStrict: config.policy.egress?.ssrfStrict
3133
+ });
3134
+ if (dest) {
3135
+ return {
3136
+ decision: "block",
3137
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
3138
+ reason: dest.reason,
3139
+ ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
3140
+ ruleDescription: dest.reason,
3141
+ tier: 3
3142
+ };
3143
+ }
3144
+ }
3061
3145
  if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
3062
3146
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
3063
3147
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
@@ -5061,6 +5145,7 @@ var ENGINE_VERSION = "1.4.0";
5061
5145
  CANONICAL_EXTRACTOR_VERSION,
5062
5146
  COST_PER_LOOP_ITER_USD,
5063
5147
  DEFAULT_EGRESS_ALLOWLIST,
5148
+ DESTINATION_ARGS,
5064
5149
  DESTRUCTIVE_OP_RE,
5065
5150
  DLP_PATTERNS,
5066
5151
  DLP_SCAN_LIMITS,
@@ -5116,6 +5201,7 @@ var ENGINE_VERSION = "1.4.0";
5116
5201
  isProtectedHomePath,
5117
5202
  isShellShapedTool,
5118
5203
  isShieldVerdict,
5204
+ isStrictGatedTier,
5119
5205
  matchCanary,
5120
5206
  matchCanaryArgs,
5121
5207
  matchSensitivePath,
@@ -5132,7 +5218,9 @@ var ENGINE_VERSION = "1.4.0";
5132
5218
  scanInjection,
5133
5219
  scanText,
5134
5220
  sensitivePathMatch,
5221
+ ssrfDestinationFloor,
5135
5222
  ssrfFloor,
5223
+ ssrfReason,
5136
5224
  summarizeBlast,
5137
5225
  summarizeScan,
5138
5226
  toScanFinding,
package/dist/index.mjs CHANGED
@@ -2752,6 +2752,12 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
2752
2752
  "100.100.100.200"
2753
2753
  ]);
2754
2754
  var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
2755
+ function isStrictGatedTier(tier) {
2756
+ return STRICT_TIERS.has(tier);
2757
+ }
2758
+ function ssrfReason(m, asWritten) {
2759
+ return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2760
+ }
2755
2761
  var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
2756
2762
  var v4Octets = (a) => {
2757
2763
  const p = a.split(".");
@@ -2810,18 +2816,76 @@ function ssrfFloor(tokens, opts = {}) {
2810
2816
  for (const { token, binary } of tokens) {
2811
2817
  const m = classifySsrf(token);
2812
2818
  if (!m) continue;
2813
- if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
2819
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2814
2820
  if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
2815
- return {
2816
- ...m,
2817
- host: token,
2818
- binary,
2819
- reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
2820
- };
2821
+ return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2821
2822
  }
2822
2823
  return null;
2823
2824
  }
2824
2825
 
2826
+ // src/egress/destinations.ts
2827
+ var DESTINATION_ARGS = /* @__PURE__ */ new Map([
2828
+ ["webfetch", ["url"]],
2829
+ ["fetch", ["url", "uri"]],
2830
+ ["navigate", ["url"]],
2831
+ ["preview_start", ["url"]],
2832
+ ["browser_batch", ["actions[].input.url"]]
2833
+ ]);
2834
+ function bareToolName(toolName) {
2835
+ const parts = toolName.split("__");
2836
+ return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
2837
+ }
2838
+ function valuesAt(args, path) {
2839
+ let cursors = [args];
2840
+ for (const rawSegment of path.split(".")) {
2841
+ const isArray = rawSegment.endsWith("[]");
2842
+ const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
2843
+ const next = [];
2844
+ for (const cursor of cursors) {
2845
+ if (cursor === null || typeof cursor !== "object") continue;
2846
+ const child = cursor[key];
2847
+ if (isArray) {
2848
+ if (Array.isArray(child)) next.push(...child);
2849
+ } else if (child !== void 0) {
2850
+ next.push(child);
2851
+ }
2852
+ }
2853
+ cursors = next;
2854
+ if (cursors.length === 0) return [];
2855
+ }
2856
+ return cursors.filter((v) => typeof v === "string");
2857
+ }
2858
+ function hostOf(value) {
2859
+ try {
2860
+ const u = new URL(value);
2861
+ const h = u.hostname;
2862
+ return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
2863
+ } catch {
2864
+ return null;
2865
+ }
2866
+ }
2867
+ function ssrfDestinationFloor(toolName, args, opts = {}) {
2868
+ try {
2869
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
2870
+ if (!paths) return null;
2871
+ const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
2872
+ for (const path of paths) {
2873
+ for (const value of valuesAt(args, path)) {
2874
+ const host = hostOf(value);
2875
+ if (!host) continue;
2876
+ const m = classifySsrf(host);
2877
+ if (!m) continue;
2878
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2879
+ if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
2880
+ return { ...m, argPath: path, host, reason: ssrfReason(m, host) };
2881
+ }
2882
+ }
2883
+ return null;
2884
+ } catch {
2885
+ return null;
2886
+ }
2887
+ }
2888
+
2825
2889
  // src/policy/index.ts
2826
2890
  function resolveCheck(v) {
2827
2891
  return v === "off" || v === "block" ? v : "review";
@@ -2935,6 +2999,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2935
2999
  };
2936
3000
  }
2937
3001
  }
3002
+ {
3003
+ const dest = ssrfDestinationFloor(toolName, args, {
3004
+ ssrfAllow: config.policy.egress?.ssrfAllow,
3005
+ ssrfStrict: config.policy.egress?.ssrfStrict
3006
+ });
3007
+ if (dest) {
3008
+ return {
3009
+ decision: "block",
3010
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
3011
+ reason: dest.reason,
3012
+ ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
3013
+ ruleDescription: dest.reason,
3014
+ tier: 3
3015
+ };
3016
+ }
3017
+ }
2938
3018
  if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2939
3019
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2940
3020
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
@@ -4937,6 +5017,7 @@ export {
4937
5017
  CANONICAL_EXTRACTOR_VERSION,
4938
5018
  COST_PER_LOOP_ITER_USD,
4939
5019
  DEFAULT_EGRESS_ALLOWLIST,
5020
+ DESTINATION_ARGS,
4940
5021
  DESTRUCTIVE_OP_RE,
4941
5022
  DLP_PATTERNS,
4942
5023
  DLP_SCAN_LIMITS,
@@ -4992,6 +5073,7 @@ export {
4992
5073
  isProtectedHomePath,
4993
5074
  isShellShapedTool,
4994
5075
  isShieldVerdict,
5076
+ isStrictGatedTier,
4995
5077
  matchCanary,
4996
5078
  matchCanaryArgs,
4997
5079
  matchSensitivePath,
@@ -5008,7 +5090,9 @@ export {
5008
5090
  scanInjection,
5009
5091
  scanText,
5010
5092
  sensitivePathMatch,
5093
+ ssrfDestinationFloor,
5011
5094
  ssrfFloor,
5095
+ ssrfReason,
5012
5096
  summarizeBlast,
5013
5097
  summarizeScan,
5014
5098
  toScanFinding,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/policy-engine",
3
- "version": "2.9.3",
3
+ "version": "2.10.0",
4
4
  "description": "Shared policy evaluation engine for node9 — DLP, smart rules, AST shell parsing, shields, loop detection. Pure functions, no I/O. Used by both node9-proxy and the node9 SaaS firewall.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://node9.ai",