From 30657b415a53bfd7d53aca4534552dfc6683eff4 Mon Sep 17 00:00:00 2001 From: Grantfox Agent <160465301+emirykl@users.noreply.github.com> Date: Mon, 31 Aug 2026 03:40:00 +0300 Subject: [PATCH] fix(resolver): redact RPC credentials from readiness output --- resolver/src/commands/readiness.ts | 36 ++++++++++------ resolver/test/readiness.test.ts | 68 ++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/resolver/src/commands/readiness.ts b/resolver/src/commands/readiness.ts index 35b2174..ede22ae 100644 --- a/resolver/src/commands/readiness.ts +++ b/resolver/src/commands/readiness.ts @@ -50,6 +50,21 @@ const SOROBAN_CONTRACT_RE = /^C[A-Z2-7]{55}$/; const RPC_TIMEOUT_MS = 4000; +function redactRpcUrl(rawUrl: string): string { + try { + const parsed = new URL(rawUrl); + return `${parsed.protocol}//${parsed.host}`; + } catch { + return "[REDACTED RPC URL]"; + } +} + +function rpcErrorDetail(error: unknown): string { + const err = error as { shortMessage?: string; message?: string }; + const message = err?.shortMessage ?? err?.message ?? String(error); + return message.replace(/\b[a-z][a-z\d+.-]*:\/\/[^\s]+/gi, (url) => redactRpcUrl(url)); +} + function checkNetwork(): { network: "testnet" | "mainnet"; check: ReadinessCheck } { const raw = process.env.NETWORK_MODE ?? "testnet"; if (raw === "testnet" || raw === "mainnet") { @@ -71,11 +86,11 @@ function checkNetwork(): { network: "testnet" | "mainnet"; check: ReadinessCheck async function pingEvmRpc(network: "testnet" | "mainnet"): Promise<{ ok: boolean; - url: string; chainId: number | null; detail: string; }> { const url = resolveEthereumRpcUrl(network); + const displayUrl = redactRpcUrl(url); const chain = network === "mainnet" ? mainnet : sepolia; const expectedChainId = network === "mainnet" ? 1 : 11_155_111; try { @@ -87,35 +102,32 @@ async function pingEvmRpc(network: "testnet" | "mainnet"): Promise<{ if (cid === expectedChainId) { return { ok: true, - url, chainId: cid, - detail: `URL=${url} chainId=${cid}` + detail: `URL=${displayUrl} chainId=${cid}` }; } return { ok: false, - url, chainId: cid, - detail: `URL=${url} — RPC reported chainId=${cid}, expected ${expectedChainId}` + detail: `URL=${displayUrl} — RPC reported chainId=${cid}, expected ${expectedChainId}` }; } catch (err: any) { return { ok: false, - url, chainId: null, - detail: `URL=${url} — error: ${err?.shortMessage ?? err?.message ?? String(err)}` + detail: `URL=${displayUrl} — error: ${rpcErrorDetail(err)}` }; } } async function pingSorobanRpc(network: "testnet" | "mainnet"): Promise<{ ok: boolean; - url: string; detail: string; }> { const url = process.env.SOROBAN_RPC_URL?.trim() || (network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org"); + const displayUrl = redactRpcUrl(url); try { const server = new rpc.Server(url, { allowHttp: url.startsWith("http://"), @@ -125,14 +137,12 @@ async function pingSorobanRpc(network: "testnet" | "mainnet"): Promise<{ const seq = latest?.sequence; return { ok: seq !== undefined && seq !== null, - url, - detail: `URL=${url} latestLedger=${seq}` + detail: `URL=${displayUrl} latestLedger=${seq}` }; } catch (err: any) { return { ok: false, - url, - detail: `URL=${url} — error: ${err?.message ?? String(err)}` + detail: `URL=${displayUrl} — error: ${rpcErrorDetail(err)}` }; } } @@ -263,7 +273,7 @@ export async function assessReadiness(): Promise { // ===== Informational / dry-run assertion ===== const rawCoordUrl = process.env.COORDINATOR_URL?.trim(); - const coordUrlDisplay = rawCoordUrl || "(default http://localhost:3001)"; + const coordUrlDisplay = rawCoordUrl ? redactRpcUrl(rawCoordUrl) : "(default http://localhost:3001)"; checks.push({ id: "coordinator-url", label: "COORDINATOR_URL", diff --git a/resolver/test/readiness.test.ts b/resolver/test/readiness.test.ts index a9924d0..2337b32 100644 --- a/resolver/test/readiness.test.ts +++ b/resolver/test/readiness.test.ts @@ -140,6 +140,29 @@ describe("assessReadiness", () => { expect(findCheck(result, "soroban-rpc").status).toBe("ok"); }); + it("redacts credentials from successful RPC diagnostics while preserving hosts and network data", async () => { + const evmSecrets = ["evm-user", "evm-password", "evm-path-key", "evm-query-key"]; + const sorobanSecrets = ["soroban-user", "soroban-password", "soroban-path-key", "soroban-query-key"]; + setEnv({ + ...FULL_ENV, + SEPOLIA_RPC_URL: + "https://evm-user:evm-password@eth.example.test/v3/evm-path-key?apiKey=evm-query-key&network=sepolia", + SOROBAN_RPC_URL: + "https://soroban-user:soroban-password@soroban.example.test/rpc/soroban-path-key?token=soroban-query-key&network=testnet" + }); + + const result = await assessReadiness(); + const evmDetail = findCheck(result, "evm-rpc").detail; + const sorobanDetail = findCheck(result, "soroban-rpc").detail; + const diagnostics = `${evmDetail}\n${sorobanDetail}`; + + expect(evmDetail).toBe("URL=https://eth.example.test chainId=11155111"); + expect(sorobanDetail).toBe("URL=https://soroban.example.test latestLedger=12345"); + for (const secret of [...evmSecrets, ...sorobanSecrets]) { + expect(diagnostics).not.toContain(secret); + } + }); + it("fails network-mode check when NETWORK_MODE is invalid", async () => { setEnv({ ...FULL_ENV, NETWORK_MODE: "stagenet" }); const result = await assessReadiness(); @@ -277,6 +300,20 @@ describe("assessReadiness", () => { expect(findCheck(result, "coordinator-url").detail).toBe("https://coord.example.test"); }); + it("redacts credentials from the coordinator URL diagnostic", async () => { + setEnv({ + ...FULL_ENV, + COORDINATOR_URL: "https://coord-user:coord-password@coord.example.test/api?token=coord-query-key" + }); + const result = await assessReadiness(); + const detail = findCheck(result, "coordinator-url").detail; + + expect(detail).toBe("https://coord.example.test"); + expect(detail).not.toContain("coord-user"); + expect(detail).not.toContain("coord-password"); + expect(detail).not.toContain("coord-query-key"); + }); + it("always emits the dry-run assertion check", async () => { setEnv(FULL_ENV); const result = await assessReadiness(); @@ -363,4 +400,35 @@ describe("readinessCommand", () => { expect(output).not.toContain(TEST_ENV_KEY); expect(output).not.toContain(TEST_ENV_STELLAR_SECRET); }); + + it("redacts credential-bearing RPC URLs echoed by connection errors", async () => { + const evmUrl = + "https://error-user:error-password@eth-error.example.test/v3/error-path-key?access_token=error-query-key"; + const sorobanUrl = + "https://stellar-user:stellar-password@soroban-error.example.test/rpc/stellar-path-key?auth=stellar-query-key"; + setEnv({ ...FULL_ENV, SEPOLIA_RPC_URL: evmUrl, SOROBAN_RPC_URL: sorobanUrl }); + mockGetChainId.mockRejectedValue(new Error(`Request failed for ${evmUrl}`)); + mockGetLatestLedger.mockRejectedValue(new Error(`Request failed for ${sorobanUrl}`)); + + const log = captureLog(); + const code = await readinessCommand(); + log.restore(); + const output = log.calls.map((args) => args.map(String).join(" ")).join("\n"); + + expect(code).toBe(1); + expect(output).toContain("https://eth-error.example.test"); + expect(output).toContain("https://soroban-error.example.test"); + for (const secret of [ + "error-user", + "error-password", + "error-path-key", + "error-query-key", + "stellar-user", + "stellar-password", + "stellar-path-key", + "stellar-query-key" + ]) { + expect(output).not.toContain(secret); + } + }); });