From b9d5d857a5bcbd825470cabced24596ddb3a8c02 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 11 Oct 2026 05:21:38 +0900 Subject: [PATCH 1/6] test: guard shipped source against raw TLS clients --- test/no-raw-tls-client.test.mjs | 111 ++++++++++++++++++++++++++ test/tls-client-detector.mjs | 136 ++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 test/no-raw-tls-client.test.mjs create mode 100644 test/tls-client-detector.mjs diff --git a/test/no-raw-tls-client.test.mjs b/test/no-raw-tls-client.test.mjs new file mode 100644 index 0000000..03b816d --- /dev/null +++ b/test/no-raw-tls-client.test.mjs @@ -0,0 +1,111 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { evaluateShippedSource, findRawTlsClients, isScannedSourceFile } from "./tls-client-detector.mjs"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +// Security contract: CVE-2026-48618 must not gain an unreviewed raw-host path. +// The bundled ws@8.21.3 connector parses new URL(address) before host/SNI; +// OmOWright supplies headers only. See senpi#3078 and oh-my-openagent#9892. +// Entries must pin { file, call: exact normalized text, count, reason }. +// Empty today: dependency bundles are reviewed separately, not file-exempted. +const ALLOWLIST = []; + +describe("raw TLS client guard (CVE-2026-48618)", () => { + it("detects every direct client and module acquisition form", () => { + const samples = [ + "tls.connect({ host, port: 443 })", + "https.request(url, callback)", + "https.get(url)", + "http2.connect(authority)", + "Bun.connect({ hostname, tls: true })", + "new https.Agent(options)", + "new tls.TLSSocket(socket, options)", + "const options = { checkServerIdentity: check }", + 'tls["connect"](options)', + "Bun[`connect`](options)", + ]; + for (const module of ["tls", "https", "http2"]) { + for (const prefix of ["", "node:"]) { + const name = prefix + module; + const member = module === "https" ? "request" : "connect"; + samples.push( + `import client from "${name}";`, + `import * as client from '${name}';`, + `import { ${member} as client } from "${name}";`, + `import client, { ${member} } from "${name}";`, + `import "${name}";`, + `const client = require("${name}");`, + `const client = await import('${name}');`, + `const client = await import(\`${name}\`);`, + `const client = require(\`${name}\`);`, + `const client = process.getBuiltinModule("${name}");`, + `const client = process.getBuiltinModule(\`${name}\`);`, + `export { ${member} as client } from "${name}";`, + `export * from "${name}";`, + `export * as client from "${name}";`, + ); + } + } + samples.push('import { connect } from "node:\\u0074ls";'); + for (const sample of samples) { + assert.ok(findRawTlsClients(sample).length > 0, `missed: ${sample}`); + } + assert.deepEqual(findRawTlsClients('\n\ntls.connect({ host: "x)" });'), [ + { line: 3, text: 'tls.connect({ host: "x)" })' }, + ]); + }); + + it("ignores comments, literal prose, type-only and inbound neighbors", () => { + for (const sample of [ + '// tls.connect(options)\n/* import client from "tls" */', + 'const text = "tls.connect(options)";', + 'const text = `import client from "tls";`;', + 'import type { Agent } from "node:https";', + 'export type { SecureContext } from "node:tls";', + 'import { createServer } from "node:https";', + 'export { createSecureServer } from "node:http2";', + 'import { createServer, type Agent } from "node:https";', + 'import { connect } from "node:tlsx";', + 'import { connect } from "./tls";', + "fetch(url); new URL(url); tls.createServer(options); Bun.serve(options);", + ]) assert.deepEqual(findRawTlsClients(sample), [], sample); + }); + + it("scans tracked shipped source, not tests or fixtures", () => { + for (const file of ["src/client.js", "bin/client.mjs", "bridge/extension/background.js", "new/client.ts"]) { + assert.equal(isScannedSourceFile(file), true, file); + } + for (const file of ["test/client.js", "src/client.test.js", "src/fixtures/client.js", "src/types.d.ts", "README.md"]) { + assert.equal(isScannedSourceFile(file), false, file); + } + const files = execFileSync("git", ["ls-files", "-z"], { cwd: root, encoding: "utf8" }) + .split("\0").filter(isScannedSourceFile); + assert.ok(files.includes("src/core.js") && files.includes("bridge/extension/background.js")); + const verdict = evaluateShippedSource(files.map(file => ({ + path: file, content: readFileSync(new URL(file, new URL("../", import.meta.url)), "utf8"), + })), ALLOWLIST); + assert.deepEqual(verdict, { offenders: [], stale: [] }, + "Raw TLS client added: prove URL-derived host/SNI, then pin exact call text and count with provenance."); + }); + + it("pins exact call text, path, count and reviewed host provenance", () => { + const entry = { file: "src/client.js", call: "http2.connect(baseUrl)", count: 2, reason: "URL-parsed authority" }; + const scan = content => [{ path: entry.file, content }]; + assert.deepEqual(evaluateShippedSource(scan("http2.connect(baseUrl); http2.connect(\nbaseUrl,\n);"), [entry]), + { offenders: [], stale: [] }); + for (const content of [ + "http2.connect(baseUrl); http2.connect(baseUrl); tls.connect(options);", + "http2.connect(otherUrl); http2.connect(baseUrl);", + ]) assert.ok(evaluateShippedSource(scan(content), [entry]).offenders.length); + for (const content of [ + "http2.connect(baseUrl);", "", "http2.connect(baseUrl);".repeat(3), + ]) assert.ok(evaluateShippedSource(scan(content), [entry]).stale.length); + assert.ok(evaluateShippedSource([], [entry]).stale.length); + assert.ok(evaluateShippedSource([{ path: "src/other.js", content: "http2.connect(baseUrl);" }], [entry]).offenders.length); + assert.ok(evaluateShippedSource(scan("http2.connect(baseUrl);".repeat(2)), [{ ...entry, reason: "" }]).stale.length); + assert.ok(evaluateShippedSource(scan(""), [{ ...entry, count: 0 }]).stale.length); + }); +}); diff --git a/test/tls-client-detector.mjs b/test/tls-client-detector.mjs new file mode 100644 index 0000000..20a4cb5 --- /dev/null +++ b/test/tls-client-detector.mjs @@ -0,0 +1,136 @@ +const MODULES = new Set(["tls", "https", "http2"]); +const CLIENTS = { + tls: new Set(["connect", "TLSSocket", "checkServerIdentity"]), + https: new Set(["request", "get", "Agent", "globalAgent"]), + http2: new Set(["connect"]), + Bun: new Set(["connect"]), +}; + +export function normalizeCallText(text) { + return text.replace(/\s+/g, " ").replace(/\(\s+/g, "(") + .replace(/,\s*\)/g, ")").replace(/\s+\)/g, ")"); +} + +function literal(token) { + if (!token || !/^["'`]/.test(token.text)) return null; + return token.text.slice(1, -1) + .replace(/\\u\{([0-9a-f]+)\}|\\u([0-9a-f]{4})|\\x([0-9a-f]{2})/gi, + (_, wide, unicode, hex) => String.fromCodePoint(parseInt(wide ?? unicode ?? hex, 16))) + .replace(/\\(["'`\\])/g, "$1"); +} + +function tokens(content) { + const pattern = /\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|[A-Za-z_$][\w$]*|[^\s]/g; + return [...content.matchAll(pattern)] + .filter(match => !match[0].startsWith("//") && !match[0].startsWith("/*")) + .map(match => ({ text: match[0], index: match.index })); +} + +function grantsClient(clause, module) { + if (module === "tls") return true; + if (clause.some(token => token.text === "*")) return true; + const open = clause.findIndex(token => token.text === "{"); + if (open !== 0) return true; // Default import, including default plus named. + let atStart = true; + for (let i = 1; i < clause.length; i += 1) { + const text = clause[i].text; + if (text === ",") atStart = true; + else if (atStart) { + atStart = false; + if (text !== "type" && CLIENTS[module].has(literal(clause[i]) ?? text)) return true; + } + } + return false; +} + +export function findRawTlsClients(content) { + const list = tokens(content); + const hits = []; + const add = (start, end) => { + hits.push({ + line: content.slice(0, list[start].index).split("\n").length, + text: normalizeCallText(content.slice(list[start].index, list[end].index + list[end].text.length)), + }); + }; + const callEnd = open => { + let depth = 0; + for (let i = open; i < list.length; i += 1) { + if (list[i].text === "(") depth += 1; + if (list[i].text === ")" && --depth === 0) return i; + } + return list.length - 1; + }; + for (let i = 0; i < list.length; i += 1) { + const text = list[i].text; + const next = list[i + 1]?.text; + if (Object.hasOwn(CLIENTS, text)) { + let property, open; + if (next === ".") { + property = list[i + 2]?.text; + open = i + 3; + } else if (next === "[" && list[i + 3]?.text === "]") { + property = literal(list[i + 2]); + open = i + 4; + } + if (CLIENTS[text].has(property) && list[open]?.text === "(") { + add(list[i - 1]?.text === "new" ? i - 1 : i, callEnd(open)); + } + } + if (text === "checkServerIdentity" && list[i + 1]?.text === ":") add(i, i); + const builtin = text === "process" && next === "." && + list[i + 2]?.text === "getBuiltinModule" && list[i + 3]?.text === "("; + if ((text === "require" || text === "import") && next === "(" || builtin) { + const open = builtin ? i + 3 : i + 1; + const name = literal(list[open + 1])?.replace(/^node:/, ""); + if (MODULES.has(name)) add(i, callEnd(open)); + continue; + } + if (text !== "import" && text !== "export") continue; + if (next === "type") continue; + const sideEffect = literal(list[i + 1])?.replace(/^node:/, ""); + if (text === "import" && MODULES.has(sideEffect)) { + add(i, i + 1); + continue; + } + for (let j = i + 1; j < list.length; j += 1) { + if ([";", "import", "export"].includes(list[j].text)) break; + if (list[j].text !== "from") continue; + const name = literal(list[j + 1])?.replace(/^node:/, ""); + if (MODULES.has(name) && grantsClient(list.slice(i + 1, j), name)) add(i, j + 1); + break; + } + } + return hits; +} + +export function isScannedSourceFile(file) { + return /\.(?:[cm]?js|jsx|tsx?)$/.test(file) && + !/\.(?:test|spec)\.[^/]+$/.test(file) && !file.endsWith(".d.ts") && + !file.split("/").some(segment => [ + "test", "tests", "__tests__", "fixtures", "__fixtures__", "test-support", "test-fixtures", + "node_modules", ".git", + ].includes(segment)); +} + +export function evaluateShippedSource(scan, allowlist) { + const offenders = []; + const counts = new Map(allowlist.map(entry => [entry, 0])); + for (const item of scan) { + for (const hit of findRawTlsClients(item.content)) { + const entry = allowlist.find(candidate => candidate.file === item.path && candidate.call === hit.text); + if (entry) counts.set(entry, counts.get(entry) + 1); + else offenders.push(`${item.path}:${hit.line} ${hit.text}`); + } + } + const stale = []; + for (const entry of allowlist) { + if (!scan.some(item => item.path === entry.file)) stale.push(`${entry.file}: no longer scanned`); + if (counts.get(entry) !== entry.count) { + stale.push(`${entry.file}: expected ${entry.call} x${entry.count}, found x${counts.get(entry)}`); + } + if (!entry.reason?.trim() || !Number.isInteger(entry.count) || entry.count < 1) { + stale.push(`${entry.file}: requires positive count and reviewed host provenance`); + } + } + return { offenders, stale }; +} From b515dc9d6a6936253f074e2ea724bcbefa9d64e0 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 11 Oct 2026 05:21:46 +0900 Subject: [PATCH 2/6] test: collect TLS guard in both CI runners --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 77622e6..6149068 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ ".": "./src/index.js" }, "scripts": { - "test": "bun test test/branding.test.mjs test/hygiene.test.mjs test/llm.test.mjs test/integration.test.mjs test/interaction.test.mjs test/captcha.test.mjs test/captcha.integration.test.mjs test/chrome.test.mjs test/cookies.test.mjs test/pipe.test.mjs test/cloak-profile.test.mjs test/cloak-profile.integration.test.mjs test/events.test.mjs test/agent-tabs.test.mjs test/events.integration.test.mjs test/bridge-transport.test.mjs test/bridge-host-dir.test.mjs test/skill-docs.test.mjs test/dialogs.test.mjs test/emulate.test.mjs test/frames-snapshot.test.mjs test/frames-snapshot.live-attach.test.mjs test/har.test.mjs test/human-handoff.test.mjs test/index-exports.test.mjs test/layers.test.mjs test/network-snoop.test.mjs test/routes.test.mjs test/routes.live.test.mjs test/scroll-collect.test.mjs test/trace.test.mjs test/trace.live.test.mjs test/bsk-ipc-client.test.mjs test/bsk-session.test.mjs test/bsk-snapshot.test.mjs test/bsk-external-extension.test.mjs test/bsk-browser-choice.test.mjs test/bsk-doctor.test.mjs test/bsk-daemon-start.test.mjs", - "test:node": "node --test test/branding.test.mjs test/hygiene.test.mjs test/llm.test.mjs test/integration.test.mjs test/interaction.test.mjs test/captcha.test.mjs test/captcha.integration.test.mjs test/chrome.test.mjs test/cookies.test.mjs test/pipe.test.mjs test/cloak-profile.test.mjs test/cloak-profile.integration.test.mjs test/events.test.mjs test/agent-tabs.test.mjs test/events.integration.test.mjs test/bridge-transport.test.mjs test/bridge-host-dir.test.mjs test/skill-docs.test.mjs test/dialogs.test.mjs test/emulate.test.mjs test/frames-snapshot.test.mjs test/frames-snapshot.live-attach.test.mjs test/har.test.mjs test/human-handoff.test.mjs test/index-exports.test.mjs test/layers.test.mjs test/network-snoop.test.mjs test/routes.test.mjs test/routes.live.test.mjs test/scroll-collect.test.mjs test/trace.test.mjs test/trace.live.test.mjs test/bsk-ipc-client.test.mjs test/bsk-session.test.mjs test/bsk-snapshot.test.mjs test/bsk-external-extension.test.mjs test/bsk-browser-choice.test.mjs test/bsk-doctor.test.mjs test/bsk-daemon-start.test.mjs", + "test": "bun test test/no-raw-tls-client.test.mjs test/branding.test.mjs test/hygiene.test.mjs test/llm.test.mjs test/integration.test.mjs test/interaction.test.mjs test/captcha.test.mjs test/captcha.integration.test.mjs test/chrome.test.mjs test/cookies.test.mjs test/pipe.test.mjs test/cloak-profile.test.mjs test/cloak-profile.integration.test.mjs test/events.test.mjs test/agent-tabs.test.mjs test/events.integration.test.mjs test/bridge-transport.test.mjs test/bridge-host-dir.test.mjs test/skill-docs.test.mjs test/dialogs.test.mjs test/emulate.test.mjs test/frames-snapshot.test.mjs test/frames-snapshot.live-attach.test.mjs test/har.test.mjs test/human-handoff.test.mjs test/index-exports.test.mjs test/layers.test.mjs test/network-snoop.test.mjs test/routes.test.mjs test/routes.live.test.mjs test/scroll-collect.test.mjs test/trace.test.mjs test/trace.live.test.mjs test/bsk-ipc-client.test.mjs test/bsk-session.test.mjs test/bsk-snapshot.test.mjs test/bsk-external-extension.test.mjs test/bsk-browser-choice.test.mjs test/bsk-doctor.test.mjs test/bsk-daemon-start.test.mjs", + "test:node": "node --test test/no-raw-tls-client.test.mjs test/branding.test.mjs test/hygiene.test.mjs test/llm.test.mjs test/integration.test.mjs test/interaction.test.mjs test/captcha.test.mjs test/captcha.integration.test.mjs test/chrome.test.mjs test/cookies.test.mjs test/pipe.test.mjs test/cloak-profile.test.mjs test/cloak-profile.integration.test.mjs test/events.test.mjs test/agent-tabs.test.mjs test/events.integration.test.mjs test/bridge-transport.test.mjs test/bridge-host-dir.test.mjs test/skill-docs.test.mjs test/dialogs.test.mjs test/emulate.test.mjs test/frames-snapshot.test.mjs test/frames-snapshot.live-attach.test.mjs test/har.test.mjs test/human-handoff.test.mjs test/index-exports.test.mjs test/layers.test.mjs test/network-snoop.test.mjs test/routes.test.mjs test/routes.live.test.mjs test/scroll-collect.test.mjs test/trace.test.mjs test/trace.live.test.mjs test/bsk-ipc-client.test.mjs test/bsk-session.test.mjs test/bsk-snapshot.test.mjs test/bsk-external-extension.test.mjs test/bsk-browser-choice.test.mjs test/bsk-doctor.test.mjs test/bsk-daemon-start.test.mjs", "test:cloak-profile": "node --test test/cloak-profile.test.mjs test/cloak-profile.integration.test.mjs", "test:cloak-profile:bun": "bun test test/cloak-profile.test.mjs test/cloak-profile.integration.test.mjs" }, From 59781e20bdd71f1fb9e72e3d6198d62b8883f5f9 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 11 Oct 2026 09:49:04 +0900 Subject: [PATCH 3/6] fix(test): parse TLS guard source with Acorn AST --- bun.lock | 5 + package.json | 3 + test/no-raw-tls-client.test.mjs | 32 ++++- test/tls-client-detector.mjs | 206 +++++++++++++++++++------------- 4 files changed, 160 insertions(+), 86 deletions(-) diff --git a/bun.lock b/bun.lock index 149492d..fff4aae 100644 --- a/bun.lock +++ b/bun.lock @@ -8,9 +8,14 @@ "ws": "^8.18.3", "zod": "^4.1.5", }, + "devDependencies": { + "acorn": "8.15.0", + }, }, }, "packages": { + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], diff --git a/package.json b/package.json index 6149068..8e7b9fc 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,9 @@ "test:cloak-profile": "node --test test/cloak-profile.test.mjs test/cloak-profile.integration.test.mjs", "test:cloak-profile:bun": "bun test test/cloak-profile.test.mjs test/cloak-profile.integration.test.mjs" }, + "devDependencies": { + "acorn": "8.15.0" + }, "dependencies": { "ws": "^8.18.3", "zod": "^4.1.5" diff --git a/test/no-raw-tls-client.test.mjs b/test/no-raw-tls-client.test.mjs index 03b816d..d0d8985 100644 --- a/test/no-raw-tls-client.test.mjs +++ b/test/no-raw-tls-client.test.mjs @@ -14,8 +14,33 @@ const root = fileURLToPath(new URL("../", import.meta.url)); const ALLOWLIST = []; describe("raw TLS client guard (CVE-2026-48618)", () => { + + it("catches reviewer H1 regex/templates and H2 acquisitions (security regression)", () => { + for (const sample of [ + "const r = /\"/; tls.connect(o); const s = \"x\";", + "const r = /`/; tls.connect(o); const s = `x`;", + "const s = `${a ? `\"` : \"\"}`; tls.connect(o); const z = \"q\";", + "const r = /a\\/*/; tls.connect(o); /* */", + "import { default as h } from \"node:https\"; h.request(o);", + "export { default } from \"node:https\";", + "export { default as h } from \"node:http2\";", + "Bun?.connect({ tls: true });", + "const { connect } = Bun; connect({ tls: true });", + "const c = Bun.connect; c({ tls: true });", + "Bun.connect.call(null, { tls: true });", + "Bun.connect.apply(null, [{ tls: true }]);" +]) assert.ok(findRawTlsClients(sample).length, sample); + }); + it("fails closed and names malformed shipped source", () => { + for (const content of ['const s = "unterminated', 'const s = `unterminated', 'function f() {']) { + assert.throws(() => evaluateShippedSource([{ path: "src/broken.js", content }], []), /src\/broken\.js/); + } + }); it("detects every direct client and module acquisition form", () => { const samples = [ + 'import { createServer } from "node:https";', + 'export { createSecureServer } from "node:http2";', + "tls.createSecureContext(options)", "tls.connect({ host, port: 443 })", "https.request(url, callback)", "https.get(url)", @@ -58,16 +83,11 @@ describe("raw TLS client guard (CVE-2026-48618)", () => { ]); }); - it("ignores comments, literal prose, type-only and inbound neighbors", () => { + it("ignores comments, literal prose and calls to inbound neighbors", () => { for (const sample of [ '// tls.connect(options)\n/* import client from "tls" */', 'const text = "tls.connect(options)";', 'const text = `import client from "tls";`;', - 'import type { Agent } from "node:https";', - 'export type { SecureContext } from "node:tls";', - 'import { createServer } from "node:https";', - 'export { createSecureServer } from "node:http2";', - 'import { createServer, type Agent } from "node:https";', 'import { connect } from "node:tlsx";', 'import { connect } from "./tls";', "fetch(url); new URL(url); tls.createServer(options); Bun.serve(options);", diff --git a/test/tls-client-detector.mjs b/test/tls-client-detector.mjs index 20a4cb5..bdeb12a 100644 --- a/test/tls-client-detector.mjs +++ b/test/tls-client-detector.mjs @@ -1,104 +1,150 @@ -const MODULES = new Set(["tls", "https", "http2"]); +import { parse } from "acorn"; + const CLIENTS = { - tls: new Set(["connect", "TLSSocket", "checkServerIdentity"]), + tls: new Set(["connect", "createConnection", "TLSSocket", "createSecureContext", "checkServerIdentity"]), https: new Set(["request", "get", "Agent", "globalAgent"]), http2: new Set(["connect"]), Bun: new Set(["connect"]), }; -export function normalizeCallText(text) { - return text.replace(/\s+/g, " ").replace(/\(\s+/g, "(") - .replace(/,\s*\)/g, ")").replace(/\s+\)/g, ")"); +function nodes(tree) { + const result = []; + const visit = node => { + if (!node || typeof node.type !== "string") return; + result.push(node); + for (const value of Object.values(node)) { + if (Array.isArray(value)) value.forEach(visit); + else if (value && typeof value === "object") visit(value); + } + }; + visit(tree); + return result; +} + +function literal(node) { + if (node?.type === "Literal" && typeof node.value === "string") return node.value; + if (node?.type === "TemplateLiteral") { + let value = node.quasis[0].value.cooked; + for (let i = 0; i < node.expressions.length; i++) { + const part = literal(node.expressions[i]); + if (part === undefined) return undefined; + value += part + node.quasis[i + 1].value.cooked; + } + return value; + } + if (node?.type === "BinaryExpression" && node.operator === "+") { + const left = literal(node.left), right = literal(node.right); + if (left !== undefined && right !== undefined) return left + right; + } } -function literal(token) { - if (!token || !/^["'`]/.test(token.text)) return null; - return token.text.slice(1, -1) - .replace(/\\u\{([0-9a-f]+)\}|\\u([0-9a-f]{4})|\\x([0-9a-f]{2})/gi, - (_, wide, unicode, hex) => String.fromCodePoint(parseInt(wide ?? unicode ?? hex, 16))) - .replace(/\\(["'`\\])/g, "$1"); +function unwrap(node) { + return node?.type === "ChainExpression" ? unwrap(node.expression) : node; } -function tokens(content) { - const pattern = /\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|[A-Za-z_$][\w$]*|[^\s]/g; - return [...content.matchAll(pattern)] - .filter(match => !match[0].startsWith("//") && !match[0].startsWith("/*")) - .map(match => ({ text: match[0], index: match.index })); +function member(node) { + node = unwrap(node); + if (node?.type !== "MemberExpression") return undefined; + return node.computed ? literal(node.property) : node.property.name; } -function grantsClient(clause, module) { - if (module === "tls") return true; - if (clause.some(token => token.text === "*")) return true; - const open = clause.findIndex(token => token.text === "{"); - if (open !== 0) return true; // Default import, including default plus named. - let atStart = true; - for (let i = 1; i < clause.length; i += 1) { - const text = clause[i].text; - if (text === ",") atStart = true; - else if (atStart) { - atStart = false; - if (text !== "type" && CLIENTS[module].has(literal(clause[i]) ?? text)) return true; - } +function normalizeNode(node, content) { + const spans = nodes(node).filter(n => n.type === "Literal" || n.type === "TemplateElement") + .sort((a, b) => a.start - b.start); + const plain = text => text.replace(/\s+/g, " ").replace(/\(\s+/g, "(") + .replace(/,\s*\)/g, ")").replace(/\s+\)/g, ")"); + let end = node.start, text = ""; + for (const span of spans) { + if (span.start < end) continue; + text += plain(content.slice(end, span.start)) + content.slice(span.start, span.end); + end = span.end; } - return false; + return (text + plain(content.slice(end, node.end))).trim().replace(/;$/, ""); } -export function findRawTlsClients(content) { - const list = tokens(content); - const hits = []; - const add = (start, end) => { - hits.push({ - line: content.slice(0, list[start].index).split("\n").length, - text: normalizeCallText(content.slice(list[start].index, list[end].index + list[end].text.length)), - }); - }; - const callEnd = open => { - let depth = 0; - for (let i = open; i < list.length; i += 1) { - if (list[i].text === "(") depth += 1; - if (list[i].text === ")" && --depth === 0) return i; +export function findRawTlsClients(content, file = "source.js") { + let tree; + try { + tree = parse(content, { ecmaVersion: "latest", sourceType: "module", locations: true, + allowAwaitOutsideFunction: true, allowReturnOutsideFunction: true }); + } catch (error) { + throw new SyntaxError(file + ": " + error.message, { cause: error }); + } + const list = nodes(tree); + const bindings = new Map(Object.keys(CLIENTS).map(name => [name, name])); + const requireNames = new Set(["require"]); + const builtinNames = new Set(); + const kind = expression => { + const node = unwrap(expression); + if (node?.type === "Identifier") return bindings.get(node.name); + if (node?.type === "AwaitExpression") return kind(node.argument); + if (node?.type === "ImportExpression") return literal(node.source)?.replace(/^node:/, ""); + if (node?.type === "CallExpression") { + const callee = unwrap(node.callee); + const name = member(callee) ?? callee.name; + if (requireNames.has(name) || name === "getBuiltinModule" || builtinNames.has(name)) { + return literal(node.arguments[0])?.replace(/^node:/, ""); + } + } + if (node?.type === "MemberExpression") { + if (node.object.name === "globalThis") return bindings.get(member(node)); + const base = kind(node.object), property = member(node); + if (CLIENTS[base]?.has(property)) return base + "#" + property; } - return list.length - 1; }; - for (let i = 0; i < list.length; i += 1) { - const text = list[i].text; - const next = list[i + 1]?.text; - if (Object.hasOwn(CLIENTS, text)) { - let property, open; - if (next === ".") { - property = list[i + 2]?.text; - open = i + 3; - } else if (next === "[" && list[i + 3]?.text === "]") { - property = literal(list[i + 2]); - open = i + 4; + for (const node of list) { + if (node.type !== "ImportDeclaration") continue; + const specifier = literal(node.source), module = specifier?.replace(/^node:/, ""); + for (const spec of node.specifiers) { + const imported = spec.imported?.name ?? spec.imported?.value; + if (module === "module" && imported === "createRequire") requireNames.add(spec.local.name); + if (CLIENTS[module]) bindings.set(spec.local.name, + imported && imported !== "default" ? module + "#" + imported : module); + } + } + let changed; + do { + changed = false; + const bind = (name, value) => { + if (value && !bindings.has(name)) { bindings.set(name, value); changed = true; } + }; + for (const node of list) { + if (node.type !== "VariableDeclarator" || !node.init) continue; + if (node.init.type === "CallExpression" && requireNames.has(node.init.callee.name)) { + requireNames.add(node.id.name); } - if (CLIENTS[text].has(property) && list[open]?.text === "(") { - add(list[i - 1]?.text === "new" ? i - 1 : i, callEnd(open)); + const value = kind(node.init); + if (node.id.type === "Identifier") bind(node.id.name, value); + if (node.id.type === "ObjectPattern") for (const prop of node.id.properties) { + const name = prop.key?.name ?? literal(prop.key); + if (value === "process" && name === "getBuiltinModule") builtinNames.add(prop.value.name); + if (CLIENTS[value]?.has(name)) bind(prop.value.name, value + "#" + name); } } - if (text === "checkServerIdentity" && list[i + 1]?.text === ":") add(i, i); - const builtin = text === "process" && next === "." && - list[i + 2]?.text === "getBuiltinModule" && list[i + 3]?.text === "("; - if ((text === "require" || text === "import") && next === "(" || builtin) { - const open = builtin ? i + 3 : i + 1; - const name = literal(list[open + 1])?.replace(/^node:/, ""); - if (MODULES.has(name)) add(i, callEnd(open)); - continue; - } - if (text !== "import" && text !== "export") continue; - if (next === "type") continue; - const sideEffect = literal(list[i + 1])?.replace(/^node:/, ""); - if (text === "import" && MODULES.has(sideEffect)) { - add(i, i + 1); - continue; + } while (changed); + const hits = [], seen = new Set(); + const add = node => { + if (seen.has(node.start)) return; + seen.add(node.start); + hits.push({ line: node.loc.start.line, text: normalizeNode(node, content) }); + }; + for (const node of list) { + if (["ImportDeclaration", "ExportNamedDeclaration", "ExportAllDeclaration"].includes(node.type) + && CLIENTS[literal(node.source)?.replace(/^node:/, "")]) add(node); + if (node.type === "ImportExpression" && CLIENTS[kind(node)]) add(node); + if (node.type === "CallExpression" && CLIENTS[kind(node)]) add(node); + if (["CallExpression", "NewExpression"].includes(node.type)) { + const value = kind(node.callee); + const [base, property] = value?.split("#") ?? []; + if (CLIENTS[base]?.has(property)) add(node); } - for (let j = i + 1; j < list.length; j += 1) { - if ([";", "import", "export"].includes(list[j].text)) break; - if (list[j].text !== "from") continue; - const name = literal(list[j + 1])?.replace(/^node:/, ""); - if (MODULES.has(name) && grantsClient(list.slice(i + 1, j), name)) add(i, j + 1); - break; + if (node.type === "MemberExpression" && kind(node) === "Bun#connect") { + if (node.parent?.type !== "CallExpression") add(node); } + if (node.type === "VariableDeclarator" && node.id.type === "ObjectPattern" && kind(node.init) === "Bun" + && node.id.properties.some(p => (p.key?.name ?? literal(p.key)) === "connect")) add(node); + if (node.type === "Property" && (node.key.name ?? literal(node.key)) === "checkServerIdentity") add(node); + if (node.type === "AssignmentExpression" && member(node.left) === "checkServerIdentity") add(node); } return hits; } @@ -116,7 +162,7 @@ export function evaluateShippedSource(scan, allowlist) { const offenders = []; const counts = new Map(allowlist.map(entry => [entry, 0])); for (const item of scan) { - for (const hit of findRawTlsClients(item.content)) { + for (const hit of findRawTlsClients(item.content, item.path)) { const entry = allowlist.find(candidate => candidate.file === item.path && candidate.call === hit.text); if (entry) counts.set(entry, counts.get(entry) + 1); else offenders.push(`${item.path}:${hit.line} ${hit.text}`); From 71abc9c71f89c011e0dd20ed3d5c365282979899 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 11 Oct 2026 10:02:32 +0900 Subject: [PATCH 4/6] fix(test): cover TLS factory bindings and exact member counts --- test/no-raw-tls-client.test.mjs | 40 ++++++++++++++++++++++++++++++++- test/tls-client-detector.mjs | 21 +++++++++++------ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/test/no-raw-tls-client.test.mjs b/test/no-raw-tls-client.test.mjs index d0d8985..ed49663 100644 --- a/test/no-raw-tls-client.test.mjs +++ b/test/no-raw-tls-client.test.mjs @@ -15,11 +15,46 @@ const ALLOWLIST = []; describe("raw TLS client guard (CVE-2026-48618)", () => { + it("detects calls independently of their module acquisition pins", () => { + for (const [sample, call] of [ + [ + "import { default as client } from \"node:https\"; client.request(options);", + "client.request(options)" + ], + [ + "import * as client from \"node:tls\"; client.connect(options);", + "client.connect(options)" + ], + [ + "const client = require(\"node:https\"); client.get(url);", + "client.get(url)" + ], + [ + "const { connect: dial } = await import(`node:tls`); dial(options);", + "dial(options)" + ], + [ + "import client from \"https\"; const request = client.request; request(options);", + "request(options)" + ], + [ + "const { default: client } = await import(\"node:https\"); client.get(url);", + "client.get(url)" + ] +]) { + assert.ok(findRawTlsClients(sample).some(hit => hit.text === call), sample); + } + }); + it("does not interpret regex or literal text as calls", () => { + for (const sample of ['const s = "tls.connect(o)";', 'const s = `tls.connect(o)`;', 'const r = /tls.connect\\(o\\)/;']) + assert.deepEqual(findRawTlsClients(sample), []); + }); + it("catches reviewer H1 regex/templates and H2 acquisitions (security regression)", () => { for (const sample of [ "const r = /\"/; tls.connect(o); const s = \"x\";", "const r = /`/; tls.connect(o); const s = `x`;", - "const s = `${a ? `\"` : \"\"}`; tls.connect(o); const z = \"q\";", +`const s = \`\${a ? \`"\` : ""}\`; tls.connect(o); const z = "q";`, "const r = /a\\/*/; tls.connect(o); /* */", "import { default as h } from \"node:https\"; h.request(o);", "export { default } from \"node:https\";", @@ -81,6 +116,9 @@ describe("raw TLS client guard (CVE-2026-48618)", () => { assert.deepEqual(findRawTlsClients('\n\ntls.connect({ host: "x)" });'), [ { line: 3, text: 'tls.connect({ host: "x)" })' }, ]); + assert.deepEqual(findRawTlsClients("Bun.connect(options)"), [ + { line: 1, text: "Bun.connect(options)" }, + ]); }); it("ignores comments, literal prose and calls to inbound neighbors", () => { diff --git a/test/tls-client-detector.mjs b/test/tls-client-detector.mjs index bdeb12a..3903717 100644 --- a/test/tls-client-detector.mjs +++ b/test/tls-client-detector.mjs @@ -72,7 +72,8 @@ export function findRawTlsClients(content, file = "source.js") { } const list = nodes(tree); const bindings = new Map(Object.keys(CLIENTS).map(name => [name, name])); - const requireNames = new Set(["require"]); + const requireNames = new Set(["require", "__require"]); + const requireFactories = new Set(); const builtinNames = new Set(); const kind = expression => { const node = unwrap(expression); @@ -82,14 +83,17 @@ export function findRawTlsClients(content, file = "source.js") { if (node?.type === "CallExpression") { const callee = unwrap(node.callee); const name = member(callee) ?? callee.name; - if (requireNames.has(name) || name === "getBuiltinModule" || builtinNames.has(name)) { - return literal(node.arguments[0])?.replace(/^node:/, ""); + const factory = callee.type === "CallExpression" && requireFactories.has(callee.callee.name); + const call = member(callee) === "call" && requireNames.has(callee.object?.name); + if (requireNames.has(name) || name === "getBuiltinModule" || builtinNames.has(name) || factory || call) { + return literal(node.arguments[call ? 1 : 0])?.replace(/^node:/, ""); } } if (node?.type === "MemberExpression") { if (node.object.name === "globalThis") return bindings.get(member(node)); const base = kind(node.object), property = member(node); if (CLIENTS[base]?.has(property)) return base + "#" + property; + if (base?.includes("#") && (property === "call" || property === "apply")) return base; } }; for (const node of list) { @@ -97,7 +101,7 @@ export function findRawTlsClients(content, file = "source.js") { const specifier = literal(node.source), module = specifier?.replace(/^node:/, ""); for (const spec of node.specifiers) { const imported = spec.imported?.name ?? spec.imported?.value; - if (module === "module" && imported === "createRequire") requireNames.add(spec.local.name); + if (module === "module" && imported === "createRequire") requireFactories.add(spec.local.name); if (CLIENTS[module]) bindings.set(spec.local.name, imported && imported !== "default" ? module + "#" + imported : module); } @@ -110,7 +114,7 @@ export function findRawTlsClients(content, file = "source.js") { }; for (const node of list) { if (node.type !== "VariableDeclarator" || !node.init) continue; - if (node.init.type === "CallExpression" && requireNames.has(node.init.callee.name)) { + if (node.init.type === "CallExpression" && requireFactories.has(node.init.callee.name)) { requireNames.add(node.id.name); } const value = kind(node.init); @@ -118,11 +122,14 @@ export function findRawTlsClients(content, file = "source.js") { if (node.id.type === "ObjectPattern") for (const prop of node.id.properties) { const name = prop.key?.name ?? literal(prop.key); if (value === "process" && name === "getBuiltinModule") builtinNames.add(prop.value.name); - if (CLIENTS[value]?.has(name)) bind(prop.value.name, value + "#" + name); + if (name === "default" && CLIENTS[value]) bind(prop.value.name, value); + else if (CLIENTS[value]?.has(name)) bind(prop.value.name, value + "#" + name); } } } while (changed); const hits = [], seen = new Set(); + const calledMembers = new Set(list.filter(n => ["CallExpression", "NewExpression"].includes(n.type)) + .map(n => unwrap(n.callee))); const add = node => { if (seen.has(node.start)) return; seen.add(node.start); @@ -139,7 +146,7 @@ export function findRawTlsClients(content, file = "source.js") { if (CLIENTS[base]?.has(property)) add(node); } if (node.type === "MemberExpression" && kind(node) === "Bun#connect") { - if (node.parent?.type !== "CallExpression") add(node); + if (!calledMembers.has(node)) add(node); } if (node.type === "VariableDeclarator" && node.id.type === "ObjectPattern" && kind(node.init) === "Bun" && node.id.properties.some(p => (p.key?.name ?? literal(p.key)) === "connect")) add(node); From a5cba3bb212ed3626db671a4b6511358261531ac Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 11 Oct 2026 10:09:37 +0900 Subject: [PATCH 5/6] fix(test): reject Bun destructuring assignment --- test/no-raw-tls-client.test.mjs | 4 ++++ test/tls-client-detector.mjs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/test/no-raw-tls-client.test.mjs b/test/no-raw-tls-client.test.mjs index ed49663..b3e24cb 100644 --- a/test/no-raw-tls-client.test.mjs +++ b/test/no-raw-tls-client.test.mjs @@ -15,6 +15,10 @@ const ALLOWLIST = []; describe("raw TLS client guard (CVE-2026-48618)", () => { + it("detects Bun destructuring assignment", () => { + assert.ok(findRawTlsClients("let connect; ({ connect } = Bun); connect({ tls: true });").length); + }); + it("detects calls independently of their module acquisition pins", () => { for (const [sample, call] of [ [ diff --git a/test/tls-client-detector.mjs b/test/tls-client-detector.mjs index 3903717..6282344 100644 --- a/test/tls-client-detector.mjs +++ b/test/tls-client-detector.mjs @@ -136,6 +136,8 @@ export function findRawTlsClients(content, file = "source.js") { hits.push({ line: node.loc.start.line, text: normalizeNode(node, content) }); }; for (const node of list) { + if (node.type === "AssignmentExpression" && node.left.type === "ObjectPattern" && kind(node.right) === "Bun" + && node.left.properties.some(p => (p.key?.name ?? literal(p.key)) === "connect")) add(node); if (["ImportDeclaration", "ExportNamedDeclaration", "ExportAllDeclaration"].includes(node.type) && CLIENTS[literal(node.source)?.replace(/^node:/, "")]) add(node); if (node.type === "ImportExpression" && CLIENTS[kind(node)]) add(node); From 8cf443f26b4ff3b411b04a5f5483861b29efc856 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 11 Oct 2026 10:59:46 +0900 Subject: [PATCH 6/6] fix(test): track global Bun access and canonicalize guard pins --- test/no-raw-tls-client.test.mjs | 44 +++++++++++++++++++++++++++++++-- test/tls-client-detector.mjs | 16 +++++++----- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/test/no-raw-tls-client.test.mjs b/test/no-raw-tls-client.test.mjs index b3e24cb..0562cac 100644 --- a/test/no-raw-tls-client.test.mjs +++ b/test/no-raw-tls-client.test.mjs @@ -11,9 +11,49 @@ const root = fileURLToPath(new URL("../", import.meta.url)); // OmOWright supplies headers only. See senpi#3078 and oh-my-openagent#9892. // Entries must pin { file, call: exact normalized text, count, reason }. // Empty today: dependency bundles are reviewed separately, not file-exempted. +// Known limits: createRequire through namespace/default module imports or +// require("node:module") chains; require.apply, require aliases/sequence calls; +// detached getBuiltinModule aliases/.call; variable Bun keys; globalThis Bun +// destructuring; variable/const-folded module specifiers; Reflect.get(Bun, "connect"). +// This is a regression guard, not a complete data-flow or evasion detector. const ALLOWLIST = []; describe("raw TLS client guard (CVE-2026-48618)", () => { + // Security regression: in-tree runtime access must expose the client, not just its global lookup. + for (const { name, sample } of [ + { + "name": "global alias", + "sample": "const target = globalThis; target.Bun?.connect(o);" + }, + { + "name": "Reflect direct", + "sample": "Reflect.get(globalThis, \"Bun\")?.connect(o);" + }, + { + "name": "Reflect alias", + "sample": "const target = globalThis; Reflect.get(target, \"Bun\").connect(o);" + }, + { + "name": "process destructure", + "sample": "const { getBuiltinModule: g } = process; g(\"node:tls\");" + } +]) { + it("detects the AST-round " + name + " client", () => { + assert.ok(findRawTlsClients(sample).some(hit => name === "process destructure" ? hit.text === 'g("node:tls")' : hit.text.endsWith("connect(o)")), sample); + }); + } + it("keeps non-client Bun lookups and unrelated Reflect receivers clean", () => { + for (const sample of ['const target = globalThis; const bun = Reflect.get(target, "Bun"); bun?.hash(value);', 'const local = {}; Reflect.get(local, "Bun").connect(o);']) + assert.deepEqual(findRawTlsClients(sample), []); + }); + it("preserves object trailing-comma reflows without changing literals or counts", () => { + const entry = { file: "src/client.js", call: 'tls.connect({ host: u.hostname, options: { servername: u.hostname, label: "comma, }" } })', count: 2, reason: "URL-parsed host" }; + const content = entry.call + ';\n tls.connect({ host: u.hostname, options: { servername: u.hostname, label: "comma, }", }, },);'; + const scan = [{ path: entry.file, content }]; + assert.deepEqual(evaluateShippedSource(scan, [entry]), { offenders: [], stale: [] }); + assert.ok(evaluateShippedSource([{ path: entry.file, content: content + entry.call }], [entry]).stale.length); + assert.ok(evaluateShippedSource([{ path: entry.file, content: content.replace('comma, }', 'comma,}') }], [entry]).offenders.length); + }); it("detects Bun destructuring assignment", () => { assert.ok(findRawTlsClients("let connect; ({ connect } = Bun); connect({ tls: true });").length); @@ -75,7 +115,7 @@ describe("raw TLS client guard (CVE-2026-48618)", () => { assert.throws(() => evaluateShippedSource([{ path: "src/broken.js", content }], []), /src\/broken\.js/); } }); - it("detects every direct client and module acquisition form", () => { + it("detects sampled direct clients and module acquisitions", () => { const samples = [ 'import { createServer } from "node:https";', 'export { createSecureServer } from "node:http2";', @@ -118,7 +158,7 @@ describe("raw TLS client guard (CVE-2026-48618)", () => { assert.ok(findRawTlsClients(sample).length > 0, `missed: ${sample}`); } assert.deepEqual(findRawTlsClients('\n\ntls.connect({ host: "x)" });'), [ - { line: 3, text: 'tls.connect({ host: "x)" })' }, + { line: 3, text: 'tls.connect({ host: "x)"})' }, ]); assert.deepEqual(findRawTlsClients("Bun.connect(options)"), [ { line: 1, text: "Bun.connect(options)" }, diff --git a/test/tls-client-detector.mjs b/test/tls-client-detector.mjs index 6282344..ed2e6fc 100644 --- a/test/tls-client-detector.mjs +++ b/test/tls-client-detector.mjs @@ -52,7 +52,7 @@ function normalizeNode(node, content) { const spans = nodes(node).filter(n => n.type === "Literal" || n.type === "TemplateElement") .sort((a, b) => a.start - b.start); const plain = text => text.replace(/\s+/g, " ").replace(/\(\s+/g, "(") - .replace(/,\s*\)/g, ")").replace(/\s+\)/g, ")"); + .replace(/,\s*([)}])/g, "$1").replace(/\s+([)}])/g, "$1"); let end = node.start, text = ""; for (const span of spans) { if (span.start < end) continue; @@ -71,7 +71,7 @@ export function findRawTlsClients(content, file = "source.js") { throw new SyntaxError(file + ": " + error.message, { cause: error }); } const list = nodes(tree); - const bindings = new Map(Object.keys(CLIENTS).map(name => [name, name])); + const bindings = new Map([...Object.keys(CLIENTS), "globalThis"].map(name => [name, name])); const requireNames = new Set(["require", "__require"]); const requireFactories = new Set(); const builtinNames = new Set(); @@ -83,6 +83,8 @@ export function findRawTlsClients(content, file = "source.js") { if (node?.type === "CallExpression") { const callee = unwrap(node.callee); const name = member(callee) ?? callee.name; + if (name === "get" && unwrap(callee.object)?.name === "Reflect" + && kind(node.arguments[0]) === "globalThis" && literal(node.arguments[1]) === "Bun") return "Bun"; const factory = callee.type === "CallExpression" && requireFactories.has(callee.callee.name); const call = member(callee) === "call" && requireNames.has(callee.object?.name); if (requireNames.has(name) || name === "getBuiltinModule" || builtinNames.has(name) || factory || call) { @@ -90,8 +92,8 @@ export function findRawTlsClients(content, file = "source.js") { } } if (node?.type === "MemberExpression") { - if (node.object.name === "globalThis") return bindings.get(member(node)); const base = kind(node.object), property = member(node); + if (base === "globalThis") return bindings.get(property); if (CLIENTS[base]?.has(property)) return base + "#" + property; if (base?.includes("#") && (property === "call" || property === "apply")) return base; } @@ -121,7 +123,7 @@ export function findRawTlsClients(content, file = "source.js") { if (node.id.type === "Identifier") bind(node.id.name, value); if (node.id.type === "ObjectPattern") for (const prop of node.id.properties) { const name = prop.key?.name ?? literal(prop.key); - if (value === "process" && name === "getBuiltinModule") builtinNames.add(prop.value.name); + if (node.init.type === "Identifier" && node.init.name === "process" && name === "getBuiltinModule") builtinNames.add(prop.value.name); if (name === "default" && CLIENTS[value]) bind(prop.value.name, value); else if (CLIENTS[value]?.has(name)) bind(prop.value.name, value + "#" + name); } @@ -141,7 +143,7 @@ export function findRawTlsClients(content, file = "source.js") { if (["ImportDeclaration", "ExportNamedDeclaration", "ExportAllDeclaration"].includes(node.type) && CLIENTS[literal(node.source)?.replace(/^node:/, "")]) add(node); if (node.type === "ImportExpression" && CLIENTS[kind(node)]) add(node); - if (node.type === "CallExpression" && CLIENTS[kind(node)]) add(node); + if (node.type === "CallExpression" && CLIENTS[kind(node)] && kind(node) !== "Bun") add(node); if (["CallExpression", "NewExpression"].includes(node.type)) { const value = kind(node.callee); const [base, property] = value?.split("#") ?? []; @@ -170,9 +172,11 @@ export function isScannedSourceFile(file) { export function evaluateShippedSource(scan, allowlist) { const offenders = []; const counts = new Map(allowlist.map(entry => [entry, 0])); + const calls = new Map(allowlist.map(entry => [entry, + normalizeNode(parse(entry.call, { ecmaVersion: "latest", sourceType: "module" }), entry.call)])); for (const item of scan) { for (const hit of findRawTlsClients(item.content, item.path)) { - const entry = allowlist.find(candidate => candidate.file === item.path && candidate.call === hit.text); + const entry = allowlist.find(candidate => candidate.file === item.path && calls.get(candidate) === hit.text); if (entry) counts.set(entry, counts.get(entry) + 1); else offenders.push(`${item.path}:${hit.line} ${hit.text}`); }