mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-16 09:30:44 +00:00
lint: replace raw socket guard with codeql
This commit is contained in:
27
.github/codeql/codeql-raw-socket-boundary-critical-quality.yml
vendored
Normal file
27
.github/codeql/codeql-raw-socket-boundary-critical-quality.yml
vendored
Normal file
@@ -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/**"
|
||||
30
.github/codeql/openclaw-boundary/codeql-pack.lock.yml
vendored
Normal file
30
.github/codeql/openclaw-boundary/codeql-pack.lock.yml
vendored
Normal file
@@ -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
|
||||
6
.github/codeql/openclaw-boundary/qlpack.yml
vendored
Normal file
6
.github/codeql/openclaw-boundary/qlpack.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
name: openclaw/codeql-boundary-queries
|
||||
version: 0.0.0
|
||||
library: false
|
||||
dependencies:
|
||||
codeql/javascript-all: 2.6.28
|
||||
extractor: javascript
|
||||
92
.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql
vendored
Normal file
92
.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql
vendored
Normal file
@@ -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."
|
||||
69
.github/workflows/codeql-critical-quality.yml
vendored
69
.github/workflows/codeql-critical-quality.yml
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -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",
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user