diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d82b47e..e893b4352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Snyk Security - Code and Open Source Dependencies Changelog +## [1.26.1] + +### Fixed + +- Expanded the server settings returned by `LanguageClientMiddleware` to include necessary attributes for consistent initialization across the application. + +### Added + +- Introduced the `defaultToTrue` utility function within `LanguageServerSettings` to treat undefined feature flags as enabled by default. + +### Changed + +- Enhanced the `ServerSettings` type to include user-specific attributes such as `integrationName`, `integrationVersion`, `automaticAuthentication`, and `deviceId`. This unification simplifies the configuration management. + +### BREAKING CHANGES + +- The `fromConfiguration` method in `LanguageServerSettings` now requires a `User` object to initialize server settings, impacting all areas of the application where server settings are consumed. +- `LanguageClientMiddleware` instantiation now requires a `User` object, aligning with new server settings structure. Consumers must now pass a `User` object upon middleware creation. + ## [1.25.1] ### Changed diff --git a/src/snyk/common/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index b970f6493..10be6c636 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -1,8 +1,7 @@ import _ from 'lodash'; import { firstValueFrom, ReplaySubject, Subject } from 'rxjs'; import { IAuthenticationService } from '../../base/services/authenticationService'; -import { CLI_INTEGRATION_NAME } from '../../cli/contants/integration'; -import { Configuration, IConfiguration } from '../configuration/configuration'; +import { IConfiguration } from '../configuration/configuration'; import { SNYK_ADD_TRUSTED_FOLDERS, SNYK_CLI_PATH, @@ -22,9 +21,8 @@ import { IVSCodeWindow } from '../vscode/window'; import { IVSCodeWorkspace } from '../vscode/workspace'; import { LsExecutable } from './lsExecutable'; import { LanguageClientMiddleware } from './middleware'; -import { InitializationOptions, LanguageServerSettings } from './settings'; +import { LanguageServerSettings, ServerSettings } from './settings'; import { CodeIssueData, IacIssueData, OssIssueData, Scan } from './types'; -import * as fs from 'fs'; export interface ILanguageServer { start(): Promise; @@ -111,7 +109,7 @@ export class LanguageServer implements ILanguageServer { synchronize: { configurationSection: CONFIGURATION_IDENTIFIER, }, - middleware: new LanguageClientMiddleware(this.configuration), + middleware: new LanguageClientMiddleware(this.configuration, this.user), /** * We reuse the output channel here as it's not properly disposed of by the language client (vscode-languageclient@8.0.0-next.2) * See: https://github.com/microsoft/vscode-languageserver-node/blob/cdf4d6fdaefe329ce417621cf0f8b14e0b9bb39d/client/src/common/client.ts#L2789 @@ -178,16 +176,9 @@ 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); - - return { - ...settings, - integrationName: CLI_INTEGRATION_NAME, - integrationVersion: await Configuration.getVersion(), - deviceId: this.user.anonymousId, - automaticAuthentication: 'false', - }; + async getInitializationOptions(): Promise { + const settings = await LanguageServerSettings.fromConfiguration(this.configuration, this.user); + return settings; } showOutputChannel(): void { diff --git a/src/snyk/common/languageServer/middleware.ts b/src/snyk/common/languageServer/middleware.ts index 2fb733985..1b695dc38 100644 --- a/src/snyk/common/languageServer/middleware.ts +++ b/src/snyk/common/languageServer/middleware.ts @@ -1,5 +1,6 @@ -import { IConfiguration } from '../configuration/configuration'; -import { +import { IConfiguration } from '../../common/configuration/configuration'; +import { User } from '../user'; +import type { CancellationToken, ConfigurationParams, ConfigurationRequestHandlerSignature, @@ -18,7 +19,7 @@ export type LanguageClientWorkspaceMiddleware = Partial & { }; export class LanguageClientMiddleware implements Middleware { - constructor(private configuration: IConfiguration) {} + constructor(private configuration: IConfiguration, private user: User) {} workspace: LanguageClientWorkspaceMiddleware = { configuration: async ( @@ -39,7 +40,7 @@ export class LanguageClientMiddleware implements Middleware { return []; } - const serverSettings = await LanguageServerSettings.fromConfiguration(this.configuration); + 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 fe92e2501..7fbe9024b 100644 --- a/src/snyk/common/languageServer/settings.ts +++ b/src/snyk/common/languageServer/settings.ts @@ -1,64 +1,94 @@ -import _ from 'lodash'; -import { IConfiguration, SeverityFilter } from '../configuration/configuration'; - -export type InitializationOptions = ServerSettings & { - integrationName?: string; - integrationVersion?: string; - automaticAuthentication?: string; - deviceId?: string; -}; +import { CLI_INTEGRATION_NAME } from '../../cli/contants/integration'; +import { Configuration, IConfiguration, SeverityFilter } from '../configuration/configuration'; +import { User } from '../user'; export type ServerSettings = { + // Feature toggles activateSnykCodeSecurity?: string; activateSnykCodeQuality?: string; activateSnykOpenSource?: string; activateSnykIac?: string; + + // Endpoint path, and organization + path?: string; + cliPath?: string; endpoint?: string; + organization?: string; + + // Authentication and parameters + token?: string; + automaticAuthentication?: string; additionalParams?: string; - path?: string; + manageBinariesAutomatically?: string; + + // Reporting and telemetry sendErrorReports?: string; - organization?: string; enableTelemetry?: string; - manageBinariesAutomatically?: string; - cliPath?: string; - token?: string; + + // Security and scanning settings filterSeverity?: SeverityFilter; + scanningMode?: string; + insecure?: string; + + // Trusted folders feature enableTrustedFoldersFeature?: string; trustedFolders?: string[]; - insecure?: string; - scanningMode?: string; + + // Snyk integration settings + integrationName?: string; + integrationVersion?: string; + deviceId?: string; +}; + +/** + * Transforms a boolean or undefined value into a string representation. + * It guarantees that undefined values are represented as 'true'. + * This utility is used to ensure feature flags are enabled by default + * when not explicitly set to false. + * + * @param {boolean | undefined} value - The value to transform. + * @returns {string} - The string 'true' if the value is undefined or truthy, 'false' if the value is false. + */ +export const defaultToTrue = (value: boolean | undefined): string => { + return `${value !== undefined ? value : true}`; }; export class LanguageServerSettings { - static async fromConfiguration(configuration: IConfiguration): Promise { + static async fromConfiguration(configuration: IConfiguration, user: User): Promise { const featuresConfiguration = configuration.getFeaturesConfiguration(); - const iacEnabled = _.isUndefined(featuresConfiguration.iacEnabled) ? true : featuresConfiguration.iacEnabled; - const codeSecurityEnabled = _.isUndefined(featuresConfiguration.codeSecurityEnabled) - ? true - : featuresConfiguration.codeSecurityEnabled; - const codeQualityEnabled = _.isUndefined(featuresConfiguration.codeQualityEnabled) - ? true - : featuresConfiguration.codeQualityEnabled; + const iacEnabled = defaultToTrue(featuresConfiguration.iacEnabled); + const codeSecurityEnabled = defaultToTrue(featuresConfiguration.codeSecurityEnabled); + const codeQualityEnabled = defaultToTrue(featuresConfiguration.codeQualityEnabled); return { - activateSnykCodeSecurity: `${codeSecurityEnabled}`, - activateSnykCodeQuality: `${codeQualityEnabled}`, + activateSnykCodeSecurity: codeSecurityEnabled, + activateSnykCodeQuality: codeQualityEnabled, activateSnykOpenSource: 'false', - activateSnykIac: `${iacEnabled}`, - enableTelemetry: `${configuration.shouldReportEvents}`, - sendErrorReports: `${configuration.shouldReportErrors}`, + activateSnykIac: iacEnabled, + cliPath: configuration.getCliPath(), endpoint: configuration.snykOssApiEndpoint, - additionalParams: configuration.getAdditionalCliParameters(), organization: configuration.organization, + token: await configuration.getToken(), + automaticAuthentication: 'false', + additionalParams: configuration.getAdditionalCliParameters(), manageBinariesAutomatically: `${configuration.isAutomaticDependencyManagementEnabled()}`, + + sendErrorReports: `${configuration.shouldReportErrors}`, + enableTelemetry: `${configuration.shouldReportEvents}`, + filterSeverity: configuration.severityFilter, + scanningMode: configuration.scanningMode, + insecure: `${configuration.getInsecure()}`, + enableTrustedFoldersFeature: 'true', trustedFolders: configuration.getTrustedFolders(), - insecure: `${configuration.getInsecure()}`, - scanningMode: configuration.scanningMode, + + integrationName: CLI_INTEGRATION_NAME, + integrationVersion: await Configuration.getVersion(), + deviceId: user.anonymousId, }; } } diff --git a/src/test/unit/common/languageServer/languageServer.test.ts b/src/test/unit/common/languageServer/languageServer.test.ts index 754de0ae3..6155e161c 100644 --- a/src/test/unit/common/languageServer/languageServer.test.ts +++ b/src/test/unit/common/languageServer/languageServer.test.ts @@ -6,7 +6,7 @@ import { v4 } from 'uuid'; import { IAuthenticationService } from '../../../../snyk/base/services/authenticationService'; import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; import { LanguageServer } from '../../../../snyk/common/languageServer/languageServer'; -import { InitializationOptions } from '../../../../snyk/common/languageServer/settings'; +import { ServerSettings } from '../../../../snyk/common/languageServer/settings'; import { DownloadService } from '../../../../snyk/common/services/downloadService'; import { User } from '../../../../snyk/common/user'; import { ILanguageClientAdapter } from '../../../../snyk/common/vscode/languageClient'; @@ -198,7 +198,7 @@ suite('Language Server', () => { }); test('LanguageServer should provide correct initialization options', async () => { - const expectedInitializationOptions: InitializationOptions = { + const expectedInitializationOptions: ServerSettings = { activateSnykCodeSecurity: 'true', activateSnykCodeQuality: 'true', activateSnykOpenSource: 'false', diff --git a/src/test/unit/common/languageServer/middleware.test.ts b/src/test/unit/common/languageServer/middleware.test.ts index f0ef0f8ca..082a383c1 100644 --- a/src/test/unit/common/languageServer/middleware.test.ts +++ b/src/test/unit/common/languageServer/middleware.test.ts @@ -4,7 +4,8 @@ import { CliExecutable } from '../../../../snyk/cli/cliExecutable'; import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; import { LanguageClientMiddleware } from '../../../../snyk/common/languageServer/middleware'; import { ServerSettings } from '../../../../snyk/common/languageServer/settings'; -import { +import { User } from '../../../../snyk/common/user'; +import type { CancellationToken, ConfigurationParams, ConfigurationRequestHandlerSignature, @@ -15,7 +16,10 @@ import { extensionContextMock } from '../../mocks/extensionContext.mock'; suite('Language Server: Middleware', () => { let configuration: IConfiguration; + let user: User; + setup(() => { + user = { anonymousId: 'anonymous-id' } as User; configuration = { shouldReportEvents: false, shouldReportErrors: false, @@ -51,7 +55,7 @@ suite('Language Server: Middleware', () => { }); test('Configuration request should translate settings', async () => { - const middleware = new LanguageClientMiddleware(configuration); + const middleware = new LanguageClientMiddleware(configuration, user); const params: ConfigurationParams = { items: [ { @@ -96,7 +100,7 @@ suite('Language Server: Middleware', () => { }); test('Configuration request should return an error', async () => { - const middleware = new LanguageClientMiddleware(configuration); + const middleware = new LanguageClientMiddleware(configuration, user); const params: ConfigurationParams = { items: [ { diff --git a/src/test/unit/common/languageServer/settings.test.ts b/src/test/unit/common/languageServer/settings.test.ts new file mode 100644 index 000000000..a85a486f3 --- /dev/null +++ b/src/test/unit/common/languageServer/settings.test.ts @@ -0,0 +1,55 @@ +import assert from 'assert'; +import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; +import { LanguageServerSettings, defaultToTrue } from '../../../../snyk/common/languageServer/settings'; +import { User } from '../../../../snyk/common/user'; + +suite('LanguageServerSettings', () => { + suite('defaultToTrue', () => { + test('should return "true" for undefined values', () => { + assert.strictEqual(defaultToTrue(undefined), 'true'); + }); + + test('should return "true" for truthy values', () => { + assert.strictEqual(defaultToTrue(true), 'true'); + }); + + test('should return "false" for false values', () => { + assert.strictEqual(defaultToTrue(false), 'false'); + }); + }); + + suite('fromConfiguration', () => { + test('should generate server settings with default true values for undefined feature toggles', async () => { + const mockUser = { anonymousId: 'anonymous-id' } as User; + const mockConfiguration: IConfiguration = { + shouldReportEvents: true, + shouldReportErrors: false, + snykOssApiEndpoint: 'https://dev.snyk.io/api', + organization: 'my-org', + // eslint-disable-next-line @typescript-eslint/require-await + getToken: async () => 'snyk-token', + getFeaturesConfiguration: () => ({}), // iacEnabled, codeSecurityEnabled, codeQualityEnabled are undefined + getCliPath: () => '/path/to/cli', + getAdditionalCliParameters: () => '--all-projects -d', + getTrustedFolders: () => ['/trusted/path'], + getInsecure: () => false, + isAutomaticDependencyManagementEnabled: () => true, + severityFilter: { critical: true, high: true, medium: true, low: false }, + scanningMode: 'scan-mode', + } as IConfiguration; + + const serverSettings = await LanguageServerSettings.fromConfiguration(mockConfiguration, mockUser); + + assert.strictEqual(serverSettings.activateSnykCodeSecurity, 'true'); + assert.strictEqual(serverSettings.activateSnykCodeQuality, 'true'); + assert.strictEqual(serverSettings.activateSnykIac, 'true'); + assert.strictEqual(serverSettings.deviceId, 'anonymous-id'); + + assert.strictEqual(serverSettings.enableTelemetry, 'true'); + assert.strictEqual(serverSettings.sendErrorReports, 'false'); + assert.strictEqual(serverSettings.cliPath, '/path/to/cli'); + + assert.strictEqual(serverSettings.token, 'snyk-token'); + }); + }); +});