@node9/policy-engine 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/index.d.mts CHANGED
@@ -402,6 +402,15 @@ interface ShellCommandAnalysis {
402
402
  */
403
403
  declare function analyzeShellCommand(command: string): ShellCommandAnalysis;
404
404
 
405
+ /**
406
+ * A destination a tool call will reach, whichever carrier declared it: a
407
+ * shell command (extractShellDestinations) or a tool argument
408
+ * (extractToolDestinations). The interface is still spelled ShellDestination
409
+ * where it lives (shell/index.ts, which is hashed by the extractor-version
410
+ * gate, so a rename there is not free); this is the name that says what the
411
+ * shape is for.
412
+ */
413
+ type Destination = ShellDestination;
405
414
  interface EgressPolicy {
406
415
  /** Master switch. Default false — opt-in, like dlp.pii. */
407
416
  enabled: boolean;
@@ -459,7 +468,7 @@ declare function isPrivateHost(host: string): boolean;
459
468
  * first review; null if everything is allowed (or policy disabled / mode off).
460
469
  * Pure.
461
470
  */
462
- declare function evaluateEgress(dests: readonly ShellDestination[], policy: EgressPolicy): EgressVerdict | null;
471
+ declare function evaluateEgress(dests: readonly Destination[], policy: EgressPolicy): EgressVerdict | null;
463
472
 
464
473
  interface PipeChainAnalysis {
465
474
  isPipeline: boolean;
@@ -628,16 +637,6 @@ declare function resolvePinned(matches: SmartRule[]): SmartRule | undefined;
628
637
  * Returns a reason string if dangerous, null if safe.
629
638
  */
630
639
  declare function checkDangerousSql(sql: string): string | null;
631
- /**
632
- * Stateless policy evaluation. Same waterfall as the original
633
- * proxy/src/policy/index.ts:evaluatePolicy, but config + context + I/O
634
- * hooks come in as parameters so this function works in any host.
635
- *
636
- * Returns 'allow' for ignored tools, the matched smart-rule verdict,
637
- * inline-execution review, eval-detection verdict, pipe-chain verdict,
638
- * provenance verdict, sandbox allow, dangerous-word review, or strict-mode
639
- * fallback. See the design doc for the full tier table.
640
- */
641
640
  declare function evaluatePolicy(config: PolicyConfig, toolName: string, args?: unknown, context?: PolicyContext, hooks?: PolicyHostHooks): Promise<PolicyVerdict>;
642
641
  /** Returns true when toolName matches the config's ignoredTools list. */
643
642
  declare function isIgnoredTool(toolName: string, config: PolicyConfig): boolean;
@@ -1464,6 +1463,25 @@ declare function ssrfFloor(tokens: ReadonlyArray<{
1464
1463
  * `[]` in a path means "every element of this array".
1465
1464
  */
1466
1465
  declare const DESTINATION_ARGS: ReadonlyMap<string, readonly string[]>;
1466
+ /**
1467
+ * The hosts a non-shell tool call will reach, in the SAME shape the shell
1468
+ * extractor produces, so both feed one evaluateEgress (G10).
1469
+ *
1470
+ * Measured 2026-09-20 on dev e748b4a, egress { mode: 'block', allow: [] }:
1471
+ * `curl https://evil.example.com/` denied, the same URL through WebFetch, MCP
1472
+ * fetch and browser navigate allowed. The allowlist was shell-only, and an
1473
+ * agent that wants to exfiltrate does not need curl. The extraction below is
1474
+ * what the floor above already does; the policy was never applied to it.
1475
+ *
1476
+ * Same closed table, same helpers, for the same reason: a tool is covered
1477
+ * because it is LISTED, never because an argument looked like a URL. A tool
1478
+ * not in DESTINATION_ARGS yields [] and is invisible to the egress policy,
1479
+ * exactly as it is invisible to the floor. `binary` carries the tool name so
1480
+ * the reason string and the audit row say which carrier it was.
1481
+ *
1482
+ * Never throws: this runs on the hook path for every tool call.
1483
+ */
1484
+ declare function extractToolDestinations(toolName: string, args: unknown): Destination[];
1467
1485
  interface DestinationHit extends SsrfMatch {
1468
1486
  /** The path that carried it, for the message and the audit row. */
1469
1487
  argPath: string;
@@ -1483,4 +1501,4 @@ declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
1483
1501
  /** Engine version stamped on audit entries for future drift detection. */
1484
1502
  declare const ENGINE_VERSION = "1.4.0";
1485
1503
 
1486
- 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 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, 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -402,6 +402,15 @@ interface ShellCommandAnalysis {
402
402
  */
403
403
  declare function analyzeShellCommand(command: string): ShellCommandAnalysis;
404
404
 
405
+ /**
406
+ * A destination a tool call will reach, whichever carrier declared it: a
407
+ * shell command (extractShellDestinations) or a tool argument
408
+ * (extractToolDestinations). The interface is still spelled ShellDestination
409
+ * where it lives (shell/index.ts, which is hashed by the extractor-version
410
+ * gate, so a rename there is not free); this is the name that says what the
411
+ * shape is for.
412
+ */
413
+ type Destination = ShellDestination;
405
414
  interface EgressPolicy {
406
415
  /** Master switch. Default false — opt-in, like dlp.pii. */
407
416
  enabled: boolean;
@@ -459,7 +468,7 @@ declare function isPrivateHost(host: string): boolean;
459
468
  * first review; null if everything is allowed (or policy disabled / mode off).
460
469
  * Pure.
461
470
  */
462
- declare function evaluateEgress(dests: readonly ShellDestination[], policy: EgressPolicy): EgressVerdict | null;
471
+ declare function evaluateEgress(dests: readonly Destination[], policy: EgressPolicy): EgressVerdict | null;
463
472
 
464
473
  interface PipeChainAnalysis {
465
474
  isPipeline: boolean;
@@ -628,16 +637,6 @@ declare function resolvePinned(matches: SmartRule[]): SmartRule | undefined;
628
637
  * Returns a reason string if dangerous, null if safe.
629
638
  */
630
639
  declare function checkDangerousSql(sql: string): string | null;
631
- /**
632
- * Stateless policy evaluation. Same waterfall as the original
633
- * proxy/src/policy/index.ts:evaluatePolicy, but config + context + I/O
634
- * hooks come in as parameters so this function works in any host.
635
- *
636
- * Returns 'allow' for ignored tools, the matched smart-rule verdict,
637
- * inline-execution review, eval-detection verdict, pipe-chain verdict,
638
- * provenance verdict, sandbox allow, dangerous-word review, or strict-mode
639
- * fallback. See the design doc for the full tier table.
640
- */
641
640
  declare function evaluatePolicy(config: PolicyConfig, toolName: string, args?: unknown, context?: PolicyContext, hooks?: PolicyHostHooks): Promise<PolicyVerdict>;
642
641
  /** Returns true when toolName matches the config's ignoredTools list. */
643
642
  declare function isIgnoredTool(toolName: string, config: PolicyConfig): boolean;
@@ -1464,6 +1463,25 @@ declare function ssrfFloor(tokens: ReadonlyArray<{
1464
1463
  * `[]` in a path means "every element of this array".
1465
1464
  */
1466
1465
  declare const DESTINATION_ARGS: ReadonlyMap<string, readonly string[]>;
1466
+ /**
1467
+ * The hosts a non-shell tool call will reach, in the SAME shape the shell
1468
+ * extractor produces, so both feed one evaluateEgress (G10).
1469
+ *
1470
+ * Measured 2026-09-20 on dev e748b4a, egress { mode: 'block', allow: [] }:
1471
+ * `curl https://evil.example.com/` denied, the same URL through WebFetch, MCP
1472
+ * fetch and browser navigate allowed. The allowlist was shell-only, and an
1473
+ * agent that wants to exfiltrate does not need curl. The extraction below is
1474
+ * what the floor above already does; the policy was never applied to it.
1475
+ *
1476
+ * Same closed table, same helpers, for the same reason: a tool is covered
1477
+ * because it is LISTED, never because an argument looked like a URL. A tool
1478
+ * not in DESTINATION_ARGS yields [] and is invisible to the egress policy,
1479
+ * exactly as it is invisible to the floor. `binary` carries the tool name so
1480
+ * the reason string and the audit row say which carrier it was.
1481
+ *
1482
+ * Never throws: this runs on the hook path for every tool call.
1483
+ */
1484
+ declare function extractToolDestinations(toolName: string, args: unknown): Destination[];
1467
1485
  interface DestinationHit extends SsrfMatch {
1468
1486
  /** The path that carried it, for the message and the audit row. */
1469
1487
  argPath: string;
@@ -1483,4 +1501,4 @@ declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
1483
1501
  /** Engine version stamped on audit entries for future drift detection. */
1484
1502
  declare const ENGINE_VERSION = "1.4.0";
1485
1503
 
1486
- 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 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, 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 };
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 };
package/dist/index.js CHANGED
@@ -90,6 +90,7 @@ __export(src_exports, {
90
90
  extractSessionLevelFindings: () => extractSessionLevelFindings,
91
91
  extractShellDestTokens: () => extractShellDestTokens,
92
92
  extractShellDestinations: () => extractShellDestinations,
93
+ extractToolDestinations: () => extractToolDestinations,
93
94
  fileOperandFlagsOf: () => fileOperandFlagsOf,
94
95
  getCompiledRegex: () => getCompiledRegex,
95
96
  getNestedValue: () => getNestedValue,
@@ -3445,18 +3446,18 @@ var DEFAULT_EGRESS_ALLOWLIST = [
3445
3446
  "*.ubuntu.com"
3446
3447
  ];
3447
3448
  function hostMatches(host, pattern) {
3448
- const h = host.toLowerCase();
3449
- const p = pattern.toLowerCase().trim();
3449
+ const p = pattern.trim().toLowerCase();
3450
3450
  if (!p) return false;
3451
3451
  if (p === "*") return true;
3452
+ const h = canonicalHost(host);
3452
3453
  if (p.startsWith("*.")) {
3453
- const suffix = p.slice(2);
3454
+ const suffix = canonicalHost(p.slice(2));
3454
3455
  return h === suffix || h.endsWith("." + suffix);
3455
3456
  }
3456
- return h === p;
3457
+ return h === canonicalHost(p);
3457
3458
  }
3458
3459
  function matchesAny(host, patterns) {
3459
- for (const p of patterns) if (hostMatches(host, p)) return true;
3460
+ for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
3460
3461
  return false;
3461
3462
  }
3462
3463
  var PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
@@ -3467,13 +3468,17 @@ function isUniqueLocalV6(host) {
3467
3468
  return g !== null && (g[0] & 65024) === 64512;
3468
3469
  }
3469
3470
  function isPrivateHost(host) {
3470
- const h = host.trim().toLowerCase();
3471
+ const h = host.trim().toLowerCase().replace(/\.$/, "");
3471
3472
  const m = classifySsrf(h);
3472
3473
  if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
3473
3474
  if (h === "localhost") return true;
3474
3475
  if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
3475
3476
  return isUniqueLocalV6(h);
3476
3477
  }
3478
+ function canonicalHost(host) {
3479
+ const ip = normalizeIpLiteral(host);
3480
+ return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
3481
+ }
3477
3482
  function evaluateEgress(dests, policy) {
3478
3483
  if (!policy.enabled) return null;
3479
3484
  let review = null;
@@ -3867,6 +3872,22 @@ function hostOf(value) {
3867
3872
  return null;
3868
3873
  }
3869
3874
  }
3875
+ function extractToolDestinations(toolName, args) {
3876
+ try {
3877
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
3878
+ if (!paths) return [];
3879
+ const out = [];
3880
+ for (const path of paths) {
3881
+ for (const value of valuesAt(args, path)) {
3882
+ const host = hostOf(value);
3883
+ if (host) out.push({ host, binary: toolName, raw: value });
3884
+ }
3885
+ }
3886
+ return out;
3887
+ } catch {
3888
+ return [];
3889
+ }
3890
+ }
3870
3891
  function ssrfDestinationFloor(toolName, args, opts = {}) {
3871
3892
  try {
3872
3893
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
@@ -3985,6 +4006,16 @@ function pipeChainVerdict(command, isTrustedHost, highAction = "review") {
3985
4006
  tier: 3
3986
4007
  };
3987
4008
  }
4009
+ function egressPolicyVerdict(eg) {
4010
+ return {
4011
+ decision: eg.verdict,
4012
+ blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
4013
+ reason: eg.reason,
4014
+ ruleName: `egress:${eg.binary}:${eg.host}`,
4015
+ ruleDescription: eg.reason,
4016
+ tier: eg.verdict === "block" ? 3 : 4
4017
+ };
4018
+ }
3988
4019
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
3989
4020
  const { agent, cwd, activeEnvironment } = context;
3990
4021
  const { checkProvenance, isTrustedHost } = hooks;
@@ -4019,7 +4050,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4019
4050
  };
4020
4051
  }
4021
4052
  }
4022
- if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
4053
+ const pendingToolEgress = config.policy.egress?.enabled ? (() => {
4054
+ const dests = extractToolDestinations(toolName, args);
4055
+ const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
4056
+ return eg ? egressPolicyVerdict(eg) : void 0;
4057
+ })() : void 0;
4058
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) {
4059
+ return pendingToolEgress ?? { decision: "allow" };
4060
+ }
4023
4061
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
4024
4062
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
4025
4063
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
@@ -4108,6 +4146,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4108
4146
  }
4109
4147
  let allTokens = [];
4110
4148
  let pathTokens = [];
4149
+ if (pendingToolEgress) return pendingToolEgress;
4111
4150
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
4112
4151
  if (shellCommand) {
4113
4152
  const analyzed = analyzeShellCommand(shellCommand);
@@ -4173,16 +4212,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4173
4212
  const dests = extractShellDestinations(shellCommand);
4174
4213
  if (dests.length > 0) {
4175
4214
  const eg = evaluateEgress(dests, config.policy.egress);
4176
- if (eg) {
4177
- return {
4178
- decision: eg.verdict,
4179
- blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
4180
- reason: eg.reason,
4181
- ruleName: `egress:${eg.binary}:${eg.host}`,
4182
- ruleDescription: eg.reason,
4183
- tier: eg.verdict === "block" ? 3 : 4
4184
- };
4185
- }
4215
+ if (eg) return egressPolicyVerdict(eg);
4186
4216
  }
4187
4217
  }
4188
4218
  const firstToken = analyzed.actions[0] ?? "";
@@ -6106,6 +6136,7 @@ var ENGINE_VERSION = "1.4.0";
6106
6136
  extractSessionLevelFindings,
6107
6137
  extractShellDestTokens,
6108
6138
  extractShellDestinations,
6139
+ extractToolDestinations,
6109
6140
  fileOperandFlagsOf,
6110
6141
  getCompiledRegex,
6111
6142
  getNestedValue,
package/dist/index.mjs CHANGED
@@ -3305,18 +3305,18 @@ var DEFAULT_EGRESS_ALLOWLIST = [
3305
3305
  "*.ubuntu.com"
3306
3306
  ];
3307
3307
  function hostMatches(host, pattern) {
3308
- const h = host.toLowerCase();
3309
- const p = pattern.toLowerCase().trim();
3308
+ const p = pattern.trim().toLowerCase();
3310
3309
  if (!p) return false;
3311
3310
  if (p === "*") return true;
3311
+ const h = canonicalHost(host);
3312
3312
  if (p.startsWith("*.")) {
3313
- const suffix = p.slice(2);
3313
+ const suffix = canonicalHost(p.slice(2));
3314
3314
  return h === suffix || h.endsWith("." + suffix);
3315
3315
  }
3316
- return h === p;
3316
+ return h === canonicalHost(p);
3317
3317
  }
3318
3318
  function matchesAny(host, patterns) {
3319
- for (const p of patterns) if (hostMatches(host, p)) return true;
3319
+ for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
3320
3320
  return false;
3321
3321
  }
3322
3322
  var PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
@@ -3327,13 +3327,17 @@ function isUniqueLocalV6(host) {
3327
3327
  return g !== null && (g[0] & 65024) === 64512;
3328
3328
  }
3329
3329
  function isPrivateHost(host) {
3330
- const h = host.trim().toLowerCase();
3330
+ const h = host.trim().toLowerCase().replace(/\.$/, "");
3331
3331
  const m = classifySsrf(h);
3332
3332
  if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
3333
3333
  if (h === "localhost") return true;
3334
3334
  if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
3335
3335
  return isUniqueLocalV6(h);
3336
3336
  }
3337
+ function canonicalHost(host) {
3338
+ const ip = normalizeIpLiteral(host);
3339
+ return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
3340
+ }
3337
3341
  function evaluateEgress(dests, policy) {
3338
3342
  if (!policy.enabled) return null;
3339
3343
  let review = null;
@@ -3727,6 +3731,22 @@ function hostOf(value) {
3727
3731
  return null;
3728
3732
  }
3729
3733
  }
3734
+ function extractToolDestinations(toolName, args) {
3735
+ try {
3736
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
3737
+ if (!paths) return [];
3738
+ const out = [];
3739
+ for (const path of paths) {
3740
+ for (const value of valuesAt(args, path)) {
3741
+ const host = hostOf(value);
3742
+ if (host) out.push({ host, binary: toolName, raw: value });
3743
+ }
3744
+ }
3745
+ return out;
3746
+ } catch {
3747
+ return [];
3748
+ }
3749
+ }
3730
3750
  function ssrfDestinationFloor(toolName, args, opts = {}) {
3731
3751
  try {
3732
3752
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
@@ -3845,6 +3865,16 @@ function pipeChainVerdict(command, isTrustedHost, highAction = "review") {
3845
3865
  tier: 3
3846
3866
  };
3847
3867
  }
3868
+ function egressPolicyVerdict(eg) {
3869
+ return {
3870
+ decision: eg.verdict,
3871
+ blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
3872
+ reason: eg.reason,
3873
+ ruleName: `egress:${eg.binary}:${eg.host}`,
3874
+ ruleDescription: eg.reason,
3875
+ tier: eg.verdict === "block" ? 3 : 4
3876
+ };
3877
+ }
3848
3878
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
3849
3879
  const { agent, cwd, activeEnvironment } = context;
3850
3880
  const { checkProvenance, isTrustedHost } = hooks;
@@ -3879,7 +3909,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3879
3909
  };
3880
3910
  }
3881
3911
  }
3882
- if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
3912
+ const pendingToolEgress = config.policy.egress?.enabled ? (() => {
3913
+ const dests = extractToolDestinations(toolName, args);
3914
+ const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
3915
+ return eg ? egressPolicyVerdict(eg) : void 0;
3916
+ })() : void 0;
3917
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) {
3918
+ return pendingToolEgress ?? { decision: "allow" };
3919
+ }
3883
3920
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
3884
3921
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
3885
3922
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
@@ -3968,6 +4005,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3968
4005
  }
3969
4006
  let allTokens = [];
3970
4007
  let pathTokens = [];
4008
+ if (pendingToolEgress) return pendingToolEgress;
3971
4009
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
3972
4010
  if (shellCommand) {
3973
4011
  const analyzed = analyzeShellCommand(shellCommand);
@@ -4033,16 +4071,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4033
4071
  const dests = extractShellDestinations(shellCommand);
4034
4072
  if (dests.length > 0) {
4035
4073
  const eg = evaluateEgress(dests, config.policy.egress);
4036
- if (eg) {
4037
- return {
4038
- decision: eg.verdict,
4039
- blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
4040
- reason: eg.reason,
4041
- ruleName: `egress:${eg.binary}:${eg.host}`,
4042
- ruleDescription: eg.reason,
4043
- tier: eg.verdict === "block" ? 3 : 4
4044
- };
4045
- }
4074
+ if (eg) return egressPolicyVerdict(eg);
4046
4075
  }
4047
4076
  }
4048
4077
  const firstToken = analyzed.actions[0] ?? "";
@@ -5965,6 +5994,7 @@ export {
5965
5994
  extractSessionLevelFindings,
5966
5995
  extractShellDestTokens,
5967
5996
  extractShellDestinations,
5997
+ extractToolDestinations,
5968
5998
  fileOperandFlagsOf,
5969
5999
  getCompiledRegex,
5970
6000
  getNestedValue,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/policy-engine",
3
- "version": "2.16.2",
3
+ "version": "2.17.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",