mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 16:00:44 +00:00
lint: classify raw socket callsites
This commit is contained in:
@@ -220,3 +220,12 @@ proxy:
|
||||
- Gateway control-plane proxy bypass is intentionally limited to `localhost` and literal loopback IP URLs. Use `ws://127.0.0.1:18789`, `ws://[::1]:18789`, or `ws://localhost:18789` for local direct Gateway control-plane connections; other hostnames route like ordinary hostname-based traffic.
|
||||
- OpenClaw does not inspect, test, or certify your proxy policy.
|
||||
- Treat proxy policy changes as security-sensitive operational changes.
|
||||
|
||||
| Surface | Managed proxy status |
|
||||
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
|
||||
| `fetch`, `node:http`, `node:https`, common WebSocket clients | Routed through managed proxy hooks when configured. |
|
||||
| APNs direct HTTP/2 | Routed through the APNs managed CONNECT helper. |
|
||||
| Gateway control-plane loopback | Direct only for the configured local loopback Gateway URL. |
|
||||
| Debug proxy upstream forwarding | Disabled while managed proxy mode is active unless explicitly enabled for local diagnostics. |
|
||||
| IRC | Raw TCP/TLS; not proxied by managed HTTP proxy mode. Disable unless direct IRC egress is approved. |
|
||||
| Other raw `net`, `tls`, or `http2` client calls | Must be classified by the raw socket guard before landing. |
|
||||
|
||||
@@ -1433,9 +1433,11 @@
|
||||
"lint:swift": "swiftlint lint --config config/swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
|
||||
"lint:tmp:channel-agnostic-boundaries": "node scripts/check-channel-agnostic-boundaries.mjs",
|
||||
"lint:tmp:dynamic-import-warts": "node scripts/check-dynamic-import-warts.mjs",
|
||||
"lint:tmp:managed-proxy-runtime-mutation": "node scripts/check-managed-proxy-runtime-mutation.mjs",
|
||||
"lint:tmp:no-random-messaging": "node scripts/check-no-random-messaging-tmp.mjs",
|
||||
"lint:tmp:no-raw-channel-fetch": "node scripts/check-no-raw-channel-fetch.mjs",
|
||||
"lint:tmp:no-raw-http2-imports": "node scripts/check-no-raw-http2-imports.mjs",
|
||||
"lint:tmp:raw-socket-classification": "node scripts/check-raw-socket-callsite-classification.mjs",
|
||||
"lint:tmp:tsgo-core-boundary": "node scripts/check-tsgo-core-boundary.mjs",
|
||||
"lint:ui:no-raw-window-open": "node scripts/check-no-raw-window-open.mjs",
|
||||
"lint:web-fetch-provider-boundaries": "node scripts/check-web-fetch-provider-boundaries.mjs",
|
||||
|
||||
448
scripts/check-managed-proxy-runtime-mutation.mjs
Normal file
448
scripts/check-managed-proxy-runtime-mutation.mjs
Normal file
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import ts from "typescript";
|
||||
import { runCallsiteGuard } from "./lib/callsite-guard.mjs";
|
||||
import { runAsScript, toLine, unwrapExpression } from "./lib/ts-guard-utils.mjs";
|
||||
|
||||
const sourceRoots = ["src", "extensions"];
|
||||
|
||||
const allowedManagedProxyRuntimeMutationCallsites = new Set([
|
||||
// Canonical managed proxy lifecycle owns process proxy env/global-agent mutation.
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:114",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:117",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:119",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:120",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:121",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:123",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:131",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:133",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:146",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:147",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:148",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:178",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:180",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:199",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:200",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:201",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:312",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:313",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:314",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:315",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:316",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:317",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:318",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:319",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:329",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:330",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:331",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:332",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:333",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:334",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:335",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:336",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:369",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:376",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:484",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:507",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:508",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:515",
|
||||
"src/infra/net/proxy/proxy-lifecycle.ts:516",
|
||||
|
||||
// Browser CDP loopback control-plane helper leases NO_PROXY only for localhost/loopback CDP URLs.
|
||||
"extensions/browser/src/browser/cdp-proxy-bypass.ts:87",
|
||||
"extensions/browser/src/browser/cdp-proxy-bypass.ts:88",
|
||||
"extensions/browser/src/browser/cdp-proxy-bypass.ts:120",
|
||||
"extensions/browser/src/browser/cdp-proxy-bypass.ts:122",
|
||||
"extensions/browser/src/browser/cdp-proxy-bypass.ts:125",
|
||||
"extensions/browser/src/browser/cdp-proxy-bypass.ts:127",
|
||||
]);
|
||||
|
||||
const forbiddenEnvKeys = new Set([
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"NO_PROXY",
|
||||
"no_proxy",
|
||||
"GLOBAL_AGENT_HTTP_PROXY",
|
||||
"GLOBAL_AGENT_HTTPS_PROXY",
|
||||
"GLOBAL_AGENT_NO_PROXY",
|
||||
"GLOBAL_AGENT_FORCE_GLOBAL_AGENT",
|
||||
"OPENCLAW_PROXY_ACTIVE",
|
||||
"OPENCLAW_PROXY_LOOPBACK_MODE",
|
||||
]);
|
||||
|
||||
const forbiddenGlobalAgentKeys = new Set(["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"]);
|
||||
|
||||
function stringLiteralText(node) {
|
||||
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : null;
|
||||
}
|
||||
|
||||
function isGlobalIdentifier(node, context = { globalAliases: new Set() }) {
|
||||
const unwrapped = unwrapExpression(node);
|
||||
return (
|
||||
ts.isIdentifier(unwrapped) &&
|
||||
(unwrapped.text === "global" ||
|
||||
unwrapped.text === "globalThis" ||
|
||||
context.globalAliases.has(unwrapped.text))
|
||||
);
|
||||
}
|
||||
|
||||
function processEnvExpression(expression, context = { envAliases: new Set() }) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (ts.isIdentifier(unwrapped) && context.envAliases.has(unwrapped.text)) {
|
||||
return unwrapped;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(unwrapped) && unwrapped.name.text === "env") {
|
||||
const base = unwrapExpression(unwrapped.expression);
|
||||
return ts.isIdentifier(base) && base.text === "process" ? unwrapped : null;
|
||||
}
|
||||
if (ts.isElementAccessExpression(unwrapped)) {
|
||||
const key = stringLiteralText(unwrapExpression(unwrapped.argumentExpression));
|
||||
if (key !== "env") {
|
||||
return null;
|
||||
}
|
||||
const base = unwrapExpression(unwrapped.expression);
|
||||
return ts.isIdentifier(base) && base.text === "process" ? unwrapped : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectStringConstants(sourceFile) {
|
||||
const constants = new Map();
|
||||
const visit = (node) => {
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
||||
const literal = stringLiteralText(unwrapExpression(node.initializer));
|
||||
if (literal) {
|
||||
constants.set(node.name.text, literal);
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
return constants;
|
||||
}
|
||||
|
||||
function collectStringArrays(sourceFile) {
|
||||
const arrays = new Map();
|
||||
for (let pass = 0; pass < 4; pass += 1) {
|
||||
const visit = (node) => {
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
||||
const initializer = unwrapExpression(node.initializer);
|
||||
if (ts.isArrayLiteralExpression(initializer)) {
|
||||
const values = [];
|
||||
let complete = true;
|
||||
for (const element of initializer.elements) {
|
||||
const unwrapped = unwrapExpression(element);
|
||||
if (
|
||||
ts.isSpreadElement(unwrapped) &&
|
||||
ts.isIdentifier(unwrapExpression(unwrapped.expression))
|
||||
) {
|
||||
const nested = arrays.get(unwrapExpression(unwrapped.expression).text);
|
||||
if (nested) {
|
||||
values.push(...nested);
|
||||
} else {
|
||||
complete = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const literal = stringLiteralText(unwrapped);
|
||||
if (literal) {
|
||||
values.push(literal);
|
||||
} else {
|
||||
complete = false;
|
||||
}
|
||||
}
|
||||
if (complete) {
|
||||
const previous = arrays.get(node.name.text);
|
||||
const sameValues = previous && previous.join("\0") === values.join("\0");
|
||||
if (!sameValues) {
|
||||
arrays.set(node.name.text, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
}
|
||||
return arrays;
|
||||
}
|
||||
|
||||
function collectForbiddenKeyArrays(sourceFile) {
|
||||
const stringArrays = collectStringArrays(sourceFile);
|
||||
const arrays = new Set();
|
||||
for (const [name, values] of stringArrays) {
|
||||
if (values.some((key) => forbiddenEnvKeys.has(key))) {
|
||||
arrays.add(name);
|
||||
}
|
||||
}
|
||||
return arrays;
|
||||
}
|
||||
|
||||
function collectForbiddenKeyVariables(sourceFile, forbiddenKeyArrays) {
|
||||
const variables = new Set();
|
||||
const visit = (node) => {
|
||||
if (ts.isForOfStatement(node)) {
|
||||
const expression = unwrapExpression(node.expression);
|
||||
if (ts.isIdentifier(expression) && forbiddenKeyArrays.has(expression.text)) {
|
||||
const initializer = node.initializer;
|
||||
if (ts.isVariableDeclarationList(initializer)) {
|
||||
for (const declaration of initializer.declarations) {
|
||||
if (ts.isIdentifier(declaration.name)) {
|
||||
variables.add(declaration.name.text);
|
||||
}
|
||||
}
|
||||
} else if (ts.isIdentifier(initializer)) {
|
||||
variables.add(initializer.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
return variables;
|
||||
}
|
||||
|
||||
function envKeyExpressionIsForbidden(argumentExpression, context) {
|
||||
const keyExpression = unwrapExpression(argumentExpression);
|
||||
const literalKey = stringLiteralText(keyExpression);
|
||||
if (literalKey) {
|
||||
return forbiddenEnvKeys.has(literalKey);
|
||||
}
|
||||
if (!ts.isIdentifier(keyExpression)) {
|
||||
return false;
|
||||
}
|
||||
if (context.forbiddenKeyVariables.has(keyExpression.text)) {
|
||||
return true;
|
||||
}
|
||||
const constant = context.stringConstants.get(keyExpression.text);
|
||||
return constant ? forbiddenEnvKeys.has(constant) : false;
|
||||
}
|
||||
|
||||
function envMutationTarget(expression, context) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (
|
||||
ts.isPropertyAccessExpression(unwrapped) &&
|
||||
processEnvExpression(unwrapped.expression, context)
|
||||
) {
|
||||
return forbiddenEnvKeys.has(unwrapped.name.text) ? unwrapped : null;
|
||||
}
|
||||
if (
|
||||
ts.isElementAccessExpression(unwrapped) &&
|
||||
processEnvExpression(unwrapped.expression, context)
|
||||
) {
|
||||
return envKeyExpressionIsForbidden(unwrapped.argumentExpression, context) ? unwrapped : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function globalAgentExpression(
|
||||
expression,
|
||||
context = { globalAgentAliases: new Set(), globalAliases: new Set() },
|
||||
) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (ts.isIdentifier(unwrapped) && context.globalAgentAliases.has(unwrapped.text)) {
|
||||
return unwrapped;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(unwrapped)) {
|
||||
const receiver = unwrapExpression(unwrapped.expression);
|
||||
if (unwrapped.name.text === "GLOBAL_AGENT" && isGlobalIdentifier(receiver, context)) {
|
||||
return unwrapped;
|
||||
}
|
||||
}
|
||||
if (ts.isElementAccessExpression(unwrapped)) {
|
||||
const receiver = unwrapExpression(unwrapped.expression);
|
||||
const key = stringLiteralText(unwrapExpression(unwrapped.argumentExpression));
|
||||
if (key === "GLOBAL_AGENT" && isGlobalIdentifier(receiver, context)) {
|
||||
return unwrapped;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectEnvAliases(sourceFile) {
|
||||
const aliases = new Set();
|
||||
const emptyContext = { envAliases: new Set() };
|
||||
const visit = (node) => {
|
||||
if (ts.isVariableDeclaration(node) && node.initializer) {
|
||||
if (ts.isIdentifier(node.name) && processEnvExpression(node.initializer, emptyContext)) {
|
||||
aliases.add(node.name.text);
|
||||
}
|
||||
if (ts.isObjectBindingPattern(node.name)) {
|
||||
const initializer = unwrapExpression(node.initializer);
|
||||
if (ts.isIdentifier(initializer) && initializer.text === "process") {
|
||||
for (const element of node.name.elements) {
|
||||
if (!ts.isIdentifier(element.name)) {
|
||||
continue;
|
||||
}
|
||||
const importedName =
|
||||
element.propertyName && ts.isIdentifier(element.propertyName)
|
||||
? element.propertyName.text
|
||||
: element.name.text;
|
||||
if (importedName === "env") {
|
||||
aliases.add(element.name.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function collectGlobalAliases(sourceFile) {
|
||||
const aliases = new Set();
|
||||
const visit = (node) => {
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
||||
if (isGlobalIdentifier(node.initializer, { globalAliases: new Set() })) {
|
||||
aliases.add(node.name.text);
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function collectGlobalAgentAliases(sourceFile, globalAliases = collectGlobalAliases(sourceFile)) {
|
||||
const aliases = new Set();
|
||||
const emptyContext = { globalAgentAliases: new Set(), globalAliases };
|
||||
const visit = (node) => {
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
||||
if (globalAgentExpression(node.initializer, emptyContext)) {
|
||||
aliases.add(node.name.text);
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function globalAgentMutationTarget(expression, context) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (globalAgentExpression(unwrapped, context)) {
|
||||
return unwrapped;
|
||||
}
|
||||
if (
|
||||
ts.isPropertyAccessExpression(unwrapped) &&
|
||||
globalAgentExpression(unwrapped.expression, context)
|
||||
) {
|
||||
return forbiddenGlobalAgentKeys.has(unwrapped.name.text) ? unwrapped : null;
|
||||
}
|
||||
if (
|
||||
ts.isElementAccessExpression(unwrapped) &&
|
||||
globalAgentExpression(unwrapped.expression, context)
|
||||
) {
|
||||
const key = stringLiteralText(unwrapExpression(unwrapped.argumentExpression));
|
||||
return key && forbiddenGlobalAgentKeys.has(key) ? unwrapped : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mutationTarget(expression, context) {
|
||||
return envMutationTarget(expression, context) ?? globalAgentMutationTarget(expression, context);
|
||||
}
|
||||
|
||||
function deleteTarget(expression, context) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
return ts.isDeleteExpression(unwrapped) ? mutationTarget(unwrapped.expression, context) : null;
|
||||
}
|
||||
|
||||
function assignmentTarget(expression, context) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (ts.isBinaryExpression(unwrapped) && ts.isAssignmentOperator(unwrapped.operatorToken.kind)) {
|
||||
return mutationTarget(unwrapped.left, context);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mutatingCallTarget(expression, context) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (!ts.isCallExpression(unwrapped)) {
|
||||
return null;
|
||||
}
|
||||
const callee = unwrapExpression(unwrapped.expression);
|
||||
if (!ts.isPropertyAccessExpression(callee)) {
|
||||
return null;
|
||||
}
|
||||
const method = callee.name.text;
|
||||
if (method !== "defineProperty" && method !== "assign") {
|
||||
return null;
|
||||
}
|
||||
const receiver = unwrapExpression(callee.expression);
|
||||
if (!ts.isIdentifier(receiver) || receiver.text !== "Object") {
|
||||
return null;
|
||||
}
|
||||
const first = unwrapped.arguments[0] ? unwrapExpression(unwrapped.arguments[0]) : null;
|
||||
if (!first) {
|
||||
return null;
|
||||
}
|
||||
if (method === "assign") {
|
||||
return globalAgentExpression(first, context) ?? processEnvExpression(first, context);
|
||||
}
|
||||
const rawKeyArg = unwrapped.arguments[1] ? unwrapExpression(unwrapped.arguments[1]) : null;
|
||||
const literalKeyArg = rawKeyArg ? stringLiteralText(rawKeyArg) : null;
|
||||
const keyArg =
|
||||
literalKeyArg ??
|
||||
(rawKeyArg && ts.isIdentifier(rawKeyArg) ? context.stringConstants.get(rawKeyArg.text) : null);
|
||||
if (keyArg && processEnvExpression(first, context)) {
|
||||
return forbiddenEnvKeys.has(keyArg) ? first : null;
|
||||
}
|
||||
if (keyArg && globalAgentExpression(first, context)) {
|
||||
return forbiddenGlobalAgentKeys.has(keyArg) ? first : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findManagedProxyRuntimeMutationLines(content, fileName = "source.ts") {
|
||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
||||
const globalAliases = collectGlobalAliases(sourceFile);
|
||||
const context = {
|
||||
forbiddenKeyVariables: collectForbiddenKeyVariables(
|
||||
sourceFile,
|
||||
collectForbiddenKeyArrays(sourceFile),
|
||||
),
|
||||
globalAgentAliases: collectGlobalAgentAliases(sourceFile, globalAliases),
|
||||
globalAliases,
|
||||
envAliases: collectEnvAliases(sourceFile),
|
||||
stringConstants: collectStringConstants(sourceFile),
|
||||
};
|
||||
const lines = [];
|
||||
const visit = (node) => {
|
||||
const match =
|
||||
assignmentTarget(node, context) ??
|
||||
deleteTarget(node, context) ??
|
||||
mutatingCallTarget(node, context);
|
||||
if (match) {
|
||||
lines.push(toLine(sourceFile, match));
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
return lines;
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
await runCallsiteGuard({
|
||||
importMetaUrl: import.meta.url,
|
||||
sourceRoots,
|
||||
extraTestSuffixes: [
|
||||
".browser.test.ts",
|
||||
".node.test.ts",
|
||||
".live.test.ts",
|
||||
".e2e.test.ts",
|
||||
".integration.test.ts",
|
||||
],
|
||||
findCallLines: findManagedProxyRuntimeMutationLines,
|
||||
allowCallsite: (callsite) => allowedManagedProxyRuntimeMutationCallsites.has(callsite),
|
||||
header: "Found unmanaged managed-proxy runtime mutation:",
|
||||
footer:
|
||||
"Only proxy lifecycle code may mutate GLOBAL_AGENT or proxy-related process.env runtime state.",
|
||||
});
|
||||
}
|
||||
|
||||
runAsScript(import.meta.url, main);
|
||||
370
scripts/check-raw-socket-callsite-classification.mjs
Normal file
370
scripts/check-raw-socket-callsite-classification.mjs
Normal file
@@ -0,0 +1,370 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import ts from "typescript";
|
||||
import { bundledPluginCallsite } from "./lib/bundled-plugin-paths.mjs";
|
||||
import { runCallsiteGuard } from "./lib/callsite-guard.mjs";
|
||||
import {
|
||||
collectCallExpressionLines,
|
||||
runAsScript,
|
||||
unwrapExpression,
|
||||
} from "./lib/ts-guard-utils.mjs";
|
||||
|
||||
const sourceRoots = ["src", "extensions"];
|
||||
|
||||
// Managed-proxy raw-socket classification allowlist.
|
||||
// Each entry is intentionally a concrete callsite so new raw socket egress fails until reviewed.
|
||||
const allowedRawSocketCallsites = new Set([
|
||||
// Local Gateway run loop readiness probe.
|
||||
"src/cli/gateway-cli/run-loop.ts:46",
|
||||
|
||||
// Local loopback readiness probe for SSH tunnels.
|
||||
"src/infra/ssh-tunnel.ts:80",
|
||||
|
||||
// Local gateway lock probe.
|
||||
"src/infra/gateway-lock.ts:147",
|
||||
|
||||
// Local Unix-domain socket IPC client.
|
||||
"src/infra/jsonl-socket.ts:35",
|
||||
|
||||
// Managed HTTP CONNECT tunnel helper used by APNs and proxy validation.
|
||||
"src/infra/net/http-connect-tunnel.ts:117",
|
||||
"src/infra/net/http-connect-tunnel.ts:123",
|
||||
"src/infra/net/http-connect-tunnel.ts:268",
|
||||
|
||||
// APNs HTTP/2 wrapper: direct only when managed proxy is inactive; tunneled when active.
|
||||
"src/infra/push-apns-http2.ts:74",
|
||||
"src/infra/push-apns-http2.ts:85",
|
||||
|
||||
// Debug proxy CONNECT internals. PR #77010 guards this path while managed proxy mode is active.
|
||||
"src/proxy-capture/proxy-server.ts:266",
|
||||
|
||||
// QA-lab tunnel/capture helpers used for local lab diagnostics.
|
||||
bundledPluginCallsite("qa-lab", "src/lab-server-capture.ts", 99),
|
||||
bundledPluginCallsite("qa-lab", "src/lab-server-ui.ts", 207),
|
||||
bundledPluginCallsite("qa-lab", "src/lab-server-ui.ts", 212),
|
||||
|
||||
// IRC is a raw TCP/TLS channel and is documented as outside managed HTTP proxy coverage.
|
||||
bundledPluginCallsite("irc", "src/client.ts", 124),
|
||||
bundledPluginCallsite("irc", "src/client.ts", 129),
|
||||
]);
|
||||
|
||||
function stringLiteralText(node) {
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
||||
return node.text;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rawModuleSpecifiers = new Map([
|
||||
["node:net", "net"],
|
||||
["net", "net"],
|
||||
["node:tls", "tls"],
|
||||
["tls", "tls"],
|
||||
["node:http2", "http2"],
|
||||
["http2", "http2"],
|
||||
]);
|
||||
|
||||
function unwrapInitializer(expression) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (ts.isAwaitExpression(unwrapped)) {
|
||||
return unwrapExpression(unwrapped.expression);
|
||||
}
|
||||
return unwrapped;
|
||||
}
|
||||
|
||||
function rawMemberAliasKind(initializer, aliases) {
|
||||
const unwrapped = unwrapExpression(initializer);
|
||||
let receiverExpression;
|
||||
let member;
|
||||
if (ts.isPropertyAccessExpression(unwrapped)) {
|
||||
receiverExpression = unwrapped.expression;
|
||||
member = unwrapped.name.text;
|
||||
} else if (ts.isElementAccessExpression(unwrapped)) {
|
||||
receiverExpression = unwrapped.expression;
|
||||
member = stringLiteralText(unwrapExpression(unwrapped.argumentExpression));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
if (!member) {
|
||||
return undefined;
|
||||
}
|
||||
const receiverKind = aliasKind(receiverExpression, aliases);
|
||||
if (
|
||||
(receiverKind === "net" || receiverKind === "tls") &&
|
||||
(member === "connect" || member === "createConnection")
|
||||
) {
|
||||
return `${receiverKind}.${member}`;
|
||||
}
|
||||
if (receiverKind === "net" && member === "Socket") {
|
||||
return "net.Socket";
|
||||
}
|
||||
if (receiverKind === "http2" && member === "connect") {
|
||||
return "http2.connect";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function bindRawModuleDestructureAliases(bindingName, moduleKind, aliases) {
|
||||
if (!ts.isObjectBindingPattern(bindingName)) {
|
||||
return;
|
||||
}
|
||||
for (const element of bindingName.elements) {
|
||||
if (!ts.isIdentifier(element.name)) {
|
||||
continue;
|
||||
}
|
||||
const importedName =
|
||||
element.propertyName && ts.isIdentifier(element.propertyName)
|
||||
? element.propertyName.text
|
||||
: element.name.text;
|
||||
if (importedName === "default") {
|
||||
aliases.set(element.name.text, moduleKind);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
importedName === "connect" ||
|
||||
importedName === "createConnection" ||
|
||||
importedName === "Socket"
|
||||
) {
|
||||
aliases.set(element.name.text, `${moduleKind}.${importedName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectSocketInstanceAliases(sourceFile, rawAliases) {
|
||||
const socketAliases = new Set();
|
||||
const visit = (node) => {
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
||||
if (isSocketConstructorExpression(node.initializer, rawAliases)) {
|
||||
socketAliases.add(node.name.text);
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
return socketAliases;
|
||||
}
|
||||
|
||||
function collectRawModuleAliases(sourceFile) {
|
||||
const aliases = new Map();
|
||||
const visit = (node) => {
|
||||
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
||||
const moduleKind = rawModuleSpecifiers.get(node.moduleSpecifier.text);
|
||||
const clause = node.importClause;
|
||||
if (moduleKind && clause) {
|
||||
if (clause.name) {
|
||||
aliases.set(clause.name.text, moduleKind);
|
||||
}
|
||||
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
|
||||
aliases.set(clause.namedBindings.name.text, moduleKind);
|
||||
}
|
||||
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
||||
for (const specifier of clause.namedBindings.elements) {
|
||||
const importedName = (specifier.propertyName ?? specifier.name).text;
|
||||
if (importedName === "default") {
|
||||
aliases.set(specifier.name.text, moduleKind);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
importedName === "connect" ||
|
||||
importedName === "createConnection" ||
|
||||
importedName === "Socket"
|
||||
) {
|
||||
aliases.set(specifier.name.text, `${moduleKind}.${importedName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ts.isVariableDeclaration(node) && node.initializer) {
|
||||
const initializer = unwrapInitializer(node.initializer);
|
||||
if (ts.isIdentifier(node.name)) {
|
||||
const moduleKind = aliasKind(initializer, aliases);
|
||||
if (moduleKind === "net" || moduleKind === "tls" || moduleKind === "http2") {
|
||||
aliases.set(node.name.text, moduleKind);
|
||||
}
|
||||
const memberKind = rawMemberAliasKind(initializer, aliases);
|
||||
if (memberKind) {
|
||||
aliases.set(node.name.text, memberKind);
|
||||
}
|
||||
}
|
||||
if (
|
||||
ts.isCallExpression(initializer) &&
|
||||
ts.isIdentifier(unwrapExpression(initializer.expression)) &&
|
||||
unwrapExpression(initializer.expression).text === "require" &&
|
||||
initializer.arguments.length === 1 &&
|
||||
ts.isStringLiteral(initializer.arguments[0])
|
||||
) {
|
||||
const moduleKind = rawModuleSpecifiers.get(initializer.arguments[0].text);
|
||||
if (moduleKind) {
|
||||
if (ts.isIdentifier(node.name)) {
|
||||
aliases.set(node.name.text, moduleKind);
|
||||
} else {
|
||||
bindRawModuleDestructureAliases(node.name, moduleKind, aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ts.isObjectBindingPattern(node.name) && ts.isIdentifier(initializer)) {
|
||||
const moduleKind = aliases.get(initializer.text);
|
||||
if (moduleKind === "net" || moduleKind === "tls" || moduleKind === "http2") {
|
||||
bindRawModuleDestructureAliases(node.name, moduleKind, aliases);
|
||||
}
|
||||
}
|
||||
if (
|
||||
ts.isCallExpression(initializer) &&
|
||||
initializer.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
||||
initializer.arguments.length === 1 &&
|
||||
ts.isStringLiteral(initializer.arguments[0])
|
||||
) {
|
||||
const moduleKind = rawModuleSpecifiers.get(initializer.arguments[0].text);
|
||||
if (moduleKind) {
|
||||
if (ts.isIdentifier(node.name)) {
|
||||
aliases.set(node.name.text, moduleKind);
|
||||
} else {
|
||||
bindRawModuleDestructureAliases(node.name, moduleKind, aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function rawModuleKindFromExpression(expression) {
|
||||
const unwrappedExpression = unwrapExpression(expression);
|
||||
const unwrapped = ts.isAwaitExpression(unwrappedExpression)
|
||||
? unwrapExpression(unwrappedExpression.expression)
|
||||
: unwrappedExpression;
|
||||
if (
|
||||
ts.isCallExpression(unwrapped) &&
|
||||
ts.isIdentifier(unwrapped.expression) &&
|
||||
unwrapped.expression.text === "require" &&
|
||||
unwrapped.arguments.length === 1
|
||||
) {
|
||||
const moduleName = stringLiteralText(unwrapExpression(unwrapped.arguments[0]));
|
||||
return moduleName ? rawModuleSpecifiers.get(moduleName) : undefined;
|
||||
}
|
||||
if (
|
||||
ts.isCallExpression(unwrapped) &&
|
||||
unwrapped.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
||||
unwrapped.arguments.length === 1
|
||||
) {
|
||||
const moduleName = stringLiteralText(unwrapExpression(unwrapped.arguments[0]));
|
||||
return moduleName ? rawModuleSpecifiers.get(moduleName) : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function aliasKind(expression, aliases) {
|
||||
const receiver = unwrapExpression(expression);
|
||||
if (ts.isIdentifier(receiver)) {
|
||||
return aliases.get(receiver.text);
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "default") {
|
||||
return rawModuleKindFromExpression(receiver.expression);
|
||||
}
|
||||
return rawModuleKindFromExpression(receiver);
|
||||
}
|
||||
|
||||
function isRawModuleAlias(expression, aliases, expectedKinds) {
|
||||
const kind = aliasKind(expression, aliases);
|
||||
return expectedKinds.has(kind);
|
||||
}
|
||||
|
||||
function isSocketConstructorExpression(expression, aliases) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (!ts.isNewExpression(unwrapped)) {
|
||||
return false;
|
||||
}
|
||||
const callee = unwrapExpression(unwrapped.expression);
|
||||
if (aliasKind(callee, aliases) === "net.Socket") {
|
||||
return true;
|
||||
}
|
||||
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "Socket") {
|
||||
return false;
|
||||
}
|
||||
return isRawModuleAlias(callee.expression, aliases, new Set(["net"]));
|
||||
}
|
||||
|
||||
function rawSocketCallee(expression, aliases, socketAliases = new Set()) {
|
||||
const callee = unwrapExpression(expression);
|
||||
if (ts.isIdentifier(callee)) {
|
||||
const kind = aliasKind(callee, aliases);
|
||||
return kind === "net.connect" ||
|
||||
kind === "tls.connect" ||
|
||||
kind === "http2.connect" ||
|
||||
kind === "net.createConnection" ||
|
||||
kind === "tls.createConnection"
|
||||
? callee
|
||||
: null;
|
||||
}
|
||||
let receiverExpression;
|
||||
let member;
|
||||
if (ts.isPropertyAccessExpression(callee)) {
|
||||
receiverExpression = callee.expression;
|
||||
member = callee.name.text;
|
||||
} else if (ts.isElementAccessExpression(callee)) {
|
||||
receiverExpression = callee.expression;
|
||||
member = stringLiteralText(unwrapExpression(callee.argumentExpression));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (!member) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
member === "connect" &&
|
||||
isRawModuleAlias(receiverExpression, aliases, new Set(["net", "tls", "http2"]))
|
||||
) {
|
||||
return callee;
|
||||
}
|
||||
if (
|
||||
member === "createConnection" &&
|
||||
isRawModuleAlias(receiverExpression, aliases, new Set(["net", "tls"]))
|
||||
) {
|
||||
return callee;
|
||||
}
|
||||
if (member === "connect" && isSocketConstructorExpression(receiverExpression, aliases)) {
|
||||
return callee;
|
||||
}
|
||||
if (
|
||||
member === "connect" &&
|
||||
ts.isIdentifier(unwrapExpression(receiverExpression)) &&
|
||||
socketAliases.has(unwrapExpression(receiverExpression).text)
|
||||
) {
|
||||
return callee;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findRawSocketClientCallLines(content, fileName = "source.ts") {
|
||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
||||
const aliases = collectRawModuleAliases(sourceFile);
|
||||
const socketAliases = collectSocketInstanceAliases(sourceFile, aliases);
|
||||
return collectCallExpressionLines(ts, sourceFile, (node) =>
|
||||
rawSocketCallee(node.expression, aliases, socketAliases),
|
||||
);
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
await runCallsiteGuard({
|
||||
importMetaUrl: import.meta.url,
|
||||
sourceRoots,
|
||||
extraTestSuffixes: [
|
||||
".browser.test.ts",
|
||||
".node.test.ts",
|
||||
".live.test.ts",
|
||||
".e2e.test.ts",
|
||||
".integration.test.ts",
|
||||
],
|
||||
findCallLines: findRawSocketClientCallLines,
|
||||
skipRelativePath: (relPath) => relPath.includes("/test-support/"),
|
||||
allowCallsite: (callsite) => allowedRawSocketCallsites.has(callsite),
|
||||
header: "Found unclassified raw socket client calls:",
|
||||
footer:
|
||||
"Classify raw net/tls/http2 egress as managed/proxied, local-only, diagnostic guarded, or documented unsupported before adding callsites.",
|
||||
});
|
||||
}
|
||||
|
||||
runAsScript(import.meta.url, main);
|
||||
@@ -10,6 +10,12 @@ export const BOUNDARY_CHECKS = [
|
||||
["lint:tmp:tsgo-core-boundary", "pnpm", ["run", "lint:tmp:tsgo-core-boundary"]],
|
||||
["lint:tmp:no-raw-channel-fetch", "pnpm", ["run", "lint:tmp:no-raw-channel-fetch"]],
|
||||
["lint:tmp:no-raw-http2-imports", "pnpm", ["run", "lint:tmp:no-raw-http2-imports"]],
|
||||
[
|
||||
"lint:tmp:managed-proxy-runtime-mutation",
|
||||
"pnpm",
|
||||
["run", "lint:tmp:managed-proxy-runtime-mutation"],
|
||||
],
|
||||
["lint:tmp:raw-socket-classification", "pnpm", ["run", "lint:tmp:raw-socket-classification"]],
|
||||
["lint:agent:ingress-owner", "pnpm", ["run", "lint:agent:ingress-owner"]],
|
||||
[
|
||||
"lint:plugins:no-register-http-handler",
|
||||
|
||||
184
test/scripts/check-managed-proxy-runtime-mutation.test.ts
Normal file
184
test/scripts/check-managed-proxy-runtime-mutation.test.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findManagedProxyRuntimeMutationLines } from "../../scripts/check-managed-proxy-runtime-mutation.mjs";
|
||||
|
||||
describe("check-managed-proxy-runtime-mutation", () => {
|
||||
it("finds assignments and deletes for proxy env vars", () => {
|
||||
const source = `
|
||||
process.env.HTTP_PROXY = "http://proxy";
|
||||
process.env["HTTPS_PROXY"] = "http://proxy";
|
||||
delete process.env.NO_PROXY;
|
||||
delete process.env["GLOBAL_AGENT_NO_PROXY"];
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
it("finds global object alias GLOBAL_AGENT mutations", () => {
|
||||
const source = `
|
||||
const globalRecord = global;
|
||||
const agent = globalRecord.GLOBAL_AGENT;
|
||||
globalRecord.GLOBAL_AGENT = {};
|
||||
globalRecord["GLOBAL_AGENT"] = {};
|
||||
delete globalRecord.GLOBAL_AGENT;
|
||||
delete globalRecord["GLOBAL_AGENT"];
|
||||
agent.HTTP_PROXY = "http://proxy";
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([4, 5, 6, 7, 8]);
|
||||
});
|
||||
|
||||
it("finds GLOBAL_AGENT mutations", () => {
|
||||
const source = `
|
||||
global.GLOBAL_AGENT = {};
|
||||
global.GLOBAL_AGENT.NO_PROXY = "localhost";
|
||||
global["GLOBAL_AGENT"].HTTP_PROXY = "http://proxy";
|
||||
delete global.GLOBAL_AGENT.HTTPS_PROXY;
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
it("finds Object.assign and Object.defineProperty mutations", () => {
|
||||
const source = `
|
||||
Object.assign(global.GLOBAL_AGENT, { NO_PROXY: "localhost" });
|
||||
Object.assign(process.env, { NO_PROXY: "localhost" });
|
||||
Object.defineProperty(process.env, "HTTP_PROXY", { value: "http://proxy" });
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4]);
|
||||
});
|
||||
|
||||
it("finds missing managed-proxy env key mutations", () => {
|
||||
const source = `
|
||||
process.env.GLOBAL_AGENT_FORCE_GLOBAL_AGENT = "true";
|
||||
process.env.OPENCLAW_PROXY_LOOPBACK_MODE = "gateway-only";
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3]);
|
||||
});
|
||||
|
||||
it("finds defineProperty mutations with constant proxy keys", () => {
|
||||
const source = `
|
||||
const proxyKey = "HTTP_PROXY";
|
||||
const agentKey = "NO_PROXY";
|
||||
Object.defineProperty(process.env, proxyKey, { value: "http://proxy" });
|
||||
Object.defineProperty(global.GLOBAL_AGENT, agentKey, { value: "localhost" });
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([4, 5]);
|
||||
});
|
||||
|
||||
it("finds destructured process.env alias mutations", () => {
|
||||
const source = `
|
||||
const { env } = process;
|
||||
env.HTTP_PROXY = "http://proxy";
|
||||
env["NO_PROXY"] = "localhost";
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([3, 4]);
|
||||
});
|
||||
|
||||
it("finds process.env alias and constant key mutations", () => {
|
||||
const source = `
|
||||
const env = process.env;
|
||||
const proxyKey = "HTTP_PROXY";
|
||||
env.HTTPS_PROXY = "http://proxy";
|
||||
env[proxyKey] = "http://proxy";
|
||||
delete env.NO_PROXY;
|
||||
Object.assign(env, { GLOBAL_AGENT_HTTP_PROXY: "http://proxy" });
|
||||
Object.defineProperty(env, "OPENCLAW_PROXY_ACTIVE", { value: "1" });
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([4, 5, 6, 7, 8]);
|
||||
});
|
||||
|
||||
it("finds dynamic process.env key mutations from forbidden key arrays", () => {
|
||||
const source = `
|
||||
const proxyKeys = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"];
|
||||
for (const key of proxyKeys) {
|
||||
process.env[key] = "http://proxy";
|
||||
}
|
||||
for (const key of proxyKeys) {
|
||||
delete process.env[key];
|
||||
}
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([4, 7]);
|
||||
});
|
||||
|
||||
it("finds dynamic process.env key mutations from spread-built forbidden key arrays", () => {
|
||||
const source = `
|
||||
const lower = ["http_proxy", "https_proxy"];
|
||||
const upper = ["HTTP_PROXY", "HTTPS_PROXY"];
|
||||
const all = [...lower, ...upper, "OPENCLAW_PROXY_LOOPBACK_MODE"];
|
||||
for (const key of all) {
|
||||
delete process.env[key];
|
||||
}
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([6]);
|
||||
});
|
||||
|
||||
it("ignores dynamic process.env key mutations from unrelated key arrays", () => {
|
||||
const source = `
|
||||
const normalKeys = ["PATH", "HOME"];
|
||||
for (const key of normalKeys) {
|
||||
process.env[key] = "value";
|
||||
}
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([]);
|
||||
});
|
||||
|
||||
it("finds GLOBAL_AGENT alias mutations", () => {
|
||||
const source = `
|
||||
const agent = global.GLOBAL_AGENT;
|
||||
agent.HTTP_PROXY = "http://proxy";
|
||||
agent["NO_PROXY"] = "localhost";
|
||||
delete agent.HTTPS_PROXY;
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([3, 4, 5]);
|
||||
});
|
||||
|
||||
it("finds globalThis.GLOBAL_AGENT mutations alongside global.GLOBAL_AGENT", () => {
|
||||
const source = `
|
||||
globalThis.GLOBAL_AGENT = {};
|
||||
globalThis.GLOBAL_AGENT.NO_PROXY = "localhost";
|
||||
globalThis["GLOBAL_AGENT"].HTTP_PROXY = "http://proxy";
|
||||
delete globalThis.GLOBAL_AGENT.HTTPS_PROXY;
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
it('finds process["env"] mixed access mutations', () => {
|
||||
const source = `
|
||||
process["env"].HTTP_PROXY = "http://proxy";
|
||||
process["env"]["HTTPS_PROXY"] = "http://proxy";
|
||||
delete process["env"].NO_PROXY;
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4]);
|
||||
});
|
||||
|
||||
it("does not flag Object.assign on a non-process .env namespace", () => {
|
||||
const source = `
|
||||
Object.assign(config.env, { NO_PROXY: "localhost" });
|
||||
Object.defineProperty(config.env, "HTTP_PROXY", { value: "http://proxy" });
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores reads, unrelated env vars, comments, and strings", () => {
|
||||
const source = `
|
||||
const current = process.env.HTTP_PROXY;
|
||||
process.env.PATH = "/usr/bin";
|
||||
const text = "process.env.NO_PROXY = '*'";
|
||||
// global.GLOBAL_AGENT.NO_PROXY = '*';
|
||||
`;
|
||||
|
||||
expect(findManagedProxyRuntimeMutationLines(source)).toEqual([]);
|
||||
});
|
||||
});
|
||||
192
test/scripts/check-raw-socket-callsite-classification.test.ts
Normal file
192
test/scripts/check-raw-socket-callsite-classification.test.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findRawSocketClientCallLines } from "../../scripts/check-raw-socket-callsite-classification.mjs";
|
||||
|
||||
describe("check-raw-socket-callsite-classification", () => {
|
||||
it("finds raw net, tls, and http2 client calls", () => {
|
||||
const source = `
|
||||
import net from "node:net";
|
||||
import * as tls from "node:tls";
|
||||
import http2 from "node:http2";
|
||||
net.connect({ host: "example.com", port: 6667 });
|
||||
tls.connect({ host: "example.com", port: 6697 });
|
||||
http2.connect("https://api.example.com");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([5, 6, 7]);
|
||||
});
|
||||
|
||||
it("ignores comments, strings, and unrelated connect methods", () => {
|
||||
const source = `
|
||||
// net.connect({ host: "example.com" });
|
||||
const text = "tls.connect({ host: 'example.com' })";
|
||||
client.connect(transport);
|
||||
websocket.connect();
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles aliased imports, requires, and dynamic literal imports", () => {
|
||||
const source = `
|
||||
import * as rawNet from "node:net";
|
||||
const rawTls = require("node:tls");
|
||||
const rawHttp2 = await import("node:http2");
|
||||
rawNet.connect({ host: "127.0.0.1", port: 1 });
|
||||
rawTls.connect({ host: "127.0.0.1", port: 1 });
|
||||
rawHttp2.connect("https://example.com");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([5, 6, 7]);
|
||||
});
|
||||
|
||||
it("finds element-access raw socket calls", () => {
|
||||
const source = `
|
||||
import net from "node:net";
|
||||
import tls from "node:tls";
|
||||
import http2 from "node:http2";
|
||||
net["connect"]({ host: "127.0.0.1", port: 1 });
|
||||
tls["createConnection"]({ host: "127.0.0.1", port: 1 });
|
||||
http2["connect"]("https://example.com");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([5, 6, 7]);
|
||||
});
|
||||
|
||||
it("finds destructured dynamic-import default raw module aliases", () => {
|
||||
const source = `
|
||||
const { default: rawNet } = await import("node:net");
|
||||
const { default: rawTls } = await import("node:tls");
|
||||
const { default: rawHttp2 } = await import("node:http2");
|
||||
rawNet.connect({ host: "127.0.0.1", port: 1 });
|
||||
rawTls.connect({ host: "127.0.0.1", port: 1 });
|
||||
rawHttp2.connect("https://example.com");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([5, 6, 7]);
|
||||
});
|
||||
|
||||
it("finds direct raw module receiver calls", () => {
|
||||
const source = `
|
||||
require("node:net").connect({ host: "127.0.0.1", port: 1 });
|
||||
require("node:tls").createConnection({ host: "127.0.0.1", port: 1 });
|
||||
(await import("node:http2")).connect("https://example.com");
|
||||
(await import("node:net")).default.connect({ host: "127.0.0.1", port: 1 });
|
||||
(await import("node:tls")).default.createConnection({ host: "127.0.0.1", port: 1 });
|
||||
(await import("node:http2")).default.connect("https://example.com");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([2, 3, 4, 5, 6, 7]);
|
||||
});
|
||||
|
||||
it("finds named default raw module imports", () => {
|
||||
const source = `
|
||||
import { default as rawNet } from "node:net";
|
||||
import { default as rawTls } from "node:tls";
|
||||
import { default as rawHttp2 } from "node:http2";
|
||||
rawNet.connect({ host: "127.0.0.1", port: 1 });
|
||||
rawTls.connect({ host: "127.0.0.1", port: 1 });
|
||||
rawHttp2.connect("https://example.com");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([5, 6, 7]);
|
||||
});
|
||||
|
||||
it("finds raw socket module object aliases", () => {
|
||||
const source = `
|
||||
import net from "node:net";
|
||||
import tls from "node:tls";
|
||||
import http2 from "node:http2";
|
||||
const rawNet = net;
|
||||
const rawTls = tls;
|
||||
const rawHttp2 = http2;
|
||||
rawNet.connect({ host: "127.0.0.1", port: 1 });
|
||||
rawTls.connect({ host: "127.0.0.1", port: 1 });
|
||||
rawHttp2.connect("https://example.com");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([8, 9, 10]);
|
||||
});
|
||||
|
||||
it("finds aliases to raw socket module members", () => {
|
||||
const source = `
|
||||
import net from "node:net";
|
||||
import tls from "node:tls";
|
||||
import http2 from "node:http2";
|
||||
const netConnect = net.connect;
|
||||
const tlsConnect = tls.connect;
|
||||
const h2Connect = http2.connect;
|
||||
const Socket = net.Socket;
|
||||
const { createConnection } = net;
|
||||
netConnect({ host: "127.0.0.1", port: 1 });
|
||||
tlsConnect({ host: "127.0.0.1", port: 1 });
|
||||
h2Connect("https://example.com");
|
||||
createConnection({ host: "127.0.0.1", port: 1 });
|
||||
new Socket().connect("/tmp/socket");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([10, 11, 12, 13, 14]);
|
||||
});
|
||||
|
||||
it("finds destructured require and dynamic import raw socket bindings", () => {
|
||||
const source = `
|
||||
const { connect, createConnection, Socket } = require("node:net");
|
||||
const { connect: tlsConnect } = await import("node:tls");
|
||||
connect({ host: "127.0.0.1", port: 1 });
|
||||
createConnection({ host: "127.0.0.1", port: 1 });
|
||||
tlsConnect({ host: "127.0.0.1", port: 1 });
|
||||
new Socket().connect("/tmp/socket");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([4, 5, 6, 7]);
|
||||
});
|
||||
|
||||
it("finds stored Socket instance connect calls", () => {
|
||||
const source = `
|
||||
import net from "node:net";
|
||||
const client = new net.Socket();
|
||||
client.connect("/tmp/socket");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([4]);
|
||||
});
|
||||
|
||||
it("finds named raw socket imports", () => {
|
||||
const source = `
|
||||
import { connect as netConnect, createConnection, Socket } from "node:net";
|
||||
import { connect as tlsConnect } from "node:tls";
|
||||
import { connect as http2Connect } from "node:http2";
|
||||
netConnect({ host: "127.0.0.1", port: 1 });
|
||||
createConnection({ host: "127.0.0.1", port: 1 });
|
||||
tlsConnect({ host: "127.0.0.1", port: 1 });
|
||||
http2Connect("https://example.com");
|
||||
new Socket().connect("/tmp/socket");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([5, 6, 7, 8, 9]);
|
||||
});
|
||||
|
||||
it("finds createConnection and constructed Socket.connect client calls", () => {
|
||||
const source = `
|
||||
import net from "node:net";
|
||||
import tls from "node:tls";
|
||||
net.createConnection({ host: "127.0.0.1", port: 1 });
|
||||
tls.createConnection({ host: "127.0.0.1", port: 1 });
|
||||
new net.Socket().connect("/tmp/socket");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([4, 5, 6]);
|
||||
});
|
||||
|
||||
it("handles parenthesized and asserted module identifiers", () => {
|
||||
const source = `
|
||||
import net from "node:net";
|
||||
import tls from "node:tls";
|
||||
import http2 from "node:http2";
|
||||
(net as typeof import("node:net")).connect({ host: "127.0.0.1", port: 1 });
|
||||
(tls as typeof import("node:tls")).connect({ host: "127.0.0.1", port: 1 });
|
||||
(http2 as typeof import("node:http2")).connect("https://example.com");
|
||||
`;
|
||||
|
||||
expect(findRawSocketClientCallLines(source)).toEqual([5, 6, 7]);
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,22 @@ describe("run-additional-boundary-checks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("runs managed proxy runtime mutation guard in CI", () => {
|
||||
expect(BOUNDARY_CHECKS).toContainEqual({
|
||||
label: "lint:tmp:managed-proxy-runtime-mutation",
|
||||
command: "pnpm",
|
||||
args: ["run", "lint:tmp:managed-proxy-runtime-mutation"],
|
||||
});
|
||||
});
|
||||
|
||||
it("runs raw socket classification guard in CI", () => {
|
||||
expect(BOUNDARY_CHECKS).toContainEqual({
|
||||
label: "lint:tmp:raw-socket-classification",
|
||||
command: "pnpm",
|
||||
args: ["run", "lint:tmp:raw-socket-classification"],
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes concurrency input", () => {
|
||||
expect(resolveConcurrency("6")).toBe(6);
|
||||
expect(resolveConcurrency("0")).toBe(4);
|
||||
|
||||
Reference in New Issue
Block a user