diff --git a/__tests__/artifactTwirpClient.test.ts b/__tests__/artifactTwirpClient.test.ts new file mode 100644 index 0000000..035031e --- /dev/null +++ b/__tests__/artifactTwirpClient.test.ts @@ -0,0 +1,84 @@ +import {ArtifactHttpClient} from '../src/internal/shared/artifact-twirp-client' +import {setSecret, debug} from '@actions/core' +import { + CreateArtifactResponse, + GetSignedArtifactURLResponse +} from '../src/generated/results/api/v1/artifact' + +jest.mock('@actions/core', () => ({ + setSecret: jest.fn(), + info: jest.fn(), + debug: jest.fn() +})) + +describe('ArtifactHttpClient', () => { + let client: ArtifactHttpClient + + beforeEach(() => { + jest.clearAllMocks() + process.env['ACTIONS_RUNTIME_TOKEN'] = 'test-token' + process.env['ACTIONS_RESULTS_URL'] = 'https://example.com' + client = new ArtifactHttpClient('test-agent') + }) + + afterEach(() => { + delete process.env['ACTIONS_RUNTIME_TOKEN'] + delete process.env['ACTIONS_RESULTS_URL'] + }) + + describe('maskSecretUrls', () => { + it('should mask signed_upload_url', () => { + const response: CreateArtifactResponse = { + ok: true, + signedUploadUrl: 'https://example.com/upload?se=2025-03-05T16%3A47%3A23Z&sig=secret-token' + } + + client.maskSecretUrls(response) + + expect(setSecret).toHaveBeenCalledWith('secret-token') + expect(debug).toHaveBeenCalledWith('Masked signed_upload_url: https://example.com/upload?se=2025-03-05T16%3A47%3A23Z&sig=***') + }) + + it('should mask signed_download_url', () => { + const response: GetSignedArtifactURLResponse = { + signedUrl: 'https://example.com/download?se=2025-03-05T16%3A47%3A23Z&sig=secret-token' + } + + client.maskSecretUrls(response) + + expect(setSecret).toHaveBeenCalledWith('secret-token') + expect(debug).toHaveBeenCalledWith('Masked signed_download_url: https://example.com/download?se=2025-03-05T16%3A47%3A23Z&sig=***') + }) + + it('should not call setSecret if URLs are missing', () => { + const response = {} as CreateArtifactResponse + + client.maskSecretUrls(response) + + expect(setSecret).not.toHaveBeenCalled() + }) + + it('should mask only the sensitive token part of signed_upload_url', () => { + const response: CreateArtifactResponse = { + ok: true, + signedUploadUrl: 'https://example.com/upload?se=2025-03-05T16%3A47%3A23Z&sig=secret-token' + } + + client.maskSecretUrls(response) + + expect(setSecret).toHaveBeenCalledWith('secret-token') + expect(debug).toHaveBeenCalledWith('Masked signed_upload_url: https://example.com/upload?se=2025-03-05T16%3A47%3A23Z&sig=***') + }) + + it('should mask only the sensitive token part of signed_download_url', () => { + const response: GetSignedArtifactURLResponse = { + signedUrl: 'https://example.com/download?se=2025-03-05T16%3A47%3A23Z&sig=secret-token' + } + + client.maskSecretUrls(response) + + expect(setSecret).toHaveBeenCalledWith('secret-token') + expect(debug).toHaveBeenCalledWith('Masked signed_download_url: https://example.com/download?se=2025-03-05T16%3A47%3A23Z&sig=***') + }) + }) +}) diff --git a/src/internal/shared/artifact-twirp-client.ts b/src/internal/shared/artifact-twirp-client.ts index 00c65bc..1d3e8c6 100644 --- a/src/internal/shared/artifact-twirp-client.ts +++ b/src/internal/shared/artifact-twirp-client.ts @@ -1,10 +1,14 @@ import {HttpClient, HttpClientResponse, HttpCodes} from '@actions/http-client' import {BearerCredentialHandler} from '@actions/http-client/lib/auth' -import {info, debug} from '@actions/core' +import {setSecret, info, debug, maskSigUrl} from '@actions/core' import {ArtifactServiceClientJSON} from '../../generated' import {getResultsServiceUrl, getRuntimeToken} from './config' import {getUserAgentString} from './user-agent' import {NetworkError, UsageError} from './errors' +import { + CreateArtifactResponse, + GetSignedArtifactURLResponse +} from '../../generated/results/api/v1/artifact' // The twirp http client must implement this interface interface Rpc { @@ -16,7 +20,7 @@ interface Rpc { ): Promise } -class ArtifactHttpClient implements Rpc { +export class ArtifactHttpClient implements Rpc { private httpClient: HttpClient private baseUrl: string private maxAttempts = 5 @@ -70,6 +74,17 @@ class ArtifactHttpClient implements Rpc { } } + maskSecretUrls( + body: CreateArtifactResponse | GetSignedArtifactURLResponse + ): void { + if ('signedUploadUrl' in body && body.signedUploadUrl) { + maskSigUrl(body.signedUploadUrl, 'signed_upload_url') + } + if ('signedUrl' in body && body.signedUrl) { + maskSigUrl(body.signedUrl, 'signed_url') + } + } + async retryableRequest( operation: () => Promise ): Promise<{response: HttpClientResponse; body: object}> { @@ -86,6 +101,7 @@ class ArtifactHttpClient implements Rpc { debug(`[Response] - ${response.message.statusCode}`) debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`) const body = JSON.parse(rawBody) + this.maskSecretUrls(body) debug(`Body: ${JSON.stringify(body, null, 2)}`) if (this.isSuccessStatusCode(statusCode)) { return {response, body}