Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-test-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Upload extension artifact
uses: actions/upload-artifact@v4
with:
name: extension-vsix
name: xygeni-scanner-vscode
path: "*.vsix"

- uses: trstringer/manual-approval@v1
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@
## [1.2.3]

- Fix scanner compatibility with asdf CLI manager
- Remove deprecate install.sh usage

## [1.2.4]

- Update MCP Setup to use current XYGENI_TOKEN
- Allow download scanner CLI from non-production Xygeni API Url (e.g. on-premise environments)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Xygeni Security Scanner is a powerful extension that brings comprehensive securi
- **In-Editor Issue Highlighting:** View security findings directly in your code, making it easy to pinpoint and fix issues.
- **Detailed Vulnerability Information:** Get rich details for each identified issue, including severity, description, and remediation guidance.
- **Proxy Support:** Configure the extension to work with your corporate proxy.
- **MCP Setup:** Generate ready-to-use MCP server configuration from the extension to connect Xygeni security tools to MCP-compatible AI assistants.

## Installation

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"publisher": "xygeni-security",
"license": "MIT",
"icon": "media/images/logo_xy.png",
"version": "1.2.3",
"version": "1.2.4",
"engines": {
"vscode": "^1.99.2"
},
Expand Down
67 changes: 67 additions & 0 deletions src/test/unit/installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,73 @@ suite('Installer Test Suite', () => {
assert.strictEqual(callCount, 3);
});

test('should use API releases endpoint and bearer token for custom API URL', async () => {
// Arrange
sandbox.stub(Platform, 'get').returns('linux');

const downloadFileStub = sandbox.stub(installer as any, 'downloadFile').callsFake(
(url: any, targetDir: any, name: any, authToken?: any) => {
const filePath = path.join(targetDir, name);
fs.writeFileSync(filePath, 'matching-hash');
return Promise.resolve(filePath);
}
);
sandbox.stub(installer as any, 'calculateChecksum').resolves('matching-hash');
sandbox.stub(installer as any, 'unzip').callsFake((zip: any, dest: any) => {
const root = path.join(dest, 'xygeni_scanner');
if (!fs.existsSync(root)) { fs.mkdirSync(root, { recursive: true }); }
return Promise.resolve();
});
sandbox.stub(installer as any, 'copyDirectoryContents').returns(undefined);
sandbox.stub(installer as any, 'makeBinaryExecutable').resolves();

// Act
await installer.install('https://onprem.xygeni.local/api', 'test-token');

// Assert
assert.strictEqual(downloadFileStub.callCount, 1);
assert.strictEqual(downloadFileStub.firstCall.args[0], 'https://onprem.xygeni.local/api/scan/releases/');
assert.strictEqual(downloadFileStub.firstCall.args[3], 'test-token');
});

test('should use public scanner URL without bearer token for default API URL', async () => {
// Arrange
sandbox.stub(Platform, 'get').returns('linux');

const downloadFileStub = sandbox.stub(installer as any, 'downloadFile').callsFake(
(url: any, targetDir: any, name: any, authToken?: any) => {
const filePath = path.join(targetDir, name);
fs.writeFileSync(filePath, 'matching-hash');
return Promise.resolve(filePath);
}
);
sandbox.stub(installer as any, 'calculateChecksum').resolves('matching-hash');
sandbox.stub(installer as any, 'unzip').callsFake((zip: any, dest: any) => {
const root = path.join(dest, 'xygeni_scanner');
if (!fs.existsSync(root)) { fs.mkdirSync(root, { recursive: true }); }
return Promise.resolve();
});
sandbox.stub(installer as any, 'copyDirectoryContents').returns(undefined);
sandbox.stub(installer as any, 'makeBinaryExecutable').resolves();

// Act
await installer.install('https://api.xygeni.io/', 'test-token');

// Assert
assert.strictEqual(downloadFileStub.callCount, 2);
assert.strictEqual(downloadFileStub.firstCall.args[0], 'https://get.xygeni.io/latest/scanner/xygeni_scanner.zip');
assert.strictEqual(downloadFileStub.firstCall.args[3], undefined);
assert.strictEqual(downloadFileStub.secondCall.args[0], 'https://get.xygeni.io/latest/scanner/xygeni_scanner.zip.sha256');
assert.strictEqual(downloadFileStub.secondCall.args[3], undefined);
});

test('should require token for custom API URL scanner download', async () => {
await assert.rejects(
installer.install('https://onprem.xygeni.local/api'),
/Xygeni token is required to download scanner from custom API URL/
);
});

test('should install successfully', async () => {
// Arrange
sandbox.stub(Platform, 'get').returns('linux');
Expand Down
7 changes: 5 additions & 2 deletions src/xygeni/common/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,11 @@ export class CommandsImpl implements Commands, ScanViewEmitter, IssueViewEmitter
this.connectionReady();

if (!override && await InstallerService.getInstance().isScannerInstalled()) {
Logger.log('=== Xygeni Scanner already installed ===');
Logger.log('=== Xygeni Scanner already installed. ===');
this.installerOk();
this.setMcpLibraryInstalled();
Logger.log(`=== MCP Library is ready. Check Xygeni MCP Setup to configure it. ===`);

return Promise.resolve();
}

Expand Down Expand Up @@ -413,7 +416,7 @@ export class CommandsImpl implements Commands, ScanViewEmitter, IssueViewEmitter
}

showMcpSetupView() {
McpSetupView.showMcpSetup(this);
void McpSetupView.showMcpSetup(this);
}

openDiffViewCommand(uri: string, tempFile: string): void {
Expand Down
77 changes: 57 additions & 20 deletions src/xygeni/service/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ export default class InstallerService {
private readonly tempDir: string;

private readonly xygeniGetScannerUrl = 'https://get.xygeni.io/latest/scanner/';
private readonly xygeniDefaultApiUrl = 'https://api.xygeni.io';
private readonly xygeniApiScannerReleasesPath = 'scan/releases';
private readonly xygeniScannerZipName = 'xygeni_scanner.zip';
private readonly xygeniScannerZipRootFolder = 'xygeni_scanner';
private readonly xygeniScannerChecksumUrl = 'https://raw.githubusercontent.com/xygeni/xygeni/main/checksum/latest/xygeni-release.zip.sha256';

private readonly xygeniMCPLibraryUrl = 'https://get.xygeni.io/latest/mcp-server/xygeni-mcp-server.jar';
private readonly xygeniMCPLibraryName = 'xygeni-mcp-server.jar';
Expand Down Expand Up @@ -111,23 +112,44 @@ export default class InstallerService {
}

const zipPath = path.join(tempDirPath, this.xygeniScannerZipName);
const scannerUrl = `${this.xygeniGetScannerUrl}${this.xygeniScannerZipName}`;
const checksumUrl = `${scannerUrl}.sha256`;
const useApiReleasesUrl = this.shouldUseApiReleasesUrl(apiUrl);

let scannerUrl = `${this.xygeniGetScannerUrl}${this.xygeniScannerZipName}`;
const scannerAuthToken = useApiReleasesUrl ? token : undefined;

if (useApiReleasesUrl) {
if (!token) {
throw new Error('Xygeni token is required to download scanner from custom API URL');
}
scannerUrl = this.buildUrlWithPath(apiUrl!, this.xygeniApiScannerReleasesPath);
}

try {
this.logger.log(" Downloading Xygeni Scanner...");
await this.downloadFile(scannerUrl, tempDirPath, this.xygeniScannerZipName);

this.logger.log(" Validating Xygeni Scanner checksum...");
await this.downloadFile(this.xygeniScannerChecksumUrl, tempDirPath, this.xygeniScannerZipName + '.sha256');
if (useApiReleasesUrl) {
this.logger.log(` Downloading Xygeni Scanner from API URL: ${apiUrl}`);
}
else {
this.logger.log(" Downloading Xygeni Scanner...");
}

const downloadedChecksum = fs.readFileSync(path.join(tempDirPath, this.xygeniScannerZipName + '.sha256'), 'utf8').trim().split(/\s+/)[0];
const fileChecksum = await this.calculateChecksum(zipPath);
await this.downloadFile(scannerUrl, tempDirPath, this.xygeniScannerZipName, scannerAuthToken);

if (!useApiReleasesUrl) {
const checksumUrl = `${scannerUrl}.sha256`;
// when downloading from xygeni cloud api - validate scanner zip checksum
this.logger.log(" Validating Xygeni Scanner checksum...");
await this.downloadFile(checksumUrl, tempDirPath, this.xygeniScannerZipName + '.sha256', scannerAuthToken);

const downloadedChecksum = fs.readFileSync(path.join(tempDirPath, this.xygeniScannerZipName + '.sha256'), 'utf8').trim().split(/\s+/)[0];
const fileChecksum = await this.calculateChecksum(zipPath);

if (downloadedChecksum.toLowerCase() !== fileChecksum.toLowerCase()) {
throw new Error(`Checksum validation failed. Expected: ${downloadedChecksum}, Got: ${fileChecksum}`);
if (downloadedChecksum.toLowerCase() !== fileChecksum.toLowerCase()) {
throw new Error(`Checksum validation failed. Expected: ${downloadedChecksum}, Got: ${fileChecksum}`);
}
this.logger.log(" Checksum validation successful.");
}
this.logger.log(" Checksum validation successful.");



this.logger.log(" Extracting Xygeni Scanner...");
await this.unzip(zipPath, tempDirPath);
Expand Down Expand Up @@ -190,10 +212,8 @@ export default class InstallerService {

// remove installPath if exists, force fresh install
if (fs.existsSync(this.mcpLibraryPath)) {
this.logger.log(` MCP Library already exist at: ${installMcpPath}`);
this.logger.log(` Check Xygeni MCP Setup to configure it.`);
this.logger.log("============================================================");
return Promise.resolve();
this.logger.log(` Removing existing installation at: ${this.mcpLibraryPath}`);
fs.rmSync(this.mcpLibraryPath, { recursive: true, force: true });
}
// create folder if not yet
if (!fs.existsSync(installMcpPath)){
Expand Down Expand Up @@ -257,7 +277,7 @@ export default class InstallerService {

}

private async downloadFile(scriptUrl: string, targetDir: string, installName: string): Promise<string> {
private async downloadFile(scriptUrl: string, targetDir: string, installName: string, authToken?: string): Promise<string> {
return new Promise((resolve, reject) => {

// local path
Expand All @@ -267,6 +287,9 @@ export default class InstallerService {
//this.logger.log(` Downloading install script from: ${scriptUrl}`);

const client = this.commands.getHttpClient(scriptUrl);
if (authToken) {
client.setAuthToken(authToken);
}

const request = client.get(scriptUrl, (response) => {
// Handle redirects
Expand All @@ -275,7 +298,7 @@ export default class InstallerService {
if (redirectUrl) {
file.close();
fs.unlinkSync(filePath);
this.downloadFile(redirectUrl, targetDir, installName).then(resolve).catch(reject);
this.downloadFile(redirectUrl, targetDir, installName, authToken).then(resolve).catch(reject);
return;
}
}
Expand Down Expand Up @@ -322,6 +345,21 @@ export default class InstallerService {
});
}

private shouldUseApiReleasesUrl(apiUrl?: string): boolean {
if (!apiUrl) {
return false;
}
return this.normalizeUrl(apiUrl) !== this.normalizeUrl(this.xygeniDefaultApiUrl);
}

private normalizeUrl(url: string): string {
return url.trim().replace(/\/+$/, '');
}

private buildUrlWithPath(baseUrl: string, pathSuffix: string): string {
return `${this.normalizeUrl(baseUrl)}/${pathSuffix}/`;
}

private async unzip(zipPath: string, destination: string): Promise<void> {
return new Promise((resolve, reject) => {
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
Expand Down Expand Up @@ -457,4 +495,3 @@ export default class InstallerService {


}

Loading
Loading