@node9/proxy 2.9.1 → 2.9.3
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 +91 -34
- package/dist/cli.mjs +91 -34
- package/dist/dashboard.mjs +86 -25
- package/dist/index.js +89 -27
- package/dist/index.mjs +89 -27
- package/dist/scan-ink.mjs +7 -7
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -235,30 +235,86 @@ var init_audit = __esm({
|
|
|
235
235
|
});
|
|
236
236
|
|
|
237
237
|
// src/config-schema.ts
|
|
238
|
+
function formatIssues(issues) {
|
|
239
|
+
const lines = issues.map((issue) => {
|
|
240
|
+
const path77 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
241
|
+
return ` \u2022 ${path77}: ${issue.message}`;
|
|
242
|
+
});
|
|
243
|
+
return `Invalid config:
|
|
244
|
+
${lines.join("\n")}`;
|
|
245
|
+
}
|
|
246
|
+
function prunePaths(root, paths) {
|
|
247
|
+
let removed = false;
|
|
248
|
+
const ordered = [...paths].sort((x, y) => {
|
|
249
|
+
if (y.length !== x.length) return y.length - x.length;
|
|
250
|
+
const xi = x[x.length - 1];
|
|
251
|
+
const yi = y[y.length - 1];
|
|
252
|
+
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
253
|
+
});
|
|
254
|
+
for (const path77 of ordered) {
|
|
255
|
+
let cur = root;
|
|
256
|
+
for (const key of path77.slice(0, -1)) {
|
|
257
|
+
if (cur === null || typeof cur !== "object") {
|
|
258
|
+
cur = void 0;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
cur = cur[key];
|
|
262
|
+
}
|
|
263
|
+
if (cur === null || typeof cur !== "object") continue;
|
|
264
|
+
const last = path77[path77.length - 1];
|
|
265
|
+
if (Array.isArray(cur)) {
|
|
266
|
+
const i = Number(last);
|
|
267
|
+
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
268
|
+
cur.splice(i, 1);
|
|
269
|
+
removed = true;
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const obj = cur;
|
|
274
|
+
if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
|
|
275
|
+
delete obj[String(last)];
|
|
276
|
+
removed = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return removed;
|
|
280
|
+
}
|
|
238
281
|
function sanitizeConfig(raw) {
|
|
239
282
|
const result = ConfigFileSchema.safeParse(raw);
|
|
240
283
|
if (result.success) {
|
|
241
284
|
return { sanitized: result.data, error: null };
|
|
242
285
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
for (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
286
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
287
|
+
return { sanitized: {}, error: formatIssues(result.error.issues) };
|
|
288
|
+
}
|
|
289
|
+
const working = structuredClone(raw);
|
|
290
|
+
for (let level = 0; level < 6; level++) {
|
|
291
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
292
|
+
const attempt = ConfigFileSchema.safeParse(working);
|
|
293
|
+
if (attempt.success) break;
|
|
294
|
+
const paths = attempt.error.issues.flatMap((issue) => {
|
|
295
|
+
const at = issue.path;
|
|
296
|
+
if (issue.code === "unrecognized_keys") {
|
|
297
|
+
return issue.keys.map((k) => [...at, k]);
|
|
298
|
+
}
|
|
299
|
+
return [at.slice(0, -level || void 0)];
|
|
300
|
+
}).filter((path77) => path77.length > 0);
|
|
301
|
+
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
252
302
|
}
|
|
303
|
+
if (ConfigFileSchema.safeParse(working).success) break;
|
|
304
|
+
}
|
|
305
|
+
const after = ConfigFileSchema.safeParse(working);
|
|
306
|
+
const sanitized = {};
|
|
307
|
+
const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
|
|
308
|
+
after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
309
|
+
);
|
|
310
|
+
for (const [key, value] of Object.entries(working)) {
|
|
311
|
+
if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
|
|
253
312
|
}
|
|
254
|
-
const lines = result.error.issues.map((issue) => {
|
|
255
|
-
const path77 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
256
|
-
return ` \u2022 ${path77}: ${issue.message}`;
|
|
257
|
-
});
|
|
258
313
|
return {
|
|
259
314
|
sanitized,
|
|
260
|
-
|
|
261
|
-
|
|
315
|
+
// The message names what the USER should fix, so it is built from the
|
|
316
|
+
// original parse, not from whatever survived the prune.
|
|
317
|
+
error: formatIssues(result.error.issues)
|
|
262
318
|
};
|
|
263
319
|
}
|
|
264
320
|
var import_zod, noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
|
|
@@ -283,9 +339,9 @@ var init_config_schema = __esm({
|
|
|
283
339
|
"notMatchesGlob"
|
|
284
340
|
],
|
|
285
341
|
{
|
|
286
|
-
errorMap
|
|
287
|
-
|
|
288
|
-
|
|
342
|
+
// zod 4 replaced errorMap with `error`. The wording is kept verbatim:
|
|
343
|
+
// it is what a user sees when their config is rejected.
|
|
344
|
+
error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
289
345
|
}
|
|
290
346
|
),
|
|
291
347
|
value: import_zod.z.string().optional(),
|
|
@@ -303,7 +359,7 @@ var init_config_schema = __esm({
|
|
|
303
359
|
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
304
360
|
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
305
361
|
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
306
|
-
|
|
362
|
+
error: () => "verdict must be one of: allow, review, block"
|
|
307
363
|
}),
|
|
308
364
|
reason: import_zod.z.string().optional(),
|
|
309
365
|
description: import_zod.z.string().optional(),
|
|
@@ -376,7 +432,7 @@ var init_config_schema = __esm({
|
|
|
376
432
|
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
377
433
|
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
378
434
|
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
379
|
-
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
435
|
+
toolInspection: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
380
436
|
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
381
437
|
dlp: import_zod.z.object({
|
|
382
438
|
enabled: import_zod.z.boolean().optional(),
|
|
@@ -421,8 +477,8 @@ var init_config_schema = __esm({
|
|
|
421
477
|
roots: import_zod.z.array(import_zod.z.string()).optional()
|
|
422
478
|
}).optional()
|
|
423
479
|
}).optional(),
|
|
424
|
-
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
425
|
-
}).strict(
|
|
480
|
+
environments: import_zod.z.record(import_zod.z.string(), import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
481
|
+
}).strict();
|
|
426
482
|
}
|
|
427
483
|
});
|
|
428
484
|
|
|
@@ -2007,7 +2063,7 @@ function classifySsrf(host) {
|
|
|
2007
2063
|
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
2008
2064
|
const o = v4Octets(ip);
|
|
2009
2065
|
if (o) {
|
|
2010
|
-
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified",
|
|
2066
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
2011
2067
|
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
2012
2068
|
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
2013
2069
|
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
@@ -2019,7 +2075,7 @@ function classifySsrf(host) {
|
|
|
2019
2075
|
}
|
|
2020
2076
|
const g = expandIpv6(ip);
|
|
2021
2077
|
if (!g) return null;
|
|
2022
|
-
if (g.every((x) => x === 0)) return hit("unspecified",
|
|
2078
|
+
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
2023
2079
|
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
2024
2080
|
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
2025
2081
|
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
@@ -2035,7 +2091,7 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
2035
2091
|
for (const { token, binary } of tokens) {
|
|
2036
2092
|
const m = classifySsrf(token);
|
|
2037
2093
|
if (!m) continue;
|
|
2038
|
-
if (m.tier
|
|
2094
|
+
if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
|
|
2039
2095
|
if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
|
|
2040
2096
|
return {
|
|
2041
2097
|
...m,
|
|
@@ -3149,7 +3205,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
3149
3205
|
}
|
|
3150
3206
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
3151
3207
|
}
|
|
3152
|
-
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, 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;
|
|
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;
|
|
3153
3209
|
var init_dist = __esm({
|
|
3154
3210
|
"packages/policy-engine/dist/index.mjs"() {
|
|
3155
3211
|
"use strict";
|
|
@@ -4304,9 +4360,15 @@ var init_dist = __esm({
|
|
|
4304
4360
|
// AWS ECS task role
|
|
4305
4361
|
"168.63.129.16",
|
|
4306
4362
|
// Azure WireServer
|
|
4307
|
-
"fd00:ec2::254"
|
|
4363
|
+
"fd00:ec2::254",
|
|
4308
4364
|
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
4365
|
+
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
4366
|
+
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
4367
|
+
// live credential endpoint. It has to be named here, above the range check,
|
|
4368
|
+
// and it is the reason relaxing cgnat is safe.
|
|
4369
|
+
"100.100.100.200"
|
|
4309
4370
|
]);
|
|
4371
|
+
STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
4310
4372
|
METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
4311
4373
|
v4Octets = (a) => {
|
|
4312
4374
|
const p = a.split(".");
|
|
@@ -4316,7 +4378,7 @@ var init_dist = __esm({
|
|
|
4316
4378
|
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
4317
4379
|
"link-local": "a link-local address",
|
|
4318
4380
|
multicast: "a multicast address",
|
|
4319
|
-
unspecified: "the unspecified address",
|
|
4381
|
+
unspecified: "the unspecified address, which reaches this host",
|
|
4320
4382
|
cgnat: "a carrier-grade NAT address",
|
|
4321
4383
|
private: "a loopback or private address"
|
|
4322
4384
|
};
|
|
@@ -54427,12 +54489,7 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
54427
54489
|
`${num2(d.approvals.approved)} approved \xB7 ${num2(d.approvals.denied)} denied \xB7 ${num2(d.approvals.timedOut)} timed-out\u2192deny`
|
|
54428
54490
|
);
|
|
54429
54491
|
dimRow("\u{1F4C1}", "Files", d.files.blocked > 0, `${num2(d.files.blocked)} jail-path reads blocked`);
|
|
54430
|
-
dimRow(
|
|
54431
|
-
"\u{1F6E0}",
|
|
54432
|
-
"Tool rules",
|
|
54433
|
-
d.toolRules.blocked > 0,
|
|
54434
|
-
`${num2(d.toolRules.blocked)} shields/rules`
|
|
54435
|
-
);
|
|
54492
|
+
dimRow("\u{1F6E0}", "Tool rules", d.toolRules.blocked > 0, `${num2(d.toolRules.blocked)} shields/rules`);
|
|
54436
54493
|
dimRow("\u{1F9E9}", "Apps (MCP)", d.apps.blocked > 0, `${num2(d.apps.blocked)} app-permission blocks`);
|
|
54437
54494
|
dimRow("\u{1F4B0}", "Cost", d.cost.totalUSD > 0, `${fmtCost2(d.cost.totalUSD)} this period`);
|
|
54438
54495
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -240,30 +240,86 @@ var init_audit = __esm({
|
|
|
240
240
|
|
|
241
241
|
// src/config-schema.ts
|
|
242
242
|
import { z } from "zod";
|
|
243
|
+
function formatIssues(issues) {
|
|
244
|
+
const lines = issues.map((issue) => {
|
|
245
|
+
const path77 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
246
|
+
return ` \u2022 ${path77}: ${issue.message}`;
|
|
247
|
+
});
|
|
248
|
+
return `Invalid config:
|
|
249
|
+
${lines.join("\n")}`;
|
|
250
|
+
}
|
|
251
|
+
function prunePaths(root, paths) {
|
|
252
|
+
let removed = false;
|
|
253
|
+
const ordered = [...paths].sort((x, y) => {
|
|
254
|
+
if (y.length !== x.length) return y.length - x.length;
|
|
255
|
+
const xi = x[x.length - 1];
|
|
256
|
+
const yi = y[y.length - 1];
|
|
257
|
+
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
258
|
+
});
|
|
259
|
+
for (const path77 of ordered) {
|
|
260
|
+
let cur = root;
|
|
261
|
+
for (const key of path77.slice(0, -1)) {
|
|
262
|
+
if (cur === null || typeof cur !== "object") {
|
|
263
|
+
cur = void 0;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
cur = cur[key];
|
|
267
|
+
}
|
|
268
|
+
if (cur === null || typeof cur !== "object") continue;
|
|
269
|
+
const last = path77[path77.length - 1];
|
|
270
|
+
if (Array.isArray(cur)) {
|
|
271
|
+
const i = Number(last);
|
|
272
|
+
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
273
|
+
cur.splice(i, 1);
|
|
274
|
+
removed = true;
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const obj = cur;
|
|
279
|
+
if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
|
|
280
|
+
delete obj[String(last)];
|
|
281
|
+
removed = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return removed;
|
|
285
|
+
}
|
|
243
286
|
function sanitizeConfig(raw) {
|
|
244
287
|
const result = ConfigFileSchema.safeParse(raw);
|
|
245
288
|
if (result.success) {
|
|
246
289
|
return { sanitized: result.data, error: null };
|
|
247
290
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
for (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
291
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
292
|
+
return { sanitized: {}, error: formatIssues(result.error.issues) };
|
|
293
|
+
}
|
|
294
|
+
const working = structuredClone(raw);
|
|
295
|
+
for (let level = 0; level < 6; level++) {
|
|
296
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
297
|
+
const attempt = ConfigFileSchema.safeParse(working);
|
|
298
|
+
if (attempt.success) break;
|
|
299
|
+
const paths = attempt.error.issues.flatMap((issue) => {
|
|
300
|
+
const at = issue.path;
|
|
301
|
+
if (issue.code === "unrecognized_keys") {
|
|
302
|
+
return issue.keys.map((k) => [...at, k]);
|
|
303
|
+
}
|
|
304
|
+
return [at.slice(0, -level || void 0)];
|
|
305
|
+
}).filter((path77) => path77.length > 0);
|
|
306
|
+
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
257
307
|
}
|
|
308
|
+
if (ConfigFileSchema.safeParse(working).success) break;
|
|
309
|
+
}
|
|
310
|
+
const after = ConfigFileSchema.safeParse(working);
|
|
311
|
+
const sanitized = {};
|
|
312
|
+
const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
|
|
313
|
+
after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
314
|
+
);
|
|
315
|
+
for (const [key, value] of Object.entries(working)) {
|
|
316
|
+
if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
|
|
258
317
|
}
|
|
259
|
-
const lines = result.error.issues.map((issue) => {
|
|
260
|
-
const path77 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
261
|
-
return ` \u2022 ${path77}: ${issue.message}`;
|
|
262
|
-
});
|
|
263
318
|
return {
|
|
264
319
|
sanitized,
|
|
265
|
-
|
|
266
|
-
|
|
320
|
+
// The message names what the USER should fix, so it is built from the
|
|
321
|
+
// original parse, not from whatever survived the prune.
|
|
322
|
+
error: formatIssues(result.error.issues)
|
|
267
323
|
};
|
|
268
324
|
}
|
|
269
325
|
var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
|
|
@@ -287,9 +343,9 @@ var init_config_schema = __esm({
|
|
|
287
343
|
"notMatchesGlob"
|
|
288
344
|
],
|
|
289
345
|
{
|
|
290
|
-
errorMap
|
|
291
|
-
|
|
292
|
-
|
|
346
|
+
// zod 4 replaced errorMap with `error`. The wording is kept verbatim:
|
|
347
|
+
// it is what a user sees when their config is rejected.
|
|
348
|
+
error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
293
349
|
}
|
|
294
350
|
),
|
|
295
351
|
value: z.string().optional(),
|
|
@@ -307,7 +363,7 @@ var init_config_schema = __esm({
|
|
|
307
363
|
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
308
364
|
conditionMode: z.enum(["all", "any"]).optional(),
|
|
309
365
|
verdict: z.enum(["allow", "review", "block"], {
|
|
310
|
-
|
|
366
|
+
error: () => "verdict must be one of: allow, review, block"
|
|
311
367
|
}),
|
|
312
368
|
reason: z.string().optional(),
|
|
313
369
|
description: z.string().optional(),
|
|
@@ -380,7 +436,7 @@ var init_config_schema = __esm({
|
|
|
380
436
|
sandboxPaths: z.array(z.string()).optional(),
|
|
381
437
|
dangerousWords: z.array(noNewlines).optional(),
|
|
382
438
|
ignoredTools: z.array(z.string()).optional(),
|
|
383
|
-
toolInspection: z.record(z.string()).optional(),
|
|
439
|
+
toolInspection: z.record(z.string(), z.string()).optional(),
|
|
384
440
|
smartRules: z.array(SmartRuleSchema).optional(),
|
|
385
441
|
dlp: z.object({
|
|
386
442
|
enabled: z.boolean().optional(),
|
|
@@ -425,8 +481,8 @@ var init_config_schema = __esm({
|
|
|
425
481
|
roots: z.array(z.string()).optional()
|
|
426
482
|
}).optional()
|
|
427
483
|
}).optional(),
|
|
428
|
-
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
429
|
-
}).strict(
|
|
484
|
+
environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
485
|
+
}).strict();
|
|
430
486
|
}
|
|
431
487
|
});
|
|
432
488
|
|
|
@@ -2018,7 +2074,7 @@ function classifySsrf(host) {
|
|
|
2018
2074
|
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
2019
2075
|
const o = v4Octets(ip);
|
|
2020
2076
|
if (o) {
|
|
2021
|
-
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified",
|
|
2077
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
2022
2078
|
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
2023
2079
|
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
2024
2080
|
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
@@ -2030,7 +2086,7 @@ function classifySsrf(host) {
|
|
|
2030
2086
|
}
|
|
2031
2087
|
const g = expandIpv6(ip);
|
|
2032
2088
|
if (!g) return null;
|
|
2033
|
-
if (g.every((x) => x === 0)) return hit("unspecified",
|
|
2089
|
+
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
2034
2090
|
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
2035
2091
|
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
2036
2092
|
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
@@ -2046,7 +2102,7 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
2046
2102
|
for (const { token, binary } of tokens) {
|
|
2047
2103
|
const m = classifySsrf(token);
|
|
2048
2104
|
if (!m) continue;
|
|
2049
|
-
if (m.tier
|
|
2105
|
+
if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
|
|
2050
2106
|
if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
|
|
2051
2107
|
return {
|
|
2052
2108
|
...m,
|
|
@@ -3160,7 +3216,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
3160
3216
|
}
|
|
3161
3217
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
3162
3218
|
}
|
|
3163
|
-
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, 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;
|
|
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;
|
|
3164
3220
|
var init_dist = __esm({
|
|
3165
3221
|
"packages/policy-engine/dist/index.mjs"() {
|
|
3166
3222
|
"use strict";
|
|
@@ -4308,9 +4364,15 @@ var init_dist = __esm({
|
|
|
4308
4364
|
// AWS ECS task role
|
|
4309
4365
|
"168.63.129.16",
|
|
4310
4366
|
// Azure WireServer
|
|
4311
|
-
"fd00:ec2::254"
|
|
4367
|
+
"fd00:ec2::254",
|
|
4312
4368
|
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
4369
|
+
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
4370
|
+
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
4371
|
+
// live credential endpoint. It has to be named here, above the range check,
|
|
4372
|
+
// and it is the reason relaxing cgnat is safe.
|
|
4373
|
+
"100.100.100.200"
|
|
4313
4374
|
]);
|
|
4375
|
+
STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
4314
4376
|
METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
4315
4377
|
v4Octets = (a) => {
|
|
4316
4378
|
const p = a.split(".");
|
|
@@ -4320,7 +4382,7 @@ var init_dist = __esm({
|
|
|
4320
4382
|
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
4321
4383
|
"link-local": "a link-local address",
|
|
4322
4384
|
multicast: "a multicast address",
|
|
4323
|
-
unspecified: "the unspecified address",
|
|
4385
|
+
unspecified: "the unspecified address, which reaches this host",
|
|
4324
4386
|
cgnat: "a carrier-grade NAT address",
|
|
4325
4387
|
private: "a loopback or private address"
|
|
4326
4388
|
};
|
|
@@ -54419,12 +54481,7 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
54419
54481
|
`${num2(d.approvals.approved)} approved \xB7 ${num2(d.approvals.denied)} denied \xB7 ${num2(d.approvals.timedOut)} timed-out\u2192deny`
|
|
54420
54482
|
);
|
|
54421
54483
|
dimRow("\u{1F4C1}", "Files", d.files.blocked > 0, `${num2(d.files.blocked)} jail-path reads blocked`);
|
|
54422
|
-
dimRow(
|
|
54423
|
-
"\u{1F6E0}",
|
|
54424
|
-
"Tool rules",
|
|
54425
|
-
d.toolRules.blocked > 0,
|
|
54426
|
-
`${num2(d.toolRules.blocked)} shields/rules`
|
|
54427
|
-
);
|
|
54484
|
+
dimRow("\u{1F6E0}", "Tool rules", d.toolRules.blocked > 0, `${num2(d.toolRules.blocked)} shields/rules`);
|
|
54428
54485
|
dimRow("\u{1F9E9}", "Apps (MCP)", d.apps.blocked > 0, `${num2(d.apps.blocked)} app-permission blocks`);
|
|
54429
54486
|
dimRow("\u{1F4B0}", "Cost", d.cost.totalUSD > 0, `${fmtCost2(d.cost.totalUSD)} this period`);
|
|
54430
54487
|
}
|
package/dist/dashboard.mjs
CHANGED
|
@@ -856,7 +856,7 @@ function classifySsrf(host) {
|
|
|
856
856
|
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
857
857
|
const o = v4Octets(ip);
|
|
858
858
|
if (o) {
|
|
859
|
-
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified",
|
|
859
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
860
860
|
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
861
861
|
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
862
862
|
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
@@ -868,7 +868,7 @@ function classifySsrf(host) {
|
|
|
868
868
|
}
|
|
869
869
|
const g = expandIpv6(ip);
|
|
870
870
|
if (!g) return null;
|
|
871
|
-
if (g.every((x) => x === 0)) return hit("unspecified",
|
|
871
|
+
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
872
872
|
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
873
873
|
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
874
874
|
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
@@ -1759,8 +1759,13 @@ var init_dist = __esm({
|
|
|
1759
1759
|
// AWS ECS task role
|
|
1760
1760
|
"168.63.129.16",
|
|
1761
1761
|
// Azure WireServer
|
|
1762
|
-
"fd00:ec2::254"
|
|
1762
|
+
"fd00:ec2::254",
|
|
1763
1763
|
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
1764
|
+
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
1765
|
+
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
1766
|
+
// live credential endpoint. It has to be named here, above the range check,
|
|
1767
|
+
// and it is the reason relaxing cgnat is safe.
|
|
1768
|
+
"100.100.100.200"
|
|
1764
1769
|
]);
|
|
1765
1770
|
METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
1766
1771
|
v4Octets = (a) => {
|
|
@@ -2737,30 +2742,86 @@ var init_daemon = __esm({
|
|
|
2737
2742
|
|
|
2738
2743
|
// src/config-schema.ts
|
|
2739
2744
|
import { z } from "zod";
|
|
2745
|
+
function formatIssues(issues) {
|
|
2746
|
+
const lines = issues.map((issue) => {
|
|
2747
|
+
const path14 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
2748
|
+
return ` \u2022 ${path14}: ${issue.message}`;
|
|
2749
|
+
});
|
|
2750
|
+
return `Invalid config:
|
|
2751
|
+
${lines.join("\n")}`;
|
|
2752
|
+
}
|
|
2753
|
+
function prunePaths(root, paths) {
|
|
2754
|
+
let removed = false;
|
|
2755
|
+
const ordered = [...paths].sort((x, y) => {
|
|
2756
|
+
if (y.length !== x.length) return y.length - x.length;
|
|
2757
|
+
const xi = x[x.length - 1];
|
|
2758
|
+
const yi = y[y.length - 1];
|
|
2759
|
+
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
2760
|
+
});
|
|
2761
|
+
for (const path14 of ordered) {
|
|
2762
|
+
let cur = root;
|
|
2763
|
+
for (const key of path14.slice(0, -1)) {
|
|
2764
|
+
if (cur === null || typeof cur !== "object") {
|
|
2765
|
+
cur = void 0;
|
|
2766
|
+
break;
|
|
2767
|
+
}
|
|
2768
|
+
cur = cur[key];
|
|
2769
|
+
}
|
|
2770
|
+
if (cur === null || typeof cur !== "object") continue;
|
|
2771
|
+
const last = path14[path14.length - 1];
|
|
2772
|
+
if (Array.isArray(cur)) {
|
|
2773
|
+
const i = Number(last);
|
|
2774
|
+
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
2775
|
+
cur.splice(i, 1);
|
|
2776
|
+
removed = true;
|
|
2777
|
+
}
|
|
2778
|
+
continue;
|
|
2779
|
+
}
|
|
2780
|
+
const obj = cur;
|
|
2781
|
+
if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
|
|
2782
|
+
delete obj[String(last)];
|
|
2783
|
+
removed = true;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
return removed;
|
|
2787
|
+
}
|
|
2740
2788
|
function sanitizeConfig(raw) {
|
|
2741
2789
|
const result = ConfigFileSchema.safeParse(raw);
|
|
2742
2790
|
if (result.success) {
|
|
2743
2791
|
return { sanitized: result.data, error: null };
|
|
2744
2792
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
const
|
|
2749
|
-
|
|
2750
|
-
for (
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2793
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
2794
|
+
return { sanitized: {}, error: formatIssues(result.error.issues) };
|
|
2795
|
+
}
|
|
2796
|
+
const working = structuredClone(raw);
|
|
2797
|
+
for (let level = 0; level < 6; level++) {
|
|
2798
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
2799
|
+
const attempt = ConfigFileSchema.safeParse(working);
|
|
2800
|
+
if (attempt.success) break;
|
|
2801
|
+
const paths = attempt.error.issues.flatMap((issue) => {
|
|
2802
|
+
const at = issue.path;
|
|
2803
|
+
if (issue.code === "unrecognized_keys") {
|
|
2804
|
+
return issue.keys.map((k) => [...at, k]);
|
|
2805
|
+
}
|
|
2806
|
+
return [at.slice(0, -level || void 0)];
|
|
2807
|
+
}).filter((path14) => path14.length > 0);
|
|
2808
|
+
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
2754
2809
|
}
|
|
2810
|
+
if (ConfigFileSchema.safeParse(working).success) break;
|
|
2811
|
+
}
|
|
2812
|
+
const after = ConfigFileSchema.safeParse(working);
|
|
2813
|
+
const sanitized = {};
|
|
2814
|
+
const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
|
|
2815
|
+
after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
2816
|
+
);
|
|
2817
|
+
for (const [key, value] of Object.entries(working)) {
|
|
2818
|
+
if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
|
|
2755
2819
|
}
|
|
2756
|
-
const lines = result.error.issues.map((issue) => {
|
|
2757
|
-
const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
2758
|
-
return ` \u2022 ${path14}: ${issue.message}`;
|
|
2759
|
-
});
|
|
2760
2820
|
return {
|
|
2761
2821
|
sanitized,
|
|
2762
|
-
|
|
2763
|
-
|
|
2822
|
+
// The message names what the USER should fix, so it is built from the
|
|
2823
|
+
// original parse, not from whatever survived the prune.
|
|
2824
|
+
error: formatIssues(result.error.issues)
|
|
2764
2825
|
};
|
|
2765
2826
|
}
|
|
2766
2827
|
var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
|
|
@@ -2784,9 +2845,9 @@ var init_config_schema = __esm({
|
|
|
2784
2845
|
"notMatchesGlob"
|
|
2785
2846
|
],
|
|
2786
2847
|
{
|
|
2787
|
-
errorMap
|
|
2788
|
-
|
|
2789
|
-
|
|
2848
|
+
// zod 4 replaced errorMap with `error`. The wording is kept verbatim:
|
|
2849
|
+
// it is what a user sees when their config is rejected.
|
|
2850
|
+
error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
2790
2851
|
}
|
|
2791
2852
|
),
|
|
2792
2853
|
value: z.string().optional(),
|
|
@@ -2804,7 +2865,7 @@ var init_config_schema = __esm({
|
|
|
2804
2865
|
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
2805
2866
|
conditionMode: z.enum(["all", "any"]).optional(),
|
|
2806
2867
|
verdict: z.enum(["allow", "review", "block"], {
|
|
2807
|
-
|
|
2868
|
+
error: () => "verdict must be one of: allow, review, block"
|
|
2808
2869
|
}),
|
|
2809
2870
|
reason: z.string().optional(),
|
|
2810
2871
|
description: z.string().optional(),
|
|
@@ -2877,7 +2938,7 @@ var init_config_schema = __esm({
|
|
|
2877
2938
|
sandboxPaths: z.array(z.string()).optional(),
|
|
2878
2939
|
dangerousWords: z.array(noNewlines).optional(),
|
|
2879
2940
|
ignoredTools: z.array(z.string()).optional(),
|
|
2880
|
-
toolInspection: z.record(z.string()).optional(),
|
|
2941
|
+
toolInspection: z.record(z.string(), z.string()).optional(),
|
|
2881
2942
|
smartRules: z.array(SmartRuleSchema).optional(),
|
|
2882
2943
|
dlp: z.object({
|
|
2883
2944
|
enabled: z.boolean().optional(),
|
|
@@ -2922,8 +2983,8 @@ var init_config_schema = __esm({
|
|
|
2922
2983
|
roots: z.array(z.string()).optional()
|
|
2923
2984
|
}).optional()
|
|
2924
2985
|
}).optional(),
|
|
2925
|
-
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
2926
|
-
}).strict(
|
|
2986
|
+
environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
2987
|
+
}).strict();
|
|
2927
2988
|
}
|
|
2928
2989
|
});
|
|
2929
2990
|
|
package/dist/index.js
CHANGED
|
@@ -265,9 +265,9 @@ var SmartConditionSchema = import_zod.z.object({
|
|
|
265
265
|
"notMatchesGlob"
|
|
266
266
|
],
|
|
267
267
|
{
|
|
268
|
-
errorMap
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
// zod 4 replaced errorMap with `error`. The wording is kept verbatim:
|
|
269
|
+
// it is what a user sees when their config is rejected.
|
|
270
|
+
error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
271
271
|
}
|
|
272
272
|
),
|
|
273
273
|
value: import_zod.z.string().optional(),
|
|
@@ -285,7 +285,7 @@ var SmartRuleSchema = import_zod.z.object({
|
|
|
285
285
|
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
286
286
|
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
287
287
|
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
288
|
-
|
|
288
|
+
error: () => "verdict must be one of: allow, review, block"
|
|
289
289
|
}),
|
|
290
290
|
reason: import_zod.z.string().optional(),
|
|
291
291
|
description: import_zod.z.string().optional(),
|
|
@@ -358,7 +358,7 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
358
358
|
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
359
359
|
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
360
360
|
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
361
|
-
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
361
|
+
toolInspection: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
362
362
|
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
363
363
|
dlp: import_zod.z.object({
|
|
364
364
|
enabled: import_zod.z.boolean().optional(),
|
|
@@ -403,32 +403,88 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
403
403
|
roots: import_zod.z.array(import_zod.z.string()).optional()
|
|
404
404
|
}).optional()
|
|
405
405
|
}).optional(),
|
|
406
|
-
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
407
|
-
}).strict(
|
|
406
|
+
environments: import_zod.z.record(import_zod.z.string(), import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
407
|
+
}).strict();
|
|
408
|
+
function formatIssues(issues) {
|
|
409
|
+
const lines = issues.map((issue) => {
|
|
410
|
+
const path15 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
411
|
+
return ` \u2022 ${path15}: ${issue.message}`;
|
|
412
|
+
});
|
|
413
|
+
return `Invalid config:
|
|
414
|
+
${lines.join("\n")}`;
|
|
415
|
+
}
|
|
416
|
+
function prunePaths(root, paths) {
|
|
417
|
+
let removed = false;
|
|
418
|
+
const ordered = [...paths].sort((x, y) => {
|
|
419
|
+
if (y.length !== x.length) return y.length - x.length;
|
|
420
|
+
const xi = x[x.length - 1];
|
|
421
|
+
const yi = y[y.length - 1];
|
|
422
|
+
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
423
|
+
});
|
|
424
|
+
for (const path15 of ordered) {
|
|
425
|
+
let cur = root;
|
|
426
|
+
for (const key of path15.slice(0, -1)) {
|
|
427
|
+
if (cur === null || typeof cur !== "object") {
|
|
428
|
+
cur = void 0;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
cur = cur[key];
|
|
432
|
+
}
|
|
433
|
+
if (cur === null || typeof cur !== "object") continue;
|
|
434
|
+
const last = path15[path15.length - 1];
|
|
435
|
+
if (Array.isArray(cur)) {
|
|
436
|
+
const i = Number(last);
|
|
437
|
+
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
438
|
+
cur.splice(i, 1);
|
|
439
|
+
removed = true;
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const obj = cur;
|
|
444
|
+
if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
|
|
445
|
+
delete obj[String(last)];
|
|
446
|
+
removed = true;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return removed;
|
|
450
|
+
}
|
|
408
451
|
function sanitizeConfig(raw) {
|
|
409
452
|
const result = ConfigFileSchema.safeParse(raw);
|
|
410
453
|
if (result.success) {
|
|
411
454
|
return { sanitized: result.data, error: null };
|
|
412
455
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
for (
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
456
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
457
|
+
return { sanitized: {}, error: formatIssues(result.error.issues) };
|
|
458
|
+
}
|
|
459
|
+
const working = structuredClone(raw);
|
|
460
|
+
for (let level = 0; level < 6; level++) {
|
|
461
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
462
|
+
const attempt = ConfigFileSchema.safeParse(working);
|
|
463
|
+
if (attempt.success) break;
|
|
464
|
+
const paths = attempt.error.issues.flatMap((issue) => {
|
|
465
|
+
const at = issue.path;
|
|
466
|
+
if (issue.code === "unrecognized_keys") {
|
|
467
|
+
return issue.keys.map((k) => [...at, k]);
|
|
468
|
+
}
|
|
469
|
+
return [at.slice(0, -level || void 0)];
|
|
470
|
+
}).filter((path15) => path15.length > 0);
|
|
471
|
+
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
422
472
|
}
|
|
473
|
+
if (ConfigFileSchema.safeParse(working).success) break;
|
|
474
|
+
}
|
|
475
|
+
const after = ConfigFileSchema.safeParse(working);
|
|
476
|
+
const sanitized = {};
|
|
477
|
+
const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
|
|
478
|
+
after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
479
|
+
);
|
|
480
|
+
for (const [key, value] of Object.entries(working)) {
|
|
481
|
+
if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
|
|
423
482
|
}
|
|
424
|
-
const lines = result.error.issues.map((issue) => {
|
|
425
|
-
const path15 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
426
|
-
return ` \u2022 ${path15}: ${issue.message}`;
|
|
427
|
-
});
|
|
428
483
|
return {
|
|
429
484
|
sanitized,
|
|
430
|
-
|
|
431
|
-
|
|
485
|
+
// The message names what the USER should fix, so it is built from the
|
|
486
|
+
// original parse, not from whatever survived the prune.
|
|
487
|
+
error: formatIssues(result.error.issues)
|
|
432
488
|
};
|
|
433
489
|
}
|
|
434
490
|
|
|
@@ -3061,9 +3117,15 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
|
3061
3117
|
// AWS ECS task role
|
|
3062
3118
|
"168.63.129.16",
|
|
3063
3119
|
// Azure WireServer
|
|
3064
|
-
"fd00:ec2::254"
|
|
3120
|
+
"fd00:ec2::254",
|
|
3065
3121
|
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
3122
|
+
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
3123
|
+
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
3124
|
+
// live credential endpoint. It has to be named here, above the range check,
|
|
3125
|
+
// and it is the reason relaxing cgnat is safe.
|
|
3126
|
+
"100.100.100.200"
|
|
3066
3127
|
]);
|
|
3128
|
+
var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
3067
3129
|
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
3068
3130
|
var v4Octets = (a) => {
|
|
3069
3131
|
const p = a.split(".");
|
|
@@ -3086,7 +3148,7 @@ function classifySsrf(host) {
|
|
|
3086
3148
|
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
3087
3149
|
const o = v4Octets(ip);
|
|
3088
3150
|
if (o) {
|
|
3089
|
-
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified",
|
|
3151
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
3090
3152
|
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
3091
3153
|
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
3092
3154
|
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
@@ -3098,7 +3160,7 @@ function classifySsrf(host) {
|
|
|
3098
3160
|
}
|
|
3099
3161
|
const g = expandIpv6(ip);
|
|
3100
3162
|
if (!g) return null;
|
|
3101
|
-
if (g.every((x) => x === 0)) return hit("unspecified",
|
|
3163
|
+
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
3102
3164
|
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
3103
3165
|
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
3104
3166
|
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
@@ -3111,7 +3173,7 @@ var TIER_REASON = {
|
|
|
3111
3173
|
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
3112
3174
|
"link-local": "a link-local address",
|
|
3113
3175
|
multicast: "a multicast address",
|
|
3114
|
-
unspecified: "the unspecified address",
|
|
3176
|
+
unspecified: "the unspecified address, which reaches this host",
|
|
3115
3177
|
cgnat: "a carrier-grade NAT address",
|
|
3116
3178
|
private: "a loopback or private address"
|
|
3117
3179
|
};
|
|
@@ -3122,7 +3184,7 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
3122
3184
|
for (const { token, binary } of tokens) {
|
|
3123
3185
|
const m = classifySsrf(token);
|
|
3124
3186
|
if (!m) continue;
|
|
3125
|
-
if (m.tier
|
|
3187
|
+
if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
|
|
3126
3188
|
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
3127
3189
|
return {
|
|
3128
3190
|
...m,
|
package/dist/index.mjs
CHANGED
|
@@ -235,9 +235,9 @@ var SmartConditionSchema = z.object({
|
|
|
235
235
|
"notMatchesGlob"
|
|
236
236
|
],
|
|
237
237
|
{
|
|
238
|
-
errorMap
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
// zod 4 replaced errorMap with `error`. The wording is kept verbatim:
|
|
239
|
+
// it is what a user sees when their config is rejected.
|
|
240
|
+
error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
241
241
|
}
|
|
242
242
|
),
|
|
243
243
|
value: z.string().optional(),
|
|
@@ -255,7 +255,7 @@ var SmartRuleSchema = z.object({
|
|
|
255
255
|
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
256
256
|
conditionMode: z.enum(["all", "any"]).optional(),
|
|
257
257
|
verdict: z.enum(["allow", "review", "block"], {
|
|
258
|
-
|
|
258
|
+
error: () => "verdict must be one of: allow, review, block"
|
|
259
259
|
}),
|
|
260
260
|
reason: z.string().optional(),
|
|
261
261
|
description: z.string().optional(),
|
|
@@ -328,7 +328,7 @@ var ConfigFileSchema = z.object({
|
|
|
328
328
|
sandboxPaths: z.array(z.string()).optional(),
|
|
329
329
|
dangerousWords: z.array(noNewlines).optional(),
|
|
330
330
|
ignoredTools: z.array(z.string()).optional(),
|
|
331
|
-
toolInspection: z.record(z.string()).optional(),
|
|
331
|
+
toolInspection: z.record(z.string(), z.string()).optional(),
|
|
332
332
|
smartRules: z.array(SmartRuleSchema).optional(),
|
|
333
333
|
dlp: z.object({
|
|
334
334
|
enabled: z.boolean().optional(),
|
|
@@ -373,32 +373,88 @@ var ConfigFileSchema = z.object({
|
|
|
373
373
|
roots: z.array(z.string()).optional()
|
|
374
374
|
}).optional()
|
|
375
375
|
}).optional(),
|
|
376
|
-
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
377
|
-
}).strict(
|
|
376
|
+
environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
377
|
+
}).strict();
|
|
378
|
+
function formatIssues(issues) {
|
|
379
|
+
const lines = issues.map((issue) => {
|
|
380
|
+
const path15 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
381
|
+
return ` \u2022 ${path15}: ${issue.message}`;
|
|
382
|
+
});
|
|
383
|
+
return `Invalid config:
|
|
384
|
+
${lines.join("\n")}`;
|
|
385
|
+
}
|
|
386
|
+
function prunePaths(root, paths) {
|
|
387
|
+
let removed = false;
|
|
388
|
+
const ordered = [...paths].sort((x, y) => {
|
|
389
|
+
if (y.length !== x.length) return y.length - x.length;
|
|
390
|
+
const xi = x[x.length - 1];
|
|
391
|
+
const yi = y[y.length - 1];
|
|
392
|
+
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
393
|
+
});
|
|
394
|
+
for (const path15 of ordered) {
|
|
395
|
+
let cur = root;
|
|
396
|
+
for (const key of path15.slice(0, -1)) {
|
|
397
|
+
if (cur === null || typeof cur !== "object") {
|
|
398
|
+
cur = void 0;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
cur = cur[key];
|
|
402
|
+
}
|
|
403
|
+
if (cur === null || typeof cur !== "object") continue;
|
|
404
|
+
const last = path15[path15.length - 1];
|
|
405
|
+
if (Array.isArray(cur)) {
|
|
406
|
+
const i = Number(last);
|
|
407
|
+
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
408
|
+
cur.splice(i, 1);
|
|
409
|
+
removed = true;
|
|
410
|
+
}
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const obj = cur;
|
|
414
|
+
if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
|
|
415
|
+
delete obj[String(last)];
|
|
416
|
+
removed = true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return removed;
|
|
420
|
+
}
|
|
378
421
|
function sanitizeConfig(raw) {
|
|
379
422
|
const result = ConfigFileSchema.safeParse(raw);
|
|
380
423
|
if (result.success) {
|
|
381
424
|
return { sanitized: result.data, error: null };
|
|
382
425
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
for (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
426
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
427
|
+
return { sanitized: {}, error: formatIssues(result.error.issues) };
|
|
428
|
+
}
|
|
429
|
+
const working = structuredClone(raw);
|
|
430
|
+
for (let level = 0; level < 6; level++) {
|
|
431
|
+
for (let pass = 0; pass < 5; pass++) {
|
|
432
|
+
const attempt = ConfigFileSchema.safeParse(working);
|
|
433
|
+
if (attempt.success) break;
|
|
434
|
+
const paths = attempt.error.issues.flatMap((issue) => {
|
|
435
|
+
const at = issue.path;
|
|
436
|
+
if (issue.code === "unrecognized_keys") {
|
|
437
|
+
return issue.keys.map((k) => [...at, k]);
|
|
438
|
+
}
|
|
439
|
+
return [at.slice(0, -level || void 0)];
|
|
440
|
+
}).filter((path15) => path15.length > 0);
|
|
441
|
+
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
392
442
|
}
|
|
443
|
+
if (ConfigFileSchema.safeParse(working).success) break;
|
|
444
|
+
}
|
|
445
|
+
const after = ConfigFileSchema.safeParse(working);
|
|
446
|
+
const sanitized = {};
|
|
447
|
+
const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
|
|
448
|
+
after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
|
|
449
|
+
);
|
|
450
|
+
for (const [key, value] of Object.entries(working)) {
|
|
451
|
+
if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
|
|
393
452
|
}
|
|
394
|
-
const lines = result.error.issues.map((issue) => {
|
|
395
|
-
const path15 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
396
|
-
return ` \u2022 ${path15}: ${issue.message}`;
|
|
397
|
-
});
|
|
398
453
|
return {
|
|
399
454
|
sanitized,
|
|
400
|
-
|
|
401
|
-
|
|
455
|
+
// The message names what the USER should fix, so it is built from the
|
|
456
|
+
// original parse, not from whatever survived the prune.
|
|
457
|
+
error: formatIssues(result.error.issues)
|
|
402
458
|
};
|
|
403
459
|
}
|
|
404
460
|
|
|
@@ -3031,9 +3087,15 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
|
3031
3087
|
// AWS ECS task role
|
|
3032
3088
|
"168.63.129.16",
|
|
3033
3089
|
// Azure WireServer
|
|
3034
|
-
"fd00:ec2::254"
|
|
3090
|
+
"fd00:ec2::254",
|
|
3035
3091
|
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
3092
|
+
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
3093
|
+
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
3094
|
+
// live credential endpoint. It has to be named here, above the range check,
|
|
3095
|
+
// and it is the reason relaxing cgnat is safe.
|
|
3096
|
+
"100.100.100.200"
|
|
3036
3097
|
]);
|
|
3098
|
+
var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
3037
3099
|
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
3038
3100
|
var v4Octets = (a) => {
|
|
3039
3101
|
const p = a.split(".");
|
|
@@ -3056,7 +3118,7 @@ function classifySsrf(host) {
|
|
|
3056
3118
|
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
3057
3119
|
const o = v4Octets(ip);
|
|
3058
3120
|
if (o) {
|
|
3059
|
-
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified",
|
|
3121
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
3060
3122
|
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
3061
3123
|
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
3062
3124
|
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
@@ -3068,7 +3130,7 @@ function classifySsrf(host) {
|
|
|
3068
3130
|
}
|
|
3069
3131
|
const g = expandIpv6(ip);
|
|
3070
3132
|
if (!g) return null;
|
|
3071
|
-
if (g.every((x) => x === 0)) return hit("unspecified",
|
|
3133
|
+
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
3072
3134
|
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
3073
3135
|
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
3074
3136
|
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
@@ -3081,7 +3143,7 @@ var TIER_REASON = {
|
|
|
3081
3143
|
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
3082
3144
|
"link-local": "a link-local address",
|
|
3083
3145
|
multicast: "a multicast address",
|
|
3084
|
-
unspecified: "the unspecified address",
|
|
3146
|
+
unspecified: "the unspecified address, which reaches this host",
|
|
3085
3147
|
cgnat: "a carrier-grade NAT address",
|
|
3086
3148
|
private: "a loopback or private address"
|
|
3087
3149
|
};
|
|
@@ -3092,7 +3154,7 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
3092
3154
|
for (const { token, binary } of tokens) {
|
|
3093
3155
|
const m = classifySsrf(token);
|
|
3094
3156
|
if (!m) continue;
|
|
3095
|
-
if (m.tier
|
|
3157
|
+
if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
|
|
3096
3158
|
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
3097
3159
|
return {
|
|
3098
3160
|
...m,
|
package/dist/scan-ink.mjs
CHANGED
|
@@ -61,9 +61,9 @@ var SmartConditionSchema = z.object({
|
|
|
61
61
|
"notMatchesGlob"
|
|
62
62
|
],
|
|
63
63
|
{
|
|
64
|
-
errorMap
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// zod 4 replaced errorMap with `error`. The wording is kept verbatim:
|
|
65
|
+
// it is what a user sees when their config is rejected.
|
|
66
|
+
error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
67
67
|
}
|
|
68
68
|
),
|
|
69
69
|
value: z.string().optional(),
|
|
@@ -81,7 +81,7 @@ var SmartRuleSchema = z.object({
|
|
|
81
81
|
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
82
82
|
conditionMode: z.enum(["all", "any"]).optional(),
|
|
83
83
|
verdict: z.enum(["allow", "review", "block"], {
|
|
84
|
-
|
|
84
|
+
error: () => "verdict must be one of: allow, review, block"
|
|
85
85
|
}),
|
|
86
86
|
reason: z.string().optional(),
|
|
87
87
|
description: z.string().optional(),
|
|
@@ -154,7 +154,7 @@ var ConfigFileSchema = z.object({
|
|
|
154
154
|
sandboxPaths: z.array(z.string()).optional(),
|
|
155
155
|
dangerousWords: z.array(noNewlines).optional(),
|
|
156
156
|
ignoredTools: z.array(z.string()).optional(),
|
|
157
|
-
toolInspection: z.record(z.string()).optional(),
|
|
157
|
+
toolInspection: z.record(z.string(), z.string()).optional(),
|
|
158
158
|
smartRules: z.array(SmartRuleSchema).optional(),
|
|
159
159
|
dlp: z.object({
|
|
160
160
|
enabled: z.boolean().optional(),
|
|
@@ -199,8 +199,8 @@ var ConfigFileSchema = z.object({
|
|
|
199
199
|
roots: z.array(z.string()).optional()
|
|
200
200
|
}).optional()
|
|
201
201
|
}).optional(),
|
|
202
|
-
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
203
|
-
}).strict(
|
|
202
|
+
environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
203
|
+
}).strict();
|
|
204
204
|
|
|
205
205
|
// src/shields.ts
|
|
206
206
|
import fs from "fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.3",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -94,23 +94,23 @@
|
|
|
94
94
|
"smol-toml": "^1.6.1",
|
|
95
95
|
"string-width": "^4.2.3",
|
|
96
96
|
"yaml": "^2.9.0",
|
|
97
|
-
"zod": "^
|
|
97
|
+
"zod": "^4.5.4"
|
|
98
98
|
},
|
|
99
99
|
"bundleDependencies": [
|
|
100
100
|
"mvdan-sh"
|
|
101
101
|
],
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@anthropic-ai/sdk": "^0.
|
|
103
|
+
"@anthropic-ai/sdk": "^0.124.0",
|
|
104
104
|
"@octokit/rest": "^22.0.1",
|
|
105
105
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
106
106
|
"@semantic-release/git": "^10.0.1",
|
|
107
107
|
"@semantic-release/github": "^12.0.6",
|
|
108
108
|
"@semantic-release/npm": "^13.1.5",
|
|
109
109
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
110
|
-
"@types/node": "^
|
|
110
|
+
"@types/node": "^26.4.1",
|
|
111
111
|
"@types/picomatch": "^4.0.2",
|
|
112
112
|
"@types/react": "^19.2.14",
|
|
113
|
-
"@vitest/coverage-v8": "
|
|
113
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
114
114
|
"cross-env": "^10.1.0",
|
|
115
115
|
"ink-testing-library": "^4.0.0",
|
|
116
116
|
"prettier": "^3.4.2",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"tsx": "^4.21.0",
|
|
120
120
|
"typescript": "^5.9.3",
|
|
121
121
|
"typescript-eslint": "^8.20.0",
|
|
122
|
-
"vitest": "
|
|
122
|
+
"vitest": "^5.0.0"
|
|
123
123
|
},
|
|
124
124
|
"overrides": {
|
|
125
125
|
"undici": "^7.24.0",
|