diff --git a/.github/workflows/update-cli.yml b/.github/workflows/update-cli.yml index baff8b33..fe4a9e22 100644 --- a/.github/workflows/update-cli.yml +++ b/.github/workflows/update-cli.yml @@ -40,15 +40,56 @@ jobs: run: | echo ${{ steps.checkmarx-ast-cli.outputs.release_tag }} > checkmarx-ast-cli.version - # Update the TypeScript file's cliDefaultVersion field - - name: Update cliDefaultVersion in CxInstaller.ts + # Download CLI binaries and generate checksums + - name: Download CLI and generate checksums if: steps.checkmarx-ast-cli.outputs.current_tag != steps.checkmarx-ast-cli.outputs.release_tag env: - NEW_CLI_VERSION: ${{ steps.checkmarx-ast-cli.outputs.release_tag }} + RELEASE_TAG: ${{ steps.checkmarx-ast-cli.outputs.release_tag }} run: | - FILE_PATH="src/main/osinstaller/CxInstaller.ts" - # Ensure that 'cliDefaultVersion' is updated correctly - sed -i "s/\(cliDefaultVersion = '\)[^']*\(';\)/\1${NEW_CLI_VERSION}\2/" $FILE_PATH + VERSION=$RELEASE_TAG + + # Initialize checksums object + CHECKSUMS='{}' + + # Platform configurations: platform_name,architecture,extension,os_platform + PLATFORMS=( + "windows,x64,zip,windows" + "darwin,x64,tar.gz,darwin" + "linux,x64,tar.gz,linux" + "linux,arm64,tar.gz,linux" + "linux,armv6,tar.gz,linux" + ) + + for PLATFORM_CONFIG in "${PLATFORMS[@]}"; do + IFS=',' read -r OS_TYPE ARCH EXT OS_PLATFORM <<< "$PLATFORM_CONFIG" + + KEY="${OS_PLATFORM}_${ARCH}" + URL="https://download.checkmarx.com/CxOne/CLI/${VERSION}/ast-cli_${VERSION}_${OS_PLATFORM}_${ARCH}.${EXT}" + + echo "Downloading checksum for ${KEY} from ${URL}..." + + # Download binary + TEMP_FILE="/tmp/ast-cli_${KEY}.${EXT}" + if curl -sL -o "$TEMP_FILE" "$URL"; then + # Calculate SHA-256 + CHECKSUM=$(sha256sum "$TEMP_FILE" | awk '{print $1}') + echo "✓ ${KEY}: ${CHECKSUM}" + + # Update checksums JSON + CHECKSUMS=$(echo "$CHECKSUMS" | jq --arg key "$KEY" --arg value "$CHECKSUM" '.[$key] = $value') + + # Cleanup + rm -f "$TEMP_FILE" + else + echo "✗ Failed to download ${KEY}" + exit 1 + fi + done + + # Write checksums to file + echo "$CHECKSUMS" | jq '.' > checkmarx-ast-cli.checksums + echo "Checksums updated:" + cat checkmarx-ast-cli.checksums # Create a Pull Request with the version changes - name: Create Pull Request diff --git a/src/main/osinstaller/CxInstaller.ts b/src/main/osinstaller/CxInstaller.ts index 9be6bdb5..20c24a7e 100644 --- a/src/main/osinstaller/CxInstaller.ts +++ b/src/main/osinstaller/CxInstaller.ts @@ -21,7 +21,7 @@ interface PlatformData { export class CxInstaller { private readonly platform: SupportedPlatforms; private cliVersion: string; - private cliChecksum: string | null; + private cliChecksum: string; private readonly resourceDirPath: string; private readonly installedCLIVersionFileName = 'cli-version'; private readonly client: AstClient; @@ -32,16 +32,6 @@ export class CxInstaller { linux: { platform: linuxOS, extension: 'tar.gz' } }; - // Default version and its paired SHA-256 checksums, keyed by "platform_architecture". - // Update both together when bumping the default CLI version. - private readonly cliDefaultVersion = '2.3.48'; - private static readonly cliDefaultChecksums: Record = { - 'windows_x64': '441ee8df46cc630ae000f8ba73925113aeed8c4d16cf274944aff3e7197e3470', - 'darwin_x64': 'b72f7e4ca14e5e56600b07d22c848a4b85e7c37d2e595424340cc699ea10006b', - 'linux_x64': 'eb3eb55add37f150188f5a8b36b2a659f902ad9569dcb7ee652531fe525022e2', - 'linux_arm64': '7df61689b3c2bbd4c27face5bdc0da97f63e4533229d6b53dd777f90d3904931', - 'linux_armv6': '99659f2e0804b197550efc6a9ddb6029babc980d32bdfeeb508199247ac95878' - }; constructor(platform: string, client: AstClient) { this.platform = platform as SupportedPlatforms; @@ -50,10 +40,9 @@ export class CxInstaller { } // Returns the CLI version and its platform-specific SHA-256 checksum. - // Tries the version file and checksums file first; falls back to the - // hardcoded defaults if the version file is absent or empty. + // Throws CxError if the version or checksums file is missing, empty, or has no entry for the current platform. // Result is cached after the first read. - async readASTCLIVersion(): Promise<{ version: string; checksum: string | null }> { + async readASTCLIVersion(): Promise<{ version: string; checksum: string }> { if (this.cliVersion) { return { version: this.cliVersion, checksum: this.cliChecksum }; } @@ -62,30 +51,28 @@ export class CxInstaller { const architecture = this.getArchitecture(); const key = `${platformData.platform}_${architecture}`; - let version: string | null = null; + let version: string; try { const content = await fsPromises.readFile(this.getVersionFilePath(), 'utf-8'); const trimmed = content.trim(); - if (trimmed) version = trimmed; - } catch { - // version file absent — fall through to defaults + if (!trimmed) throw new CxError('CLI version not found'); + version = trimmed; + } catch (error) { + if (error instanceof CxError) throw error; + throw new CxError('CLI version not found'); } - let checksum: string | null; - if (version === null) { - version = this.cliDefaultVersion; - checksum = CxInstaller.cliDefaultChecksums[key] ?? null; - } else { - try { - const content = await fsPromises.readFile(this.getChecksumsFilePath(), 'utf-8'); - checksum = (JSON.parse(content) as Record)[key] ?? null; - if (checksum === null) { - logger.warn(`No checksum found for ${key} in checksums file. Download will not be verified.`); - } - } catch { - logger.warn(`Checksums file not found. Download of version ${version} will not be verified.`); - checksum = null; + let checksum: string; + try { + const content = await fsPromises.readFile(this.getChecksumsFilePath(), 'utf-8'); + const parsed = (JSON.parse(content) as Record); + if (!parsed[key]) { + throw new CxError(`No checksum found for ${key} in checksums file.`); } + checksum = parsed[key]; + } catch (error) { + if (error instanceof CxError) throw error; + throw new CxError(`Checksums file not found. Download of version ${version} will not be verified.`); } this.cliVersion = version; diff --git a/src/tests/CxInstallerTest.test.ts b/src/tests/CxInstallerTest.test.ts index 07954406..505fe091 100644 --- a/src/tests/CxInstallerTest.test.ts +++ b/src/tests/CxInstallerTest.test.ts @@ -3,6 +3,7 @@ import { anyString, mock, instance, when, verify } from "ts-mockito"; import { AstClient } from "../main/client/AstClient"; import * as fs from "fs"; import * as crypto from "crypto"; +import * as path from "path"; // Mock AstClient and set up an instance from it const astClientMock = mock(AstClient); @@ -15,41 +16,44 @@ const cxInstallerWindows = new CxInstaller("win32", astClientInstance); describe("CxInstaller cases", () => { it('CxInstaller getDownloadURL Linux Successful case', async () => { + const testVersion = '2.3.48'; + jest.spyOn(cxInstallerLinux as any, 'readASTCLIVersion').mockResolvedValue({ version: testVersion, checksum: 'mock-checksum' }); const { url } = await cxInstallerLinux.getDownloadURL(); - const { version } = await cxInstallerLinux.readASTCLIVersion(); const architecture = getArchitecture(cxInstallerLinux.getPlatform()); - expect(url).toBe(`https://download.checkmarx.com/CxOne/CLI/${version}/ast-cli_${version}_linux_${architecture}.tar.gz`); + expect(url).toBe(`https://download.checkmarx.com/CxOne/CLI/${testVersion}/ast-cli_${testVersion}_linux_${architecture}.tar.gz`); }); it('CxInstaller getDownloadURL Mac Successful case', async () => { + const testVersion = '2.3.48'; + jest.spyOn(cxInstallerMac as any, 'readASTCLIVersion').mockResolvedValue({ version: testVersion, checksum: 'mock-checksum' }); const { url } = await cxInstallerMac.getDownloadURL(); - const { version } = await cxInstallerLinux.readASTCLIVersion(); const architecture = getArchitecture(cxInstallerMac.getPlatform()); - expect(url).toBe(`https://download.checkmarx.com/CxOne/CLI/${version}/ast-cli_${version}_darwin_${architecture}.tar.gz`); + expect(url).toBe(`https://download.checkmarx.com/CxOne/CLI/${testVersion}/ast-cli_${testVersion}_darwin_${architecture}.tar.gz`); }); it('CxInstaller getDownloadURL Windows Successful case', async () => { + const testVersion = '2.3.48'; + jest.spyOn(cxInstallerWindows as any, 'readASTCLIVersion').mockResolvedValue({ version: testVersion, checksum: 'mock-checksum' }); const { url } = await cxInstallerWindows.getDownloadURL(); - const { version } = await cxInstallerLinux.readASTCLIVersion(); const architecture = getArchitecture(cxInstallerWindows.getPlatform()); - expect(url).toBe(`https://download.checkmarx.com/CxOne/CLI/${version}/ast-cli_${version}_windows_${architecture}.zip`); + expect(url).toBe(`https://download.checkmarx.com/CxOne/CLI/${testVersion}/ast-cli_${testVersion}_windows_${architecture}.zip`); }); }); describe("CxInstaller getExecutablePath cases", () => { it('CxInstaller getExecutablePath Linux Successful case', () => { const executablePath = cxInstallerLinux.getExecutablePath(); - expect(executablePath).toContain(`src/main/wrapper/resources/cx`); + expect(executablePath).toContain(path.join('src', 'main', 'wrapper', 'resources', 'cx')); }); it('CxInstaller getExecutablePath Mac Successful case', () => { const executablePath = cxInstallerMac.getExecutablePath(); - expect(executablePath).toContain(`src/main/wrapper/resources/cx`); + expect(executablePath).toContain(path.join('src', 'main', 'wrapper', 'resources', 'cx')); }); it('CxInstaller getExecutablePath Windows Successful case', () => { const executablePath = cxInstallerWindows.getExecutablePath(); - expect(executablePath).toContain(`src/main/wrapper/resources/cx.exe`); + expect(executablePath).toContain(path.join('src', 'main', 'wrapper', 'resources', 'cx.exe')); }); }); @@ -140,13 +144,6 @@ describe("CxInstaller checksum verification cases", () => { expect(exitSpy).toHaveBeenCalledWith(1); }); - it('CxInstaller null checksum skips verification', async () => { - jest.spyOn(localLinux as any, 'readASTCLIVersion').mockResolvedValue({ version: '9.9.99', checksum: null }); - when(localMock.downloadFile(anyString(), anyString())).thenResolve(); - await localLinux.downloadIfNotInstalledCLI(); - expect(exitSpy).not.toHaveBeenCalled(); - }); - it('CxInstaller CX_CLI_LOCATION skips checksum verification', async () => { process.env.CX_CLI_LOCATION = 'https://internal.example.com/cli'; jest.spyOn(localLinux as any, 'readASTCLIVersion').mockResolvedValue({ version: '2.3.48', checksum: 'irrelevant' });