@node9/policy-engine 2.17.0 → 2.19.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
@@ -1438,6 +1438,33 @@ interface SsrfFloorOptions {
1438
1438
  * those constantly (72 of 308 destinations on measured real history). */
1439
1439
  ssrfStrict?: boolean;
1440
1440
  }
1441
+ /**
1442
+ * Does this exemption list release this address?
1443
+ *
1444
+ * ONE function, because the shell floor and the declared-URL floor must answer
1445
+ * identically. They had two spellings of it — `normalizeIpLiteral(e) ?? e.trim()
1446
+ * .toLowerCase()` here and `classifySsrf(a)?.normalized ?? a` in
1447
+ * destinations.ts — and for an entry classifySsrf does not match they already
1448
+ * disagreed. Nothing depended on the difference, which is exactly when it is
1449
+ * cheap to remove.
1450
+ *
1451
+ * An entry is an exact address or a CIDR range. The range form exists so an
1452
+ * operator can exempt the mesh-VPN range they actually use (100.64.0.0/10)
1453
+ * instead of listing peers one at a time, which is what the settings panel's
1454
+ * "remove this default" control needs.
1455
+ *
1456
+ * ⚠ This answers "is it in the list", NOT "may it be released". The caller
1457
+ * keeps its `m.overridable` guard, and that guard is per ADDRESS, decided by
1458
+ * classifySsrf before this is consulted. So a range can never release a
1459
+ * protected address inside it: 100.100.100.200 (Alibaba IMDS) sits inside
1460
+ * 100.64.0.0/10 and is named in METADATA_ADDRESSES above the range check, so
1461
+ * it classifies as metadata / overridable:false and stays blocked with the
1462
+ * whole range exempted. Pinned by E2 in exempt-range.spec.ts.
1463
+ *
1464
+ * Never throws: this runs on the hook path for every tool call, and a bad
1465
+ * entry in a hand-edited config must not break every command on the machine.
1466
+ */
1467
+ declare function ssrfExemptMatches(entries: readonly string[] | undefined, normalized: string | undefined): boolean;
1441
1468
  /**
1442
1469
  * The floor. Returns the first protected destination, or null.
1443
1470
  *
@@ -1501,4 +1528,4 @@ declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
1501
1528
  /** Engine version stamped on audit entries for future drift detection. */
1502
1529
  declare const ENGINE_VERSION = "1.4.0";
1503
1530
 
1504
- 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, COMMAND_WRAPPERS, COPY_VERBS, 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 Destination, 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, MAX_BLAST_PATH, NET_BINARIES, PATTERN_VERB_NAMES, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type PositionedArg, 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, extractToolDestinations, fileOperandFlagsOf, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, patternShapeOf, positionedArgs, previewArgs, redactText, resolvePinned, safeMessage, sampleCopyCommand, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, stripControlChars, stripTerminalEscapes, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, unwrapCommandHead, validateOverrides, validateRegex, validateShieldDefinition };
1531
+ 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, COMMAND_WRAPPERS, COPY_VERBS, 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 Destination, 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, MAX_BLAST_PATH, NET_BINARIES, PATTERN_VERB_NAMES, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type PositionedArg, 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, extractToolDestinations, fileOperandFlagsOf, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, patternShapeOf, positionedArgs, previewArgs, redactText, resolvePinned, safeMessage, sampleCopyCommand, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfExemptMatches, ssrfFloor, ssrfReason, stripControlChars, stripTerminalEscapes, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, unwrapCommandHead, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.d.ts CHANGED
@@ -1438,6 +1438,33 @@ interface SsrfFloorOptions {
1438
1438
  * those constantly (72 of 308 destinations on measured real history). */
1439
1439
  ssrfStrict?: boolean;
1440
1440
  }
1441
+ /**
1442
+ * Does this exemption list release this address?
1443
+ *
1444
+ * ONE function, because the shell floor and the declared-URL floor must answer
1445
+ * identically. They had two spellings of it — `normalizeIpLiteral(e) ?? e.trim()
1446
+ * .toLowerCase()` here and `classifySsrf(a)?.normalized ?? a` in
1447
+ * destinations.ts — and for an entry classifySsrf does not match they already
1448
+ * disagreed. Nothing depended on the difference, which is exactly when it is
1449
+ * cheap to remove.
1450
+ *
1451
+ * An entry is an exact address or a CIDR range. The range form exists so an
1452
+ * operator can exempt the mesh-VPN range they actually use (100.64.0.0/10)
1453
+ * instead of listing peers one at a time, which is what the settings panel's
1454
+ * "remove this default" control needs.
1455
+ *
1456
+ * ⚠ This answers "is it in the list", NOT "may it be released". The caller
1457
+ * keeps its `m.overridable` guard, and that guard is per ADDRESS, decided by
1458
+ * classifySsrf before this is consulted. So a range can never release a
1459
+ * protected address inside it: 100.100.100.200 (Alibaba IMDS) sits inside
1460
+ * 100.64.0.0/10 and is named in METADATA_ADDRESSES above the range check, so
1461
+ * it classifies as metadata / overridable:false and stays blocked with the
1462
+ * whole range exempted. Pinned by E2 in exempt-range.spec.ts.
1463
+ *
1464
+ * Never throws: this runs on the hook path for every tool call, and a bad
1465
+ * entry in a hand-edited config must not break every command on the machine.
1466
+ */
1467
+ declare function ssrfExemptMatches(entries: readonly string[] | undefined, normalized: string | undefined): boolean;
1441
1468
  /**
1442
1469
  * The floor. Returns the first protected destination, or null.
1443
1470
  *
@@ -1501,4 +1528,4 @@ declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
1501
1528
  /** Engine version stamped on audit entries for future drift detection. */
1502
1529
  declare const ENGINE_VERSION = "1.4.0";
1503
1530
 
1504
- 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, COMMAND_WRAPPERS, COPY_VERBS, 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 Destination, 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, MAX_BLAST_PATH, NET_BINARIES, PATTERN_VERB_NAMES, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type PositionedArg, 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, extractToolDestinations, fileOperandFlagsOf, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, patternShapeOf, positionedArgs, previewArgs, redactText, resolvePinned, safeMessage, sampleCopyCommand, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, stripControlChars, stripTerminalEscapes, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, unwrapCommandHead, validateOverrides, validateRegex, validateShieldDefinition };
1531
+ 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, COMMAND_WRAPPERS, COPY_VERBS, 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 Destination, 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, MAX_BLAST_PATH, NET_BINARIES, PATTERN_VERB_NAMES, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type PositionedArg, 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, extractToolDestinations, fileOperandFlagsOf, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, patternShapeOf, positionedArgs, previewArgs, redactText, resolvePinned, safeMessage, sampleCopyCommand, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfExemptMatches, ssrfFloor, ssrfReason, stripControlChars, stripTerminalEscapes, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, unwrapCommandHead, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.js CHANGED
@@ -123,6 +123,7 @@ __export(src_exports, {
123
123
  scanText: () => scanText,
124
124
  sensitivePathMatch: () => sensitivePathMatch,
125
125
  ssrfDestinationFloor: () => ssrfDestinationFloor,
126
+ ssrfExemptMatches: () => ssrfExemptMatches,
126
127
  ssrfFloor: () => ssrfFloor,
127
128
  ssrfReason: () => ssrfReason,
128
129
  stripControlChars: () => stripControlChars,
@@ -3399,6 +3400,38 @@ function classifySsrf(host) {
3399
3400
  return null;
3400
3401
  }
3401
3402
  }
3403
+ function ssrfExemptMatches(entries, normalized) {
3404
+ if (!entries?.length || !normalized) return false;
3405
+ const target = bitsOf(normalized);
3406
+ for (const raw of entries) {
3407
+ const entry = raw.trim().toLowerCase();
3408
+ if (!entry) continue;
3409
+ const slash = entry.indexOf("/");
3410
+ if (slash === -1) {
3411
+ if ((normalizeIpLiteral(entry) ?? entry) === normalized) return true;
3412
+ continue;
3413
+ }
3414
+ if (!target) continue;
3415
+ const base = bitsOf(normalizeIpLiteral(entry.slice(0, slash)) ?? "");
3416
+ const prefixText = entry.slice(slash + 1);
3417
+ const prefix = /^\d+$/.test(prefixText) ? Number(prefixText) : NaN;
3418
+ if (!base || !Number.isInteger(prefix) || prefix < 0 || // A v4 range never matches a v6 address, and the reverse: the widths
3419
+ // differ, so `0.0.0.0/0` does not release `::1`.
3420
+ base.length !== target.length || prefix > base.length) {
3421
+ continue;
3422
+ }
3423
+ if (base.slice(0, prefix) === target.slice(0, prefix)) return true;
3424
+ }
3425
+ return false;
3426
+ }
3427
+ function bitsOf(normalized) {
3428
+ const o = v4Octets(normalized);
3429
+ if (o) {
3430
+ return o.every((n) => Number.isInteger(n) && n >= 0 && n <= 255) ? o.map((n) => n.toString(2).padStart(8, "0")).join("") : null;
3431
+ }
3432
+ const g = expandIpv6(normalized);
3433
+ return g ? g.map((n) => n.toString(2).padStart(16, "0")).join("") : null;
3434
+ }
3402
3435
  var TIER_REASON = {
3403
3436
  metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
3404
3437
  "link-local": "a link-local address",
@@ -3408,14 +3441,11 @@ var TIER_REASON = {
3408
3441
  private: "a loopback or private address"
3409
3442
  };
3410
3443
  function ssrfFloor(tokens, opts = {}) {
3411
- const exempt = new Set(
3412
- (opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
3413
- );
3414
3444
  for (const { token, binary } of tokens) {
3415
3445
  const m = classifySsrf(token);
3416
3446
  if (!m) continue;
3417
3447
  if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3418
- if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3448
+ if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
3419
3449
  return { ...m, host: token, binary, reason: ssrfReason(m, token) };
3420
3450
  }
3421
3451
  return null;
@@ -3892,7 +3922,6 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
3892
3922
  try {
3893
3923
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
3894
3924
  if (!paths) return null;
3895
- const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
3896
3925
  for (const path of paths) {
3897
3926
  for (const value of valuesAt(args, path)) {
3898
3927
  const host = hostOf(value);
@@ -3900,7 +3929,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
3900
3929
  const m = classifySsrf(host);
3901
3930
  if (!m) continue;
3902
3931
  if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3903
- if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3932
+ if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
3904
3933
  return { ...m, argPath: path, host, reason: ssrfReason(m, host) };
3905
3934
  }
3906
3935
  }
@@ -6169,6 +6198,7 @@ var ENGINE_VERSION = "1.4.0";
6169
6198
  scanText,
6170
6199
  sensitivePathMatch,
6171
6200
  ssrfDestinationFloor,
6201
+ ssrfExemptMatches,
6172
6202
  ssrfFloor,
6173
6203
  ssrfReason,
6174
6204
  stripControlChars,
package/dist/index.mjs CHANGED
@@ -3258,6 +3258,38 @@ function classifySsrf(host) {
3258
3258
  return null;
3259
3259
  }
3260
3260
  }
3261
+ function ssrfExemptMatches(entries, normalized) {
3262
+ if (!entries?.length || !normalized) return false;
3263
+ const target = bitsOf(normalized);
3264
+ for (const raw of entries) {
3265
+ const entry = raw.trim().toLowerCase();
3266
+ if (!entry) continue;
3267
+ const slash = entry.indexOf("/");
3268
+ if (slash === -1) {
3269
+ if ((normalizeIpLiteral(entry) ?? entry) === normalized) return true;
3270
+ continue;
3271
+ }
3272
+ if (!target) continue;
3273
+ const base = bitsOf(normalizeIpLiteral(entry.slice(0, slash)) ?? "");
3274
+ const prefixText = entry.slice(slash + 1);
3275
+ const prefix = /^\d+$/.test(prefixText) ? Number(prefixText) : NaN;
3276
+ if (!base || !Number.isInteger(prefix) || prefix < 0 || // A v4 range never matches a v6 address, and the reverse: the widths
3277
+ // differ, so `0.0.0.0/0` does not release `::1`.
3278
+ base.length !== target.length || prefix > base.length) {
3279
+ continue;
3280
+ }
3281
+ if (base.slice(0, prefix) === target.slice(0, prefix)) return true;
3282
+ }
3283
+ return false;
3284
+ }
3285
+ function bitsOf(normalized) {
3286
+ const o = v4Octets(normalized);
3287
+ if (o) {
3288
+ return o.every((n) => Number.isInteger(n) && n >= 0 && n <= 255) ? o.map((n) => n.toString(2).padStart(8, "0")).join("") : null;
3289
+ }
3290
+ const g = expandIpv6(normalized);
3291
+ return g ? g.map((n) => n.toString(2).padStart(16, "0")).join("") : null;
3292
+ }
3261
3293
  var TIER_REASON = {
3262
3294
  metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
3263
3295
  "link-local": "a link-local address",
@@ -3267,14 +3299,11 @@ var TIER_REASON = {
3267
3299
  private: "a loopback or private address"
3268
3300
  };
3269
3301
  function ssrfFloor(tokens, opts = {}) {
3270
- const exempt = new Set(
3271
- (opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
3272
- );
3273
3302
  for (const { token, binary } of tokens) {
3274
3303
  const m = classifySsrf(token);
3275
3304
  if (!m) continue;
3276
3305
  if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3277
- if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3306
+ if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
3278
3307
  return { ...m, host: token, binary, reason: ssrfReason(m, token) };
3279
3308
  }
3280
3309
  return null;
@@ -3751,7 +3780,6 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
3751
3780
  try {
3752
3781
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
3753
3782
  if (!paths) return null;
3754
- const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
3755
3783
  for (const path of paths) {
3756
3784
  for (const value of valuesAt(args, path)) {
3757
3785
  const host = hostOf(value);
@@ -3759,7 +3787,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
3759
3787
  const m = classifySsrf(host);
3760
3788
  if (!m) continue;
3761
3789
  if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3762
- if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3790
+ if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
3763
3791
  return { ...m, argPath: path, host, reason: ssrfReason(m, host) };
3764
3792
  }
3765
3793
  }
@@ -6027,6 +6055,7 @@ export {
6027
6055
  scanText,
6028
6056
  sensitivePathMatch,
6029
6057
  ssrfDestinationFloor,
6058
+ ssrfExemptMatches,
6030
6059
  ssrfFloor,
6031
6060
  ssrfReason,
6032
6061
  stripControlChars,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/policy-engine",
3
- "version": "2.17.0",
3
+ "version": "2.19.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",