From dd0a9bf8693083841cb195dfdf15e87a2f371b4b Mon Sep 17 00:00:00 2001 From: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Thu, 7 May 2026 16:02:54 +1000 Subject: [PATCH] lint: replace raw socket guard with codeql --- ...l-raw-socket-boundary-critical-quality.yml | 27 ++ .../openclaw-boundary/codeql-pack.lock.yml | 30 ++ .github/codeql/openclaw-boundary/qlpack.yml | 6 + .../raw-socket-callsite-classification.ql | 92 +++++ .github/workflows/codeql-critical-quality.yml | 69 ++++ package.json | 1 - ...eck-raw-socket-callsite-classification.mjs | 370 ------------------ scripts/run-additional-boundary-checks.mjs | 1 - ...raw-socket-callsite-classification.test.ts | 192 --------- .../run-additional-boundary-checks.test.ts | 8 - 10 files changed, 224 insertions(+), 572 deletions(-) create mode 100644 .github/codeql/codeql-raw-socket-boundary-critical-quality.yml create mode 100644 .github/codeql/openclaw-boundary/codeql-pack.lock.yml create mode 100644 .github/codeql/openclaw-boundary/qlpack.yml create mode 100644 .github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql delete mode 100644 scripts/check-raw-socket-callsite-classification.mjs delete mode 100644 test/scripts/check-raw-socket-callsite-classification.test.ts diff --git a/.github/codeql/codeql-raw-socket-boundary-critical-quality.yml b/.github/codeql/codeql-raw-socket-boundary-critical-quality.yml new file mode 100644 index 00000000000..5bdb6b2f626 --- /dev/null +++ b/.github/codeql/codeql-raw-socket-boundary-critical-quality.yml @@ -0,0 +1,27 @@ +name: openclaw-codeql-raw-socket-boundary-critical-quality + +disable-default-queries: true + +queries: + - uses: ./.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql + +paths: + - src + - extensions + +paths-ignore: + - "**/node_modules" + - "**/coverage" + - "**/*.generated.ts" + - "**/*.bundle.js" + - "**/*-runtime.js" + - "**/*.test.ts" + - "**/*.test.tsx" + - "**/*.e2e.test.ts" + - "**/*.e2e.test.tsx" + - "**/*test-support*" + - "**/*test-helper*" + - "**/*mock*" + - "**/*fixture*" + - "**/*bench*" + - "extensions/diffs/assets/**" diff --git a/.github/codeql/openclaw-boundary/codeql-pack.lock.yml b/.github/codeql/openclaw-boundary/codeql-pack.lock.yml new file mode 100644 index 00000000000..8e3e256c0e5 --- /dev/null +++ b/.github/codeql/openclaw-boundary/codeql-pack.lock.yml @@ -0,0 +1,30 @@ +--- +lockVersion: 1.0.0 +dependencies: + codeql/concepts: + version: 0.0.22 + codeql/controlflow: + version: 2.0.32 + codeql/dataflow: + version: 2.1.4 + codeql/javascript-all: + version: 2.6.28 + codeql/mad: + version: 1.0.48 + codeql/regex: + version: 1.0.48 + codeql/ssa: + version: 2.0.24 + codeql/threat-models: + version: 1.0.48 + codeql/tutorial: + version: 1.0.48 + codeql/typetracking: + version: 2.0.32 + codeql/util: + version: 2.0.35 + codeql/xml: + version: 1.0.48 + codeql/yaml: + version: 1.0.48 +compiled: false diff --git a/.github/codeql/openclaw-boundary/qlpack.yml b/.github/codeql/openclaw-boundary/qlpack.yml new file mode 100644 index 00000000000..5cfe6638d94 --- /dev/null +++ b/.github/codeql/openclaw-boundary/qlpack.yml @@ -0,0 +1,6 @@ +name: openclaw/codeql-boundary-queries +version: 0.0.0 +library: false +dependencies: + codeql/javascript-all: 2.6.28 +extractor: javascript diff --git a/.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql b/.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql new file mode 100644 index 00000000000..555651b03df --- /dev/null +++ b/.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql @@ -0,0 +1,92 @@ +/** + * @name Raw socket client callsite classification + * @description Raw net/tls/http2 client egress must be classified before landing. + * @kind problem + * @problem.severity error + * @precision high + * @id js/openclaw/raw-socket-callsite-classification + * @tags maintainability + * security + * external/cwe/cwe-441 + */ + +import javascript + +predicate rawModule(string moduleName) { + moduleName = ["net", "node:net", "tls", "node:tls", "http2", "node:http2"] +} + +predicate netModule(string moduleName) { moduleName = ["net", "node:net"] } + +predicate rawConnectMember(string memberName) { memberName = ["connect", "createConnection"] } + +predicate relevantSourceFile(File file) { + exists(string path | + path = file.getRelativePath() and + path.regexpMatch("^(src|extensions)/.*\\.ts$") and + not path.regexpMatch(".*\\.(test|spec|test-utils|test-harness|e2e-harness)\\.ts$") and + not path.regexpMatch(".*/test-support/.*") and + not path.regexpMatch("^extensions/diffs/assets/.*") + ) +} + +Expr rawSocketClientCall() { + exists(API::CallNode call, string moduleName, string memberName | + rawModule(moduleName) and + rawConnectMember(memberName) and + call = API::moduleImport(moduleName).getMember(memberName).getACall() and + result = call.asExpr() + ) + or + exists(string moduleName | + netModule(moduleName) and + result = + DataFlow::moduleMember(moduleName, "Socket") + .getAnInstantiation() + .getAMethodCall("connect") + .asExpr() + ) +} + +predicate allowedOwnerScope(Expr call, string path, string functionName) { + exists(Function owner | + call.getFile().getRelativePath() = path and + owner.getFile() = call.getFile() and + owner.getName() = functionName and + call.getParent*() = owner.getBody() + ) +} + +predicate allowedRawSocketClientCall(Expr call) { + allowedOwnerScope(call, "src/cli/gateway-cli/run-loop.ts", "waitForGatewayPortReady") + or + allowedOwnerScope(call, "src/infra/ssh-tunnel.ts", "canConnectLocal") + or + allowedOwnerScope(call, "src/infra/gateway-lock.ts", "checkPortFree") + or + allowedOwnerScope(call, "src/infra/jsonl-socket.ts", "requestJsonlSocket") + or + allowedOwnerScope(call, "src/infra/net/http-connect-tunnel.ts", "connectToProxy") + or + allowedOwnerScope(call, "src/infra/net/http-connect-tunnel.ts", "startTargetTls") + or + allowedOwnerScope(call, "src/infra/push-apns-http2.ts", "openProxiedApnsHttp2Session") + or + allowedOwnerScope(call, "src/infra/push-apns-http2.ts", "connectApnsHttp2Session") + or + allowedOwnerScope(call, "src/proxy-capture/proxy-server.ts", "startDebugProxyServer") + or + allowedOwnerScope(call, "extensions/irc/src/client.ts", "connectIrcClient") + or + allowedOwnerScope(call, "extensions/qa-lab/src/lab-server-capture.ts", "probeTcpReachability") + or + allowedOwnerScope(call, "extensions/qa-lab/src/lab-server-ui.ts", "proxyUpgradeRequest") +} + +from Expr call +where + rawSocketClientCall() = call and + relevantSourceFile(call.getFile()) and + not allowedRawSocketClientCall(call) +select call, + "Classify raw net/tls/http2 client egress as managed/proxied, local-only, diagnostic guarded, or documented unsupported before adding this callsite." diff --git a/.github/workflows/codeql-critical-quality.yml b/.github/workflows/codeql-critical-quality.yml index 06f0d136f54..be50d6687ed 100644 --- a/.github/workflows/codeql-critical-quality.yml +++ b/.github/workflows/codeql-critical-quality.yml @@ -21,15 +21,20 @@ on: - plugin-sdk-package-contract - plugin-sdk-reply-runtime - provider-runtime-boundary + - raw-socket-boundary - session-diagnostics-boundary pull_request: types: [opened, synchronize, reopened, ready_for_review] paths: - ".github/codeql/**" - ".github/workflows/codeql-critical-quality.yml" + - "extensions/*.ts" + - "extensions/**/*.ts" - "packages/plugin-package-contract/**" - "packages/plugin-sdk/**" - "packages/memory-host-sdk/**" + - "src/*.ts" + - "src/**/*.ts" - "src/config/**" - "extensions/bluebubbles/src/**" - "extensions/discord/src/**" @@ -159,6 +164,7 @@ jobs: plugin_sdk_package: ${{ steps.detect.outputs.plugin_sdk_package }} plugin_sdk_reply: ${{ steps.detect.outputs.plugin_sdk_reply }} provider: ${{ steps.detect.outputs.provider }} + raw_socket: ${{ steps.detect.outputs.raw_socket }} session_diagnostics: ${{ steps.detect.outputs.session_diagnostics }} steps: - name: Detect PR shard paths @@ -182,6 +188,7 @@ jobs: plugin_sdk_package=false plugin_sdk_reply=false provider=false + raw_socket=false session_diagnostics=false if [[ "${EVENT_NAME}" != "pull_request" ]]; then @@ -196,6 +203,7 @@ jobs: plugin_sdk_package=true plugin_sdk_reply=true provider=true + raw_socket=true session_diagnostics=true else while IFS= read -r file; do @@ -212,8 +220,12 @@ jobs: plugin_sdk_package=true plugin_sdk_reply=true provider=true + raw_socket=true session_diagnostics=true ;; + src/*.ts|src/**/*.ts|extensions/*.ts|extensions/**/*.ts) + raw_socket=true + ;; src/acp/control-plane/*|src/agents/cli-runner/*|src/agents/command/*|src/agents/pi-embedded-runner/*|src/agents/tools/*|src/agents/*completion*.ts|src/agents/*transport*.ts|src/agents/model-*.ts|src/agents/openclaw-tools*.ts|src/agents/provider-*.ts|src/agents/session*.ts|src/agents/tool-call*.ts|src/auto-reply/reply/agent-runner*.ts|src/auto-reply/reply/commands*.ts|src/auto-reply/reply/directive-handling*.ts|src/auto-reply/reply/dispatch-*.ts|src/auto-reply/reply/get-reply-run*.ts|src/auto-reply/reply/provider-dispatcher*.ts|src/auto-reply/reply/queue*.ts|src/auto-reply/reply/reply-run-registry*.ts|src/auto-reply/reply/session*.ts) agent=true ;; @@ -296,6 +308,7 @@ jobs: echo "plugin_sdk_package=${plugin_sdk_package}" echo "plugin_sdk_reply=${plugin_sdk_reply}" echo "provider=${provider}" + echo "raw_socket=${raw_socket}" echo "session_diagnostics=${session_diagnostics}" } >> "${GITHUB_OUTPUT}" @@ -391,6 +404,62 @@ jobs: with: category: "/codeql-critical-quality/channel-runtime-boundary" + raw-socket-boundary: + name: Critical Quality (raw-socket-boundary) + needs: quality-shards + if: ${{ needs.quality-shards.outputs.raw_socket == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'raw-socket-boundary') }} + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + submodules: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + with: + languages: javascript-typescript + config-file: ./.github/codeql/codeql-raw-socket-boundary-critical-quality.yml + + - name: Analyze + id: analyze + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + with: + output: sarif-results + category: "/codeql-critical-quality/raw-socket-boundary" + + - name: Fail on raw socket findings + env: + SARIF_OUTPUT: sarif-results + run: | + set -euo pipefail + shopt -s nullglob + + files=("$SARIF_OUTPUT"/*.sarif) + if [ "${#files[@]}" -eq 0 ]; then + echo "No SARIF files found in $SARIF_OUTPUT" >&2 + exit 1 + fi + + findings="$(jq -s '[.[].runs[]?.results[]?] | length' "${files[@]}")" + if [ "$findings" = "0" ]; then + exit 0 + fi + + echo "Found ${findings} unclassified raw socket client callsite(s):" >&2 + jq -r ' + .runs[]?.results[]? + | .locations[0].physicalLocation as $location + | "- " + + ($location.artifactLocation.uri // "unknown") + + ":" + + (($location.region.startLine // 0) | tostring) + + " " + + (.message.text // .ruleId) + ' "${files[@]}" >&2 + exit 1 + agent-runtime-boundary: name: Critical Quality (agent-runtime-boundary) needs: quality-shards diff --git a/package.json b/package.json index be9f77b1df4..7bb342362c0 100644 --- a/package.json +++ b/package.json @@ -1436,7 +1436,6 @@ "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", diff --git a/scripts/check-raw-socket-callsite-classification.mjs b/scripts/check-raw-socket-callsite-classification.mjs deleted file mode 100644 index 3d240bede17..00000000000 --- a/scripts/check-raw-socket-callsite-classification.mjs +++ /dev/null @@ -1,370 +0,0 @@ -#!/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); diff --git a/scripts/run-additional-boundary-checks.mjs b/scripts/run-additional-boundary-checks.mjs index 43e0732e8fd..b40d9a7fb99 100644 --- a/scripts/run-additional-boundary-checks.mjs +++ b/scripts/run-additional-boundary-checks.mjs @@ -10,7 +10,6 @@ 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: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", diff --git a/test/scripts/check-raw-socket-callsite-classification.test.ts b/test/scripts/check-raw-socket-callsite-classification.test.ts deleted file mode 100644 index e4c29d2287f..00000000000 --- a/test/scripts/check-raw-socket-callsite-classification.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -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]); - }); -}); diff --git a/test/scripts/run-additional-boundary-checks.test.ts b/test/scripts/run-additional-boundary-checks.test.ts index 3c866e58619..1a5e9f099a7 100644 --- a/test/scripts/run-additional-boundary-checks.test.ts +++ b/test/scripts/run-additional-boundary-checks.test.ts @@ -30,14 +30,6 @@ describe("run-additional-boundary-checks", () => { }); }); - 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);