Skip to content

Commit ebfdd0a

Browse files
authored
Merge pull request #28 from CheckmarxDev/feature/benalvo/add-proxy-support
AzureDevops | Add Seamless Proxy Ability (AST-67328)
2 parents be69cbd + 520dba8 commit ebfdd0a

File tree

7 files changed

+273
-41
lines changed

7 files changed

+273
-41
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"postbuild": "copyfiles -u 1 src/tests/data/* dist/;",
2323
"lint": "eslint . --ext .ts",
2424
"lint-and-fix": "eslint . --ext .ts --fix",
25-
"test": "copyfiles -u 1 src/tests/data/* dist/; tsc && jest"
25+
"test": "copyfiles -u 1 src/tests/data/* dist/; tsc && jest --runInBand"
2626
},
2727
"repository": "https://github.com/CheckmarxDev/ast-cli-javascript-wrapper-runtime-cli.git",
2828
"author": "Jay Nanduri",

src/main/client/AstClient.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {logger} from '../wrapper/loggerConfig';
2+
import * as fs from 'fs';
3+
import {finished} from 'stream/promises';
4+
import {Client} from "./Client";
5+
6+
export class AstClient {
7+
private client: Client;
8+
9+
constructor(client: Client) {
10+
this.client = client;
11+
}
12+
13+
public async downloadFile(url: string, outputPath: string): Promise<void> {
14+
logger.info(`Starting download from URL: ${url}`);
15+
const writer = fs.createWriteStream(outputPath);
16+
try {
17+
const response = await this.client.request(url, 'GET', null);
18+
response.data.pipe(writer);
19+
await finished(writer);
20+
logger.info(`Download completed successfully. File saved to: ${outputPath}`);
21+
} catch (error) {
22+
logger.error(`Error downloading file from ${url}: ${error.message || error}`);
23+
throw error;
24+
} finally {
25+
writer.close();
26+
logger.info('Write stream closed.');
27+
}
28+
}
29+
}

src/main/client/Client.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {AxiosResponse} from "axios";
2+
3+
export interface Client {
4+
getProxyConfig(): any;
5+
request(url: string, method: string, data: any): Promise<AxiosResponse<any, any>>;
6+
}

src/main/client/HttpClient.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
2+
import {logger} from '../wrapper/loggerConfig';
3+
import {Client} from "./Client";
4+
5+
export class HttpClient implements Client {
6+
private readonly axiosConfig: AxiosRequestConfig;
7+
8+
constructor() {
9+
this.axiosConfig = {
10+
responseType: 'stream',
11+
proxy: this.getProxyConfig(),
12+
};
13+
}
14+
15+
public getProxyConfig() {
16+
const proxyUrl = process.env.HTTP_PROXY;
17+
if (proxyUrl) {
18+
logger.info(`Detected proxy configuration in HTTP_PROXY`);
19+
const parsedProxy = new URL(proxyUrl);
20+
21+
return {
22+
host: parsedProxy.hostname,
23+
port: parseInt(parsedProxy.port, 10),
24+
protocol: parsedProxy.protocol.replace(':', ''), // remove the colon
25+
auth: parsedProxy.username && parsedProxy.password
26+
? {username: parsedProxy.username, password: parsedProxy.password}
27+
: undefined,
28+
};
29+
}
30+
logger.info('No proxy configuration detected.');
31+
return undefined;
32+
}
33+
34+
public async request(url: string, method: string, data: any): Promise<AxiosResponse<any, any>> {
35+
logger.info(`Sending ${method} request to URL: ${url}`);
36+
if (this.axiosConfig.proxy) {
37+
logger.info(
38+
`Using proxy - Host: ${this.axiosConfig.proxy.host}, Port: ${this.axiosConfig.proxy.port},` +
39+
`Protocol: ${this.axiosConfig.proxy.protocol}, Auth: ${this.axiosConfig.proxy.auth ? 'Yes' : 'No'}`
40+
);
41+
}
42+
try {
43+
const response = await axios({...this.axiosConfig, url, method, data});
44+
logger.info(`Request completed successfully.`);
45+
return response;
46+
} catch (error) {
47+
logger.error(`Error sending ${method} request to ${url}: ${error.message || error}`);
48+
throw error;
49+
}
50+
}
51+
}

src/main/osinstaller/CxInstaller.ts

Lines changed: 110 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,94 @@ import * as fsPromises from 'fs/promises';
22
import * as fs from 'fs';
33
import * as path from 'path';
44
import * as tar from 'tar';
5-
import axios from 'axios';
65
import * as unzipper from 'unzipper';
76
import {logger} from "../wrapper/loggerConfig";
8-
import {finished} from 'stream/promises';
7+
import {AstClient} from "../client/AstClient";
98

9+
const linuxOS = 'linux';
10+
const macOS = 'darwin';
11+
const winOS = 'win32';
1012
type SupportedPlatforms = 'win32' | 'darwin' | 'linux';
1113

14+
interface PlatformData {
15+
platform: string;
16+
extension: string;
17+
}
18+
1219
export class CxInstaller {
13-
private readonly platform: string;
20+
private readonly platform: SupportedPlatforms;
1421
private cliVersion: string;
1522
private readonly resourceDirPath: string;
16-
private readonly cliDefaultVersion = '2.2.5'; // This will be used if the version file is not found. Should be updated with the latest version.
17-
18-
constructor(platform: string) {
19-
this.platform = platform;
20-
this.resourceDirPath = path.join(__dirname, `../wrapper/resources`);
23+
private readonly installedCLIVersionFileName = 'cli-version';
24+
private readonly cliDefaultVersion = '2.2.5'; // Update this with the latest version.
25+
private readonly client: AstClient;
26+
27+
private static readonly PLATFORMS: Record<SupportedPlatforms, PlatformData> = {
28+
win32: { platform: 'windows', extension: 'zip' },
29+
darwin: { platform: macOS, extension: 'tar.gz' },
30+
linux: { platform: linuxOS, extension: 'tar.gz' }
31+
};
32+
33+
constructor(platform: string, client: AstClient) {
34+
this.platform = platform as SupportedPlatforms;
35+
this.resourceDirPath = path.join(__dirname, '../wrapper/resources');
36+
this.client = client;
2137
}
2238

23-
private async getDownloadURL(): Promise<string> {
39+
async getDownloadURL(): Promise<string> {
2440
const cliVersion = await this.readASTCLIVersion();
41+
const platformData = CxInstaller.PLATFORMS[this.platform];
2542

26-
const platforms: Record<SupportedPlatforms, { platform: string; extension: string }> = {
27-
win32: {platform: 'windows', extension: 'zip'},
28-
darwin: {platform: 'darwin', extension: 'tar.gz'},
29-
linux: {platform: 'linux', extension: 'tar.gz'}
30-
};
31-
32-
const platformKey = this.platform as SupportedPlatforms;
33-
34-
const platformData = platforms[platformKey];
3543
if (!platformData) {
3644
throw new Error('Unsupported platform or architecture');
3745
}
3846

39-
return `https://download.checkmarx.com/CxOne/CLI/${cliVersion}/ast-cli_${cliVersion}_${platformData.platform}_x64.${platformData.extension}`;
47+
const architecture = this.getArchitecture();
48+
49+
return `https://download.checkmarx.com/CxOne/CLI/${cliVersion}/ast-cli_${cliVersion}_${platformData.platform}_${architecture}.${platformData.extension}`;
50+
}
51+
52+
private getArchitecture(): string {
53+
// For non-linux platforms we default to x64.
54+
if (this.platform !== linuxOS) {
55+
return 'x64';
56+
}
57+
58+
const archMap: Record<string, string> = {
59+
'arm64': 'arm64',
60+
'arm': 'armv6'
61+
};
62+
63+
// Default to 'x64' if the current architecture is not found in the map.
64+
return archMap[process.arch] || 'x64';
4065
}
4166

4267
public getExecutablePath(): string {
43-
const executableName = this.platform === 'win32' ? 'cx.exe' : 'cx';
68+
const executableName = this.platform === winOS ? 'cx.exe' : 'cx';
4469
return path.join(this.resourceDirPath, executableName);
4570
}
4671

4772
public async downloadIfNotInstalledCLI(): Promise<void> {
4873
try {
4974
await fs.promises.mkdir(this.resourceDirPath, {recursive: true});
75+
const cliVersion = await this.readASTCLIVersion();
5076

5177
if (this.checkExecutableExists()) {
52-
logger.info('Executable already installed.');
53-
return;
78+
const installedVersion = await this.readInstalledVersionFile(this.resourceDirPath);
79+
if (installedVersion === cliVersion) {
80+
logger.info('Executable already installed.');
81+
return;
82+
}
5483
}
84+
85+
await this.cleanDirectoryContents(this.resourceDirPath);
5586
const url = await this.getDownloadURL();
5687
const zipPath = path.join(this.resourceDirPath, this.getCompressFolderName());
5788

58-
await this.downloadFile(url, zipPath);
59-
logger.info('Downloaded CLI to:', zipPath);
89+
await this.client.downloadFile(url, zipPath);
6090

6191
await this.extractArchive(zipPath, this.resourceDirPath);
92+
await this.saveVersionFile(this.resourceDirPath, cliVersion);
6293

6394
fs.unlink(zipPath, (err) => {
6495
if (err) {
@@ -75,6 +106,33 @@ export class CxInstaller {
75106
}
76107
}
77108

109+
private async cleanDirectoryContents(directoryPath: string): Promise<void> {
110+
try {
111+
const files = await fsPromises.readdir(directoryPath);
112+
113+
await Promise.all(files.map(async (file) => {
114+
const filePath = path.join(directoryPath, file);
115+
const fileStat = await fsPromises.stat(filePath);
116+
117+
if (fileStat.isDirectory()) {
118+
await fsPromises.rm(filePath, {recursive: true, force: true});
119+
logger.info(`Directory ${filePath} deleted.`);
120+
} else {
121+
await fsPromises.unlink(filePath);
122+
logger.info(`File ${filePath} deleted.`);
123+
}
124+
}));
125+
126+
logger.info(`All contents in ${directoryPath} have been cleaned.`);
127+
} catch (error) {
128+
if (error.code === 'ENOENT') {
129+
logger.info(`Directory at ${directoryPath} does not exist.`);
130+
} else {
131+
logger.error(`Failed to clean directory contents: ${error.message}`);
132+
}
133+
}
134+
}
135+
78136
private async extractArchive(zipPath: string, extractPath: string): Promise<void> {
79137
if (zipPath.endsWith('.zip')) {
80138
await unzipper.Open.file(zipPath)
@@ -86,24 +144,33 @@ export class CxInstaller {
86144
}
87145
}
88146

89-
private async downloadFile(url: string, outputPath: string) {
90-
logger.info('Downloading file from:', url);
91-
const writer = fs.createWriteStream(outputPath);
92-
147+
private async saveVersionFile(resourcePath: string, version: string): Promise<void> {
148+
const versionFilePath = path.join(resourcePath, this.installedCLIVersionFileName);
93149
try {
94-
const response = await axios({url, responseType: 'stream'});
95-
response.data.pipe(writer);
150+
await fsPromises.writeFile(versionFilePath, `${version}`, 'utf8');
151+
logger.info(`Version file created at ${versionFilePath} with version ${version}`);
152+
} catch (error) {
153+
logger.error(`Failed to create version file: ${error.message}`);
154+
}
155+
}
96156

97-
await finished(writer); // Use stream promises to await the writer
98-
logger.info('Download finished');
157+
private async readInstalledVersionFile(resourcePath: string): Promise<string | null> {
158+
const versionFilePath = path.join(resourcePath, this.installedCLIVersionFileName);
159+
try {
160+
const content = await fsPromises.readFile(versionFilePath, 'utf8');
161+
logger.info(`Version file content: ${content}`);
162+
return content;
99163
} catch (error) {
100-
logger.error('Error downloading file:', error.message || error);
101-
} finally {
102-
writer.close();
164+
if (error.code === 'ENOENT') {
165+
logger.warn(`Version file not found at ${versionFilePath}.`);
166+
} else {
167+
logger.error(`Failed to read version file: ${error.message}`);
168+
}
169+
return null;
103170
}
104171
}
105-
106-
private checkExecutableExists(): boolean {
172+
173+
public checkExecutableExists(): boolean {
107174
return fs.existsSync(this.getExecutablePath());
108175
}
109176

@@ -122,10 +189,14 @@ export class CxInstaller {
122189
}
123190

124191
private getVersionFilePath(): string {
125-
return path.join(__dirname,'../../../checkmarx-ast-cli.version');
192+
return path.join(__dirname, '../../../checkmarx-ast-cli.version');
126193
}
127194

128195
private getCompressFolderName(): string {
129-
return `ast-cli.${this.platform === 'win32' ? 'zip' : 'tar.gz'}`;
196+
return `ast-cli.${this.platform === winOS ? 'zip' : 'tar.gz'}`;
197+
}
198+
199+
public getPlatform(): SupportedPlatforms {
200+
return this.platform;
130201
}
131202
}

src/main/wrapper/CxWrapper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import * as os from "os";
88
import CxBFL from "../bfl/CxBFL";
99
import {CxInstaller} from "../osinstaller/CxInstaller";
1010
import {Semaphore} from "async-mutex";
11+
import {HttpClient} from "../client/HttpClient";
12+
import {AstClient} from "../client/AstClient";
1113

1214

1315
type ParamTypeMap = Map<CxParamType, string>;
@@ -18,7 +20,7 @@ export class CxWrapper {
1820
config: CxConfig;
1921
cxInstaller: CxInstaller;
2022
private constructor(cxScanConfig: CxConfig, logFilePath?: string) {
21-
this.cxInstaller = new CxInstaller(process.platform);
23+
this.cxInstaller = new CxInstaller(process.platform, new AstClient(new HttpClient()));
2224
this.config = new CxConfig();
2325
getLoggerWithFilePath(logFilePath)
2426
if (cxScanConfig.apiKey) {

0 commit comments

Comments
 (0)