Skip to content

Commit dc34e80

Browse files
committed
feat(osinstaller): add SHA-256 checksum verification for downloaded CLI binaries
- readASTCLIVersion() now returns {version, checksum} and caches both; falls back to cliDefaultVersion and cliDefaultChecksums when the version file is absent or empty, otherwise pairs the version file with checkmarx-ast-cli.checksums - getDownloadURL() returns {url, checksum}, passing null when CX_CLI_LOCATION is set - downloadIfNotInstalledCLI() verifies the downloaded archive when a checksum is available - Add checkmarx-ast-cli.checksums shipped with the package for custom version pinning - Add checksum verification test cases covering match, mismatch, null, CX_CLI_LOCATION bypass, and custom version scenarios
1 parent 8048a28 commit dc34e80

5 files changed

Lines changed: 210 additions & 31 deletions

File tree

checkmarx-ast-cli.checksums

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"windows_x64": "441ee8df46cc630ae000f8ba73925113aeed8c4d16cf274944aff3e7197e3470",
3+
"darwin_x64": "b72f7e4ca14e5e56600b07d22c848a4b85e7c37d2e595424340cc699ea10006b",
4+
"linux_x64": "eb3eb55add37f150188f5a8b36b2a659f902ad9569dcb7ee652531fe525022e2",
5+
"linux_arm64": "7df61689b3c2bbd4c27face5bdc0da97f63e4533229d6b53dd777f90d3904931",
6+
"linux_armv6": "99659f2e0804b197550efc6a9ddb6029babc980d32bdfeeb508199247ac95878"
7+
}

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"files": [
88
"dist/main/**/*",
99
"README.md",
10-
"checkmarx-ast-cli.version"
10+
"checkmarx-ast-cli.version",
11+
"checkmarx-ast-cli.checksums"
1112
],
1213
"dependencies": {
1314
"async-mutex": "^0.5.0",

src/main/osinstaller/CxInstaller.ts

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as fs from 'fs';
33
import * as path from 'path';
44
import * as tar from 'tar';
55
import * as unzipper from 'unzipper';
6+
import * as crypto from 'crypto';
67
import {logger} from "../wrapper/loggerConfig";
78
import {AstClient} from "../client/AstClient";
89
import {CxError} from "../errors/CxError";
@@ -20,9 +21,9 @@ interface PlatformData {
2021
export 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

Comments
 (0)