diff --git a/package-lock.json b/package-lock.json index ac1912397..6ffcf13ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@babel/traverse": "^7.23.9", "@babel/types": "^7.23.9", "@deepcode/dcignore": "^1.0.4", - "axios": "^1.7.4", + "axios": "^1.7.8", "diff": "^5.2.0", "glob": "^9.3.5", "he": "^1.2.0", @@ -2514,9 +2514,9 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -10494,9 +10494,9 @@ } }, "axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/package.json b/package.json index 697fb08da..628aed4a3 100644 --- a/package.json +++ b/package.json @@ -553,7 +553,7 @@ "@babel/traverse": "^7.23.9", "@babel/types": "^7.23.9", "@deepcode/dcignore": "^1.0.4", - "axios": "^1.7.4", + "axios": "^1.7.8", "diff": "^5.2.0", "glob": "^9.3.5", "he": "^1.2.0", diff --git a/src/snyk/cli/cliExecutable.ts b/src/snyk/cli/cliExecutable.ts index b87d4e78c..4db555edd 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -5,6 +5,22 @@ import { Checksum } from './checksum'; import { Platform } from '../common/platform'; export class CliExecutable { + public static defaultPaths: Record = { + linux: process.env.XDG_DATA_HOME ?? '/.local/share/', + // eslint-disable-next-line camelcase + linux_arm64: process.env.XDG_DATA_HOME ?? '/.local/share/', + // eslint-disable-next-line camelcase + linux_alpine: process.env.XDG_DATA_HOME ?? '/.local/share/', + // eslint-disable-next-line camelcase + linux_alpine_arm64: process.env.XDG_DATA_HOME ?? '/.local/share/', + macos: process.env.XDG_DATA_HOME ?? '/Library/Application Support/', + // eslint-disable-next-line camelcase + macos_arm64: process.env.XDG_DATA_HOME ?? '/Library/Application Support/', + windows: process.env.XDG_DATA_HOME ?? '\\AppData\\Local\\', + // eslint-disable-next-line camelcase + windows_arm64: process.env.XDG_DATA_HOME ?? '\\AppData\\Local\\', + }; + public static filenameSuffixes: Record = { linux: 'snyk-linux', // eslint-disable-next-line camelcase @@ -20,16 +36,19 @@ export class CliExecutable { // eslint-disable-next-line camelcase windows_arm64: 'snyk-win.exe', }; + constructor(public readonly version: string, public readonly checksum: Checksum) {} - static async getPath(extensionDir: string, customPath?: string): Promise { + static async getPath(customPath?: string): Promise { if (customPath) { return customPath; } - const platform = await CliExecutable.getCurrentWithArch(); + const homeDir = Platform.getHomeDir(); + const defaultPath = this.defaultPaths[platform]; const fileName = CliExecutable.getFileName(platform); - return path.join(extensionDir, fileName); + const cliDir = path.join(homeDir, defaultPath, 'snyk', 'vscode-cli'); + return path.join(cliDir, fileName); } static getFileName(platform: CliSupportedPlatform): string { @@ -67,9 +86,16 @@ export class CliExecutable { return platform; } - static async exists(extensionDir: string, customPath?: string): Promise { + public static isPathInExtensionDirectory(dirPath: string, filePath: string): boolean { + const normalizedDir = path.resolve(dirPath) + path.sep; + const normalizedFile = path.resolve(filePath); + + return normalizedFile.toLowerCase().startsWith(normalizedDir.toLowerCase()); + } + + static async exists(customPath?: string): Promise { return fs - .access(await CliExecutable.getPath(extensionDir, customPath)) + .access(await CliExecutable.getPath(customPath)) .then(() => true) .catch(() => false); } diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 51065a7f3..187d8e68b 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -33,7 +33,6 @@ import { import SecretStorageAdapter from '../vscode/secretStorage'; import { IVSCodeWorkspace } from '../vscode/workspace'; import { CliExecutable } from '../../cli/cliExecutable'; -import { extensionContext } from '../vscode/extensionContext'; const NEWISSUES = 'Net new issues'; @@ -122,7 +121,7 @@ export interface IConfiguration { isAutomaticDependencyManagementEnabled(): boolean; - getCliPath(): Promise; + getCliPath(): Promise; getCliReleaseChannel(): Promise; getCliBaseDownloadUrl(): string; getInsecure(): boolean; @@ -352,7 +351,7 @@ export class Configuration implements IConfiguration { async setCliPath(cliPath: string | undefined): Promise { if (!cliPath) { - cliPath = await CliExecutable.getPath(extensionContext.extensionPath); + cliPath = await CliExecutable.getPath(); } return this.workspace.updateConfiguration( CONFIGURATION_IDENTIFIER, @@ -555,7 +554,7 @@ export class Configuration implements IConfiguration { ); } - async getCliPath(): Promise { + async getCliPath(): Promise { let cliPath = this.workspace.getConfiguration( CONFIGURATION_IDENTIFIER, this.getConfigName(ADVANCED_CLI_PATH), @@ -574,7 +573,7 @@ export class Configuration implements IConfiguration { const isAutomaticDependencyManagementEnabled = this.isAutomaticDependencyManagementEnabled(); const snykLsPath = this.getSnykLanguageServerPath(); if (!isAutomaticDependencyManagementEnabled && snykLsPath) return snykLsPath; - const defaultPath = await CliExecutable.getPath(extensionContext.extensionPath); + const defaultPath = await CliExecutable.getPath(); return defaultPath; } getTrustedFolders(): string[] { diff --git a/src/snyk/common/download/downloader.ts b/src/snyk/common/download/downloader.ts index 8ab54f425..11542e9cc 100644 --- a/src/snyk/common/download/downloader.ts +++ b/src/snyk/common/download/downloader.ts @@ -1,7 +1,9 @@ import axios, { CancelTokenSource } from 'axios'; import * as fs from 'fs'; +import * as path from 'path'; import * as fsPromises from 'fs/promises'; import * as stream from 'stream'; +import { mkdirSync } from 'fs'; import { Progress } from 'vscode'; import { Checksum } from '../../cli/checksum'; import { messages } from '../../cli/messages/messages'; @@ -43,10 +45,9 @@ export class Downloader { } private async getCliExecutable(platform: CliSupportedPlatform): Promise { - const cliPath = await CliExecutable.getPath( - this.extensionContext.extensionPath, - await this.configuration.getCliPath(), - ); + const cliPath = await this.configuration.getCliPath(); + const cliDir = path.dirname(cliPath); + mkdirSync(cliDir, { recursive: true }); if (await this.binaryExists(cliPath)) { await this.deleteFileAtPath(cliPath); } diff --git a/src/snyk/common/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index 2dc10243b..11ee70504 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -78,10 +78,7 @@ export class LanguageServer implements ILanguageServer { }; } - const cliBinaryPath = await CliExecutable.getPath( - this.extensionContext.extensionPath, - await this.configuration.getCliPath(), - ); + const cliBinaryPath = await this.configuration.getCliPath(); // log level is set to info by default let logLevel = 'info'; @@ -164,11 +161,7 @@ export class LanguageServer implements ILanguageServer { // Initialization options are not semantically equal to server settings, thus separated here // https://github.com/microsoft/language-server-protocol/issues/567 async getInitializationOptions(): Promise { - const settings = await LanguageServerSettings.fromConfiguration( - this.configuration, - this.user, - this.extensionContext, - ); + const settings = await LanguageServerSettings.fromConfiguration(this.configuration, this.user); return settings; } diff --git a/src/snyk/common/languageServer/middleware.ts b/src/snyk/common/languageServer/middleware.ts index 8f6d4fe41..3f920fb23 100644 --- a/src/snyk/common/languageServer/middleware.ts +++ b/src/snyk/common/languageServer/middleware.ts @@ -41,11 +41,7 @@ export class LanguageClientMiddleware implements Middleware { return []; } - const serverSettings = await LanguageServerSettings.fromConfiguration( - this.configuration, - this.user, - this.extensionContext, - ); + const serverSettings = await LanguageServerSettings.fromConfiguration(this.configuration, this.user); return [serverSettings]; }, }; diff --git a/src/snyk/common/languageServer/settings.ts b/src/snyk/common/languageServer/settings.ts index 54a3be767..e1351d348 100644 --- a/src/snyk/common/languageServer/settings.ts +++ b/src/snyk/common/languageServer/settings.ts @@ -3,8 +3,6 @@ import { CLI_INTEGRATION_NAME } from '../../cli/contants/integration'; import { Configuration, FolderConfig, IConfiguration, SeverityFilter } from '../configuration/configuration'; import { User } from '../user'; import { PROTOCOL_VERSION } from '../constants/languageServer'; -import { CliExecutable } from '../../cli/cliExecutable'; -import { ExtensionContext } from '../vscode/extensionContext'; export type ServerSettings = { // Feature toggles @@ -50,11 +48,7 @@ export type ServerSettings = { }; export class LanguageServerSettings { - static async fromConfiguration( - configuration: IConfiguration, - user: User, - extensionContext: ExtensionContext, - ): Promise { + static async fromConfiguration(configuration: IConfiguration, user: User): Promise { const featuresConfiguration = configuration.getFeaturesConfiguration(); const ossEnabled = _.isUndefined(featuresConfiguration.ossEnabled) ? true : featuresConfiguration.ossEnabled; @@ -74,7 +68,7 @@ export class LanguageServerSettings { activateSnykIac: `${iacEnabled}`, enableDeltaFindings: `${configuration.getDeltaFindingsEnabled()}`, sendErrorReports: `${configuration.shouldReportErrors}`, - cliPath: await CliExecutable.getPath(extensionContext.extensionPath, await configuration.getCliPath()), + cliPath: await configuration.getCliPath(), endpoint: configuration.snykApiEndpoint, organization: configuration.organization, token: await configuration.getToken(), diff --git a/src/snyk/common/services/downloadService.ts b/src/snyk/common/services/downloadService.ts index bb7814002..9d30706ba 100644 --- a/src/snyk/common/services/downloadService.ts +++ b/src/snyk/common/services/downloadService.ts @@ -1,4 +1,5 @@ import { ReplaySubject } from 'rxjs'; +import * as fsPromises from 'fs/promises'; import { Checksum } from '../../cli/checksum'; import { messages } from '../../cli/messages/messages'; import { IConfiguration } from '../configuration/configuration'; @@ -89,10 +90,7 @@ export class DownloadService { } async isCliInstalled() { - const cliExecutableExists = await CliExecutable.exists( - this.extensionContext.extensionPath, - await this.configuration.getCliPath(), - ); + const cliExecutableExists = await CliExecutable.exists(this.extensionContext.extensionPath); const cliChecksumWritten = !!this.getCliChecksum(); return cliExecutableExists && cliChecksumWritten; @@ -105,11 +103,17 @@ export class DownloadService { return false; } const latestChecksum = await this.cliApi.getSha256Checksum(version, platform); - const path = await CliExecutable.getPath( - this.extensionContext.extensionPath, - await this.configuration.getCliPath(), - ); - + const path = await this.configuration.getCliPath(); + // migration for moving from default extension path to xdg dirs. + if (CliExecutable.isPathInExtensionDirectory(this.extensionContext.extensionPath, path)) { + try { + await fsPromises.unlink(path); + } catch { + // eslint-disable-next-line no-empty + } + await this.configuration.setCliPath(await CliExecutable.getPath()); + return true; + } // Update is available if fetched checksum not matching the current one const checksum = await Checksum.getChecksumOf(path, latestChecksum); if (checksum.verify()) { diff --git a/src/test/unit/cli/cliExecutable.test.ts b/src/test/unit/cli/cliExecutable.test.ts index c23a946d2..748c7f1a8 100644 --- a/src/test/unit/cli/cliExecutable.test.ts +++ b/src/test/unit/cli/cliExecutable.test.ts @@ -1,5 +1,6 @@ import { strictEqual } from 'assert'; import path from 'path'; +import os from 'os'; import fs from 'fs/promises'; import sinon from 'sinon'; import { CliExecutable } from '../../../snyk/cli/cliExecutable'; @@ -19,52 +20,62 @@ suite('CliExecutable', () => { }); test('Returns correct extension paths', async () => { - const unixExtensionDir = '/Users/user/.vscode/extensions/snyk-security.snyk-vulnerability-scanner-1.1.0'; - const winExtensionDir = `C:\\Users\\user\\.vscode\\extensions`; + const homedirStub = sinon.stub(os, 'homedir'); + const unixExtensionDir = '/.local/share/snyk/vscode-cli'; + const macOsExtensionDir = '/Library/Application Support/snyk/vscode-cli'; const osStub = sinon.stub(Platform, 'getCurrent').returns('darwin'); const archStub = sinon.stub(Platform, 'getArch').returns('x64'); const fsStub = sinon.stub(fs, 'access').returns(Promise.reject()); + let homedir = '/home/user'; + homedirStub.returns(homedir); - let expectedCliPath = path.join(unixExtensionDir, 'snyk-macos'); - strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + let expectedCliPath = path.join(Platform.getHomeDir(), macOsExtensionDir, 'snyk-macos'); + strictEqual(await CliExecutable.getPath(), expectedCliPath); osStub.returns('linux'); - expectedCliPath = path.join(unixExtensionDir, 'snyk-linux'); - strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + expectedCliPath = path.join(Platform.getHomeDir(), unixExtensionDir, 'snyk-linux'); + strictEqual(await CliExecutable.getPath(), expectedCliPath); fsStub.returns(Promise.resolve()); - expectedCliPath = path.join(unixExtensionDir, 'snyk-alpine'); - strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + expectedCliPath = path.join(Platform.getHomeDir(), unixExtensionDir, 'snyk-alpine'); + strictEqual(await CliExecutable.getPath(), expectedCliPath); fsStub.returns(Promise.reject()); osStub.returns('win32'); - expectedCliPath = path.join(winExtensionDir, 'snyk-win.exe'); - strictEqual(await CliExecutable.getPath(winExtensionDir), expectedCliPath); + homedir = 'C:\\Users\\user'; + homedirStub.returns(homedir); + expectedCliPath = path.join(Platform.getHomeDir(), '\\AppData\\Local\\', 'snyk', 'vscode-cli', 'snyk-win.exe'); + const actualPath = await CliExecutable.getPath(); + strictEqual(actualPath, expectedCliPath); // test arm64 archStub.returns('arm64'); osStub.returns('darwin'); - expectedCliPath = path.join(unixExtensionDir, 'snyk-macos-arm64'); - strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + homedir = '/home/user'; + homedirStub.returns(homedir); + expectedCliPath = path.join(Platform.getHomeDir(), macOsExtensionDir, 'snyk-macos-arm64'); + strictEqual(await CliExecutable.getPath(), expectedCliPath); osStub.returns('linux'); - expectedCliPath = path.join(unixExtensionDir, 'snyk-linux-arm64'); - strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + expectedCliPath = path.join(Platform.getHomeDir(), unixExtensionDir, 'snyk-linux-arm64'); + strictEqual(await CliExecutable.getPath(), expectedCliPath); fsStub.returns(Promise.resolve()); - expectedCliPath = path.join(unixExtensionDir, 'snyk-alpine-arm64'); - strictEqual(await CliExecutable.getPath(unixExtensionDir), expectedCliPath); + expectedCliPath = path.join(Platform.getHomeDir(), unixExtensionDir, 'snyk-alpine-arm64'); + strictEqual(await CliExecutable.getPath(), expectedCliPath); fsStub.returns(Promise.reject()); osStub.returns('win32'); - expectedCliPath = path.join(winExtensionDir, 'snyk-win.exe'); - strictEqual(await CliExecutable.getPath(winExtensionDir), expectedCliPath); + homedir = 'C:\\Users\\user'; + homedirStub.returns(homedir); + expectedCliPath = path.join(Platform.getHomeDir(), '\\AppData\\Local\\', 'snyk', 'vscode-cli', 'snyk-win.exe'); + strictEqual(await CliExecutable.getPath(), expectedCliPath); }); test('Return custom path, if provided', async () => { const customPath = '/path/to/cli'; - strictEqual(await CliExecutable.getPath('', customPath), customPath); + strictEqual(await CliExecutable.getPath(customPath), customPath); }); }); diff --git a/src/test/unit/common/configuration.test.ts b/src/test/unit/common/configuration.test.ts index 96f6c6871..f8398148d 100644 --- a/src/test/unit/common/configuration.test.ts +++ b/src/test/unit/common/configuration.test.ts @@ -149,7 +149,7 @@ suite('Configuration', () => { const cliPath = await configuration.getCliPath(); - const expectedCliPath = path.join('path/to/extension/', 'snyk-linux'); + const expectedCliPath = path.join(Platform.getHomeDir(), '.local/share/snyk/vscode-cli', 'snyk-linux'); strictEqual(cliPath, expectedCliPath); }); diff --git a/src/test/unit/common/languageServer/middleware.test.ts b/src/test/unit/common/languageServer/middleware.test.ts index 2dbbd054e..95ce7342c 100644 --- a/src/test/unit/common/languageServer/middleware.test.ts +++ b/src/test/unit/common/languageServer/middleware.test.ts @@ -110,10 +110,7 @@ suite('Language Server: Middleware', () => { serverResult.manageBinariesAutomatically, `${configuration.isAutomaticDependencyManagementEnabled()}`, ); - assert.strictEqual( - serverResult.cliPath, - await CliExecutable.getPath(extensionContextMock.extensionPath, await configuration.getCliPath()), - ); + assert.strictEqual(serverResult.cliPath, await configuration.getCliPath()); assert.strictEqual(serverResult.enableTrustedFoldersFeature, 'true'); assert.deepStrictEqual(serverResult.trustedFolders, configuration.getTrustedFolders()); }); diff --git a/src/test/unit/common/languageServer/settings.test.ts b/src/test/unit/common/languageServer/settings.test.ts index dfc82d8f5..d40fb7ef2 100644 --- a/src/test/unit/common/languageServer/settings.test.ts +++ b/src/test/unit/common/languageServer/settings.test.ts @@ -46,11 +46,7 @@ suite('LanguageServerSettings', () => { scanningMode: 'scan-mode', } as unknown as IConfiguration; - const serverSettings = await LanguageServerSettings.fromConfiguration( - mockConfiguration, - mockUser, - extensionContextMock, - ); + const serverSettings = await LanguageServerSettings.fromConfiguration(mockConfiguration, mockUser); assert.strictEqual(serverSettings.activateSnykCodeSecurity, 'true'); assert.strictEqual(serverSettings.activateSnykCodeQuality, 'true');