146 lines
4.1 KiB
TypeScript
146 lines
4.1 KiB
TypeScript
import * as core from '@actions/core'
|
|
import {getRuntimeToken} from './config'
|
|
import jwt_decode from 'jwt-decode'
|
|
import {debug, setSecret} from '@actions/core'
|
|
|
|
export interface BackendIds {
|
|
workflowRunBackendId: string
|
|
workflowJobRunBackendId: string
|
|
}
|
|
|
|
interface ActionsToken {
|
|
scp: string
|
|
}
|
|
|
|
const InvalidJwtError = new Error(
|
|
'Failed to get backend IDs: The provided JWT token is invalid and/or missing claims'
|
|
)
|
|
|
|
// uses the JWT token claims to get the
|
|
// workflow run and workflow job run backend ids
|
|
export function getBackendIdsFromToken(): BackendIds {
|
|
const token = getRuntimeToken()
|
|
const decoded = jwt_decode<ActionsToken>(token)
|
|
if (!decoded.scp) {
|
|
throw InvalidJwtError
|
|
}
|
|
|
|
/*
|
|
* example decoded:
|
|
* {
|
|
* scp: "Actions.ExampleScope Actions.Results:ce7f54c7-61c7-4aae-887f-30da475f5f1a:ca395085-040a-526b-2ce8-bdc85f692774"
|
|
* }
|
|
*/
|
|
|
|
const scpParts = decoded.scp.split(' ')
|
|
if (scpParts.length === 0) {
|
|
throw InvalidJwtError
|
|
}
|
|
/*
|
|
* example scpParts:
|
|
* ["Actions.ExampleScope", "Actions.Results:ce7f54c7-61c7-4aae-887f-30da475f5f1a:ca395085-040a-526b-2ce8-bdc85f692774"]
|
|
*/
|
|
|
|
for (const scopes of scpParts) {
|
|
const scopeParts = scopes.split(':')
|
|
if (scopeParts?.[0] !== 'Actions.Results') {
|
|
// not the Actions.Results scope
|
|
continue
|
|
}
|
|
|
|
/*
|
|
* example scopeParts:
|
|
* ["Actions.Results", "ce7f54c7-61c7-4aae-887f-30da475f5f1a", "ca395085-040a-526b-2ce8-bdc85f692774"]
|
|
*/
|
|
if (scopeParts.length !== 3) {
|
|
// missing expected number of claims
|
|
throw InvalidJwtError
|
|
}
|
|
|
|
const ids = {
|
|
workflowRunBackendId: scopeParts[1],
|
|
workflowJobRunBackendId: scopeParts[2]
|
|
}
|
|
|
|
core.debug(`Workflow Run Backend ID: ${ids.workflowRunBackendId}`)
|
|
core.debug(`Workflow Job Run Backend ID: ${ids.workflowJobRunBackendId}`)
|
|
|
|
return ids
|
|
}
|
|
|
|
throw InvalidJwtError
|
|
}
|
|
|
|
/**
|
|
* Masks the `sig` parameter in a URL and sets it as a secret.
|
|
*
|
|
* @param url - The URL containing the signature parameter to mask
|
|
* @remarks
|
|
* This function attempts to parse the provided URL and identify the 'sig' query parameter.
|
|
* If found, it registers both the raw and URL-encoded signature values as secrets using
|
|
* the Actions `setSecret` API, which prevents them from being displayed in logs.
|
|
*
|
|
* The function handles errors gracefully if URL parsing fails, logging them as debug messages.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Mask a signature in an Azure SAS token URL
|
|
* maskSigUrl('https://example.blob.core.windows.net/container/file.txt?sig=abc123&se=2023-01-01');
|
|
* ```
|
|
*/
|
|
export function maskSigUrl(url: string): void {
|
|
if (!url) return
|
|
try {
|
|
const parsedUrl = new URL(url)
|
|
const signature = parsedUrl.searchParams.get('sig')
|
|
if (signature) {
|
|
setSecret(signature)
|
|
setSecret(encodeURIComponent(signature))
|
|
}
|
|
} catch (error) {
|
|
debug(
|
|
`Failed to parse URL: ${url} ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Masks sensitive information in URLs containing signature parameters.
|
|
* Currently supports masking 'sig' parameters in the 'signed_upload_url'
|
|
* and 'signed_download_url' properties of the provided object.
|
|
*
|
|
* @param body - The object should contain a signature
|
|
* @remarks
|
|
* This function extracts URLs from the object properties and calls maskSigUrl
|
|
* on each one to redact sensitive signature information. The function doesn't
|
|
* modify the original object; it only marks the signatures as secrets for
|
|
* logging purposes.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const responseBody = {
|
|
* signed_upload_url: 'https://example.com?sig=abc123',
|
|
* signed_download_url: 'https://example.com?sig=def456'
|
|
* };
|
|
* maskSecretUrls(responseBody);
|
|
* ```
|
|
*/
|
|
export function maskSecretUrls(body: Record<string, unknown> | null): void {
|
|
if (typeof body !== 'object' || body === null) {
|
|
debug('body is not an object or is null')
|
|
return
|
|
}
|
|
|
|
if (
|
|
'signed_upload_url' in body &&
|
|
typeof body.signed_upload_url === 'string'
|
|
) {
|
|
maskSigUrl(body.signed_upload_url)
|
|
}
|
|
if ('signed_url' in body && typeof body.signed_url === 'string') {
|
|
maskSigUrl(body.signed_url)
|
|
}
|
|
}
|