@node9/proxy 2.17.0 → 2.19.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/README.md +76 -331
- package/dist/cli.js +143 -51
- package/dist/cli.mjs +143 -51
- package/dist/dashboard.mjs +4 -2
- package/dist/index.js +42 -9
- package/dist/index.mjs +42 -9
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2251,15 +2251,44 @@ function classifySsrf(host) {
|
|
|
2251
2251
|
return null;
|
|
2252
2252
|
}
|
|
2253
2253
|
}
|
|
2254
|
+
function ssrfExemptMatches(entries, normalized) {
|
|
2255
|
+
if (!entries?.length || !normalized) return false;
|
|
2256
|
+
const target = bitsOf(normalized);
|
|
2257
|
+
for (const raw of entries) {
|
|
2258
|
+
const entry = raw.trim().toLowerCase();
|
|
2259
|
+
if (!entry) continue;
|
|
2260
|
+
const slash = entry.indexOf("/");
|
|
2261
|
+
if (slash === -1) {
|
|
2262
|
+
if ((normalizeIpLiteral(entry) ?? entry) === normalized) return true;
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
if (!target) continue;
|
|
2266
|
+
const base = bitsOf(normalizeIpLiteral(entry.slice(0, slash)) ?? "");
|
|
2267
|
+
const prefixText = entry.slice(slash + 1);
|
|
2268
|
+
const prefix = /^\d+$/.test(prefixText) ? Number(prefixText) : NaN;
|
|
2269
|
+
if (!base || !Number.isInteger(prefix) || prefix < 0 || // A v4 range never matches a v6 address, and the reverse: the widths
|
|
2270
|
+
// differ, so `0.0.0.0/0` does not release `::1`.
|
|
2271
|
+
base.length !== target.length || prefix > base.length) {
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
if (base.slice(0, prefix) === target.slice(0, prefix)) return true;
|
|
2275
|
+
}
|
|
2276
|
+
return false;
|
|
2277
|
+
}
|
|
2278
|
+
function bitsOf(normalized) {
|
|
2279
|
+
const o = v4Octets(normalized);
|
|
2280
|
+
if (o) {
|
|
2281
|
+
return o.every((n) => Number.isInteger(n) && n >= 0 && n <= 255) ? o.map((n) => n.toString(2).padStart(8, "0")).join("") : null;
|
|
2282
|
+
}
|
|
2283
|
+
const g = expandIpv6(normalized);
|
|
2284
|
+
return g ? g.map((n) => n.toString(2).padStart(16, "0")).join("") : null;
|
|
2285
|
+
}
|
|
2254
2286
|
function ssrfFloor(tokens, opts = {}) {
|
|
2255
|
-
const exempt2 = new Set(
|
|
2256
|
-
(opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
|
|
2257
|
-
);
|
|
2258
2287
|
for (const { token, binary } of tokens) {
|
|
2259
2288
|
const m = classifySsrf(token);
|
|
2260
2289
|
if (!m) continue;
|
|
2261
2290
|
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2262
|
-
if (m.overridable &&
|
|
2291
|
+
if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
|
|
2263
2292
|
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
2264
2293
|
}
|
|
2265
2294
|
return null;
|
|
@@ -2568,7 +2597,6 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
2568
2597
|
try {
|
|
2569
2598
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
2570
2599
|
if (!paths) return null;
|
|
2571
|
-
const exempt2 = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
|
|
2572
2600
|
for (const path78 of paths) {
|
|
2573
2601
|
for (const value of valuesAt(args, path78)) {
|
|
2574
2602
|
const host = hostOf(value);
|
|
@@ -2576,7 +2604,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
2576
2604
|
const m = classifySsrf(host);
|
|
2577
2605
|
if (!m) continue;
|
|
2578
2606
|
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2579
|
-
if (m.overridable &&
|
|
2607
|
+
if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
|
|
2580
2608
|
return { ...m, argPath: path78, host, reason: ssrfReason(m, host) };
|
|
2581
2609
|
}
|
|
2582
2610
|
}
|
|
@@ -6799,10 +6827,12 @@ var init_api_url = __esm({
|
|
|
6799
6827
|
function sanitizeSsrfAllow(entries, source) {
|
|
6800
6828
|
const kept = [];
|
|
6801
6829
|
for (const entry of entries) {
|
|
6802
|
-
const
|
|
6830
|
+
const base = entry.includes("/") ? entry.slice(0, entry.indexOf("/")).trim() : entry;
|
|
6831
|
+
const m = classifySsrf(base);
|
|
6803
6832
|
if (m && !m.overridable) {
|
|
6833
|
+
const what = entry.includes("/") ? "covers only protected addresses" : "is a protected address";
|
|
6804
6834
|
process.emitWarning(
|
|
6805
|
-
`[node9] ${source} ssrfAllow entry "${entry}"
|
|
6835
|
+
`[node9] ${source} ssrfAllow entry "${entry}" ${what} (${m.tier}) and cannot be exempted; ignoring it.`
|
|
6806
6836
|
);
|
|
6807
6837
|
continue;
|
|
6808
6838
|
}
|
|
@@ -9746,6 +9776,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
9746
9776
|
}
|
|
9747
9777
|
return _authorizeHeadlessCore(toolName, args, meta, options);
|
|
9748
9778
|
}
|
|
9779
|
+
function approvalTimeoutReason(approvalTimeoutMs) {
|
|
9780
|
+
return `No human response within ${approvalTimeoutMs / 1e3}s \u2014 auto-denied by timeout policy. Approve from your phone next time: run \`node9 login\`.`;
|
|
9781
|
+
}
|
|
9749
9782
|
async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
9750
9783
|
const meta = options?.cwd && !metaArg?.workingDir ? { ...metaArg, workingDir: options.cwd } : metaArg;
|
|
9751
9784
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
@@ -10335,7 +10368,7 @@ ${appPermReview}`
|
|
|
10335
10368
|
const timer = setTimeout(() => {
|
|
10336
10369
|
resolve2({
|
|
10337
10370
|
approved: false,
|
|
10338
|
-
reason:
|
|
10371
|
+
reason: approvalTimeoutReason(approvalTimeoutMs),
|
|
10339
10372
|
blockedBy: "timeout",
|
|
10340
10373
|
blockedByLabel: "Approval Timeout"
|
|
10341
10374
|
});
|
|
@@ -20044,19 +20077,19 @@ function evaluateEgressConfig(egress) {
|
|
|
20044
20077
|
function checkEgressFloor(egress) {
|
|
20045
20078
|
const detail = [
|
|
20046
20079
|
"the cloud instance-metadata endpoint",
|
|
20047
|
-
"link-local
|
|
20048
|
-
egress.ssrfStrict ? "the strict tier is on: loopback
|
|
20080
|
+
"link-local and multicast addresses",
|
|
20081
|
+
egress.ssrfStrict ? "the strict tier is on: loopback, the private ranges and CGNAT are blocked too" : `the strict tier is off: loopback, the private ranges and CGNAT (100.64/10) stay reachable (${egress.policySource === "workspace" ? "turn it on in the dashboard, Enforcement \u2192 Network" : "`node9 egress strict on`"})`
|
|
20049
20082
|
];
|
|
20050
20083
|
if (egress.ssrfAllow.length) {
|
|
20051
20084
|
detail.push(`you exempted: ${egress.ssrfAllow.join(", ")}`);
|
|
20052
20085
|
}
|
|
20053
|
-
detail.push("not covered:
|
|
20086
|
+
detail.push("not covered: an interpreter one-liner (node -e, python3 -c) hides its destination");
|
|
20054
20087
|
return [
|
|
20055
20088
|
{
|
|
20056
20089
|
category: "Egress",
|
|
20057
20090
|
severity: "advisory",
|
|
20058
|
-
title: "The cloud metadata endpoint is blocked
|
|
20059
|
-
what: "node9 blocks it before any egress policy is consulted, and no setting releases it. It sees shell commands (curl, wget, ssh)
|
|
20091
|
+
title: "The cloud metadata endpoint is blocked",
|
|
20092
|
+
what: "node9 blocks it before any egress policy is consulted, and no setting releases it. It sees shell commands (curl, wget, ssh) and tools that declare a URL (WebFetch, an MCP fetch tool, browser navigate); an interpreter one-liner does not reach it.",
|
|
20060
20093
|
why: "One request to that address returns this machine's cloud credentials, to anyone who can make the agent send it.",
|
|
20061
20094
|
who: "An agent talked into fetching that address hands over the keys and cannot, here.",
|
|
20062
20095
|
owner: "node9",
|
|
@@ -56099,6 +56132,7 @@ var import_os52 = __toESM(require("os"));
|
|
|
56099
56132
|
var import_https6 = __toESM(require("https"));
|
|
56100
56133
|
init_core();
|
|
56101
56134
|
init_setup();
|
|
56135
|
+
init_machine_id();
|
|
56102
56136
|
init_shields();
|
|
56103
56137
|
init_service();
|
|
56104
56138
|
init_core();
|
|
@@ -56110,7 +56144,8 @@ function buildTelemetryPayload(agents, firstInstall) {
|
|
|
56110
56144
|
agents_detected: agents,
|
|
56111
56145
|
os: process.platform,
|
|
56112
56146
|
node9_version: node9Version(),
|
|
56113
|
-
first_install: firstInstall
|
|
56147
|
+
first_install: firstInstall,
|
|
56148
|
+
machine_id: getMachineId()
|
|
56114
56149
|
};
|
|
56115
56150
|
}
|
|
56116
56151
|
function fireTelemetryPing(agents, firstInstall) {
|
|
@@ -57103,10 +57138,11 @@ function addEgressHost(list, host) {
|
|
|
57103
57138
|
writeEgressRawConfig(config);
|
|
57104
57139
|
}
|
|
57105
57140
|
function addSsrfExemption(address) {
|
|
57106
|
-
const
|
|
57141
|
+
const slash = address.indexOf("/");
|
|
57142
|
+
const m = classifySsrf(slash === -1 ? address : address.slice(0, slash));
|
|
57107
57143
|
if (m && !m.overridable) {
|
|
57108
57144
|
throw new Error(
|
|
57109
|
-
`${address} is a protected address (${m.tier}) and cannot be exempted by anyone. This is the one part of the floor no setting releases.`
|
|
57145
|
+
`${address} ${slash === -1 ? "is a protected address" : "covers only protected addresses"} (${m.tier}) and cannot be exempted by anyone. This is the one part of the floor no setting releases.`
|
|
57110
57146
|
);
|
|
57111
57147
|
}
|
|
57112
57148
|
const config = readEgressRawConfig();
|
|
@@ -57378,7 +57414,7 @@ var TOOLS = [
|
|
|
57378
57414
|
},
|
|
57379
57415
|
{
|
|
57380
57416
|
name: "node9_egress_status",
|
|
57381
|
-
description: "Show egress (outbound network) control: whether it is enabled, the mode (off / review / block), and your allow + deny host lists. Common dev/LLM hosts (github, npm, pypi, anthropic, \u2026) are always allowed by a built-in list. Also reports the SSRF floor: the addresses blocked
|
|
57417
|
+
description: "Show egress (outbound network) control: whether it is enabled, the mode (off / review / block), and your allow + deny host lists. Common dev/LLM hosts (github, npm, pypi, anthropic, \u2026) are always allowed by a built-in list. Also reports the SSRF floor: the addresses blocked before any policy is consulted (cloud metadata, link-local, multicast), the carriers it covers, whether the strict tier (loopback, private ranges, CGNAT) is on, and the exemptions in force. Read-only.",
|
|
57382
57418
|
inputSchema: { type: "object", properties: {}, required: [] }
|
|
57383
57419
|
},
|
|
57384
57420
|
{
|
|
@@ -57538,11 +57574,12 @@ function handleEgressStatus() {
|
|
|
57538
57574
|
// The SSRF floor. Without these lines an agent reading this answer
|
|
57539
57575
|
// concludes that internal addresses are reachable, because nothing said
|
|
57540
57576
|
// otherwise. The LIMITS are here for the same reason and matter more on
|
|
57541
|
-
// this surface than on any other: an agent treats this as ground truth
|
|
57542
|
-
//
|
|
57543
|
-
// shell
|
|
57544
|
-
|
|
57545
|
-
"
|
|
57577
|
+
// this surface than on any other: an agent treats this as ground truth.
|
|
57578
|
+
// The first version said the floor was absolute; the correction said it
|
|
57579
|
+
// was shell-only; both were wrong by the time they were read. What it
|
|
57580
|
+
// actually covers is measured in egress.integration.test.ts.
|
|
57581
|
+
"Protected addresses: cloud metadata, link-local and multicast are blocked before any of the above is consulted, in shell commands AND in tools that declare a URL (WebFetch, an MCP fetch tool, browser navigate). No setting releases them, though `node9 pause` suspends all enforcement.",
|
|
57582
|
+
"NOT covered by that: an interpreter one-liner (node -e, python3 -c) carries its destination inside a program and does not reach this gate. CGNAT (100.64/10) is NOT in the always-blocked set \u2014 it is reachable until the strict tier is on, and an exemption can release it.",
|
|
57546
57583
|
`Internal addresses: ${e.ssrfStrict ? "on" : "off"} \u2014 loopback and the private ranges are ${e.ssrfStrict ? "blocked too" : "reachable"}.`,
|
|
57547
57584
|
`Floor exemptions: ${e.ssrfAllow?.length ? e.ssrfAllow.join(", ") : "(none)"}`
|
|
57548
57585
|
];
|
|
@@ -60301,26 +60338,76 @@ function exempt(address) {
|
|
|
60301
60338
|
if (!cliGuardPolicyWrite(`egress exempt ${address}`)) return false;
|
|
60302
60339
|
return guard(() => addSsrfExemption(address));
|
|
60303
60340
|
}
|
|
60341
|
+
var INTERNAL_FIELDS = {
|
|
60342
|
+
allowed: { ssrfStrict: false, allowPrivate: true },
|
|
60343
|
+
listed: { ssrfStrict: false, allowPrivate: false },
|
|
60344
|
+
blocked: { ssrfStrict: true, allowPrivate: false }
|
|
60345
|
+
};
|
|
60346
|
+
var INTERNAL_SAID = {
|
|
60347
|
+
allowed: "reachable without listing them",
|
|
60348
|
+
listed: "reachable only if they are on your allowlist",
|
|
60349
|
+
blocked: "blocked"
|
|
60350
|
+
};
|
|
60351
|
+
function readInternalState(e) {
|
|
60352
|
+
if (e.ssrfStrict === true) return "blocked";
|
|
60353
|
+
return e.allowPrivate === false ? "listed" : "allowed";
|
|
60354
|
+
}
|
|
60355
|
+
function setInternal(value) {
|
|
60356
|
+
const state = value.trim().toLowerCase();
|
|
60357
|
+
if (!(state in INTERNAL_FIELDS)) {
|
|
60358
|
+
console.error(
|
|
60359
|
+
import_chalk34.default.red(`
|
|
60360
|
+
\u2717 Expected "allowed", "listed" or "blocked", got "${value}".`) + import_chalk34.default.gray(
|
|
60361
|
+
"\n allowed loopback, 10/172.16/192.168 and CGNAT are reachable (default)\n listed they are reachable only if you allowlist them\n blocked they are blocked at the floor\n"
|
|
60362
|
+
)
|
|
60363
|
+
);
|
|
60364
|
+
process.exitCode = 1;
|
|
60365
|
+
return;
|
|
60366
|
+
}
|
|
60367
|
+
if (!mutate(`egress internal ${state}`, INTERNAL_FIELDS[state])) return;
|
|
60368
|
+
_resetConfigCache();
|
|
60369
|
+
const effective = readInternalState(getConfig().policy.egress);
|
|
60370
|
+
if (effective !== state) {
|
|
60371
|
+
console.log(
|
|
60372
|
+
import_chalk34.default.yellow(
|
|
60373
|
+
`
|
|
60374
|
+
\u26A0 Saved, but not in effect: your workspace sets internal addresses to ${effective.toUpperCase()} and that governs this machine.
|
|
60375
|
+
Change it in the dashboard, Enforcement \u2192 Network.
|
|
60376
|
+
`
|
|
60377
|
+
)
|
|
60378
|
+
);
|
|
60379
|
+
return;
|
|
60380
|
+
}
|
|
60381
|
+
const line = `
|
|
60382
|
+
\u2713 Internal addresses: ${state} \u2014 loopback, the private ranges and CGNAT are ${INTERNAL_SAID[state]}.
|
|
60383
|
+
`;
|
|
60384
|
+
console.log(state === "allowed" ? import_chalk34.default.yellow(line) : import_chalk34.default.green(line));
|
|
60385
|
+
}
|
|
60304
60386
|
function showFloor(e, ssrfStrictSource) {
|
|
60305
|
-
console.log(
|
|
60387
|
+
console.log(
|
|
60388
|
+
import_chalk34.default.gray("\n Protected addresses") + import_chalk34.default.gray(" \u2014 in shell commands and declared URLs")
|
|
60389
|
+
);
|
|
60306
60390
|
console.log(
|
|
60307
60391
|
import_chalk34.default.gray(
|
|
60308
|
-
" always blocked: cloud metadata, link-local, multicast
|
|
60392
|
+
" always blocked: cloud metadata, link-local, multicast\n no setting releases these, though `node9 pause` suspends all enforcement"
|
|
60309
60393
|
)
|
|
60310
60394
|
);
|
|
60311
|
-
const
|
|
60395
|
+
const state = readInternalState(e);
|
|
60312
60396
|
const by = ssrfStrictSource === "workspace" ? "workspace (app.node9.ai)" : ssrfStrictSource === "local" ? "this machine (config.json)" : "the shipped default";
|
|
60397
|
+
const STATE_LABEL = {
|
|
60398
|
+
allowed: "allowed",
|
|
60399
|
+
listed: "allowlist only",
|
|
60400
|
+
blocked: "blocked"
|
|
60401
|
+
};
|
|
60313
60402
|
console.log(
|
|
60314
|
-
` Internal addresses: ${
|
|
60315
|
-
strict ? " loopback and 10/172.16/192.168 are blocked too" : " loopback and 10/172.16/192.168 are reachable"
|
|
60316
|
-
)
|
|
60403
|
+
` Internal addresses: ${state === "blocked" ? import_chalk34.default.green(STATE_LABEL[state]) : import_chalk34.default.yellow(STATE_LABEL[state])}` + import_chalk34.default.gray(` loopback, 10/172.16/192.168 and CGNAT are ${INTERNAL_SAID[state]}`)
|
|
60317
60404
|
);
|
|
60318
60405
|
console.log(import_chalk34.default.gray(` set by: ${by}`));
|
|
60319
60406
|
const exemptions = e.ssrfAllow ?? [];
|
|
60320
60407
|
console.log(import_chalk34.default.gray(` Exemptions: ${exemptions.length ? exemptions.join(", ") : "none"}`));
|
|
60321
60408
|
console.log(
|
|
60322
60409
|
import_chalk34.default.gray(
|
|
60323
|
-
"
|
|
60410
|
+
" Covered: shell commands, and tools that declare a URL (WebFetch, an MCP\n fetch tool, browser navigate).\n Not covered: an interpreter one-liner (node -e, python3 -c) hides its\n destination inside a program and does not reach this gate.\n CGNAT (100.64/10) is NOT in the always-blocked set: it is reachable\n until Internal addresses is on, and an exemption can release it."
|
|
60324
60411
|
)
|
|
60325
60412
|
);
|
|
60326
60413
|
}
|
|
@@ -60385,7 +60472,10 @@ function registerEgressCommand(program2) {
|
|
|
60385
60472
|
import_chalk34.default.yellow("\n\u2713 Egress control is off \u2014 the agent can reach any host again.\n")
|
|
60386
60473
|
);
|
|
60387
60474
|
});
|
|
60388
|
-
egress.command("
|
|
60475
|
+
egress.command("internal <allowed|listed|blocked>").description(
|
|
60476
|
+
"How loopback, private ranges and CGNAT are treated: allowed (default), listed (must be on the allowlist), or blocked"
|
|
60477
|
+
).action((value) => setInternal(value));
|
|
60478
|
+
egress.command("strict <on|off>").description("Deprecated alias for `egress internal blocked|allowed`").action((value) => {
|
|
60389
60479
|
const v = value.trim().toLowerCase();
|
|
60390
60480
|
if (v !== "on" && v !== "off") {
|
|
60391
60481
|
console.error(import_chalk34.default.red(`
|
|
@@ -60394,38 +60484,40 @@ function registerEgressCommand(program2) {
|
|
|
60394
60484
|
process.exitCode = 1;
|
|
60395
60485
|
return;
|
|
60396
60486
|
}
|
|
60397
|
-
|
|
60398
|
-
|
|
60399
|
-
|
|
60400
|
-
|
|
60401
|
-
|
|
60402
|
-
|
|
60403
|
-
|
|
60404
|
-
|
|
60405
|
-
|
|
60406
|
-
|
|
60487
|
+
const state = v === "on" ? "blocked" : getConfig().policy.egress.allowPrivate === false ? "listed" : "allowed";
|
|
60488
|
+
console.log(import_chalk34.default.gray(`
|
|
60489
|
+
(\`egress strict ${v}\` is now \`egress internal ${state}\`)`));
|
|
60490
|
+
setInternal(state);
|
|
60491
|
+
});
|
|
60492
|
+
egress.command("exempt <address>").description("Let an address or a CIDR range through the floor (e.g. 100.64.0.0/10)").action((address) => {
|
|
60493
|
+
const a = normalizeEgressHost(address);
|
|
60494
|
+
const slash = a.indexOf("/");
|
|
60495
|
+
const base = slash === -1 ? a : a.slice(0, slash);
|
|
60496
|
+
const prefixText = slash === -1 ? null : a.slice(slash + 1);
|
|
60497
|
+
if (!normalizeIpLiteral(base) || prefixText !== null && !/^\d{1,3}$/.test(prefixText)) {
|
|
60498
|
+
console.error(
|
|
60499
|
+
import_chalk34.default.red(`
|
|
60500
|
+
\u2717 "${address}" is not an address or a range.`) + import_chalk34.default.gray(
|
|
60501
|
+
"\n Exemptions are matched as an address (10.0.0.5) or a CIDR\n range (100.64.0.0/10), never a name.\n"
|
|
60407
60502
|
)
|
|
60408
60503
|
);
|
|
60504
|
+
process.exitCode = 1;
|
|
60409
60505
|
return;
|
|
60410
60506
|
}
|
|
60411
|
-
|
|
60412
|
-
|
|
60413
|
-
);
|
|
60414
|
-
});
|
|
60415
|
-
egress.command("exempt <address>").description("Let ONE address through the floor (exact address, not a range)").action((address) => {
|
|
60416
|
-
const a = normalizeEgressHost(address);
|
|
60417
|
-
if (!normalizeIpLiteral(a)) {
|
|
60507
|
+
const m = classifySsrf(base);
|
|
60508
|
+
if (m && !m.overridable) {
|
|
60418
60509
|
console.error(
|
|
60419
60510
|
import_chalk34.default.red(`
|
|
60420
|
-
\u2717
|
|
60421
|
-
|
|
60511
|
+
\u2717 ${a} cannot be exempted.`) + import_chalk34.default.gray(
|
|
60512
|
+
`
|
|
60513
|
+
${slash === -1 ? "That address is" : "That range covers only"} protected addresses (${m.tier}), which no setting releases.
|
|
60514
|
+
`
|
|
60422
60515
|
)
|
|
60423
60516
|
);
|
|
60424
60517
|
process.exitCode = 1;
|
|
60425
60518
|
return;
|
|
60426
60519
|
}
|
|
60427
60520
|
if (!exempt(a)) return;
|
|
60428
|
-
const m = classifySsrf(a);
|
|
60429
60521
|
console.log(import_chalk34.default.green(`
|
|
60430
60522
|
\u2713 ${a} is exempt from the floor.`));
|
|
60431
60523
|
if (!m)
|