@@ -3,6 +3,7 @@ import * as fs from 'fs';
33import * as path from 'path' ;
44import * as tar from 'tar' ;
55import * as unzipper from 'unzipper' ;
6+ import * as crypto from 'crypto' ;
67import { logger } from "../wrapper/loggerConfig" ;
78import { AstClient } from "../client/AstClient" ;
89import { CxError } from "../errors/CxError" ;
@@ -20,9 +21,9 @@ interface PlatformData {
2021export class CxInstaller {
2122 private readonly platform : SupportedPlatforms ;
2223 private cliVersion : string ;
24+ private cliChecksum : string | null ;
2325 private readonly resourceDirPath : string ;
2426 private readonly installedCLIVersionFileName = 'cli-version' ;
25- private readonly cliDefaultVersion = '2.3.48' ; // Update this with the latest version.
2627 private readonly client : AstClient ;
2728
2829 private static readonly PLATFORMS : Record < SupportedPlatforms , PlatformData > = {
@@ -31,14 +32,65 @@ export class CxInstaller {
3132 linux : { platform : linuxOS , extension : 'tar.gz' }
3233 } ;
3334
35+ // Default version and its paired SHA-256 checksums, keyed by "platform_architecture".
36+ // Update both together when bumping the default CLI version.
37+ private readonly cliDefaultVersion = '2.3.48' ;
38+ private static readonly cliDefaultChecksums : Record < string , string > = {
39+ 'windows_x64' : '441ee8df46cc630ae000f8ba73925113aeed8c4d16cf274944aff3e7197e3470' ,
40+ 'darwin_x64' : 'b72f7e4ca14e5e56600b07d22c848a4b85e7c37d2e595424340cc699ea10006b' ,
41+ 'linux_x64' : 'eb3eb55add37f150188f5a8b36b2a659f902ad9569dcb7ee652531fe525022e2' ,
42+ 'linux_arm64' : '7df61689b3c2bbd4c27face5bdc0da97f63e4533229d6b53dd777f90d3904931' ,
43+ 'linux_armv6' : '99659f2e0804b197550efc6a9ddb6029babc980d32bdfeeb508199247ac95878'
44+ } ;
45+
3446 constructor ( platform : string , client : AstClient ) {
3547 this . platform = platform as SupportedPlatforms ;
3648 this . resourceDirPath = path . join ( __dirname , '../wrapper/resources' ) ;
3749 this . client = client ;
3850 }
3951
40- async getDownloadURL ( ) : Promise < string > {
41- const cliVersion = await this . readASTCLIVersion ( ) ;
52+ // Returns the CLI version and its platform-specific SHA-256 checksum.
53+ // Tries the version file and checksums file first; falls back to the
54+ // hardcoded defaults if the version file is absent or empty.
55+ // Result is cached after the first read.
56+ async readASTCLIVersion ( ) : Promise < { version : string ; checksum : string | null } > {
57+ if ( this . cliVersion ) {
58+ return { version : this . cliVersion , checksum : this . cliChecksum } ;
59+ }
60+
61+ const platformData = CxInstaller . PLATFORMS [ this . platform ] ;
62+ const architecture = this . getArchitecture ( ) ;
63+ const key = `${ platformData . platform } _${ architecture } ` ;
64+
65+ let version : string | null = null ;
66+ try {
67+ const content = await fsPromises . readFile ( this . getVersionFilePath ( ) , 'utf-8' ) ;
68+ const trimmed = content . trim ( ) ;
69+ if ( trimmed ) version = trimmed ;
70+ } catch {
71+ // version file absent — fall through to defaults
72+ }
73+
74+ let checksum : string | null ;
75+ if ( version === null ) {
76+ version = this . cliDefaultVersion ;
77+ checksum = CxInstaller . cliDefaultChecksums [ key ] ?? null ;
78+ } else {
79+ try {
80+ const content = await fsPromises . readFile ( this . getChecksumsFilePath ( ) , 'utf-8' ) ;
81+ checksum = ( JSON . parse ( content ) as Record < string , string > ) [ key ] ?? null ;
82+ } catch {
83+ checksum = null ;
84+ }
85+ }
86+
87+ this . cliVersion = version ;
88+ this . cliChecksum = checksum ;
89+ return { version, checksum } ;
90+ }
91+
92+ async getDownloadURL ( ) : Promise < { url : string ; checksum : string | null } > {
93+ const { version, checksum } = await this . readASTCLIVersion ( ) ;
4294 const platformData = CxInstaller . PLATFORMS [ this . platform ] ;
4395
4496 if ( ! platformData ) {
@@ -49,10 +101,16 @@ export class CxInstaller {
49101
50102 const envVar = process . env . CX_CLI_LOCATION ;
51103 if ( envVar !== undefined ) {
52- return `${ envVar } /ast-cli_${ cliVersion } _${ platformData . platform } _${ architecture } .${ platformData . extension } ` ;
104+ return {
105+ url : `${ envVar } /ast-cli_${ version } _${ platformData . platform } _${ architecture } .${ platformData . extension } ` ,
106+ checksum : null
107+ } ;
53108 }
54-
55- return `https://download.checkmarx.com/CxOne/CLI/${ cliVersion } /ast-cli_${ cliVersion } _${ platformData . platform } _${ architecture } .${ platformData . extension } ` ;
109+
110+ return {
111+ url : `https://download.checkmarx.com/CxOne/CLI/${ version } /ast-cli_${ version } _${ platformData . platform } _${ architecture } .${ platformData . extension } ` ,
112+ checksum
113+ } ;
56114 }
57115
58116 private getArchitecture ( ) : string {
@@ -78,7 +136,7 @@ export class CxInstaller {
78136 public async downloadIfNotInstalledCLI ( ) : Promise < void > {
79137 try {
80138 await fs . promises . mkdir ( this . resourceDirPath , { recursive : true } ) ;
81- const cliVersion = await this . readASTCLIVersion ( ) ;
139+ const { version : cliVersion } = await this . readASTCLIVersion ( ) ;
82140
83141 if ( this . checkExecutableExists ( ) ) {
84142 const installedVersion = await this . readInstalledVersionFile ( this . resourceDirPath ) ;
@@ -89,11 +147,15 @@ export class CxInstaller {
89147 }
90148
91149 await this . cleanDirectoryContents ( this . resourceDirPath ) ;
92- const url = await this . getDownloadURL ( ) ;
150+ const { url, checksum } = await this . getDownloadURL ( ) ;
93151 const zipPath = path . join ( this . resourceDirPath , this . getCompressFolderName ( ) ) ;
94152
95153 await this . client . downloadFile ( url , zipPath ) ;
96154
155+ if ( checksum ) {
156+ await this . verifyChecksum ( zipPath , checksum ) ;
157+ }
158+
97159 await this . extractArchive ( zipPath , this . resourceDirPath ) ;
98160 await this . saveVersionFile ( this . resourceDirPath , cliVersion ) ;
99161
@@ -183,28 +245,36 @@ export class CxInstaller {
183245 return fs . existsSync ( this . getExecutablePath ( ) ) ;
184246 }
185247
186- async readASTCLIVersion ( ) : Promise < string > {
187- if ( this . cliVersion ) {
188- return this . cliVersion ;
189- }
190- try {
191- const versionFilePath = this . getVersionFilePath ( ) ;
192- const versionContent = await fsPromises . readFile ( versionFilePath , 'utf-8' ) ;
193- return versionContent . trim ( ) ;
194- } catch ( error ) {
195- logger . warn ( 'Error reading AST CLI version: ' + error . message ) ;
196- return this . cliDefaultVersion ;
197- }
198- }
199-
200248 private getVersionFilePath ( ) : string {
201249 return path . join ( __dirname , '../../../checkmarx-ast-cli.version' ) ;
202250 }
203251
252+ private getChecksumsFilePath ( ) : string {
253+ return path . join ( __dirname , '../../../checkmarx-ast-cli.checksums' ) ;
254+ }
255+
204256 private getCompressFolderName ( ) : string {
205257 return `ast-cli.${ this . platform === winOS ? 'zip' : 'tar.gz' } ` ;
206258 }
207-
259+
260+ private async verifyChecksum ( zipPath : string , expected : string ) : Promise < void > {
261+ const actual = await this . computeSHA256 ( zipPath ) ;
262+ if ( actual !== expected ) {
263+ throw new CxError ( `Checksum mismatch for ${ path . basename ( zipPath ) } : expected ${ expected } , got ${ actual } ` ) ;
264+ }
265+ logger . info ( `Checksum verified for ${ path . basename ( zipPath ) } .` ) ;
266+ }
267+
268+ private computeSHA256 ( filePath : string ) : Promise < string > {
269+ return new Promise ( ( resolve , reject ) => {
270+ const hash = crypto . createHash ( 'sha256' ) ;
271+ fs . createReadStream ( filePath )
272+ . on ( 'data' , chunk => hash . update ( chunk ) )
273+ . on ( 'end' , ( ) => resolve ( hash . digest ( 'hex' ) ) )
274+ . on ( 'error' , reject ) ;
275+ } ) ;
276+ }
277+
208278 public getPlatform ( ) : SupportedPlatforms {
209279 return this . platform ;
210280 }
0 commit comments