@node9/policy-engine 1.30.0 → 1.32.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
@@ -165,6 +165,27 @@ declare const AST_FS_REGEX_RULES: Set<string>;
165
165
  * the tool-managed cache allow-list. Used to gate `rm -rf` on home paths.
166
166
  */
167
167
  declare function isProtectedHomePath(rawPath: string): boolean;
168
+ interface ShellDestination {
169
+ /** Extracted hostname, lowercased (e.g. "evil.com", "10.0.0.5"). */
170
+ host: string;
171
+ /** The network binary it belongs to (e.g. "curl"). */
172
+ binary: string;
173
+ /** The raw argument token the host came from (for UI / audit). */
174
+ raw: string;
175
+ }
176
+ /**
177
+ * Parse a destination host out of a single token. Handles scheme URLs
178
+ * (`https://h/p`), scheme-less curl targets (`evil.com/p`), `user@host:path`
179
+ * (scp/ssh), and `host:port`. Returns the lowercased hostname, or null if the
180
+ * token doesn't resolve to a plausible host. IPv6 literals are out of scope v1.
181
+ */
182
+ declare function parseDestHost(token: string): string | null;
183
+ /**
184
+ * AST-extract every network destination host in a shell command. Walks each
185
+ * CallExpr; for curl/wget/scp/ssh/nc it resolves the destination argument(s)
186
+ * and parses the host. Deduplicated by host. Pure — no I/O, no DNS.
187
+ */
188
+ declare function extractShellDestinations(command: string): ShellDestination[];
168
189
  declare function analyzeFsOperation(command: string): FsOpVerdict | null;
169
190
  interface ShellCommandAnalysis {
170
191
  /** First word of every CallExpr — the command names invoked. */
@@ -181,6 +202,39 @@ interface ShellCommandAnalysis {
181
202
  */
182
203
  declare function analyzeShellCommand(command: string): ShellCommandAnalysis;
183
204
 
205
+ interface EgressPolicy {
206
+ /** Master switch. Default false — opt-in, like dlp.pii. */
207
+ enabled: boolean;
208
+ /** Verdict for an UNKNOWN host (not in allow, not in deny, not private). */
209
+ mode: 'off' | 'review' | 'block';
210
+ /** Host globs always allowed (e.g. "*.github.com", "api.openai.com"). */
211
+ allow: string[];
212
+ /** Host globs always blocked — wins over allow/private. */
213
+ deny: string[];
214
+ /** Auto-allow localhost / RFC1918 / *.local. Default true. */
215
+ allowPrivate: boolean;
216
+ }
217
+ interface EgressVerdict {
218
+ verdict: 'block' | 'review';
219
+ host: string;
220
+ binary: string;
221
+ reason: string;
222
+ }
223
+ declare const DEFAULT_EGRESS_ALLOWLIST: readonly string[];
224
+ /** Glob host match: "*" = any, "*.x" = apex x + any subdomain, else exact. */
225
+ declare function hostMatches(host: string, pattern: string): boolean;
226
+ /** localhost / loopback / RFC1918 / link-local-ish — never a real exfil target. */
227
+ declare function isPrivateHost(host: string): boolean;
228
+ /**
229
+ * Evaluate extracted destinations against the egress policy. Precedence per
230
+ * host: deny (block) > private-allow > allow/default-allow (skip) > unknown
231
+ * (policy.mode). Returns the most severe actionable verdict across all
232
+ * destinations — a deny or block-mode-unknown short-circuits; otherwise the
233
+ * first review; null if everything is allowed (or policy disabled / mode off).
234
+ * Pure.
235
+ */
236
+ declare function evaluateEgress(dests: readonly ShellDestination[], policy: EgressPolicy): EgressVerdict | null;
237
+
184
238
  interface PipeChainAnalysis {
185
239
  isPipeline: boolean;
186
240
  hasSensitiveSource: boolean;
@@ -243,6 +297,9 @@ interface PolicyConfig {
243
297
  enabled: boolean;
244
298
  scanIgnoredTools: boolean;
245
299
  };
300
+ /** Egress / destination control (GAP-5). Optional for back-compat with
301
+ * callers/tests that build a PolicyConfig without it. */
302
+ egress?: EgressPolicy;
246
303
  };
247
304
  settings: {
248
305
  mode: string;
@@ -777,6 +834,13 @@ type PiiPattern = 'Email' | 'SSN' | 'Phone' | 'Credit Card';
777
834
  * per distinct pattern type, never multiple "Email" findings from one input.
778
835
  */
779
836
  declare function detectPii(text: string): PiiPattern[];
837
+ declare const REALTIME_PII_PATTERNS: readonly PiiPattern[];
838
+ /**
839
+ * Realtime adapter for detectPii: walks a tool-args value (stringifying
840
+ * objects/arrays) and returns only the high-signal PII patterns found. Used by
841
+ * the authorize path to gate SSN / Credit Card in tool arguments. Pure.
842
+ */
843
+ declare function detectArgsPii(args: unknown): PiiPattern[];
780
844
 
781
845
  type CanonicalFindingType = 'smart-rule' | 'ast-fs-op' | 'dlp' | 'pii' | 'sensitive-file-read' | 'privilege-escalation' | 'destructive-op' | 'pipe-to-shell' | 'eval-of-remote' | 'loop' | 'long-output-redacted';
782
846
  type CanonicalAgent = 'claude' | 'gemini' | 'codex' | 'shell';
@@ -918,7 +982,7 @@ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
918
982
  * files changed, this hash must change too, and you must consciously
919
983
  * decide whether to bump CANONICAL_EXTRACTOR_VERSION."
920
984
  */
921
- declare const CANONICAL_EXTRACTOR_HASH = "64a6a63a27f4646f";
985
+ declare const CANONICAL_EXTRACTOR_HASH = "dbe0199dae0f29f6";
922
986
  declare function extractCanonicalFindings(call: ToolCallEntry, ctx: ExtractContext): CanonicalFinding[];
923
987
  declare function extractSessionLevelFindings(calls: ReadonlyArray<SessionToolCall>, ctx: SessionExtractContext): CanonicalFinding[];
924
988
  /**
@@ -945,4 +1009,4 @@ declare function previewArgs(input: Record<string, unknown>, max: number): strin
945
1009
  /** Engine version stamped on audit entries for future drift detection. */
946
1010
  declare const ENGINE_VERSION = "1.4.0";
947
1011
 
948
- export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DESTRUCTIVE_OP_RE, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, type FsOpVerdict, 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, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectDangerousEval, detectDangerousShellExec, detectPii, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, getCompiledRegex, getNestedValue, isBashTool, isIgnoredTool, isProtectedHomePath, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, previewArgs, redactText, scanArgs, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, toScanFinding, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
1012
+ export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, type FsOpVerdict, 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, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, scanArgs, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, toScanFinding, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.d.ts CHANGED
@@ -165,6 +165,27 @@ declare const AST_FS_REGEX_RULES: Set<string>;
165
165
  * the tool-managed cache allow-list. Used to gate `rm -rf` on home paths.
166
166
  */
167
167
  declare function isProtectedHomePath(rawPath: string): boolean;
168
+ interface ShellDestination {
169
+ /** Extracted hostname, lowercased (e.g. "evil.com", "10.0.0.5"). */
170
+ host: string;
171
+ /** The network binary it belongs to (e.g. "curl"). */
172
+ binary: string;
173
+ /** The raw argument token the host came from (for UI / audit). */
174
+ raw: string;
175
+ }
176
+ /**
177
+ * Parse a destination host out of a single token. Handles scheme URLs
178
+ * (`https://h/p`), scheme-less curl targets (`evil.com/p`), `user@host:path`
179
+ * (scp/ssh), and `host:port`. Returns the lowercased hostname, or null if the
180
+ * token doesn't resolve to a plausible host. IPv6 literals are out of scope v1.
181
+ */
182
+ declare function parseDestHost(token: string): string | null;
183
+ /**
184
+ * AST-extract every network destination host in a shell command. Walks each
185
+ * CallExpr; for curl/wget/scp/ssh/nc it resolves the destination argument(s)
186
+ * and parses the host. Deduplicated by host. Pure — no I/O, no DNS.
187
+ */
188
+ declare function extractShellDestinations(command: string): ShellDestination[];
168
189
  declare function analyzeFsOperation(command: string): FsOpVerdict | null;
169
190
  interface ShellCommandAnalysis {
170
191
  /** First word of every CallExpr — the command names invoked. */
@@ -181,6 +202,39 @@ interface ShellCommandAnalysis {
181
202
  */
182
203
  declare function analyzeShellCommand(command: string): ShellCommandAnalysis;
183
204
 
205
+ interface EgressPolicy {
206
+ /** Master switch. Default false — opt-in, like dlp.pii. */
207
+ enabled: boolean;
208
+ /** Verdict for an UNKNOWN host (not in allow, not in deny, not private). */
209
+ mode: 'off' | 'review' | 'block';
210
+ /** Host globs always allowed (e.g. "*.github.com", "api.openai.com"). */
211
+ allow: string[];
212
+ /** Host globs always blocked — wins over allow/private. */
213
+ deny: string[];
214
+ /** Auto-allow localhost / RFC1918 / *.local. Default true. */
215
+ allowPrivate: boolean;
216
+ }
217
+ interface EgressVerdict {
218
+ verdict: 'block' | 'review';
219
+ host: string;
220
+ binary: string;
221
+ reason: string;
222
+ }
223
+ declare const DEFAULT_EGRESS_ALLOWLIST: readonly string[];
224
+ /** Glob host match: "*" = any, "*.x" = apex x + any subdomain, else exact. */
225
+ declare function hostMatches(host: string, pattern: string): boolean;
226
+ /** localhost / loopback / RFC1918 / link-local-ish — never a real exfil target. */
227
+ declare function isPrivateHost(host: string): boolean;
228
+ /**
229
+ * Evaluate extracted destinations against the egress policy. Precedence per
230
+ * host: deny (block) > private-allow > allow/default-allow (skip) > unknown
231
+ * (policy.mode). Returns the most severe actionable verdict across all
232
+ * destinations — a deny or block-mode-unknown short-circuits; otherwise the
233
+ * first review; null if everything is allowed (or policy disabled / mode off).
234
+ * Pure.
235
+ */
236
+ declare function evaluateEgress(dests: readonly ShellDestination[], policy: EgressPolicy): EgressVerdict | null;
237
+
184
238
  interface PipeChainAnalysis {
185
239
  isPipeline: boolean;
186
240
  hasSensitiveSource: boolean;
@@ -243,6 +297,9 @@ interface PolicyConfig {
243
297
  enabled: boolean;
244
298
  scanIgnoredTools: boolean;
245
299
  };
300
+ /** Egress / destination control (GAP-5). Optional for back-compat with
301
+ * callers/tests that build a PolicyConfig without it. */
302
+ egress?: EgressPolicy;
246
303
  };
247
304
  settings: {
248
305
  mode: string;
@@ -777,6 +834,13 @@ type PiiPattern = 'Email' | 'SSN' | 'Phone' | 'Credit Card';
777
834
  * per distinct pattern type, never multiple "Email" findings from one input.
778
835
  */
779
836
  declare function detectPii(text: string): PiiPattern[];
837
+ declare const REALTIME_PII_PATTERNS: readonly PiiPattern[];
838
+ /**
839
+ * Realtime adapter for detectPii: walks a tool-args value (stringifying
840
+ * objects/arrays) and returns only the high-signal PII patterns found. Used by
841
+ * the authorize path to gate SSN / Credit Card in tool arguments. Pure.
842
+ */
843
+ declare function detectArgsPii(args: unknown): PiiPattern[];
780
844
 
781
845
  type CanonicalFindingType = 'smart-rule' | 'ast-fs-op' | 'dlp' | 'pii' | 'sensitive-file-read' | 'privilege-escalation' | 'destructive-op' | 'pipe-to-shell' | 'eval-of-remote' | 'loop' | 'long-output-redacted';
782
846
  type CanonicalAgent = 'claude' | 'gemini' | 'codex' | 'shell';
@@ -918,7 +982,7 @@ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
918
982
  * files changed, this hash must change too, and you must consciously
919
983
  * decide whether to bump CANONICAL_EXTRACTOR_VERSION."
920
984
  */
921
- declare const CANONICAL_EXTRACTOR_HASH = "64a6a63a27f4646f";
985
+ declare const CANONICAL_EXTRACTOR_HASH = "dbe0199dae0f29f6";
922
986
  declare function extractCanonicalFindings(call: ToolCallEntry, ctx: ExtractContext): CanonicalFinding[];
923
987
  declare function extractSessionLevelFindings(calls: ReadonlyArray<SessionToolCall>, ctx: SessionExtractContext): CanonicalFinding[];
924
988
  /**
@@ -945,4 +1009,4 @@ declare function previewArgs(input: Record<string, unknown>, max: number): strin
945
1009
  /** Engine version stamped on audit entries for future drift detection. */
946
1010
  declare const ENGINE_VERSION = "1.4.0";
947
1011
 
948
- export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DESTRUCTIVE_OP_RE, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, type FsOpVerdict, 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, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectDangerousEval, detectDangerousShellExec, detectPii, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, getCompiledRegex, getNestedValue, isBashTool, isIgnoredTool, isProtectedHomePath, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, previewArgs, redactText, scanArgs, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, toScanFinding, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
1012
+ export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, type FsOpVerdict, 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, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShieldVerdict, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, scanArgs, scanText, sensitivePathMatch, summarizeBlast, summarizeScan, toScanFinding, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ __export(src_exports, {
36
36
  CANONICAL_EXTRACTOR_HASH: () => CANONICAL_EXTRACTOR_HASH,
37
37
  CANONICAL_EXTRACTOR_VERSION: () => CANONICAL_EXTRACTOR_VERSION,
38
38
  COST_PER_LOOP_ITER_USD: () => COST_PER_LOOP_ITER_USD,
39
+ DEFAULT_EGRESS_ALLOWLIST: () => DEFAULT_EGRESS_ALLOWLIST,
39
40
  DESTRUCTIVE_OP_RE: () => DESTRUCTIVE_OP_RE,
40
41
  DLP_PATTERNS: () => DLP_PATTERNS,
41
42
  ENGINE_VERSION: () => ENGINE_VERSION,
@@ -45,6 +46,7 @@ __export(src_exports, {
45
46
  LOOP_MAX_RECORDS: () => LOOP_MAX_RECORDS,
46
47
  LOOP_THRESHOLD_FOR_WASTE: () => LOOP_THRESHOLD_FOR_WASTE,
47
48
  PRIVILEGE_ESCALATION_RE: () => PRIVILEGE_ESCALATION_RE,
49
+ REALTIME_PII_PATTERNS: () => REALTIME_PII_PATTERNS,
48
50
  SCAN_SIGNAL_WEIGHTS: () => SCAN_SIGNAL_WEIGHTS,
49
51
  SENSITIVE_PATH_RE: () => SENSITIVE_PATH_RE,
50
52
  SENSITIVE_PATH_REGEXES: () => SENSITIVE_PATH_REGEXES,
@@ -60,9 +62,11 @@ __export(src_exports, {
60
62
  computeScanScore: () => computeScanScore,
61
63
  computeSecurityScore: () => computeSecurityScore,
62
64
  dedupeCanonicalFindings: () => dedupeCanonicalFindings,
65
+ detectArgsPii: () => detectArgsPii,
63
66
  detectDangerousEval: () => detectDangerousEval,
64
67
  detectDangerousShellExec: () => detectDangerousShellExec,
65
68
  detectPii: () => detectPii,
69
+ evaluateEgress: () => evaluateEgress,
66
70
  evaluateLoopWindow: () => evaluateLoopWindow,
67
71
  evaluatePolicy: () => evaluatePolicy,
68
72
  evaluateSmartConditions: () => evaluateSmartConditions,
@@ -71,10 +75,13 @@ __export(src_exports, {
71
75
  extractNetworkTargets: () => extractNetworkTargets,
72
76
  extractPositionalArgs: () => extractPositionalArgs,
73
77
  extractSessionLevelFindings: () => extractSessionLevelFindings,
78
+ extractShellDestinations: () => extractShellDestinations,
74
79
  getCompiledRegex: () => getCompiledRegex,
75
80
  getNestedValue: () => getNestedValue,
81
+ hostMatches: () => hostMatches,
76
82
  isBashTool: () => isBashTool,
77
83
  isIgnoredTool: () => isIgnoredTool,
84
+ isPrivateHost: () => isPrivateHost,
78
85
  isProtectedHomePath: () => isProtectedHomePath,
79
86
  isShieldVerdict: () => isShieldVerdict,
80
87
  matchSensitivePath: () => matchSensitivePath,
@@ -82,6 +89,7 @@ __export(src_exports, {
82
89
  narrativeRuleLabel: () => narrativeRuleLabel,
83
90
  normalizeCommandForPolicy: () => normalizeCommandForPolicy,
84
91
  parseAllSshHostsFromCommand: () => parseAllSshHostsFromCommand,
92
+ parseDestHost: () => parseDestHost,
85
93
  previewArgs: () => previewArgs,
86
94
  redactText: () => redactText,
87
95
  scanArgs: () => scanArgs,
@@ -1072,6 +1080,201 @@ function extractLiteralArgs(callExpr) {
1072
1080
  }
1073
1081
  return { name, flags, paths };
1074
1082
  }
1083
+ var NET_BINARIES = /* @__PURE__ */ new Set(["curl", "wget", "scp", "ssh", "nc", "ncat", "netcat"]);
1084
+ var VALUE_FLAGS = {
1085
+ curl: /* @__PURE__ */ new Set([
1086
+ "-d",
1087
+ "--data",
1088
+ "--data-ascii",
1089
+ "--data-binary",
1090
+ "--data-raw",
1091
+ "--data-urlencode",
1092
+ "-F",
1093
+ "--form",
1094
+ "-H",
1095
+ "--header",
1096
+ "-X",
1097
+ "--request",
1098
+ "-o",
1099
+ "--output",
1100
+ "-T",
1101
+ "--upload-file",
1102
+ "-u",
1103
+ "--user",
1104
+ "-e",
1105
+ "--referer",
1106
+ "-A",
1107
+ "--user-agent",
1108
+ "-b",
1109
+ "--cookie",
1110
+ "-c",
1111
+ "--cookie-jar",
1112
+ "--connect-to",
1113
+ "--resolve",
1114
+ "--cacert",
1115
+ "--cert",
1116
+ "--key",
1117
+ "-x",
1118
+ "--proxy",
1119
+ "-m",
1120
+ "--max-time",
1121
+ "--retry"
1122
+ ]),
1123
+ wget: /* @__PURE__ */ new Set([
1124
+ "-O",
1125
+ "--output-document",
1126
+ "--post-data",
1127
+ "--post-file",
1128
+ "--header",
1129
+ "-U",
1130
+ "--user-agent",
1131
+ "--user",
1132
+ "--password",
1133
+ "-o",
1134
+ "--output-file",
1135
+ "-P",
1136
+ "--directory-prefix",
1137
+ "-t",
1138
+ "--tries",
1139
+ "-T",
1140
+ "--timeout"
1141
+ ]),
1142
+ scp: /* @__PURE__ */ new Set(["-i", "-F", "-l", "-o", "-c", "-S", "-P", "-J", "-D", "-W"]),
1143
+ ssh: /* @__PURE__ */ new Set([
1144
+ "-i",
1145
+ "-p",
1146
+ "-o",
1147
+ "-l",
1148
+ "-F",
1149
+ "-c",
1150
+ "-L",
1151
+ "-R",
1152
+ "-D",
1153
+ "-W",
1154
+ "-b",
1155
+ "-e",
1156
+ "-m",
1157
+ "-O",
1158
+ "-Q",
1159
+ "-S",
1160
+ "-J",
1161
+ "-w",
1162
+ "-B",
1163
+ "-I",
1164
+ "-E"
1165
+ ]),
1166
+ nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
1167
+ };
1168
+ function resolveWordLiteral(w) {
1169
+ const parts = w?.Parts || [];
1170
+ let s = "";
1171
+ for (const p of parts) {
1172
+ const t = syntax.NodeType(p);
1173
+ if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
1174
+ else if (t === "SglQuoted") s += p.Value ?? "";
1175
+ else if (t === "DblQuoted") {
1176
+ const inner = p.Parts || [];
1177
+ if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
1178
+ s += inner.map((ip) => ip.Value ?? "").join("");
1179
+ } else {
1180
+ return null;
1181
+ }
1182
+ }
1183
+ return s;
1184
+ }
1185
+ function parseDestHost(token) {
1186
+ if (!token) return null;
1187
+ let t = token.trim();
1188
+ if (!t || t.startsWith("-")) return null;
1189
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) {
1190
+ try {
1191
+ const h = new URL(t).hostname.toLowerCase();
1192
+ return h || null;
1193
+ } catch {
1194
+ return null;
1195
+ }
1196
+ }
1197
+ const at = t.lastIndexOf("@");
1198
+ if (at >= 0) t = t.slice(at + 1);
1199
+ t = t.split("/")[0];
1200
+ t = t.replace(/:\d+$/, "");
1201
+ t = t.split(":")[0];
1202
+ t = t.toLowerCase();
1203
+ if (t.length > 253) return null;
1204
+ if (t === "localhost") return t;
1205
+ if (/^[a-z0-9.-]+\.[a-z0-9.-]+$/.test(t)) return t;
1206
+ return null;
1207
+ }
1208
+ function destTokensForBinary(binary, args) {
1209
+ const valueFlags = VALUE_FLAGS[binary] ?? /* @__PURE__ */ new Set();
1210
+ const positionals = [];
1211
+ const urlFlagValues = [];
1212
+ for (let i = 0; i < args.length; i++) {
1213
+ const tok = args[i];
1214
+ if (tok === null) continue;
1215
+ if (tok.startsWith("-")) {
1216
+ if (tok.startsWith("--url=")) {
1217
+ urlFlagValues.push(tok.slice("--url=".length));
1218
+ continue;
1219
+ }
1220
+ if (tok === "--url") {
1221
+ const next = args[i + 1];
1222
+ if (typeof next === "string") urlFlagValues.push(next);
1223
+ i++;
1224
+ continue;
1225
+ }
1226
+ if (tok.includes("=")) continue;
1227
+ if (valueFlags.has(tok)) i++;
1228
+ continue;
1229
+ }
1230
+ positionals.push(tok);
1231
+ }
1232
+ switch (binary) {
1233
+ case "curl":
1234
+ case "wget":
1235
+ return [...urlFlagValues, ...positionals];
1236
+ case "ssh":
1237
+ return positionals.slice(0, 1);
1238
+ case "scp":
1239
+ return positionals.filter((p) => p.includes(":") || p.includes("@"));
1240
+ case "nc":
1241
+ case "ncat":
1242
+ case "netcat":
1243
+ return positionals.slice(0, 1);
1244
+ default:
1245
+ return [];
1246
+ }
1247
+ }
1248
+ function extractShellDestinations(command) {
1249
+ const f = parseShared(command);
1250
+ if (f === PARSE_FAIL) return [];
1251
+ const out = [];
1252
+ const seen = /* @__PURE__ */ new Set();
1253
+ try {
1254
+ syntax.Walk(f, (node) => {
1255
+ if (!node) return false;
1256
+ const n = node;
1257
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1258
+ const callArgs = n.Args || [];
1259
+ if (callArgs.length === 0) return true;
1260
+ const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
1261
+ if (!NET_BINARIES.has(name)) return true;
1262
+ const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
1263
+ for (const raw of destTokensForBinary(name, rest)) {
1264
+ const host = parseDestHost(raw);
1265
+ if (!host) continue;
1266
+ const key = `${name}:${host}`;
1267
+ if (seen.has(key)) continue;
1268
+ seen.add(key);
1269
+ out.push({ host, binary: name, raw });
1270
+ }
1271
+ return true;
1272
+ });
1273
+ } catch {
1274
+ return out;
1275
+ }
1276
+ return out;
1277
+ }
1075
1278
  var FS_OP_CACHE_MAX = 5e3;
1076
1279
  var fsOpCache = /* @__PURE__ */ new Map();
1077
1280
  function analyzeFsOperation(command) {
@@ -1202,6 +1405,85 @@ function analyzeShellCommand(command) {
1202
1405
  return { actions, paths, allTokens };
1203
1406
  }
1204
1407
 
1408
+ // src/egress/index.ts
1409
+ var DEFAULT_EGRESS_ALLOWLIST = [
1410
+ "*.github.com",
1411
+ "*.githubusercontent.com",
1412
+ "*.npmjs.org",
1413
+ "pypi.org",
1414
+ "*.pythonhosted.org",
1415
+ "crates.io",
1416
+ "*.crates.io",
1417
+ "rubygems.org",
1418
+ "proxy.golang.org",
1419
+ "sum.golang.org",
1420
+ "*.anthropic.com",
1421
+ "*.openai.com",
1422
+ "*.googleapis.com",
1423
+ "*.docker.io",
1424
+ "*.docker.com",
1425
+ "deb.debian.org",
1426
+ "*.ubuntu.com"
1427
+ ];
1428
+ function hostMatches(host, pattern) {
1429
+ const h = host.toLowerCase();
1430
+ const p = pattern.toLowerCase().trim();
1431
+ if (!p) return false;
1432
+ if (p === "*") return true;
1433
+ if (p.startsWith("*.")) {
1434
+ const suffix = p.slice(2);
1435
+ return h === suffix || h.endsWith("." + suffix);
1436
+ }
1437
+ return h === p;
1438
+ }
1439
+ function matchesAny(host, patterns) {
1440
+ for (const p of patterns) if (hostMatches(host, p)) return true;
1441
+ return false;
1442
+ }
1443
+ function isPrivateHost(host) {
1444
+ const h = host.toLowerCase();
1445
+ if (h === "localhost" || h === "0.0.0.0") return true;
1446
+ if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localhost")) return true;
1447
+ if (/^127\./.test(h)) return true;
1448
+ if (/^10\./.test(h)) return true;
1449
+ if (/^192\.168\./.test(h)) return true;
1450
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
1451
+ return false;
1452
+ }
1453
+ function evaluateEgress(dests, policy) {
1454
+ if (!policy.enabled) return null;
1455
+ let review = null;
1456
+ for (const d of dests) {
1457
+ if (matchesAny(d.host, policy.deny)) {
1458
+ return {
1459
+ verdict: "block",
1460
+ host: d.host,
1461
+ binary: d.binary,
1462
+ reason: `Egress to ${d.host} is on the deny list.`
1463
+ };
1464
+ }
1465
+ if (policy.allowPrivate && isPrivateHost(d.host)) continue;
1466
+ if (matchesAny(d.host, policy.allow) || matchesAny(d.host, DEFAULT_EGRESS_ALLOWLIST)) continue;
1467
+ if (policy.mode === "block") {
1468
+ return {
1469
+ verdict: "block",
1470
+ host: d.host,
1471
+ binary: d.binary,
1472
+ reason: `Egress to unknown host ${d.host} is blocked (egress policy: block).`
1473
+ };
1474
+ }
1475
+ if (policy.mode === "review" && !review) {
1476
+ review = {
1477
+ verdict: "review",
1478
+ host: d.host,
1479
+ binary: d.binary,
1480
+ reason: `${d.binary} is sending data to an unrecognized host (${d.host}). Approve this destination?`
1481
+ };
1482
+ }
1483
+ }
1484
+ return review;
1485
+ }
1486
+
1205
1487
  // src/policy/pipe-chain.ts
1206
1488
  var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
1207
1489
  "cat",
@@ -1795,6 +2077,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1795
2077
  }
1796
2078
  const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost);
1797
2079
  if (ptVerdict) return ptVerdict;
2080
+ if (config.policy.egress?.enabled) {
2081
+ const dests = extractShellDestinations(shellCommand);
2082
+ if (dests.length > 0) {
2083
+ const eg = evaluateEgress(dests, config.policy.egress);
2084
+ if (eg) {
2085
+ return {
2086
+ decision: eg.verdict,
2087
+ blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
2088
+ reason: eg.reason,
2089
+ ruleName: `egress:${eg.binary}:${eg.host}`,
2090
+ ruleDescription: eg.reason,
2091
+ tier: eg.verdict === "block" ? 3 : 4
2092
+ };
2093
+ }
2094
+ }
2095
+ }
1798
2096
  const firstToken = analyzed.actions[0] ?? "";
1799
2097
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
1800
2098
  const rawTokens = shellCommand.trim().split(/\s+/);
@@ -3014,11 +3312,25 @@ function detectPii(text) {
3014
3312
  if (PII_CC_RE.test(text)) found.add("Credit Card");
3015
3313
  return [...found];
3016
3314
  }
3315
+ var REALTIME_PII_PATTERNS = ["SSN", "Credit Card"];
3316
+ var MAX_PII_SCAN_BYTES = 1e5;
3317
+ function detectArgsPii(args) {
3318
+ if (args === null || args === void 0) return [];
3319
+ let text;
3320
+ try {
3321
+ text = typeof args === "string" ? args : JSON.stringify(args);
3322
+ } catch {
3323
+ return [];
3324
+ }
3325
+ if (typeof text !== "string") return [];
3326
+ if (text.length > MAX_PII_SCAN_BYTES) text = text.slice(0, MAX_PII_SCAN_BYTES);
3327
+ return detectPii(text).filter((p) => REALTIME_PII_PATTERNS.includes(p));
3328
+ }
3017
3329
 
3018
3330
  // src/scan/canonical.ts
3019
3331
  var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
3020
3332
  var CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
3021
- var CANONICAL_EXTRACTOR_HASH = "64a6a63a27f4646f";
3333
+ var CANONICAL_EXTRACTOR_HASH = "dbe0199dae0f29f6";
3022
3334
  var DEDUPE_PREVIEW_LEN = 120;
3023
3335
  function extractCanonicalFindings(call, ctx) {
3024
3336
  const out = [];
@@ -3373,6 +3685,7 @@ var ENGINE_VERSION = "1.4.0";
3373
3685
  CANONICAL_EXTRACTOR_HASH,
3374
3686
  CANONICAL_EXTRACTOR_VERSION,
3375
3687
  COST_PER_LOOP_ITER_USD,
3688
+ DEFAULT_EGRESS_ALLOWLIST,
3376
3689
  DESTRUCTIVE_OP_RE,
3377
3690
  DLP_PATTERNS,
3378
3691
  ENGINE_VERSION,
@@ -3382,6 +3695,7 @@ var ENGINE_VERSION = "1.4.0";
3382
3695
  LOOP_MAX_RECORDS,
3383
3696
  LOOP_THRESHOLD_FOR_WASTE,
3384
3697
  PRIVILEGE_ESCALATION_RE,
3698
+ REALTIME_PII_PATTERNS,
3385
3699
  SCAN_SIGNAL_WEIGHTS,
3386
3700
  SENSITIVE_PATH_RE,
3387
3701
  SENSITIVE_PATH_REGEXES,
@@ -3397,9 +3711,11 @@ var ENGINE_VERSION = "1.4.0";
3397
3711
  computeScanScore,
3398
3712
  computeSecurityScore,
3399
3713
  dedupeCanonicalFindings,
3714
+ detectArgsPii,
3400
3715
  detectDangerousEval,
3401
3716
  detectDangerousShellExec,
3402
3717
  detectPii,
3718
+ evaluateEgress,
3403
3719
  evaluateLoopWindow,
3404
3720
  evaluatePolicy,
3405
3721
  evaluateSmartConditions,
@@ -3408,10 +3724,13 @@ var ENGINE_VERSION = "1.4.0";
3408
3724
  extractNetworkTargets,
3409
3725
  extractPositionalArgs,
3410
3726
  extractSessionLevelFindings,
3727
+ extractShellDestinations,
3411
3728
  getCompiledRegex,
3412
3729
  getNestedValue,
3730
+ hostMatches,
3413
3731
  isBashTool,
3414
3732
  isIgnoredTool,
3733
+ isPrivateHost,
3415
3734
  isProtectedHomePath,
3416
3735
  isShieldVerdict,
3417
3736
  matchSensitivePath,
@@ -3419,6 +3738,7 @@ var ENGINE_VERSION = "1.4.0";
3419
3738
  narrativeRuleLabel,
3420
3739
  normalizeCommandForPolicy,
3421
3740
  parseAllSshHostsFromCommand,
3741
+ parseDestHost,
3422
3742
  previewArgs,
3423
3743
  redactText,
3424
3744
  scanArgs,
package/dist/index.mjs CHANGED
@@ -973,6 +973,201 @@ function extractLiteralArgs(callExpr) {
973
973
  }
974
974
  return { name, flags, paths };
975
975
  }
976
+ var NET_BINARIES = /* @__PURE__ */ new Set(["curl", "wget", "scp", "ssh", "nc", "ncat", "netcat"]);
977
+ var VALUE_FLAGS = {
978
+ curl: /* @__PURE__ */ new Set([
979
+ "-d",
980
+ "--data",
981
+ "--data-ascii",
982
+ "--data-binary",
983
+ "--data-raw",
984
+ "--data-urlencode",
985
+ "-F",
986
+ "--form",
987
+ "-H",
988
+ "--header",
989
+ "-X",
990
+ "--request",
991
+ "-o",
992
+ "--output",
993
+ "-T",
994
+ "--upload-file",
995
+ "-u",
996
+ "--user",
997
+ "-e",
998
+ "--referer",
999
+ "-A",
1000
+ "--user-agent",
1001
+ "-b",
1002
+ "--cookie",
1003
+ "-c",
1004
+ "--cookie-jar",
1005
+ "--connect-to",
1006
+ "--resolve",
1007
+ "--cacert",
1008
+ "--cert",
1009
+ "--key",
1010
+ "-x",
1011
+ "--proxy",
1012
+ "-m",
1013
+ "--max-time",
1014
+ "--retry"
1015
+ ]),
1016
+ wget: /* @__PURE__ */ new Set([
1017
+ "-O",
1018
+ "--output-document",
1019
+ "--post-data",
1020
+ "--post-file",
1021
+ "--header",
1022
+ "-U",
1023
+ "--user-agent",
1024
+ "--user",
1025
+ "--password",
1026
+ "-o",
1027
+ "--output-file",
1028
+ "-P",
1029
+ "--directory-prefix",
1030
+ "-t",
1031
+ "--tries",
1032
+ "-T",
1033
+ "--timeout"
1034
+ ]),
1035
+ scp: /* @__PURE__ */ new Set(["-i", "-F", "-l", "-o", "-c", "-S", "-P", "-J", "-D", "-W"]),
1036
+ ssh: /* @__PURE__ */ new Set([
1037
+ "-i",
1038
+ "-p",
1039
+ "-o",
1040
+ "-l",
1041
+ "-F",
1042
+ "-c",
1043
+ "-L",
1044
+ "-R",
1045
+ "-D",
1046
+ "-W",
1047
+ "-b",
1048
+ "-e",
1049
+ "-m",
1050
+ "-O",
1051
+ "-Q",
1052
+ "-S",
1053
+ "-J",
1054
+ "-w",
1055
+ "-B",
1056
+ "-I",
1057
+ "-E"
1058
+ ]),
1059
+ nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
1060
+ };
1061
+ function resolveWordLiteral(w) {
1062
+ const parts = w?.Parts || [];
1063
+ let s = "";
1064
+ for (const p of parts) {
1065
+ const t = syntax.NodeType(p);
1066
+ if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
1067
+ else if (t === "SglQuoted") s += p.Value ?? "";
1068
+ else if (t === "DblQuoted") {
1069
+ const inner = p.Parts || [];
1070
+ if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
1071
+ s += inner.map((ip) => ip.Value ?? "").join("");
1072
+ } else {
1073
+ return null;
1074
+ }
1075
+ }
1076
+ return s;
1077
+ }
1078
+ function parseDestHost(token) {
1079
+ if (!token) return null;
1080
+ let t = token.trim();
1081
+ if (!t || t.startsWith("-")) return null;
1082
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) {
1083
+ try {
1084
+ const h = new URL(t).hostname.toLowerCase();
1085
+ return h || null;
1086
+ } catch {
1087
+ return null;
1088
+ }
1089
+ }
1090
+ const at = t.lastIndexOf("@");
1091
+ if (at >= 0) t = t.slice(at + 1);
1092
+ t = t.split("/")[0];
1093
+ t = t.replace(/:\d+$/, "");
1094
+ t = t.split(":")[0];
1095
+ t = t.toLowerCase();
1096
+ if (t.length > 253) return null;
1097
+ if (t === "localhost") return t;
1098
+ if (/^[a-z0-9.-]+\.[a-z0-9.-]+$/.test(t)) return t;
1099
+ return null;
1100
+ }
1101
+ function destTokensForBinary(binary, args) {
1102
+ const valueFlags = VALUE_FLAGS[binary] ?? /* @__PURE__ */ new Set();
1103
+ const positionals = [];
1104
+ const urlFlagValues = [];
1105
+ for (let i = 0; i < args.length; i++) {
1106
+ const tok = args[i];
1107
+ if (tok === null) continue;
1108
+ if (tok.startsWith("-")) {
1109
+ if (tok.startsWith("--url=")) {
1110
+ urlFlagValues.push(tok.slice("--url=".length));
1111
+ continue;
1112
+ }
1113
+ if (tok === "--url") {
1114
+ const next = args[i + 1];
1115
+ if (typeof next === "string") urlFlagValues.push(next);
1116
+ i++;
1117
+ continue;
1118
+ }
1119
+ if (tok.includes("=")) continue;
1120
+ if (valueFlags.has(tok)) i++;
1121
+ continue;
1122
+ }
1123
+ positionals.push(tok);
1124
+ }
1125
+ switch (binary) {
1126
+ case "curl":
1127
+ case "wget":
1128
+ return [...urlFlagValues, ...positionals];
1129
+ case "ssh":
1130
+ return positionals.slice(0, 1);
1131
+ case "scp":
1132
+ return positionals.filter((p) => p.includes(":") || p.includes("@"));
1133
+ case "nc":
1134
+ case "ncat":
1135
+ case "netcat":
1136
+ return positionals.slice(0, 1);
1137
+ default:
1138
+ return [];
1139
+ }
1140
+ }
1141
+ function extractShellDestinations(command) {
1142
+ const f = parseShared(command);
1143
+ if (f === PARSE_FAIL) return [];
1144
+ const out = [];
1145
+ const seen = /* @__PURE__ */ new Set();
1146
+ try {
1147
+ syntax.Walk(f, (node) => {
1148
+ if (!node) return false;
1149
+ const n = node;
1150
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1151
+ const callArgs = n.Args || [];
1152
+ if (callArgs.length === 0) return true;
1153
+ const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
1154
+ if (!NET_BINARIES.has(name)) return true;
1155
+ const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
1156
+ for (const raw of destTokensForBinary(name, rest)) {
1157
+ const host = parseDestHost(raw);
1158
+ if (!host) continue;
1159
+ const key = `${name}:${host}`;
1160
+ if (seen.has(key)) continue;
1161
+ seen.add(key);
1162
+ out.push({ host, binary: name, raw });
1163
+ }
1164
+ return true;
1165
+ });
1166
+ } catch {
1167
+ return out;
1168
+ }
1169
+ return out;
1170
+ }
976
1171
  var FS_OP_CACHE_MAX = 5e3;
977
1172
  var fsOpCache = /* @__PURE__ */ new Map();
978
1173
  function analyzeFsOperation(command) {
@@ -1103,6 +1298,85 @@ function analyzeShellCommand(command) {
1103
1298
  return { actions, paths, allTokens };
1104
1299
  }
1105
1300
 
1301
+ // src/egress/index.ts
1302
+ var DEFAULT_EGRESS_ALLOWLIST = [
1303
+ "*.github.com",
1304
+ "*.githubusercontent.com",
1305
+ "*.npmjs.org",
1306
+ "pypi.org",
1307
+ "*.pythonhosted.org",
1308
+ "crates.io",
1309
+ "*.crates.io",
1310
+ "rubygems.org",
1311
+ "proxy.golang.org",
1312
+ "sum.golang.org",
1313
+ "*.anthropic.com",
1314
+ "*.openai.com",
1315
+ "*.googleapis.com",
1316
+ "*.docker.io",
1317
+ "*.docker.com",
1318
+ "deb.debian.org",
1319
+ "*.ubuntu.com"
1320
+ ];
1321
+ function hostMatches(host, pattern) {
1322
+ const h = host.toLowerCase();
1323
+ const p = pattern.toLowerCase().trim();
1324
+ if (!p) return false;
1325
+ if (p === "*") return true;
1326
+ if (p.startsWith("*.")) {
1327
+ const suffix = p.slice(2);
1328
+ return h === suffix || h.endsWith("." + suffix);
1329
+ }
1330
+ return h === p;
1331
+ }
1332
+ function matchesAny(host, patterns) {
1333
+ for (const p of patterns) if (hostMatches(host, p)) return true;
1334
+ return false;
1335
+ }
1336
+ function isPrivateHost(host) {
1337
+ const h = host.toLowerCase();
1338
+ if (h === "localhost" || h === "0.0.0.0") return true;
1339
+ if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localhost")) return true;
1340
+ if (/^127\./.test(h)) return true;
1341
+ if (/^10\./.test(h)) return true;
1342
+ if (/^192\.168\./.test(h)) return true;
1343
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
1344
+ return false;
1345
+ }
1346
+ function evaluateEgress(dests, policy) {
1347
+ if (!policy.enabled) return null;
1348
+ let review = null;
1349
+ for (const d of dests) {
1350
+ if (matchesAny(d.host, policy.deny)) {
1351
+ return {
1352
+ verdict: "block",
1353
+ host: d.host,
1354
+ binary: d.binary,
1355
+ reason: `Egress to ${d.host} is on the deny list.`
1356
+ };
1357
+ }
1358
+ if (policy.allowPrivate && isPrivateHost(d.host)) continue;
1359
+ if (matchesAny(d.host, policy.allow) || matchesAny(d.host, DEFAULT_EGRESS_ALLOWLIST)) continue;
1360
+ if (policy.mode === "block") {
1361
+ return {
1362
+ verdict: "block",
1363
+ host: d.host,
1364
+ binary: d.binary,
1365
+ reason: `Egress to unknown host ${d.host} is blocked (egress policy: block).`
1366
+ };
1367
+ }
1368
+ if (policy.mode === "review" && !review) {
1369
+ review = {
1370
+ verdict: "review",
1371
+ host: d.host,
1372
+ binary: d.binary,
1373
+ reason: `${d.binary} is sending data to an unrecognized host (${d.host}). Approve this destination?`
1374
+ };
1375
+ }
1376
+ }
1377
+ return review;
1378
+ }
1379
+
1106
1380
  // src/policy/pipe-chain.ts
1107
1381
  var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
1108
1382
  "cat",
@@ -1696,6 +1970,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1696
1970
  }
1697
1971
  const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost);
1698
1972
  if (ptVerdict) return ptVerdict;
1973
+ if (config.policy.egress?.enabled) {
1974
+ const dests = extractShellDestinations(shellCommand);
1975
+ if (dests.length > 0) {
1976
+ const eg = evaluateEgress(dests, config.policy.egress);
1977
+ if (eg) {
1978
+ return {
1979
+ decision: eg.verdict,
1980
+ blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
1981
+ reason: eg.reason,
1982
+ ruleName: `egress:${eg.binary}:${eg.host}`,
1983
+ ruleDescription: eg.reason,
1984
+ tier: eg.verdict === "block" ? 3 : 4
1985
+ };
1986
+ }
1987
+ }
1988
+ }
1699
1989
  const firstToken = analyzed.actions[0] ?? "";
1700
1990
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
1701
1991
  const rawTokens = shellCommand.trim().split(/\s+/);
@@ -2915,11 +3205,25 @@ function detectPii(text) {
2915
3205
  if (PII_CC_RE.test(text)) found.add("Credit Card");
2916
3206
  return [...found];
2917
3207
  }
3208
+ var REALTIME_PII_PATTERNS = ["SSN", "Credit Card"];
3209
+ var MAX_PII_SCAN_BYTES = 1e5;
3210
+ function detectArgsPii(args) {
3211
+ if (args === null || args === void 0) return [];
3212
+ let text;
3213
+ try {
3214
+ text = typeof args === "string" ? args : JSON.stringify(args);
3215
+ } catch {
3216
+ return [];
3217
+ }
3218
+ if (typeof text !== "string") return [];
3219
+ if (text.length > MAX_PII_SCAN_BYTES) text = text.slice(0, MAX_PII_SCAN_BYTES);
3220
+ return detectPii(text).filter((p) => REALTIME_PII_PATTERNS.includes(p));
3221
+ }
2918
3222
 
2919
3223
  // src/scan/canonical.ts
2920
3224
  var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
2921
3225
  var CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
2922
- var CANONICAL_EXTRACTOR_HASH = "64a6a63a27f4646f";
3226
+ var CANONICAL_EXTRACTOR_HASH = "dbe0199dae0f29f6";
2923
3227
  var DEDUPE_PREVIEW_LEN = 120;
2924
3228
  function extractCanonicalFindings(call, ctx) {
2925
3229
  const out = [];
@@ -3273,6 +3577,7 @@ export {
3273
3577
  CANONICAL_EXTRACTOR_HASH,
3274
3578
  CANONICAL_EXTRACTOR_VERSION,
3275
3579
  COST_PER_LOOP_ITER_USD,
3580
+ DEFAULT_EGRESS_ALLOWLIST,
3276
3581
  DESTRUCTIVE_OP_RE,
3277
3582
  DLP_PATTERNS,
3278
3583
  ENGINE_VERSION,
@@ -3282,6 +3587,7 @@ export {
3282
3587
  LOOP_MAX_RECORDS,
3283
3588
  LOOP_THRESHOLD_FOR_WASTE,
3284
3589
  PRIVILEGE_ESCALATION_RE,
3590
+ REALTIME_PII_PATTERNS,
3285
3591
  SCAN_SIGNAL_WEIGHTS,
3286
3592
  SENSITIVE_PATH_RE,
3287
3593
  SENSITIVE_PATH_REGEXES,
@@ -3297,9 +3603,11 @@ export {
3297
3603
  computeScanScore,
3298
3604
  computeSecurityScore,
3299
3605
  dedupeCanonicalFindings,
3606
+ detectArgsPii,
3300
3607
  detectDangerousEval,
3301
3608
  detectDangerousShellExec,
3302
3609
  detectPii,
3610
+ evaluateEgress,
3303
3611
  evaluateLoopWindow,
3304
3612
  evaluatePolicy,
3305
3613
  evaluateSmartConditions,
@@ -3308,10 +3616,13 @@ export {
3308
3616
  extractNetworkTargets,
3309
3617
  extractPositionalArgs,
3310
3618
  extractSessionLevelFindings,
3619
+ extractShellDestinations,
3311
3620
  getCompiledRegex,
3312
3621
  getNestedValue,
3622
+ hostMatches,
3313
3623
  isBashTool,
3314
3624
  isIgnoredTool,
3625
+ isPrivateHost,
3315
3626
  isProtectedHomePath,
3316
3627
  isShieldVerdict,
3317
3628
  matchSensitivePath,
@@ -3319,6 +3630,7 @@ export {
3319
3630
  narrativeRuleLabel,
3320
3631
  normalizeCommandForPolicy,
3321
3632
  parseAllSshHostsFromCommand,
3633
+ parseDestHost,
3322
3634
  previewArgs,
3323
3635
  redactText,
3324
3636
  scanArgs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/policy-engine",
3
- "version": "1.30.0",
3
+ "version": "1.32.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",