Skip to content
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"postbuild": "copyfiles -u 1 src/tests/data/* dist/;",
"lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix",
"test": "copyfiles -u 1 src/tests/data/* dist/; tsc && jest"
"test": "copyfiles -u 1 src/tests/data/* dist/; tsc && jest --runInBand"
},
"repository": "https://github.com/CheckmarxDev/ast-cli-javascript-wrapper-runtime-cli.git",
"author": "Jay Nanduri",
Expand Down
11 changes: 11 additions & 0 deletions src/main/client/Client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface Client {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure the naming convention in TS is without "I" for interfaces. I believe it needs to be IClient ( that says that it's an interface)

/**
* Downloads a file from the given URL and saves it to the specified output path.
*
* @param url - The URL to download the file from.
* @param outputPath - The path where the downloaded file will be saved.
* @throws An error if the download fails.
*/
downloadFile(url: string, outputPath: string): Promise<void>;
getProxyConfig(): any;
}
59 changes: 59 additions & 0 deletions src/main/client/HttpClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import axios, {AxiosRequestConfig} from 'axios';
import {logger} from '../wrapper/loggerConfig';
import * as fs from 'fs';
import {finished} from 'stream/promises';
import {Client} from "./Client";

export class HttpClient implements Client {
private readonly axiosConfig: AxiosRequestConfig;

constructor() {
this.axiosConfig = {
responseType: 'stream',
proxy: this.getProxyConfig(),
};
}

public getProxyConfig() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe better to save this value to avoid parsing the URL every time it’s called

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's being call once every httpClient object creation

const proxyUrl = process.env.HTTP_PROXY;
if (proxyUrl) {
logger.info(`Detected proxy configuration in HTTP_PROXY`);
const parsedProxy = new URL(proxyUrl);

return {
host: parsedProxy.hostname,
port: parseInt(parsedProxy.port, 10),
protocol: parsedProxy.protocol.replace(':', ''), // remove the colon
auth: parsedProxy.username && parsedProxy.password
? {username: parsedProxy.username, password: parsedProxy.password}
: undefined,
};
}
logger.info('No proxy configuration detected.');
return undefined;
}

public async downloadFile(url: string, outputPath: string): Promise<void> {
logger.info(`Starting download from URL: ${url}`);
const writer = fs.createWriteStream(outputPath);

try {
if (this.axiosConfig.proxy) {
logger.info(
`Using proxy - Host: ${this.axiosConfig.proxy.host}, Port: ${this.axiosConfig.proxy.port},` +
`Protocol: ${this.axiosConfig.proxy.protocol}, Auth: ${this.axiosConfig.proxy.auth ? 'Yes' : 'No'}`
);
}
const response = await axios({...this.axiosConfig, url});
response.data.pipe(writer);
await finished(writer);
logger.info(`Download completed successfully. File saved to: ${outputPath}`);
} catch (error) {
logger.error(`Error downloading file from ${url}: ${error.message || error}`);
throw error;
} finally {
writer.close();
logger.info('Write stream closed.');
}
}
}
133 changes: 99 additions & 34 deletions src/main/osinstaller/CxInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,63 @@ import * as fsPromises from 'fs/promises';
import * as fs from 'fs';
import * as path from 'path';
import * as tar from 'tar';
import axios from 'axios';
import * as unzipper from 'unzipper';
import {logger} from "../wrapper/loggerConfig";
import {finished} from 'stream/promises';
import {Client} from "../client/Client";

type SupportedPlatforms = 'win32' | 'darwin' | 'linux';

interface PlatformData {
platform: string;
extension: string;
}

export class CxInstaller {
private readonly platform: string;
private readonly platform: SupportedPlatforms;
private cliVersion: string;
private readonly resourceDirPath: string;
private readonly cliDefaultVersion = '2.2.5'; // This will be used if the version file is not found. Should be updated with the latest version.

constructor(platform: string) {
this.platform = platform;
this.resourceDirPath = path.join(__dirname, `../wrapper/resources`);
private readonly installedCLIVersionFileName = 'cli-version';
private readonly cliDefaultVersion = '2.2.5'; // Update this with the latest version.
private readonly client: Client;

private static readonly PLATFORMS: Record<SupportedPlatforms, PlatformData> = {
win32: { platform: 'windows', extension: 'zip' },
darwin: { platform: 'darwin', extension: 'tar.gz' },
linux: { platform: 'linux', extension: 'tar.gz' }
};

constructor(platform: string, client: Client) {
this.platform = platform as SupportedPlatforms;
this.resourceDirPath = path.join(__dirname, '../wrapper/resources');
this.client = client;
}

private async getDownloadURL(): Promise<string> {
const cliVersion = await this.readASTCLIVersion();
const platformData = CxInstaller.PLATFORMS[this.platform];

const platforms: Record<SupportedPlatforms, { platform: string; extension: string }> = {
win32: {platform: 'windows', extension: 'zip'},
darwin: {platform: 'darwin', extension: 'tar.gz'},
linux: {platform: 'linux', extension: 'tar.gz'}
};

const platformKey = this.platform as SupportedPlatforms;

const platformData = platforms[platformKey];
if (!platformData) {
throw new Error('Unsupported platform or architecture');
}

return `https://download.checkmarx.com/CxOne/CLI/${cliVersion}/ast-cli_${cliVersion}_${platformData.platform}_x64.${platformData.extension}`;
const architecture = this.getArchitecture();

return `https://download.checkmarx.com/CxOne/CLI/${cliVersion}/ast-cli_${cliVersion}_${platformData.platform}_${architecture}.${platformData.extension}`;
}

private getArchitecture(): string {
// For non-linux platforms we default to x64.
if (this.platform !== 'linux') {
return 'x64';
}

const archMap: Record<string, string> = {
'arm64': 'arm64',
'arm': 'armv6'
};

// Default to 'x64' if the current architecture is not found in the map.
return archMap[process.arch] || 'x64';
}

public getExecutablePath(): string {
Expand All @@ -47,18 +69,25 @@ export class CxInstaller {
public async downloadIfNotInstalledCLI(): Promise<void> {
try {
await fs.promises.mkdir(this.resourceDirPath, {recursive: true});
const cliVersion = await this.readASTCLIVersion();

if (this.checkExecutableExists()) {
logger.info('Executable already installed.');
return;
const installedVersion = await this.readInstalledVersionFile(this.resourceDirPath);
if (installedVersion === cliVersion) {
logger.info('Executable already installed.');
return;
}
}

await this.cleanDirectoryContents(this.resourceDirPath);
const url = await this.getDownloadURL();
const zipPath = path.join(this.resourceDirPath, this.getCompressFolderName());

await this.downloadFile(url, zipPath);
await this.client.downloadFile(url, zipPath);
logger.info('Downloaded CLI to:', zipPath);

await this.extractArchive(zipPath, this.resourceDirPath);
await this.saveVersionFile(this.resourceDirPath, cliVersion);

fs.unlink(zipPath, (err) => {
if (err) {
Expand All @@ -75,6 +104,33 @@ export class CxInstaller {
}
}

private async cleanDirectoryContents(directoryPath: string): Promise<void> {
try {
const files = await fsPromises.readdir(directoryPath);

await Promise.all(files.map(async (file) => {
const filePath = path.join(directoryPath, file);
const fileStat = await fsPromises.stat(filePath);

if (fileStat.isDirectory()) {
await fsPromises.rm(filePath, {recursive: true, force: true});
logger.info(`Directory ${filePath} deleted.`);
} else {
await fsPromises.unlink(filePath);
logger.info(`File ${filePath} deleted.`);
}
}));

logger.info(`All contents in ${directoryPath} have been cleaned.`);
} catch (error) {
if (error.code === 'ENOENT') {
logger.info(`Directory at ${directoryPath} does not exist.`);
} else {
logger.error(`Failed to clean directory contents: ${error.message}`);
}
}
}

private async extractArchive(zipPath: string, extractPath: string): Promise<void> {
if (zipPath.endsWith('.zip')) {
await unzipper.Open.file(zipPath)
Expand All @@ -86,23 +142,32 @@ export class CxInstaller {
}
}

private async downloadFile(url: string, outputPath: string) {
logger.info('Downloading file from:', url);
const writer = fs.createWriteStream(outputPath);

private async saveVersionFile(resourcePath: string, version: string): Promise<void> {
const versionFilePath = path.join(resourcePath, this.installedCLIVersionFileName);
try {
const response = await axios({url, responseType: 'stream'});
response.data.pipe(writer);
await fsPromises.writeFile(versionFilePath, `${version}`, 'utf8');
logger.info(`Version file created at ${versionFilePath} with version ${version}`);
} catch (error) {
logger.error(`Failed to create version file: ${error.message}`);
}
}

await finished(writer); // Use stream promises to await the writer
logger.info('Download finished');
private async readInstalledVersionFile(resourcePath: string): Promise<string | null> {
const versionFilePath = path.join(resourcePath, this.installedCLIVersionFileName);
try {
const content = await fsPromises.readFile(versionFilePath, 'utf8');
logger.info(`Version file content: ${content}`);
return content;
} catch (error) {
logger.error('Error downloading file:', error.message || error);
} finally {
writer.close();
if (error.code === 'ENOENT') {
logger.warn(`Version file not found at ${versionFilePath}.`);
} else {
logger.error(`Failed to read version file: ${error.message}`);
}
return null;
}
}

private checkExecutableExists(): boolean {
return fs.existsSync(this.getExecutablePath());
}
Expand All @@ -122,7 +187,7 @@ export class CxInstaller {
}

private getVersionFilePath(): string {
return path.join(__dirname,'../../../checkmarx-ast-cli.version');
return path.join(__dirname, '../../../checkmarx-ast-cli.version');
}

private getCompressFolderName(): string {
Expand Down
3 changes: 2 additions & 1 deletion src/main/wrapper/CxWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as os from "os";
import CxBFL from "../bfl/CxBFL";
import {CxInstaller} from "../osinstaller/CxInstaller";
import {Semaphore} from "async-mutex";
import {HttpClient} from "../client/HttpClient";


type ParamTypeMap = Map<CxParamType, string>;
Expand All @@ -18,7 +19,7 @@ export class CxWrapper {
config: CxConfig;
cxInstaller: CxInstaller;
private constructor(cxScanConfig: CxConfig, logFilePath?: string) {
this.cxInstaller = new CxInstaller(process.platform);
this.cxInstaller = new CxInstaller(process.platform, new HttpClient());
this.config = new CxConfig();
getLoggerWithFilePath(logFilePath)
if (cxScanConfig.apiKey) {
Expand Down
Loading