@node9/policy-engine 2.9.2 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +105 -11
- package/dist/index.mjs +101 -11
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1254,6 +1254,13 @@ declare const SSRF_MAX_HOST = 253;
|
|
|
1254
1254
|
* id are stripped here: the caller does NOT strip them (measured).
|
|
1255
1255
|
*/
|
|
1256
1256
|
declare function normalizeIpLiteral(host: string): string | null;
|
|
1257
|
+
/** Is this tier reachable by default, waiting on the `ssrfStrict` knob?
|
|
1258
|
+
* Exported so the non-shell path asks the SAME question rather than
|
|
1259
|
+
* approximating it with `overridable`, which is a different property. */
|
|
1260
|
+
declare function isStrictGatedTier(tier: SsrfTier): boolean;
|
|
1261
|
+
/** The sentence a user reads when the floor stops them. One builder, so the
|
|
1262
|
+
* shell path and the non-shell path cannot word the same block differently. */
|
|
1263
|
+
declare function ssrfReason(m: SsrfMatch, asWritten: string): string;
|
|
1257
1264
|
/**
|
|
1258
1265
|
* Classify a destination host. Returns null when it is not a protected address,
|
|
1259
1266
|
* which means the ordinary egress policy decides. Never throws.
|
|
@@ -1288,7 +1295,36 @@ declare function ssrfFloor(tokens: ReadonlyArray<{
|
|
|
1288
1295
|
binary: string;
|
|
1289
1296
|
}>, opts?: SsrfFloorOptions): SsrfVerdict | null;
|
|
1290
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* Tools whose named argument really is a destination the agent is about to
|
|
1300
|
+
* reach. Keyed on the BARE tool name (an `mcp__server__` prefix is stripped
|
|
1301
|
+
* before matching), because the browser tools arrive over MCP.
|
|
1302
|
+
*
|
|
1303
|
+
* Derived from real history, not imagination: walking 240,058 audit entries
|
|
1304
|
+
* produced exactly these argument paths. A tool is here because its network
|
|
1305
|
+
* semantics are declared by its name, not because it happens to carry a key
|
|
1306
|
+
* called `url` (founder call, 2026-09-08: ignore those).
|
|
1307
|
+
*
|
|
1308
|
+
* `[]` in a path means "every element of this array".
|
|
1309
|
+
*/
|
|
1310
|
+
declare const DESTINATION_ARGS: ReadonlyMap<string, readonly string[]>;
|
|
1311
|
+
interface DestinationHit extends SsrfMatch {
|
|
1312
|
+
/** The path that carried it, for the message and the audit row. */
|
|
1313
|
+
argPath: string;
|
|
1314
|
+
host: string;
|
|
1315
|
+
reason: string;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Judge a non-shell tool call. Returns the first protected destination, or
|
|
1319
|
+
* null. Never throws: this runs on the hook path, where an exception would
|
|
1320
|
+
* break every tool call on the machine.
|
|
1321
|
+
*/
|
|
1322
|
+
declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
|
|
1323
|
+
ssrfStrict?: boolean;
|
|
1324
|
+
ssrfAllow?: readonly string[];
|
|
1325
|
+
}): DestinationHit | null;
|
|
1326
|
+
|
|
1291
1327
|
/** Engine version stamped on audit entries for future drift detection. */
|
|
1292
1328
|
declare const ENGINE_VERSION = "1.4.0";
|
|
1293
1329
|
|
|
1294
|
-
export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfFloor, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
|
|
1330
|
+
export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTINATION_ARGS, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
|
package/dist/index.d.ts
CHANGED
|
@@ -1254,6 +1254,13 @@ declare const SSRF_MAX_HOST = 253;
|
|
|
1254
1254
|
* id are stripped here: the caller does NOT strip them (measured).
|
|
1255
1255
|
*/
|
|
1256
1256
|
declare function normalizeIpLiteral(host: string): string | null;
|
|
1257
|
+
/** Is this tier reachable by default, waiting on the `ssrfStrict` knob?
|
|
1258
|
+
* Exported so the non-shell path asks the SAME question rather than
|
|
1259
|
+
* approximating it with `overridable`, which is a different property. */
|
|
1260
|
+
declare function isStrictGatedTier(tier: SsrfTier): boolean;
|
|
1261
|
+
/** The sentence a user reads when the floor stops them. One builder, so the
|
|
1262
|
+
* shell path and the non-shell path cannot word the same block differently. */
|
|
1263
|
+
declare function ssrfReason(m: SsrfMatch, asWritten: string): string;
|
|
1257
1264
|
/**
|
|
1258
1265
|
* Classify a destination host. Returns null when it is not a protected address,
|
|
1259
1266
|
* which means the ordinary egress policy decides. Never throws.
|
|
@@ -1288,7 +1295,36 @@ declare function ssrfFloor(tokens: ReadonlyArray<{
|
|
|
1288
1295
|
binary: string;
|
|
1289
1296
|
}>, opts?: SsrfFloorOptions): SsrfVerdict | null;
|
|
1290
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* Tools whose named argument really is a destination the agent is about to
|
|
1300
|
+
* reach. Keyed on the BARE tool name (an `mcp__server__` prefix is stripped
|
|
1301
|
+
* before matching), because the browser tools arrive over MCP.
|
|
1302
|
+
*
|
|
1303
|
+
* Derived from real history, not imagination: walking 240,058 audit entries
|
|
1304
|
+
* produced exactly these argument paths. A tool is here because its network
|
|
1305
|
+
* semantics are declared by its name, not because it happens to carry a key
|
|
1306
|
+
* called `url` (founder call, 2026-09-08: ignore those).
|
|
1307
|
+
*
|
|
1308
|
+
* `[]` in a path means "every element of this array".
|
|
1309
|
+
*/
|
|
1310
|
+
declare const DESTINATION_ARGS: ReadonlyMap<string, readonly string[]>;
|
|
1311
|
+
interface DestinationHit extends SsrfMatch {
|
|
1312
|
+
/** The path that carried it, for the message and the audit row. */
|
|
1313
|
+
argPath: string;
|
|
1314
|
+
host: string;
|
|
1315
|
+
reason: string;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Judge a non-shell tool call. Returns the first protected destination, or
|
|
1319
|
+
* null. Never throws: this runs on the hook path, where an exception would
|
|
1320
|
+
* break every tool call on the machine.
|
|
1321
|
+
*/
|
|
1322
|
+
declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
|
|
1323
|
+
ssrfStrict?: boolean;
|
|
1324
|
+
ssrfAllow?: readonly string[];
|
|
1325
|
+
}): DestinationHit | null;
|
|
1326
|
+
|
|
1291
1327
|
/** Engine version stamped on audit entries for future drift detection. */
|
|
1292
1328
|
declare const ENGINE_VERSION = "1.4.0";
|
|
1293
1329
|
|
|
1294
|
-
export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfFloor, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
|
|
1330
|
+
export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTINATION_ARGS, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, validateOverrides, validateRegex, validateShieldDefinition };
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ __export(src_exports, {
|
|
|
38
38
|
CANONICAL_EXTRACTOR_VERSION: () => CANONICAL_EXTRACTOR_VERSION,
|
|
39
39
|
COST_PER_LOOP_ITER_USD: () => COST_PER_LOOP_ITER_USD,
|
|
40
40
|
DEFAULT_EGRESS_ALLOWLIST: () => DEFAULT_EGRESS_ALLOWLIST,
|
|
41
|
+
DESTINATION_ARGS: () => DESTINATION_ARGS,
|
|
41
42
|
DESTRUCTIVE_OP_RE: () => DESTRUCTIVE_OP_RE,
|
|
42
43
|
DLP_PATTERNS: () => DLP_PATTERNS,
|
|
43
44
|
DLP_SCAN_LIMITS: () => DLP_SCAN_LIMITS,
|
|
@@ -93,6 +94,7 @@ __export(src_exports, {
|
|
|
93
94
|
isProtectedHomePath: () => isProtectedHomePath,
|
|
94
95
|
isShellShapedTool: () => isShellShapedTool,
|
|
95
96
|
isShieldVerdict: () => isShieldVerdict,
|
|
97
|
+
isStrictGatedTier: () => isStrictGatedTier,
|
|
96
98
|
matchCanary: () => matchCanary,
|
|
97
99
|
matchCanaryArgs: () => matchCanaryArgs,
|
|
98
100
|
matchSensitivePath: () => matchSensitivePath,
|
|
@@ -109,7 +111,9 @@ __export(src_exports, {
|
|
|
109
111
|
scanInjection: () => scanInjection,
|
|
110
112
|
scanText: () => scanText,
|
|
111
113
|
sensitivePathMatch: () => sensitivePathMatch,
|
|
114
|
+
ssrfDestinationFloor: () => ssrfDestinationFloor,
|
|
112
115
|
ssrfFloor: () => ssrfFloor,
|
|
116
|
+
ssrfReason: () => ssrfReason,
|
|
113
117
|
summarizeBlast: () => summarizeBlast,
|
|
114
118
|
summarizeScan: () => summarizeScan,
|
|
115
119
|
toScanFinding: () => toScanFinding,
|
|
@@ -2866,9 +2870,21 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
|
2866
2870
|
// AWS ECS task role
|
|
2867
2871
|
"168.63.129.16",
|
|
2868
2872
|
// Azure WireServer
|
|
2869
|
-
"fd00:ec2::254"
|
|
2873
|
+
"fd00:ec2::254",
|
|
2870
2874
|
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
2875
|
+
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
2876
|
+
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
2877
|
+
// live credential endpoint. It has to be named here, above the range check,
|
|
2878
|
+
// and it is the reason relaxing cgnat is safe.
|
|
2879
|
+
"100.100.100.200"
|
|
2871
2880
|
]);
|
|
2881
|
+
var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
2882
|
+
function isStrictGatedTier(tier) {
|
|
2883
|
+
return STRICT_TIERS.has(tier);
|
|
2884
|
+
}
|
|
2885
|
+
function ssrfReason(m, asWritten) {
|
|
2886
|
+
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
2887
|
+
}
|
|
2872
2888
|
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
2873
2889
|
var v4Octets = (a) => {
|
|
2874
2890
|
const p = a.split(".");
|
|
@@ -2891,7 +2907,7 @@ function classifySsrf(host) {
|
|
|
2891
2907
|
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
2892
2908
|
const o = v4Octets(ip);
|
|
2893
2909
|
if (o) {
|
|
2894
|
-
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified",
|
|
2910
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
2895
2911
|
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
2896
2912
|
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
2897
2913
|
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
@@ -2903,7 +2919,7 @@ function classifySsrf(host) {
|
|
|
2903
2919
|
}
|
|
2904
2920
|
const g = expandIpv6(ip);
|
|
2905
2921
|
if (!g) return null;
|
|
2906
|
-
if (g.every((x) => x === 0)) return hit("unspecified",
|
|
2922
|
+
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
2907
2923
|
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
2908
2924
|
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
2909
2925
|
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
@@ -2916,7 +2932,7 @@ var TIER_REASON = {
|
|
|
2916
2932
|
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
2917
2933
|
"link-local": "a link-local address",
|
|
2918
2934
|
multicast: "a multicast address",
|
|
2919
|
-
unspecified: "the unspecified address",
|
|
2935
|
+
unspecified: "the unspecified address, which reaches this host",
|
|
2920
2936
|
cgnat: "a carrier-grade NAT address",
|
|
2921
2937
|
private: "a loopback or private address"
|
|
2922
2938
|
};
|
|
@@ -2927,18 +2943,76 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
2927
2943
|
for (const { token, binary } of tokens) {
|
|
2928
2944
|
const m = classifySsrf(token);
|
|
2929
2945
|
if (!m) continue;
|
|
2930
|
-
if (m.tier
|
|
2946
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2931
2947
|
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
2932
|
-
return {
|
|
2933
|
-
...m,
|
|
2934
|
-
host: token,
|
|
2935
|
-
binary,
|
|
2936
|
-
reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
|
|
2937
|
-
};
|
|
2948
|
+
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
2938
2949
|
}
|
|
2939
2950
|
return null;
|
|
2940
2951
|
}
|
|
2941
2952
|
|
|
2953
|
+
// src/egress/destinations.ts
|
|
2954
|
+
var DESTINATION_ARGS = /* @__PURE__ */ new Map([
|
|
2955
|
+
["webfetch", ["url"]],
|
|
2956
|
+
["fetch", ["url", "uri"]],
|
|
2957
|
+
["navigate", ["url"]],
|
|
2958
|
+
["preview_start", ["url"]],
|
|
2959
|
+
["browser_batch", ["actions[].input.url"]]
|
|
2960
|
+
]);
|
|
2961
|
+
function bareToolName(toolName) {
|
|
2962
|
+
const parts = toolName.split("__");
|
|
2963
|
+
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
2964
|
+
}
|
|
2965
|
+
function valuesAt(args, path) {
|
|
2966
|
+
let cursors = [args];
|
|
2967
|
+
for (const rawSegment of path.split(".")) {
|
|
2968
|
+
const isArray = rawSegment.endsWith("[]");
|
|
2969
|
+
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
2970
|
+
const next = [];
|
|
2971
|
+
for (const cursor of cursors) {
|
|
2972
|
+
if (cursor === null || typeof cursor !== "object") continue;
|
|
2973
|
+
const child = cursor[key];
|
|
2974
|
+
if (isArray) {
|
|
2975
|
+
if (Array.isArray(child)) next.push(...child);
|
|
2976
|
+
} else if (child !== void 0) {
|
|
2977
|
+
next.push(child);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
cursors = next;
|
|
2981
|
+
if (cursors.length === 0) return [];
|
|
2982
|
+
}
|
|
2983
|
+
return cursors.filter((v) => typeof v === "string");
|
|
2984
|
+
}
|
|
2985
|
+
function hostOf(value) {
|
|
2986
|
+
try {
|
|
2987
|
+
const u = new URL(value);
|
|
2988
|
+
const h = u.hostname;
|
|
2989
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
2990
|
+
} catch {
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
2995
|
+
try {
|
|
2996
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
2997
|
+
if (!paths) return null;
|
|
2998
|
+
const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
|
|
2999
|
+
for (const path of paths) {
|
|
3000
|
+
for (const value of valuesAt(args, path)) {
|
|
3001
|
+
const host = hostOf(value);
|
|
3002
|
+
if (!host) continue;
|
|
3003
|
+
const m = classifySsrf(host);
|
|
3004
|
+
if (!m) continue;
|
|
3005
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
3006
|
+
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
3007
|
+
return { ...m, argPath: path, host, reason: ssrfReason(m, host) };
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
return null;
|
|
3011
|
+
} catch {
|
|
3012
|
+
return null;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
|
|
2942
3016
|
// src/policy/index.ts
|
|
2943
3017
|
function resolveCheck(v) {
|
|
2944
3018
|
return v === "off" || v === "block" ? v : "review";
|
|
@@ -3052,6 +3126,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3052
3126
|
};
|
|
3053
3127
|
}
|
|
3054
3128
|
}
|
|
3129
|
+
{
|
|
3130
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
3131
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
3132
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
3133
|
+
});
|
|
3134
|
+
if (dest) {
|
|
3135
|
+
return {
|
|
3136
|
+
decision: "block",
|
|
3137
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
3138
|
+
reason: dest.reason,
|
|
3139
|
+
ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
3140
|
+
ruleDescription: dest.reason,
|
|
3141
|
+
tier: 3
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3055
3145
|
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
3056
3146
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3057
3147
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
@@ -5055,6 +5145,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5055
5145
|
CANONICAL_EXTRACTOR_VERSION,
|
|
5056
5146
|
COST_PER_LOOP_ITER_USD,
|
|
5057
5147
|
DEFAULT_EGRESS_ALLOWLIST,
|
|
5148
|
+
DESTINATION_ARGS,
|
|
5058
5149
|
DESTRUCTIVE_OP_RE,
|
|
5059
5150
|
DLP_PATTERNS,
|
|
5060
5151
|
DLP_SCAN_LIMITS,
|
|
@@ -5110,6 +5201,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5110
5201
|
isProtectedHomePath,
|
|
5111
5202
|
isShellShapedTool,
|
|
5112
5203
|
isShieldVerdict,
|
|
5204
|
+
isStrictGatedTier,
|
|
5113
5205
|
matchCanary,
|
|
5114
5206
|
matchCanaryArgs,
|
|
5115
5207
|
matchSensitivePath,
|
|
@@ -5126,7 +5218,9 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5126
5218
|
scanInjection,
|
|
5127
5219
|
scanText,
|
|
5128
5220
|
sensitivePathMatch,
|
|
5221
|
+
ssrfDestinationFloor,
|
|
5129
5222
|
ssrfFloor,
|
|
5223
|
+
ssrfReason,
|
|
5130
5224
|
summarizeBlast,
|
|
5131
5225
|
summarizeScan,
|
|
5132
5226
|
toScanFinding,
|
package/dist/index.mjs
CHANGED
|
@@ -2743,9 +2743,21 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
|
2743
2743
|
// AWS ECS task role
|
|
2744
2744
|
"168.63.129.16",
|
|
2745
2745
|
// Azure WireServer
|
|
2746
|
-
"fd00:ec2::254"
|
|
2746
|
+
"fd00:ec2::254",
|
|
2747
2747
|
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
2748
|
+
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
2749
|
+
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
2750
|
+
// live credential endpoint. It has to be named here, above the range check,
|
|
2751
|
+
// and it is the reason relaxing cgnat is safe.
|
|
2752
|
+
"100.100.100.200"
|
|
2748
2753
|
]);
|
|
2754
|
+
var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
2755
|
+
function isStrictGatedTier(tier) {
|
|
2756
|
+
return STRICT_TIERS.has(tier);
|
|
2757
|
+
}
|
|
2758
|
+
function ssrfReason(m, asWritten) {
|
|
2759
|
+
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
2760
|
+
}
|
|
2749
2761
|
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
2750
2762
|
var v4Octets = (a) => {
|
|
2751
2763
|
const p = a.split(".");
|
|
@@ -2768,7 +2780,7 @@ function classifySsrf(host) {
|
|
|
2768
2780
|
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
2769
2781
|
const o = v4Octets(ip);
|
|
2770
2782
|
if (o) {
|
|
2771
|
-
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified",
|
|
2783
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
2772
2784
|
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
2773
2785
|
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
2774
2786
|
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
@@ -2780,7 +2792,7 @@ function classifySsrf(host) {
|
|
|
2780
2792
|
}
|
|
2781
2793
|
const g = expandIpv6(ip);
|
|
2782
2794
|
if (!g) return null;
|
|
2783
|
-
if (g.every((x) => x === 0)) return hit("unspecified",
|
|
2795
|
+
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
2784
2796
|
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
2785
2797
|
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
2786
2798
|
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
@@ -2793,7 +2805,7 @@ var TIER_REASON = {
|
|
|
2793
2805
|
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
2794
2806
|
"link-local": "a link-local address",
|
|
2795
2807
|
multicast: "a multicast address",
|
|
2796
|
-
unspecified: "the unspecified address",
|
|
2808
|
+
unspecified: "the unspecified address, which reaches this host",
|
|
2797
2809
|
cgnat: "a carrier-grade NAT address",
|
|
2798
2810
|
private: "a loopback or private address"
|
|
2799
2811
|
};
|
|
@@ -2804,18 +2816,76 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
2804
2816
|
for (const { token, binary } of tokens) {
|
|
2805
2817
|
const m = classifySsrf(token);
|
|
2806
2818
|
if (!m) continue;
|
|
2807
|
-
if (m.tier
|
|
2819
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2808
2820
|
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
2809
|
-
return {
|
|
2810
|
-
...m,
|
|
2811
|
-
host: token,
|
|
2812
|
-
binary,
|
|
2813
|
-
reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
|
|
2814
|
-
};
|
|
2821
|
+
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
2815
2822
|
}
|
|
2816
2823
|
return null;
|
|
2817
2824
|
}
|
|
2818
2825
|
|
|
2826
|
+
// src/egress/destinations.ts
|
|
2827
|
+
var DESTINATION_ARGS = /* @__PURE__ */ new Map([
|
|
2828
|
+
["webfetch", ["url"]],
|
|
2829
|
+
["fetch", ["url", "uri"]],
|
|
2830
|
+
["navigate", ["url"]],
|
|
2831
|
+
["preview_start", ["url"]],
|
|
2832
|
+
["browser_batch", ["actions[].input.url"]]
|
|
2833
|
+
]);
|
|
2834
|
+
function bareToolName(toolName) {
|
|
2835
|
+
const parts = toolName.split("__");
|
|
2836
|
+
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
2837
|
+
}
|
|
2838
|
+
function valuesAt(args, path) {
|
|
2839
|
+
let cursors = [args];
|
|
2840
|
+
for (const rawSegment of path.split(".")) {
|
|
2841
|
+
const isArray = rawSegment.endsWith("[]");
|
|
2842
|
+
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
2843
|
+
const next = [];
|
|
2844
|
+
for (const cursor of cursors) {
|
|
2845
|
+
if (cursor === null || typeof cursor !== "object") continue;
|
|
2846
|
+
const child = cursor[key];
|
|
2847
|
+
if (isArray) {
|
|
2848
|
+
if (Array.isArray(child)) next.push(...child);
|
|
2849
|
+
} else if (child !== void 0) {
|
|
2850
|
+
next.push(child);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
cursors = next;
|
|
2854
|
+
if (cursors.length === 0) return [];
|
|
2855
|
+
}
|
|
2856
|
+
return cursors.filter((v) => typeof v === "string");
|
|
2857
|
+
}
|
|
2858
|
+
function hostOf(value) {
|
|
2859
|
+
try {
|
|
2860
|
+
const u = new URL(value);
|
|
2861
|
+
const h = u.hostname;
|
|
2862
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
2863
|
+
} catch {
|
|
2864
|
+
return null;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
2868
|
+
try {
|
|
2869
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
2870
|
+
if (!paths) return null;
|
|
2871
|
+
const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
|
|
2872
|
+
for (const path of paths) {
|
|
2873
|
+
for (const value of valuesAt(args, path)) {
|
|
2874
|
+
const host = hostOf(value);
|
|
2875
|
+
if (!host) continue;
|
|
2876
|
+
const m = classifySsrf(host);
|
|
2877
|
+
if (!m) continue;
|
|
2878
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2879
|
+
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
2880
|
+
return { ...m, argPath: path, host, reason: ssrfReason(m, host) };
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return null;
|
|
2884
|
+
} catch {
|
|
2885
|
+
return null;
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2819
2889
|
// src/policy/index.ts
|
|
2820
2890
|
function resolveCheck(v) {
|
|
2821
2891
|
return v === "off" || v === "block" ? v : "review";
|
|
@@ -2929,6 +2999,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2929
2999
|
};
|
|
2930
3000
|
}
|
|
2931
3001
|
}
|
|
3002
|
+
{
|
|
3003
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
3004
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
3005
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
3006
|
+
});
|
|
3007
|
+
if (dest) {
|
|
3008
|
+
return {
|
|
3009
|
+
decision: "block",
|
|
3010
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
3011
|
+
reason: dest.reason,
|
|
3012
|
+
ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
3013
|
+
ruleDescription: dest.reason,
|
|
3014
|
+
tier: 3
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
2932
3018
|
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2933
3019
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2934
3020
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
@@ -4931,6 +5017,7 @@ export {
|
|
|
4931
5017
|
CANONICAL_EXTRACTOR_VERSION,
|
|
4932
5018
|
COST_PER_LOOP_ITER_USD,
|
|
4933
5019
|
DEFAULT_EGRESS_ALLOWLIST,
|
|
5020
|
+
DESTINATION_ARGS,
|
|
4934
5021
|
DESTRUCTIVE_OP_RE,
|
|
4935
5022
|
DLP_PATTERNS,
|
|
4936
5023
|
DLP_SCAN_LIMITS,
|
|
@@ -4986,6 +5073,7 @@ export {
|
|
|
4986
5073
|
isProtectedHomePath,
|
|
4987
5074
|
isShellShapedTool,
|
|
4988
5075
|
isShieldVerdict,
|
|
5076
|
+
isStrictGatedTier,
|
|
4989
5077
|
matchCanary,
|
|
4990
5078
|
matchCanaryArgs,
|
|
4991
5079
|
matchSensitivePath,
|
|
@@ -5002,7 +5090,9 @@ export {
|
|
|
5002
5090
|
scanInjection,
|
|
5003
5091
|
scanText,
|
|
5004
5092
|
sensitivePathMatch,
|
|
5093
|
+
ssrfDestinationFloor,
|
|
5005
5094
|
ssrfFloor,
|
|
5095
|
+
ssrfReason,
|
|
5006
5096
|
summarizeBlast,
|
|
5007
5097
|
summarizeScan,
|
|
5008
5098
|
toScanFinding,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/policy-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Shared policy evaluation engine for node9 — DLP, smart rules, AST shell parsing, shields, loop detection. Pure functions, no I/O. Used by both node9-proxy and the node9 SaaS firewall.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://node9.ai",
|