@node9/proxy 2.9.3 → 2.11.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/cli.js +107 -8
- package/dist/cli.mjs +107 -8
- package/dist/index.js +105 -7
- package/dist/index.mjs +105 -7
- package/docs/README.md +49 -0
- package/docs/agents/README.md +29 -0
- package/docs/agents/antigravity.md +33 -0
- package/docs/agents/claude-code.md +40 -0
- package/docs/agents/claude-desktop.md +32 -0
- package/docs/agents/codex.md +37 -0
- package/docs/agents/copilot-cli.md +34 -0
- package/docs/agents/cursor.md +36 -0
- package/docs/agents/gemini-cli.md +34 -0
- package/docs/agents/hermes.md +35 -0
- package/docs/agents/opencode.md +36 -0
- package/docs/agents/pi.md +34 -0
- package/docs/agents/vscode.md +33 -0
- package/docs/agents/windsurf.md +31 -0
- package/docs/badges.md +98 -0
- package/docs/comparison.md +66 -0
- package/docs/egress.md +119 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -2046,6 +2046,12 @@ function normalizeIpLiteral(host) {
|
|
|
2046
2046
|
return null;
|
|
2047
2047
|
}
|
|
2048
2048
|
}
|
|
2049
|
+
function isStrictGatedTier(tier) {
|
|
2050
|
+
return STRICT_TIERS.has(tier);
|
|
2051
|
+
}
|
|
2052
|
+
function ssrfReason(m, asWritten) {
|
|
2053
|
+
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
2054
|
+
}
|
|
2049
2055
|
function classifySsrf(host) {
|
|
2050
2056
|
try {
|
|
2051
2057
|
if (typeof host !== "string" || !host) return null;
|
|
@@ -2091,17 +2097,66 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
2091
2097
|
for (const { token, binary } of tokens) {
|
|
2092
2098
|
const m = classifySsrf(token);
|
|
2093
2099
|
if (!m) continue;
|
|
2094
|
-
if (
|
|
2100
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2095
2101
|
if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
|
|
2096
|
-
return {
|
|
2097
|
-
...m,
|
|
2098
|
-
host: token,
|
|
2099
|
-
binary,
|
|
2100
|
-
reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
|
|
2101
|
-
};
|
|
2102
|
+
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
2102
2103
|
}
|
|
2103
2104
|
return null;
|
|
2104
2105
|
}
|
|
2106
|
+
function bareToolName(toolName) {
|
|
2107
|
+
const parts = toolName.split("__");
|
|
2108
|
+
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
2109
|
+
}
|
|
2110
|
+
function valuesAt(args, path77) {
|
|
2111
|
+
let cursors = [args];
|
|
2112
|
+
for (const rawSegment of path77.split(".")) {
|
|
2113
|
+
const isArray = rawSegment.endsWith("[]");
|
|
2114
|
+
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
2115
|
+
const next = [];
|
|
2116
|
+
for (const cursor of cursors) {
|
|
2117
|
+
if (cursor === null || typeof cursor !== "object") continue;
|
|
2118
|
+
const child = cursor[key];
|
|
2119
|
+
if (isArray) {
|
|
2120
|
+
if (Array.isArray(child)) next.push(...child);
|
|
2121
|
+
} else if (child !== void 0) {
|
|
2122
|
+
next.push(child);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
cursors = next;
|
|
2126
|
+
if (cursors.length === 0) return [];
|
|
2127
|
+
}
|
|
2128
|
+
return cursors.filter((v) => typeof v === "string");
|
|
2129
|
+
}
|
|
2130
|
+
function hostOf(value) {
|
|
2131
|
+
try {
|
|
2132
|
+
const u = new URL(value);
|
|
2133
|
+
const h = u.hostname;
|
|
2134
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
2135
|
+
} catch {
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
2140
|
+
try {
|
|
2141
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
2142
|
+
if (!paths) return null;
|
|
2143
|
+
const exempt2 = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
|
|
2144
|
+
for (const path77 of paths) {
|
|
2145
|
+
for (const value of valuesAt(args, path77)) {
|
|
2146
|
+
const host = hostOf(value);
|
|
2147
|
+
if (!host) continue;
|
|
2148
|
+
const m = classifySsrf(host);
|
|
2149
|
+
if (!m) continue;
|
|
2150
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2151
|
+
if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
|
|
2152
|
+
return { ...m, argPath: path77, host, reason: ssrfReason(m, host) };
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return null;
|
|
2156
|
+
} catch {
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2105
2160
|
function resolveCheck(v) {
|
|
2106
2161
|
return v === "off" || v === "block" ? v : "review";
|
|
2107
2162
|
}
|
|
@@ -2199,6 +2254,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2199
2254
|
};
|
|
2200
2255
|
}
|
|
2201
2256
|
}
|
|
2257
|
+
{
|
|
2258
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
2259
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
2260
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
2261
|
+
});
|
|
2262
|
+
if (dest) {
|
|
2263
|
+
return {
|
|
2264
|
+
decision: "block",
|
|
2265
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
2266
|
+
reason: dest.reason,
|
|
2267
|
+
ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
2268
|
+
ruleDescription: dest.reason,
|
|
2269
|
+
tier: 3
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2202
2273
|
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2203
2274
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2204
2275
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
@@ -3205,7 +3276,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
3205
3276
|
}
|
|
3206
3277
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
3207
3278
|
}
|
|
3208
|
-
var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
3279
|
+
var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
3209
3280
|
var init_dist = __esm({
|
|
3210
3281
|
"packages/policy-engine/dist/index.mjs"() {
|
|
3211
3282
|
"use strict";
|
|
@@ -4382,6 +4453,13 @@ var init_dist = __esm({
|
|
|
4382
4453
|
cgnat: "a carrier-grade NAT address",
|
|
4383
4454
|
private: "a loopback or private address"
|
|
4384
4455
|
};
|
|
4456
|
+
DESTINATION_ARGS = /* @__PURE__ */ new Map([
|
|
4457
|
+
["webfetch", ["url"]],
|
|
4458
|
+
["fetch", ["url", "uri"]],
|
|
4459
|
+
["navigate", ["url"]],
|
|
4460
|
+
["preview_start", ["url"]],
|
|
4461
|
+
["browser_batch", ["actions[].input.url"]]
|
|
4462
|
+
]);
|
|
4385
4463
|
VERDICT_RANK = {
|
|
4386
4464
|
allow: 0,
|
|
4387
4465
|
review: 1,
|
|
@@ -8752,6 +8830,26 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8752
8830
|
};
|
|
8753
8831
|
}
|
|
8754
8832
|
}
|
|
8833
|
+
{
|
|
8834
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
8835
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
8836
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
8837
|
+
});
|
|
8838
|
+
if (dest && !isObserveMode) {
|
|
8839
|
+
if (!isManual)
|
|
8840
|
+
appendLocalAudit(toolName, args, "deny", "ssrf-destination", meta, hashAuditArgs);
|
|
8841
|
+
return {
|
|
8842
|
+
approved: false,
|
|
8843
|
+
checkedBy: "local-policy",
|
|
8844
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
8845
|
+
reason: dest.reason,
|
|
8846
|
+
// AuthResult carries no ruleName; the rule identity the audit row needs
|
|
8847
|
+
// travels in ruleHit, as it does for a smart-rule block.
|
|
8848
|
+
ruleHit: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
8849
|
+
ruleDescription: dest.reason
|
|
8850
|
+
};
|
|
8851
|
+
}
|
|
8852
|
+
}
|
|
8755
8853
|
if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
8756
8854
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
8757
8855
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
@@ -9427,6 +9525,7 @@ var init_orchestrator = __esm({
|
|
|
9427
9525
|
init_dlp();
|
|
9428
9526
|
init_registry();
|
|
9429
9527
|
init_dist();
|
|
9528
|
+
init_dist();
|
|
9430
9529
|
init_audit();
|
|
9431
9530
|
init_config();
|
|
9432
9531
|
init_policy();
|
package/dist/cli.mjs
CHANGED
|
@@ -2057,6 +2057,12 @@ function normalizeIpLiteral(host) {
|
|
|
2057
2057
|
return null;
|
|
2058
2058
|
}
|
|
2059
2059
|
}
|
|
2060
|
+
function isStrictGatedTier(tier) {
|
|
2061
|
+
return STRICT_TIERS.has(tier);
|
|
2062
|
+
}
|
|
2063
|
+
function ssrfReason(m, asWritten) {
|
|
2064
|
+
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
2065
|
+
}
|
|
2060
2066
|
function classifySsrf(host) {
|
|
2061
2067
|
try {
|
|
2062
2068
|
if (typeof host !== "string" || !host) return null;
|
|
@@ -2102,17 +2108,66 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
2102
2108
|
for (const { token, binary } of tokens) {
|
|
2103
2109
|
const m = classifySsrf(token);
|
|
2104
2110
|
if (!m) continue;
|
|
2105
|
-
if (
|
|
2111
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2106
2112
|
if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
|
|
2107
|
-
return {
|
|
2108
|
-
...m,
|
|
2109
|
-
host: token,
|
|
2110
|
-
binary,
|
|
2111
|
-
reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
|
|
2112
|
-
};
|
|
2113
|
+
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
2113
2114
|
}
|
|
2114
2115
|
return null;
|
|
2115
2116
|
}
|
|
2117
|
+
function bareToolName(toolName) {
|
|
2118
|
+
const parts = toolName.split("__");
|
|
2119
|
+
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
2120
|
+
}
|
|
2121
|
+
function valuesAt(args, path77) {
|
|
2122
|
+
let cursors = [args];
|
|
2123
|
+
for (const rawSegment of path77.split(".")) {
|
|
2124
|
+
const isArray = rawSegment.endsWith("[]");
|
|
2125
|
+
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
2126
|
+
const next = [];
|
|
2127
|
+
for (const cursor of cursors) {
|
|
2128
|
+
if (cursor === null || typeof cursor !== "object") continue;
|
|
2129
|
+
const child = cursor[key];
|
|
2130
|
+
if (isArray) {
|
|
2131
|
+
if (Array.isArray(child)) next.push(...child);
|
|
2132
|
+
} else if (child !== void 0) {
|
|
2133
|
+
next.push(child);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
cursors = next;
|
|
2137
|
+
if (cursors.length === 0) return [];
|
|
2138
|
+
}
|
|
2139
|
+
return cursors.filter((v) => typeof v === "string");
|
|
2140
|
+
}
|
|
2141
|
+
function hostOf(value) {
|
|
2142
|
+
try {
|
|
2143
|
+
const u = new URL(value);
|
|
2144
|
+
const h = u.hostname;
|
|
2145
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
2146
|
+
} catch {
|
|
2147
|
+
return null;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
2151
|
+
try {
|
|
2152
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
2153
|
+
if (!paths) return null;
|
|
2154
|
+
const exempt2 = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
|
|
2155
|
+
for (const path77 of paths) {
|
|
2156
|
+
for (const value of valuesAt(args, path77)) {
|
|
2157
|
+
const host = hostOf(value);
|
|
2158
|
+
if (!host) continue;
|
|
2159
|
+
const m = classifySsrf(host);
|
|
2160
|
+
if (!m) continue;
|
|
2161
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2162
|
+
if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
|
|
2163
|
+
return { ...m, argPath: path77, host, reason: ssrfReason(m, host) };
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return null;
|
|
2167
|
+
} catch {
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2116
2171
|
function resolveCheck(v) {
|
|
2117
2172
|
return v === "off" || v === "block" ? v : "review";
|
|
2118
2173
|
}
|
|
@@ -2210,6 +2265,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2210
2265
|
};
|
|
2211
2266
|
}
|
|
2212
2267
|
}
|
|
2268
|
+
{
|
|
2269
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
2270
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
2271
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
2272
|
+
});
|
|
2273
|
+
if (dest) {
|
|
2274
|
+
return {
|
|
2275
|
+
decision: "block",
|
|
2276
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
2277
|
+
reason: dest.reason,
|
|
2278
|
+
ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
2279
|
+
ruleDescription: dest.reason,
|
|
2280
|
+
tier: 3
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2213
2284
|
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2214
2285
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2215
2286
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
@@ -3216,7 +3287,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
3216
3287
|
}
|
|
3217
3288
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
3218
3289
|
}
|
|
3219
|
-
var IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
3290
|
+
var IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
3220
3291
|
var init_dist = __esm({
|
|
3221
3292
|
"packages/policy-engine/dist/index.mjs"() {
|
|
3222
3293
|
"use strict";
|
|
@@ -4386,6 +4457,13 @@ var init_dist = __esm({
|
|
|
4386
4457
|
cgnat: "a carrier-grade NAT address",
|
|
4387
4458
|
private: "a loopback or private address"
|
|
4388
4459
|
};
|
|
4460
|
+
DESTINATION_ARGS = /* @__PURE__ */ new Map([
|
|
4461
|
+
["webfetch", ["url"]],
|
|
4462
|
+
["fetch", ["url", "uri"]],
|
|
4463
|
+
["navigate", ["url"]],
|
|
4464
|
+
["preview_start", ["url"]],
|
|
4465
|
+
["browser_batch", ["actions[].input.url"]]
|
|
4466
|
+
]);
|
|
4389
4467
|
VERDICT_RANK = {
|
|
4390
4468
|
allow: 0,
|
|
4391
4469
|
review: 1,
|
|
@@ -8755,6 +8833,26 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
8755
8833
|
};
|
|
8756
8834
|
}
|
|
8757
8835
|
}
|
|
8836
|
+
{
|
|
8837
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
8838
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
8839
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
8840
|
+
});
|
|
8841
|
+
if (dest && !isObserveMode) {
|
|
8842
|
+
if (!isManual)
|
|
8843
|
+
appendLocalAudit(toolName, args, "deny", "ssrf-destination", meta, hashAuditArgs);
|
|
8844
|
+
return {
|
|
8845
|
+
approved: false,
|
|
8846
|
+
checkedBy: "local-policy",
|
|
8847
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
8848
|
+
reason: dest.reason,
|
|
8849
|
+
// AuthResult carries no ruleName; the rule identity the audit row needs
|
|
8850
|
+
// travels in ruleHit, as it does for a smart-rule block.
|
|
8851
|
+
ruleHit: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
8852
|
+
ruleDescription: dest.reason
|
|
8853
|
+
};
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8758
8856
|
if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
8759
8857
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
8760
8858
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
@@ -9429,6 +9527,7 @@ var init_orchestrator = __esm({
|
|
|
9429
9527
|
init_dlp();
|
|
9430
9528
|
init_registry();
|
|
9431
9529
|
init_dist();
|
|
9530
|
+
init_dist();
|
|
9432
9531
|
init_audit();
|
|
9433
9532
|
init_config();
|
|
9434
9533
|
init_policy();
|
package/dist/index.js
CHANGED
|
@@ -3126,6 +3126,12 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
|
3126
3126
|
"100.100.100.200"
|
|
3127
3127
|
]);
|
|
3128
3128
|
var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
3129
|
+
function isStrictGatedTier(tier) {
|
|
3130
|
+
return STRICT_TIERS.has(tier);
|
|
3131
|
+
}
|
|
3132
|
+
function ssrfReason(m, asWritten) {
|
|
3133
|
+
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
3134
|
+
}
|
|
3129
3135
|
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
3130
3136
|
var v4Octets = (a) => {
|
|
3131
3137
|
const p = a.split(".");
|
|
@@ -3184,17 +3190,73 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
3184
3190
|
for (const { token, binary } of tokens) {
|
|
3185
3191
|
const m = classifySsrf(token);
|
|
3186
3192
|
if (!m) continue;
|
|
3187
|
-
if (
|
|
3193
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
3188
3194
|
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
3189
|
-
return {
|
|
3190
|
-
...m,
|
|
3191
|
-
host: token,
|
|
3192
|
-
binary,
|
|
3193
|
-
reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
|
|
3194
|
-
};
|
|
3195
|
+
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
3195
3196
|
}
|
|
3196
3197
|
return null;
|
|
3197
3198
|
}
|
|
3199
|
+
var DESTINATION_ARGS = /* @__PURE__ */ new Map([
|
|
3200
|
+
["webfetch", ["url"]],
|
|
3201
|
+
["fetch", ["url", "uri"]],
|
|
3202
|
+
["navigate", ["url"]],
|
|
3203
|
+
["preview_start", ["url"]],
|
|
3204
|
+
["browser_batch", ["actions[].input.url"]]
|
|
3205
|
+
]);
|
|
3206
|
+
function bareToolName(toolName) {
|
|
3207
|
+
const parts = toolName.split("__");
|
|
3208
|
+
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
3209
|
+
}
|
|
3210
|
+
function valuesAt(args, path15) {
|
|
3211
|
+
let cursors = [args];
|
|
3212
|
+
for (const rawSegment of path15.split(".")) {
|
|
3213
|
+
const isArray = rawSegment.endsWith("[]");
|
|
3214
|
+
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
3215
|
+
const next = [];
|
|
3216
|
+
for (const cursor of cursors) {
|
|
3217
|
+
if (cursor === null || typeof cursor !== "object") continue;
|
|
3218
|
+
const child = cursor[key];
|
|
3219
|
+
if (isArray) {
|
|
3220
|
+
if (Array.isArray(child)) next.push(...child);
|
|
3221
|
+
} else if (child !== void 0) {
|
|
3222
|
+
next.push(child);
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
cursors = next;
|
|
3226
|
+
if (cursors.length === 0) return [];
|
|
3227
|
+
}
|
|
3228
|
+
return cursors.filter((v) => typeof v === "string");
|
|
3229
|
+
}
|
|
3230
|
+
function hostOf(value) {
|
|
3231
|
+
try {
|
|
3232
|
+
const u = new URL(value);
|
|
3233
|
+
const h = u.hostname;
|
|
3234
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
3235
|
+
} catch {
|
|
3236
|
+
return null;
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
3240
|
+
try {
|
|
3241
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
3242
|
+
if (!paths) return null;
|
|
3243
|
+
const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
|
|
3244
|
+
for (const path15 of paths) {
|
|
3245
|
+
for (const value of valuesAt(args, path15)) {
|
|
3246
|
+
const host = hostOf(value);
|
|
3247
|
+
if (!host) continue;
|
|
3248
|
+
const m = classifySsrf(host);
|
|
3249
|
+
if (!m) continue;
|
|
3250
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
3251
|
+
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
3252
|
+
return { ...m, argPath: path15, host, reason: ssrfReason(m, host) };
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
return null;
|
|
3256
|
+
} catch {
|
|
3257
|
+
return null;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3198
3260
|
function resolveCheck(v) {
|
|
3199
3261
|
return v === "off" || v === "block" ? v : "review";
|
|
3200
3262
|
}
|
|
@@ -3298,6 +3360,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3298
3360
|
};
|
|
3299
3361
|
}
|
|
3300
3362
|
}
|
|
3363
|
+
{
|
|
3364
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
3365
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
3366
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
3367
|
+
});
|
|
3368
|
+
if (dest) {
|
|
3369
|
+
return {
|
|
3370
|
+
decision: "block",
|
|
3371
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
3372
|
+
reason: dest.reason,
|
|
3373
|
+
ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
3374
|
+
ruleDescription: dest.reason,
|
|
3375
|
+
tier: 3
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3301
3379
|
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
3302
3380
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3303
3381
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
@@ -7170,6 +7248,26 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
7170
7248
|
};
|
|
7171
7249
|
}
|
|
7172
7250
|
}
|
|
7251
|
+
{
|
|
7252
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
7253
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
7254
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
7255
|
+
});
|
|
7256
|
+
if (dest && !isObserveMode) {
|
|
7257
|
+
if (!isManual)
|
|
7258
|
+
appendLocalAudit(toolName, args, "deny", "ssrf-destination", meta, hashAuditArgs);
|
|
7259
|
+
return {
|
|
7260
|
+
approved: false,
|
|
7261
|
+
checkedBy: "local-policy",
|
|
7262
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
7263
|
+
reason: dest.reason,
|
|
7264
|
+
// AuthResult carries no ruleName; the rule identity the audit row needs
|
|
7265
|
+
// travels in ruleHit, as it does for a smart-rule block.
|
|
7266
|
+
ruleHit: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
7267
|
+
ruleDescription: dest.reason
|
|
7268
|
+
};
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7173
7271
|
if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
7174
7272
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
7175
7273
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
package/dist/index.mjs
CHANGED
|
@@ -3096,6 +3096,12 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
|
3096
3096
|
"100.100.100.200"
|
|
3097
3097
|
]);
|
|
3098
3098
|
var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
3099
|
+
function isStrictGatedTier(tier) {
|
|
3100
|
+
return STRICT_TIERS.has(tier);
|
|
3101
|
+
}
|
|
3102
|
+
function ssrfReason(m, asWritten) {
|
|
3103
|
+
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
3104
|
+
}
|
|
3099
3105
|
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
3100
3106
|
var v4Octets = (a) => {
|
|
3101
3107
|
const p = a.split(".");
|
|
@@ -3154,17 +3160,73 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
3154
3160
|
for (const { token, binary } of tokens) {
|
|
3155
3161
|
const m = classifySsrf(token);
|
|
3156
3162
|
if (!m) continue;
|
|
3157
|
-
if (
|
|
3163
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
3158
3164
|
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
3159
|
-
return {
|
|
3160
|
-
...m,
|
|
3161
|
-
host: token,
|
|
3162
|
-
binary,
|
|
3163
|
-
reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
|
|
3164
|
-
};
|
|
3165
|
+
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
3165
3166
|
}
|
|
3166
3167
|
return null;
|
|
3167
3168
|
}
|
|
3169
|
+
var DESTINATION_ARGS = /* @__PURE__ */ new Map([
|
|
3170
|
+
["webfetch", ["url"]],
|
|
3171
|
+
["fetch", ["url", "uri"]],
|
|
3172
|
+
["navigate", ["url"]],
|
|
3173
|
+
["preview_start", ["url"]],
|
|
3174
|
+
["browser_batch", ["actions[].input.url"]]
|
|
3175
|
+
]);
|
|
3176
|
+
function bareToolName(toolName) {
|
|
3177
|
+
const parts = toolName.split("__");
|
|
3178
|
+
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
3179
|
+
}
|
|
3180
|
+
function valuesAt(args, path15) {
|
|
3181
|
+
let cursors = [args];
|
|
3182
|
+
for (const rawSegment of path15.split(".")) {
|
|
3183
|
+
const isArray = rawSegment.endsWith("[]");
|
|
3184
|
+
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
3185
|
+
const next = [];
|
|
3186
|
+
for (const cursor of cursors) {
|
|
3187
|
+
if (cursor === null || typeof cursor !== "object") continue;
|
|
3188
|
+
const child = cursor[key];
|
|
3189
|
+
if (isArray) {
|
|
3190
|
+
if (Array.isArray(child)) next.push(...child);
|
|
3191
|
+
} else if (child !== void 0) {
|
|
3192
|
+
next.push(child);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
cursors = next;
|
|
3196
|
+
if (cursors.length === 0) return [];
|
|
3197
|
+
}
|
|
3198
|
+
return cursors.filter((v) => typeof v === "string");
|
|
3199
|
+
}
|
|
3200
|
+
function hostOf(value) {
|
|
3201
|
+
try {
|
|
3202
|
+
const u = new URL(value);
|
|
3203
|
+
const h = u.hostname;
|
|
3204
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
3205
|
+
} catch {
|
|
3206
|
+
return null;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
3210
|
+
try {
|
|
3211
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
3212
|
+
if (!paths) return null;
|
|
3213
|
+
const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
|
|
3214
|
+
for (const path15 of paths) {
|
|
3215
|
+
for (const value of valuesAt(args, path15)) {
|
|
3216
|
+
const host = hostOf(value);
|
|
3217
|
+
if (!host) continue;
|
|
3218
|
+
const m = classifySsrf(host);
|
|
3219
|
+
if (!m) continue;
|
|
3220
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
3221
|
+
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
3222
|
+
return { ...m, argPath: path15, host, reason: ssrfReason(m, host) };
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
return null;
|
|
3226
|
+
} catch {
|
|
3227
|
+
return null;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3168
3230
|
function resolveCheck(v) {
|
|
3169
3231
|
return v === "off" || v === "block" ? v : "review";
|
|
3170
3232
|
}
|
|
@@ -3268,6 +3330,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3268
3330
|
};
|
|
3269
3331
|
}
|
|
3270
3332
|
}
|
|
3333
|
+
{
|
|
3334
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
3335
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
3336
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
3337
|
+
});
|
|
3338
|
+
if (dest) {
|
|
3339
|
+
return {
|
|
3340
|
+
decision: "block",
|
|
3341
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
3342
|
+
reason: dest.reason,
|
|
3343
|
+
ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
3344
|
+
ruleDescription: dest.reason,
|
|
3345
|
+
tier: 3
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3271
3349
|
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
3272
3350
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3273
3351
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
@@ -7140,6 +7218,26 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
7140
7218
|
};
|
|
7141
7219
|
}
|
|
7142
7220
|
}
|
|
7221
|
+
{
|
|
7222
|
+
const dest = ssrfDestinationFloor(toolName, args, {
|
|
7223
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
7224
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
7225
|
+
});
|
|
7226
|
+
if (dest && !isObserveMode) {
|
|
7227
|
+
if (!isManual)
|
|
7228
|
+
appendLocalAudit(toolName, args, "deny", "ssrf-destination", meta, hashAuditArgs);
|
|
7229
|
+
return {
|
|
7230
|
+
approved: false,
|
|
7231
|
+
checkedBy: "local-policy",
|
|
7232
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
7233
|
+
reason: dest.reason,
|
|
7234
|
+
// AuthResult carries no ruleName; the rule identity the audit row needs
|
|
7235
|
+
// travels in ruleHit, as it does for a smart-rule block.
|
|
7236
|
+
ruleHit: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
7237
|
+
ruleDescription: dest.reason
|
|
7238
|
+
};
|
|
7239
|
+
}
|
|
7240
|
+
}
|
|
7143
7241
|
if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
7144
7242
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
7145
7243
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# node9 documentation
|
|
2
|
+
|
|
3
|
+
These pages are the source of truth for how node9 behaves. They live next to the code so the tests
|
|
4
|
+
in this repository can check them: a page that names a command the CLI does not have, or a command
|
|
5
|
+
with no page at all, fails CI.
|
|
6
|
+
|
|
7
|
+
The site at [node9.ai/docs](https://node9.ai/docs) renders these same files.
|
|
8
|
+
|
|
9
|
+
## Protections
|
|
10
|
+
|
|
11
|
+
| Page | What it covers |
|
|
12
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| [Egress Control](egress.md) | Which hosts an agent may reach, the always-on floor around cloud metadata and private ranges, and what a destination gate does not cover |
|
|
14
|
+
|
|
15
|
+
## Per agent
|
|
16
|
+
|
|
17
|
+
[One page per agent](agents/README.md): how node9 is wired in, what that covers, and what it does
|
|
18
|
+
not. Twelve agents, split by control model.
|
|
19
|
+
|
|
20
|
+
## Reference
|
|
21
|
+
|
|
22
|
+
| Page | What it covers |
|
|
23
|
+
| --------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| [Comparison](comparison.md) | Where node9 sits among agent-security tools, on the axis of what a tool can read before an agent runs |
|
|
25
|
+
| [Badges](badges.md) | The `scanned by node9` badge for your own README |
|
|
26
|
+
|
|
27
|
+
## Writing a page here
|
|
28
|
+
|
|
29
|
+
Every page starts with front matter:
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
---
|
|
33
|
+
id: egress
|
|
34
|
+
label: Egress Control
|
|
35
|
+
description: One sentence. It becomes the page's search description.
|
|
36
|
+
group: Protections
|
|
37
|
+
order: 20
|
|
38
|
+
---
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`id` becomes the URL (`/docs/egress`), `group` places it in the site's left navigation, and `order`
|
|
42
|
+
sorts it within that group. Use GitHub-flavoured Markdown plus GitHub alerts (`> [!NOTE]`,
|
|
43
|
+
`> [!WARNING]`), which render in both places.
|
|
44
|
+
|
|
45
|
+
Two rules the tests enforce:
|
|
46
|
+
|
|
47
|
+
- Every command a page names must exist. Check with `node9 <command> --help` before you write it.
|
|
48
|
+
- Every page must state what the feature does **not** do. A page that only sells is a page that
|
|
49
|
+
will be wrong within a release.
|