From f1588cde0c45f2e7466fe27cf8b4bdeb1d68e91e Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 29 Jan 2026 12:38:04 +0000 Subject: [PATCH] Add `StartProxyError` for status-report-safe errors, and use for proxy download --- lib/start-proxy-action.js | 44 +++++++++++++++++-------- src/start-proxy-action.ts | 10 ++---- src/start-proxy.test.ts | 45 ++++++++++++++++++++++++++ src/start-proxy.ts | 67 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 144 insertions(+), 22 deletions(-) diff --git a/lib/start-proxy-action.js b/lib/start-proxy-action.js index 7faaa97bb..ac344d501 100644 --- a/lib/start-proxy-action.js +++ b/lib/start-proxy-action.js @@ -115932,7 +115932,7 @@ var require_cache5 = __commonJS({ var import_child_process = require("child_process"); var path = __toESM(require("path")); var core11 = __toESM(require_core()); -var toolcache = __toESM(require_tool_cache()); +var toolcache2 = __toESM(require_tool_cache()); var import_node_forge = __toESM(require_lib2()); // src/actions-util.ts @@ -118929,6 +118929,7 @@ function getActionsLogger() { // src/start-proxy.ts var core10 = __toESM(require_core()); +var toolcache = __toESM(require_tool_cache()); // src/artifact-scanner.ts var exec = __toESM(require_exec()); @@ -119573,6 +119574,11 @@ async function sendUnhandledErrorStatusReport(actionName, actionStartedAt, error } // src/start-proxy.ts +var StartProxyError = class extends Error { + constructor(errorType) { + super(errorType); + } +}; async function sendSuccessStatusReport(startedAt, config, registry_types, logger) { const statusReportBase = await createStatusReportBase( "start-proxy" /* StartProxy */, @@ -119590,9 +119596,16 @@ async function sendSuccessStatusReport(startedAt, config, registry_types, logger await sendStatusReport(statusReport); } } +function getSafeErrorMessage(error3) { + if (error3 instanceof StartProxyError) { + return error3.message; + } + return `Error from start-proxy Action omitted (${typeof error3}).`; +} async function sendFailedStatusReport(logger, startedAt, language, unwrappedError) { const error3 = wrapError(unwrappedError); core10.setFailed(`start-proxy action failed: ${error3.message}`); + const statusReportMessage = getSafeErrorMessage(error3); const errorStatusReportBase = await createStatusReportBase( "start-proxy" /* StartProxy */, getActionsStatus(error3), @@ -119602,7 +119615,7 @@ async function sendFailedStatusReport(logger, startedAt, language, unwrappedErro }, await checkDiskUsage(logger), logger, - "Error from start-proxy Action omitted" + statusReportMessage ); if (errorStatusReportBase !== void 0) { await sendStatusReport(errorStatusReportBase); @@ -119761,6 +119774,18 @@ async function getDownloadUrl(logger) { function credentialToStr(c) { return `Type: ${c.type}; Host: ${c.host}; Url: ${c.url} Username: ${c.username}; Password: ${c.password !== void 0}; Token: ${c.token !== void 0}`; } +async function downloadProxy(logger, url, authorization) { + try { + return toolcache.downloadTool(url, void 0, authorization, { + accept: "application/octet-stream" + }); + } catch (error3) { + logger.error( + `Failed to download proxy archive from ${url}: ${getErrorMessage(error3)}` + ); + throw new StartProxyError("Failed to download proxy archive." /* DownloadFailed */); + } +} // src/start-proxy-action.ts var KEY_SIZE = 2048; @@ -119915,7 +119940,7 @@ async function startProxy(binPath, config, logFilePath, logger) { async function getProxyBinaryPath(logger) { const proxyFileName = process.platform === "win32" ? `${UPDATEJOB_PROXY}.exe` : UPDATEJOB_PROXY; const proxyInfo = await getDownloadUrl(logger); - let proxyBin = toolcache.find(proxyFileName, proxyInfo.version); + let proxyBin = toolcache2.find(proxyFileName, proxyInfo.version); if (!proxyBin) { const apiDetails = getApiDetails(); const authorization = getAuthorizationHeaderFor( @@ -119923,16 +119948,9 @@ async function getProxyBinaryPath(logger) { apiDetails, proxyInfo.url ); - const temp = await toolcache.downloadTool( - proxyInfo.url, - void 0, - authorization, - { - accept: "application/octet-stream" - } - ); - const extracted = await toolcache.extractTar(temp); - proxyBin = await toolcache.cacheDir( + const temp = await downloadProxy(logger, proxyInfo.url, authorization); + const extracted = await toolcache2.extractTar(temp); + proxyBin = await toolcache2.cacheDir( extracted, proxyFileName, proxyInfo.version diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index 531dcbb7b..2e70cfaef 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -12,6 +12,7 @@ import { getActionsLogger, Logger } from "./logging"; import { Credential, credentialToStr, + downloadProxy, getCredentials, getDownloadUrl, parseLanguage, @@ -238,14 +239,7 @@ async function getProxyBinaryPath(logger: Logger): Promise { apiDetails, proxyInfo.url, ); - const temp = await toolcache.downloadTool( - proxyInfo.url, - undefined, - authorization, - { - accept: "application/octet-stream", - }, - ); + const temp = await downloadProxy(logger, proxyInfo.url, authorization); const extracted = await toolcache.extractTar(temp); proxyBin = await toolcache.cacheDir( extracted, diff --git a/src/start-proxy.test.ts b/src/start-proxy.test.ts index 4fcee49bc..6b8834e0d 100644 --- a/src/start-proxy.test.ts +++ b/src/start-proxy.test.ts @@ -1,3 +1,4 @@ +import * as toolcache from "@actions/tool-cache"; import test from "ava"; import sinon from "sinon"; @@ -318,3 +319,47 @@ test("credentialToStr - hides passwords/tokens", (t) => { .includes(secret), ); }); + +test("getSafeErrorMessage - returns actual message for `StartProxyError`", (t) => { + const error = new startProxyExports.StartProxyError( + startProxyExports.StartProxyErrorType.DownloadFailed, + ); + t.is(startProxyExports.getSafeErrorMessage(error), error.message); +}); + +test("getSafeErrorMessage - does not return message for arbitrary errors", (t) => { + const error = new Error(startProxyExports.StartProxyErrorType.DownloadFailed); + + const message = startProxyExports.getSafeErrorMessage(error); + + t.not(message, error.message); + t.assert(message.startsWith("Error from start-proxy Action omitted")); + t.assert(message.includes(typeof error)); +}); + +test("downloadProxy - returns file path on success", async (t) => { + const loggedMessages = []; + const logger = getRecordingLogger(loggedMessages); + const testPath = "/some/path"; + sinon.stub(toolcache, "downloadTool").resolves(testPath); + + const result = await startProxyExports.downloadProxy( + logger, + "url", + undefined, + ); + t.is(result, testPath); +}); + +test("downloadProxy - wraps errors on failure", async (t) => { + const loggedMessages = []; + const logger = getRecordingLogger(loggedMessages); + sinon.stub(toolcache, "downloadTool").throws(); + + await t.throwsAsync( + startProxyExports.downloadProxy(logger, "url", undefined), + { + instanceOf: startProxyExports.StartProxyError, + }, + ); +}); diff --git a/src/start-proxy.ts b/src/start-proxy.ts index c527239e5..9ff1d4b5b 100644 --- a/src/start-proxy.ts +++ b/src/start-proxy.ts @@ -1,4 +1,5 @@ import * as core from "@actions/core"; +import * as toolcache from "@actions/tool-cache"; import { getApiClient } from "./api-client"; import * as artifactScanner from "./artifact-scanner"; @@ -16,6 +17,26 @@ import { import * as util from "./util"; import { ConfigurationError, getErrorMessage, isDefined } from "./util"; +/** + * Enumerates specific error types, along with suitable error messages, for errors + * that we want to track in status reports. + */ +export enum StartProxyErrorType { + DownloadFailed = "Failed to download proxy archive.", +} + +/** + * We want to avoid accidentally leaking secrets that may be contained in exception + * messages in the `start-proxy` action. Consequently, we don't report the messages + * of arbitrary exceptions. This type of error ensures that the message is one from + * `StartProxyErrorType` and therefore safe to include in a status report. + */ +export class StartProxyError extends Error { + constructor(errorType: StartProxyErrorType) { + super(errorType); + } +} + interface StartProxyStatus extends StatusReportBase { // A comma-separated list of registry types which are configured for CodeQL. // This only includes registry types we support, not all that are configured. @@ -53,6 +74,23 @@ export async function sendSuccessStatusReport( } } +/** + * Returns an error message for `error` that can safely be reported in a status report, + * i.e. that does not contain sensitive information. + * + * @param error The error for which to get an error message. + */ +export function getSafeErrorMessage(error: Error): string { + // If the error is a `StartProxyError`, the constructor ensures that the + // message comes from `StartProxyErrorType`. + if (error instanceof StartProxyError) { + return error.message; + } + + // Otherwise, omit the actual error message. + return `Error from start-proxy Action omitted (${typeof error}).`; +} + /** * Sends a status report for the `start-proxy` action indicating a failure. * @@ -70,6 +108,8 @@ export async function sendFailedStatusReport( const error = util.wrapError(unwrappedError); core.setFailed(`start-proxy action failed: ${error.message}`); + const statusReportMessage = getSafeErrorMessage(error); + // We skip sending the error message and stack trace here to avoid the possibility // of leaking any sensitive information into the telemetry. const errorStatusReportBase = await createStatusReportBase( @@ -81,7 +121,7 @@ export async function sendFailedStatusReport( }, await util.checkDiskUsage(logger), logger, - "Error from start-proxy Action omitted", + statusReportMessage, ); if (errorStatusReportBase !== undefined) { await sendStatusReport(errorStatusReportBase); @@ -369,3 +409,28 @@ export function credentialToStr(c: Credential): string { c.username }; Password: ${c.password !== undefined}; Token: ${c.token !== undefined}`; } + +/** + * Attempts to download a file from `url` into the toolcache. + * + * @param logger THe logger to use. + * @param url The URL to download the proxy binary from. + * @param authorization The authorization information to use. + * @returns If successful, the path to the downloaded file. + */ +export async function downloadProxy( + logger: Logger, + url: string, + authorization: string | undefined, +) { + try { + return toolcache.downloadTool(url, undefined, authorization, { + accept: "application/octet-stream", + }); + } catch (error) { + logger.error( + `Failed to download proxy archive from ${url}: ${getErrorMessage(error)}`, + ); + throw new StartProxyError(StartProxyErrorType.DownloadFailed); + } +}