Code Injection
Description
The package snyk before 1.1064.0 are vulnerable to Code Injection when analyzing a project. An attacker who can convince a user to scan a malicious project can include commands in a build file such as build.gradle or gradle-wrapper.jar, which will be executed with the privileges of the application. This vulnerability may be triggered when running the the CLI tool directly, or when running a scan with one of the IDE plugins that invoke the Snyk CLI. Successful exploitation of this issue would likely require some level of social engineering - to coerce an untrusted project to be downloaded and analyzed via the Snyk CLI or opened in an IDE where a Snyk IDE plugin is installed and enabled. Additionally, if the IDE has a Trust feature then the target folder must be marked as ‘trusted’ in order to be vulnerable. NOTE: This issue is independent of the one reported in CVE-2022-40764, and upgrading to a fixed version for this addresses that issue as well. The affected IDE plugins and versions are: - VS Code - Affected: <=1.8.0, Fixed: 1.9.0 - IntelliJ - Affected: <=2.4.47, Fixed: 2.4.48 - Visual Studio - Affected: <=1.1.30, Fixed: 1.1.31 - Eclipse - Affected: <=v20221115.132308, Fixed: All subsequent versions - Language Server - Affected: <=v20221109.114426, Fixed: All subsequent versions
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Snyk CLI and IDE plugins before certain versions are vulnerable to code injection via malicious project build files, allowing arbitrary command execution.
Vulnerability
Overview CVE-2022-24441 is a code injection vulnerability in the Snyk CLI tool (versions before 1.1064.0) and its associated IDE plugins [3]. The vulnerability arises because Snyk does not properly sanitize build files (e.g., build.gradle or gradle-wrapper.jar) during project analysis [1][2]. An attacker can embed malicious commands within these files, which are then executed with the privileges of the application when Snyk scans the project.
Exploitation
Exploitation requires social engineering to convince a user to download and analyze an untrusted project via the Snyk CLI or via an IDE with a Snyk plugin installed and enabled [3]. If the IDE implements a trust feature, the target folder must be marked as trusted for the exploit to work [1][2]. The attack can be triggered either by running the CLI directly or by opening the project in an IDE where the Snyk plugin is active.
Impact
Successful exploitation allows an attacker to execute arbitrary commands on the host system, potentially leading to data theft, file modification, or installation of malware [4]. The vulnerability is independent of CVE-2022-40764, but upgrading to a fixed version addresses both issues [3].
Mitigation
Snyk has released fixed versions: CLI 1.1064.0 or later, Visual Studio plugin 1.1.31+, IntelliJ plugin 2.4.48+, Eclipse plugin post-v20221115.132308, and Language Server post-v20221109.114426 [3]. The fix introduces a workspace trust mechanism to ensure scans are only performed on trusted projects [1][2]. Users are advised to update to the latest versions.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
snyknpm | < 1.1064.0 | 1.1064.0 |
Affected products
2- snyk/snykdescription
Patches
50db3b4240be0feat: workspace trust (#306)
23 files changed · +213 −45
CHANGELOG.md+7 −1 modified@@ -1,6 +1,12 @@ # Snyk Security - Code and Open Source Dependencies Changelog -## [1.7.8] +## [1.9.0] + +### Added + +- Added workspace trust feature. + +## [1.8.0] ### Added
package.json+6 −1 modified@@ -175,6 +175,11 @@ "description": "Severity issues to display.", "scope": "window" }, + "snyk.trustedFolders": { + "type": "array", + "default": [], + "description": "Folders to trust for Snyk scans." + }, "snyk.features.preview": { "type": "object", "default": {}, @@ -271,7 +276,7 @@ }, { "view": "snyk.views.welcome", - "contents": "Welcome to Snyk for Visual Studio Code. 👋\nLet's start by connecting VS Code with Snyk:\n[Connect VS Code with Snyk](command:snyk.initiateLogin 'Connect with Snyk')\n👉 Snyk's mission is to finds bugs, fast. Connect with Snyk to start your first analysis!\nBy connecting your account with Snyk, you agree to the Snyk [Privacy Policy](https://snyk.io/policies/privacy), and the Snyk [Terms of Service](https://snyk.io/policies/terms-of-service).", + "contents": "Welcome to Snyk for Visual Studio Code. 👋\n👉 Connect with Snyk to start your first analysis!\nWhen scanning folder files, Snyk may automatically execute code such as invoking the package manager to get dependency information. You should only scan projects you trust. [More info](https://docs.snyk.io/ide-tools/visual-studio-code-extension/workspace-trust)\n[Trust workspace and connect](command:snyk.initiateLogin 'Connect with Snyk')\nBy connecting your account with Snyk, you agree to the Snyk [Privacy Policy](https://snyk.io/policies/privacy), and the Snyk [Terms of Service](https://snyk.io/policies/terms-of-service).", "when": "!snyk:error && !snyk:loggedIn" }, {
src/snyk/base/modules/snykLib.ts+2 −2 modified@@ -126,14 +126,14 @@ export default class SnykLib extends BaseSnykModule implements ISnykLib { if (!configuration.getFeaturesConfiguration()?.ossEnabled) return; if (!this.ossService) throw new Error('OSS service is not initialized.'); - // wait until Snyk CLI is downloaded + // wait until Snyk Language Server is downloaded await firstValueFrom(this.downloadService.downloadReady$); try { const oldResult = this.ossService.getResult(); const result = await this.ossService.test(manual, reportTriggeredEvent); - if (result instanceof CliError) { + if (result instanceof CliError || !result) { return; }
src/snyk/cli/services/cliService.ts+25 −6 modified@@ -2,9 +2,11 @@ import { firstValueFrom } from 'rxjs'; import parseArgsStringToArgv from 'string-argv'; import { AnalysisStatusProvider } from '../../common/analysis/statusProvider'; import { IConfiguration } from '../../common/configuration/configuration'; +import { getTrustedFolders } from '../../common/configuration/trustedFolders'; import { ErrorHandler } from '../../common/error/errorHandler'; import { ILanguageServer } from '../../common/languageServer/languageServer'; import { ILog } from '../../common/logger/interfaces'; +import { messages as analysisMessages } from '../../common/messages/analysisMessages'; import { DownloadService } from '../../common/services/downloadService'; import { ExtensionContext } from '../../common/vscode/extensionContext'; import { IVSCodeWorkspace } from '../../common/vscode/workspace'; @@ -23,6 +25,7 @@ export abstract class CliService<CliResult> extends AnalysisStatusProvider { private cliProcess?: CliProcess; private _isLsDownloadSuccessful = true; private _isCliReady: boolean; + private _isAnyWorkspaceFolderTrusted = true; constructor( protected readonly extensionContext: ExtensionContext, @@ -43,7 +46,11 @@ export abstract class CliService<CliResult> extends AnalysisStatusProvider { return this._isCliReady; } - async test(manualTrigger: boolean, reportTriggeredEvent: boolean): Promise<CliResult | CliError> { + get isAnyWorkspaceFolderTrusted(): boolean { + return this._isAnyWorkspaceFolderTrusted; + } + + async test(manualTrigger: boolean, reportTriggeredEvent: boolean): Promise<CliResult | CliError | void> { this.ensureDependencies(); const currentCliPath = CliExecutable.getPath(this.extensionContext.extensionPath, this.config.getCliPath()); @@ -66,6 +73,19 @@ export abstract class CliService<CliResult> extends AnalysisStatusProvider { const cliPath = await firstValueFrom(this.languageServer.cliReady$); this._isCliReady = true; + let foldersToTest = this.workspace.getWorkspaceFolders(); + if (foldersToTest.length == 0) { + throw new Error('No workspace was opened.'); + } + + foldersToTest = getTrustedFolders(this.config, foldersToTest); + if (foldersToTest.length == 0) { + this.handleNoTrustedFolders(); + this.logger.info(`Skipping Open Source scan. ${analysisMessages.noWorkspaceTrustDescription}`); + return; + } + this._isAnyWorkspaceFolderTrusted = true; + // Start test this.analysisStarted(); this.beforeTest(manualTrigger, reportTriggeredEvent); @@ -76,11 +96,6 @@ export abstract class CliService<CliResult> extends AnalysisStatusProvider { if (!killed) this.logger.error('Failed to kill an already running CLI instance.'); } - const foldersToTest = this.workspace.getWorkspaceFolders(); - if (foldersToTest.length == 0) { - throw new Error('No workspace was opened.'); - } - this.cliProcess = new CliProcess(this.logger, this.config, this.workspace); const args = this.buildArguments(foldersToTest); @@ -130,6 +145,10 @@ export abstract class CliService<CliResult> extends AnalysisStatusProvider { this._isLsDownloadSuccessful = false; } + handleNoTrustedFolders() { + this._isAnyWorkspaceFolderTrusted = false; + } + private buildArguments(foldersToTest: string[]): string[] { const args = [];
src/snyk/common/commands/commandController.ts+2 −1 modified@@ -20,7 +20,7 @@ import { VSCODE_GO_TO_SETTINGS_COMMAND, } from '../constants/commands'; import { COMMAND_DEBOUNCE_INTERVAL, IDE_NAME, SNYK_NAME_EXTENSION, SNYK_PUBLISHER } from '../constants/general'; -import { SNYK_LOGIN_COMMAND } from '../constants/languageServer'; +import { SNYK_LOGIN_COMMAND, SNYK_TRUST_WORKSPACE_FOLDERS_COMMAND } from '../constants/languageServer'; import { ErrorHandler } from '../error/errorHandler'; import { ILog } from '../logger/interfaces'; import { IOpenerService } from '../services/openerService'; @@ -55,6 +55,7 @@ export class CommandController { this.logger.info('Initiating login'); await this.executeCommand(SNYK_INITIATE_LOGIN_COMMAND, this.authService.initiateLogin.bind(this.authService)); await this.commands.executeCommand(SNYK_LOGIN_COMMAND); + await this.commands.executeCommand(SNYK_TRUST_WORKSPACE_FOLDERS_COMMAND); } async setToken(): Promise<void> {
src/snyk/common/configuration/configuration.ts+19 −0 modified@@ -17,6 +17,7 @@ import { FEATURES_PREVIEW_SETTING, OSS_ENABLED_SETTING, SEVERITY_FILTER_SETTING, + TRUSTED_FOLDERS, YES_BACKGROUND_OSS_NOTIFICATION_SETTING, YES_CRASH_REPORT_SETTING, YES_TELEMETRY_SETTING, @@ -98,6 +99,10 @@ export interface IConfiguration { getSnykLanguageServerPath(): string | undefined; setShouldReportEvents(b: boolean): Promise<void>; + + getTrustedFolders(): string[]; + + setTrustedFolders(trustedFolders: string[]): Promise<void>; } export class Configuration implements IConfiguration { @@ -424,6 +429,20 @@ export class Configuration implements IConfiguration { return this.workspace.getConfiguration<string>(CONFIGURATION_IDENTIFIER, this.getConfigName(ADVANCED_CLI_PATH)); } + getTrustedFolders(): string[] { + return ( + this.workspace.getConfiguration<string[]>(CONFIGURATION_IDENTIFIER, this.getConfigName(TRUSTED_FOLDERS)) || [] + ); + } + + async setTrustedFolders(trustedFolders: string[]): Promise<void> { + await this.workspace.updateConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(TRUSTED_FOLDERS), + trustedFolders, + true, + ); + } private getConfigName = (setting: string) => setting.replace(`${CONFIGURATION_IDENTIFIER}.`, ''); private static isSingleTenant(url: URL): boolean {
src/snyk/common/configuration/trustedFolders.ts+7 −0 added@@ -0,0 +1,7 @@ +import { IConfiguration } from './configuration'; + +export function getTrustedFolders(config: IConfiguration, workspaceFolders: string[]): string[] { + const trustedFolders = config.getTrustedFolders(); + + return workspaceFolders.filter(folder => trustedFolders.includes(folder)); +}
src/snyk/common/constants/languageServer.ts+2 −0 modified@@ -10,7 +10,9 @@ export const DID_CHANGE_CONFIGURATION_METHOD = 'workspace/didChangeConfiguration // custom methods export const SNYK_HAS_AUTHENTICATED = '$/snyk.hasAuthenticated'; export const SNYK_CLI_PATH = '$/snyk.isAvailableCli'; +export const SNYK_ADD_TRUSTED_FOLDERS = '$/snyk.addTrustedFolders'; // commands export const SNYK_LOGIN_COMMAND = 'snyk.login'; export const SNYK_WORKSPACE_SCAN_COMMAND = 'snyk.workspace.scan'; +export const SNYK_TRUST_WORKSPACE_FOLDERS_COMMAND = 'snyk.trustWorkspaceFolders';
src/snyk/common/constants/settings.ts+1 −0 modified@@ -22,3 +22,4 @@ export const ADVANCED_CLI_PATH = `${CONFIGURATION_IDENTIFIER}.advanced.cliPath`; export const ADVANCED_CUSTOM_LS_PATH = `${CONFIGURATION_IDENTIFIER}.advanced.languageServerPath`; export const SEVERITY_FILTER_SETTING = `${CONFIGURATION_IDENTIFIER}.severity`; +export const TRUSTED_FOLDERS = `${CONFIGURATION_IDENTIFIER}.trustedFolders`;
src/snyk/common/languageServer/languageServer.ts+45 −31 modified@@ -2,7 +2,12 @@ import { firstValueFrom, ReplaySubject } from 'rxjs'; import { IAuthenticationService } from '../../base/services/authenticationService'; import { CLI_INTEGRATION_NAME } from '../../cli/contants/integration'; import { Configuration, IConfiguration } from '../configuration/configuration'; -import { SNYK_CLI_PATH, SNYK_HAS_AUTHENTICATED, SNYK_LANGUAGE_SERVER_NAME } from '../constants/languageServer'; +import { + SNYK_ADD_TRUSTED_FOLDERS, + SNYK_CLI_PATH, + SNYK_HAS_AUTHENTICATED, + SNYK_LANGUAGE_SERVER_NAME, +} from '../constants/languageServer'; import { CONFIGURATION_IDENTIFIER } from '../constants/settings'; import { ErrorHandler } from '../error/errorHandler'; import { ILog } from '../logger/interfaces'; @@ -91,39 +96,10 @@ export class LanguageServer implements ILanguageServer { // Create the language client and start the client. this.client = this.languageClientAdapter.create('Snyk LS', SNYK_LANGUAGE_SERVER_NAME, serverOptions, clientOptions); - this.client .onReady() .then(() => { - this.client.onNotification(SNYK_HAS_AUTHENTICATED, ({ token }: { token: string }) => { - this.authenticationService.updateToken(token).catch((error: Error) => { - ErrorHandler.handle(error, this.logger, error.message); - }); - }); - - this.client.onNotification(SNYK_CLI_PATH, ({ cliPath }: { cliPath: string }) => { - if (!cliPath) { - ErrorHandler.handle( - new Error("CLI path wasn't provided by language server on $/snyk.isAvailableCli notification " + cliPath), - this.logger, - "CLI path wasn't provided by language server on notification", - ); - return; - } - - const currentCliPath = this.configuration.getCliPath(); - if (currentCliPath != cliPath) { - this.logger.info('Setting Snyk CLI path to: ' + cliPath); - void this.configuration - .setCliPath(cliPath) - .then(() => { - this.cliReady$.next(cliPath); - }) - .catch((error: Error) => { - ErrorHandler.handle(error, this.logger, error.message); - }); - } - }); + this.registerListeners(this.client); }) .catch((error: Error) => ErrorHandler.handle(error, this.logger, error.message)); @@ -132,6 +108,44 @@ export class LanguageServer implements ILanguageServer { this.logger.info('Snyk Language Server started'); } + private registerListeners(client: LanguageClient): void { + client.onNotification(SNYK_HAS_AUTHENTICATED, ({ token }: { token: string }) => { + this.authenticationService.updateToken(token).catch((error: Error) => { + ErrorHandler.handle(error, this.logger, error.message); + }); + }); + + client.onNotification(SNYK_CLI_PATH, ({ cliPath }: { cliPath: string }) => { + if (!cliPath) { + ErrorHandler.handle( + new Error("CLI path wasn't provided by language server on $/snyk.isAvailableCli notification " + cliPath), + this.logger, + "CLI path wasn't provided by language server on notification", + ); + return; + } + + const currentCliPath = this.configuration.getCliPath(); + if (currentCliPath != cliPath) { + this.logger.info('Setting Snyk CLI path to: ' + cliPath); + void this.configuration + .setCliPath(cliPath) + .then(() => { + this.cliReady$.next(cliPath); + }) + .catch((error: Error) => { + ErrorHandler.handle(error, this.logger, error.message); + }); + } + }); + + client.onNotification(SNYK_ADD_TRUSTED_FOLDERS, ({ trustedFolders }: { trustedFolders: string[] }) => { + this.configuration.setTrustedFolders(trustedFolders).catch((error: Error) => { + ErrorHandler.handle(error, this.logger, error.message); + }); + }); + } + // Initialization options are not semantically equal to server settings, thus separated here // https://github.com/microsoft/language-server-protocol/issues/567 async getInitializationOptions(): Promise<InitializationOptions> {
src/snyk/common/languageServer/settings.ts+4 −0 modified@@ -20,6 +20,8 @@ export type ServerSettings = { manageBinariesAutomatically?: string; cliPath?: string; token?: string; + enableTrustedFoldersFeature?: string; + trustedFolders?: string[]; }; export class LanguageServerSettings { @@ -36,6 +38,8 @@ export class LanguageServerSettings { organization: configuration.organization, token: await configuration.getToken(), manageBinariesAutomatically: `${configuration.isAutomaticDependencyManagementEnabled()}`, + enableTrustedFoldersFeature: 'true', + trustedFolders: configuration.getTrustedFolders(), }; } }
src/snyk/common/messages/analysisMessages.ts+3 −0 modified@@ -1,6 +1,9 @@ export const messages = { scanFailed: 'Scan failed', + noWorkspaceTrust: 'No workspace folder was granted trust', clickToProblem: 'Click here to see the problem.', allSeverityFiltersDisabled: 'Please enable severity filters to see the results.', duration: (time: string, day: string): string => `Analysis finished at ${time}, ${day}`, + noWorkspaceTrustDescription: + 'None of workspace folders were trusted. If you trust the workspace, you can add it to the list of trusted folders in the extension settings, or when prompted by the extension next time.', };
src/snyk/common/views/analysisTreeNodeProvider.ts+10 −0 modified@@ -72,5 +72,15 @@ export abstract class AnalysisTreeNodeProvder extends TreeNodeProvider { }); } + protected getNoWorkspaceTrustTreeNode(): TreeNode { + return new TreeNode({ + text: messages.noWorkspaceTrust, + command: { + command: SNYK_SHOW_OUTPUT_COMMAND, + title: '', + }, + }); + } + protected abstract getFilteredIssues(issues: readonly unknown[]): readonly unknown[]; }
src/snyk/common/watchers/configurationWatcher.ts+2 −0 modified@@ -13,6 +13,7 @@ import { CODE_SECURITY_ENABLED_SETTING, OSS_ENABLED_SETTING, SEVERITY_FILTER_SETTING, + TRUSTED_FOLDERS, YES_TELEMETRY_SETTING, } from '../constants/settings'; import { ErrorHandler } from '../error/errorHandler'; @@ -68,6 +69,7 @@ class ConfigurationWatcher implements IWatcher { SEVERITY_FILTER_SETTING, ADVANCED_CUSTOM_ENDPOINT, ADVANCED_CUSTOM_LS_PATH, + TRUSTED_FOLDERS, ].find(config => event.affectsConfiguration(config)); if (change) {
src/snyk/snykCode/codeService.ts+16 −0 modified@@ -4,10 +4,12 @@ import { v4 as uuidv4 } from 'uuid'; import { AnalysisStatusProvider } from '../common/analysis/statusProvider'; import { IAnalytics, SupportedAnalysisProperties } from '../common/analytics/itly'; import { FeaturesConfiguration, IConfiguration } from '../common/configuration/configuration'; +import { getTrustedFolders } from '../common/configuration/trustedFolders'; import { IDE_NAME } from '../common/constants/general'; import { ErrorHandler } from '../common/error/errorHandler'; import { ILog } from '../common/logger/interfaces'; import { Logger } from '../common/logger/logger'; +import { messages as generalAnalysisMessages } from '../common/messages/analysisMessages'; import { LearnService } from '../common/services/learnService'; import { IViewManagerService } from '../common/services/viewManagerService'; import { User } from '../common/user'; @@ -47,6 +49,7 @@ export interface ISnykCodeService extends AnalysisStatusProvider, Disposable { readonly falsePositiveProvider: IWebViewProvider<FalsePositiveWebviewModel>; hasError: boolean; hasTransientError: boolean; + isAnyWorkspaceFolderTrusted: boolean; startAnalysis(paths: string[], manual: boolean, reportTriggeredEvent: boolean): Promise<void>; clearBundle(): void; @@ -74,6 +77,7 @@ export class SnykCodeService extends AnalysisStatusProvider implements ISnykCode private _analysisProgress = ''; private temporaryFailed = false; private failed = false; + private _isAnyWorkspaceFolderTrusted = true; constructor( readonly extensionContext: ExtensionContext, @@ -134,6 +138,9 @@ export class SnykCodeService extends AnalysisStatusProvider implements ISnykCode get hasTransientError(): boolean { return this.temporaryFailed; } + get isAnyWorkspaceFolderTrusted(): boolean { + return this._isAnyWorkspaceFolderTrusted; + } get analysisStatus(): string { return this._analysisStatus; @@ -150,6 +157,15 @@ export class SnykCodeService extends AnalysisStatusProvider implements ISnykCode const enabledFeatures = this.config.getFeaturesConfiguration(); const requestId = uuidv4(); + paths = getTrustedFolders(this.config, paths); + if (!paths.length) { + this._isAnyWorkspaceFolderTrusted = false; + this.viewManagerService.refreshCodeAnalysisViews(enabledFeatures); + this.logger.info(`Skipping Code scan. ${generalAnalysisMessages.noWorkspaceTrustDescription}`); + return; + } + this._isAnyWorkspaceFolderTrusted = true; + try { Logger.info(analysisMessages.started);
src/snyk/snykCode/views/issueTreeProvider.ts+2 −0 modified@@ -51,6 +51,8 @@ export class IssueTreeProvider extends AnalysisTreeNodeProvder { return this.getTransientErrorTreeNodes(); } else if (this.snykCode.hasError) { return [this.getErrorEncounteredTreeNode()]; + } else if (!this.snykCode.isAnyWorkspaceFolderTrusted) { + return [this.getNoWorkspaceTrustTreeNode()]; } if (this.diagnosticCollection) {
src/snyk/snykOss/services/ossService.ts+5 −0 modified@@ -106,6 +106,11 @@ export class OssService extends CliService<OssResult> { super.handleLsDownloadFailure(error); } + override handleNoTrustedFolders(): void { + super.handleNoTrustedFolders(); + this.viewManagerService.refreshOssView(); + } + activateSuggestionProvider(): void { this.suggestionProvider.activate(); }
src/snyk/snykOss/views/ossVulnerabilityTreeProvider.ts+2 −0 modified@@ -50,6 +50,8 @@ export class OssVulnerabilityTreeProvider extends AnalysisTreeNodeProvder { text: messages.cookingDependencies, }), ]; + } else if (!this.ossService.isAnyWorkspaceFolderTrusted) { + return [this.getNoWorkspaceTrustTreeNode()]; } if (this.ossService.isAnalysisRunning) {
src/test/unit/cli/services/cliService.test.ts+3 −1 modified@@ -50,10 +50,12 @@ suite('CliService', () => { getGlobalStateValue: () => undefined, } as unknown as ExtensionContext; + const testFolderPath = 'test-folder'; configuration = { getAdditionalCliParameters: () => '', isAutomaticDependencyManagementEnabled: () => true, getCliPath: () => undefined, + getTrustedFolders: () => [testFolderPath], } as unknown as IConfiguration; downloadService = { @@ -71,7 +73,7 @@ suite('CliService', () => { logger, configuration, { - getWorkspaceFolders: () => ['test-folder'], + getWorkspaceFolders: () => [testFolderPath], } as IVSCodeWorkspace, downloadService, ls,
src/test/unit/common/configuration/trustedFolders.test.ts+38 −0 added@@ -0,0 +1,38 @@ +import { deepStrictEqual } from 'assert'; +import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; +import { getTrustedFolders } from '../../../../snyk/common/configuration/trustedFolders'; + +suite('Trusted Folders', () => { + test('Single folder trusted', () => { + const config = { + getTrustedFolders: () => ['/test/workspace', '/test/workspace2'], + } as IConfiguration; + const workspaceFolders = ['/test/workspace']; + + const trustedFolders = getTrustedFolders(config, workspaceFolders); + + deepStrictEqual(trustedFolders, ['/test/workspace']); + }); + + test('Multiple folders trusted', () => { + const config = { + getTrustedFolders: () => ['/test/workspace', '/test/workspace2'], + } as IConfiguration; + const workspaceFolders = ['/test/workspace', '/test/workspace2']; + + const trustedFolders = getTrustedFolders(config, workspaceFolders); + + deepStrictEqual(trustedFolders, ['/test/workspace', '/test/workspace2']); + }); + + test('No folders trusted', () => { + const config = { + getTrustedFolders: (): string[] => [], + } as IConfiguration; + const workspaceFolders = ['/test/workspace', '/test/workspace2']; + + const trustedFolders = getTrustedFolders(config, workspaceFolders); + + deepStrictEqual(trustedFolders, []); + }); +});
src/test/unit/common/languageServer/languageServer.test.ts+5 −0 modified@@ -49,6 +49,9 @@ suite('Language Server', () => { reportFalsePositives: false, }; }, + getTrustedFolders(): string[] { + return ['/trusted/test/folder']; + }, } as IConfiguration; downloadService = { @@ -87,6 +90,8 @@ suite('Language Server', () => { additionalParams: '--all-projects', manageBinariesAutomatically: 'true', deviceId: user.anonymousId, + enableTrustedFoldersFeature: 'true', + trustedFolders: ['/trusted/test/folder'], }; deepStrictEqual(await languageServer.getInitializationOptions(), expectedInitializationOptions);
src/test/unit/common/languageServer/middleware.test.ts+3 −0 modified@@ -31,6 +31,7 @@ suite('Language Server: Middleware', () => { reportFalsePositives: false, }; }, + getTrustedFolders: () => ['/trusted/test/folder'], } as IConfiguration; }); @@ -78,6 +79,8 @@ suite('Language Server: Middleware', () => { serverResult.cliPath, CliExecutable.getPath(extensionContextMock.extensionPath, configuration.getCliPath()), ); + assert.strictEqual(serverResult.enableTrustedFoldersFeature, 'true'); + assert.deepStrictEqual(serverResult.trustedFolders, configuration.getTrustedFolders()); }); test('Configuration request should return an error', async () => {
src/test/unit/snykOss/services/ossService.test.ts+4 −2 modified@@ -33,6 +33,7 @@ suite('OssService', () => { } as unknown as ILanguageServer; ls.cliReady$.next(''); + const testFolderPath = ''; ossService = new OssService( { extensionPath, @@ -42,10 +43,11 @@ suite('OssService', () => { getAdditionalCliParameters: () => '', getCliPath: () => undefined, isAutomaticDependencyManagementEnabled: () => true, - } as IConfiguration, + getTrustedFolders: () => [testFolderPath], + } as unknown as IConfiguration, {} as IWebViewProvider<OssIssueCommandArg>, { - getWorkspaceFolders: () => [''], + getWorkspaceFolders: () => [testFolderPath], } as IVSCodeWorkspace, { refreshOssView: () => undefined,
56682f4ba608Merge pull request #417 from snyk/feat/trust
14 files changed · +363 −7
build.gradle.kts+1 −0 modified@@ -35,6 +35,7 @@ dependencies { exclude(group = "org.slf4j") } implementation("ly.iterative.itly:sdk-jvm:1.2.11") + testImplementation("com.google.jimfs:jimfs:1.2") testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0") testImplementation("junit:junit:4.13.2") { exclude(group = "org.hamcrest")
CHANGELOG.md+5 −0 modified@@ -1,5 +1,10 @@ # Snyk Changelog +## [2.4.48] +### Added + +- Project trust feature. + ## [2.4.47] ### Fixed
src/integTest/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt+4 −0 modified@@ -36,6 +36,8 @@ import org.junit.Test import snyk.container.ContainerResult import snyk.iac.IacResult import snyk.oss.OssResult +import snyk.trust.WorkspaceTrustService +import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.util.concurrent.TimeUnit @Suppress("FunctionName") @@ -51,13 +53,15 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { mockSnykApiServiceSastEnabled() replaceSnykApiServiceMockInContainer() mockkStatic("io.snyk.plugin.UtilsKt") + mockkStatic("snyk.trust.TrustedProjectsKt") downloaderServiceMock = spyk(SnykCliDownloaderService()) every { downloaderServiceMock.requestLatestReleasesInformation() } returns LatestReleaseInfo( "http://testUrl", "testReleaseInfo", "testTag" ) every { getSnykCliDownloaderService() } returns downloaderServiceMock + every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true } private fun mockSnykApiServiceSastEnabled() {
src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykAuthPanelIntegTest.kt+17 −4 modified@@ -6,32 +6,40 @@ import com.intellij.testFramework.LightPlatform4TestCase import com.intellij.testFramework.replaceService import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.verify import io.snyk.plugin.services.SnykAnalyticsService import io.snyk.plugin.services.SnykCliAuthenticationService import io.snyk.plugin.ui.toolwindow.panels.SnykAuthPanel import org.junit.Test +import snyk.trust.WorkspaceTrustService +import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import javax.swing.JButton import javax.swing.JLabel class SnykAuthPanelIntegTest : LightPlatform4TestCase() { private val analyticsService: SnykAnalyticsService = mockk(relaxed = true) private val cliAuthenticationService = mockk<SnykCliAuthenticationService>(relaxed = true) + private val workspaceTrustServiceMock = mockk<WorkspaceTrustService>(relaxed = true) override fun setUp() { super.setUp() unmockkAll() + mockkStatic("snyk.trust.TrustedProjectsKt") + every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true val application = ApplicationManager.getApplication() application.replaceService(SnykAnalyticsService::class.java, analyticsService, application) + application.replaceService(WorkspaceTrustService::class.java, workspaceTrustServiceMock, application) project.replaceService(SnykCliAuthenticationService::class.java, cliAuthenticationService, project) } override fun tearDown() { unmockkAll() val application = ApplicationManager.getApplication() application.replaceService(SnykAnalyticsService::class.java, SnykAnalyticsService(), application) + application.replaceService(WorkspaceTrustService::class.java, workspaceTrustServiceMock, application) project.replaceService(SnykCliAuthenticationService::class.java, SnykCliAuthenticationService(project), project) super.tearDown() } @@ -40,20 +48,25 @@ class SnykAuthPanelIntegTest : LightPlatform4TestCase() { fun `should display right authenticate button text`() { val cut = SnykAuthPanel(project) val authenticateButton = UIComponentFinder.getComponentByCondition(cut, JButton::class) { - it.text == SnykAuthPanel.AUTHENTICATE_BUTTON_TEXT + it.text == SnykAuthPanel.TRUST_AND_SCAN_BUTTON_TEXT } assertNotNull(authenticateButton) - assertEquals("Test code now", authenticateButton!!.text) + assertEquals("Trust project and scan", authenticateButton!!.text) } @Test fun `should display right description label`() { val expectedText = """ - |<html><ol> + |<html> + |<ol> | <li align="left">Authenticate to Snyk.io</li> | <li align="left">Analyze code for issues and vulnerabilities</li> | <li align="left">Improve your code and upgrade dependencies</li> |</ol> + |<br> + |When scanning project files, Snyk may automatically execute code<br>such as invoking the package manager to get dependency information.<br>You should only scan projects you trust. <a href="https://docs.snyk.io/ide-tools/jetbrains-plugins/folder-trust">More info</a> + |<br> + |<br> |</html> """.trimMargin() @@ -72,7 +85,7 @@ class SnykAuthPanelIntegTest : LightPlatform4TestCase() { val cut = SnykAuthPanel(project) val authenticateButton = UIComponentFinder.getComponentByCondition(cut, JButton::class) { - it.text == SnykAuthPanel.AUTHENTICATE_BUTTON_TEXT + it.text == SnykAuthPanel.TRUST_AND_SCAN_BUTTON_TEXT } assertNotNull(authenticateButton)
src/integTest/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt+3 −0 modified@@ -66,6 +66,7 @@ import snyk.iac.IgnoreButtonActionListener import snyk.iac.ui.toolwindow.IacFileTreeNode import snyk.iac.ui.toolwindow.IacIssueTreeNode import snyk.oss.Vulnerability +import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import javax.swing.JButton import javax.swing.JEditorPane import javax.swing.JLabel @@ -94,12 +95,14 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { super.setUp() unmockkAll() resetSettings(project) + mockkStatic("snyk.trust.TrustedProjectsKt") pluginSettings().token = fakeApiToken // needed to avoid forced Auth panel showing pluginSettings().pluginFirstRun = false // ToolWindow need to be reinitialised for every test as Project is recreated for Heavy tests // also we MUST do it *before* any actual test code due to initialisation of SnykScanListener in init{} toolWindowPanel = project.service() setupDummyCliFile() + every { confirmScanningAndSetWorkspaceTrustedStateIfNeeded(any()) } returns true } override fun tearDown() {
src/integTest/kotlin/snyk/trust/WorkspaceTrustServiceTest.kt+59 −0 added@@ -0,0 +1,59 @@ +@file:Suppress("FunctionName") + +package snyk.trust + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.replaceService +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import org.hamcrest.core.IsEqual.equalTo +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import java.nio.file.Paths + +class WorkspaceTrustServiceTest : BasePlatformTestCase() { + + private val workspaceTrustSettingsMock = mockk<WorkspaceTrustSettings>() + private lateinit var cut: WorkspaceTrustService + + private class IntegTestDisposable : Disposable { + override fun dispose() {} + } + + @Before + public override fun setUp() { + super.setUp() + unmockkAll() + + val application = ApplicationManager.getApplication() + application.replaceService( + WorkspaceTrustSettings::class.java, + workspaceTrustSettingsMock, + IntegTestDisposable() + ) + + cut = WorkspaceTrustService() + } + + @Test + fun `test isPathTrusted should return false if no trusted path in settings available`() { + every { workspaceTrustSettingsMock.getTrustedPaths() } returns listOf() + + val path = Paths.get("/project") + + assertThat(cut.isPathTrusted(path), equalTo(false)) + } + + @Test + fun `test isPathTrusted should return true if trusted path in settings available`() { + every { workspaceTrustSettingsMock.getTrustedPaths() } returns listOf("/project") + + val path = Paths.get("/project") + + assertThat(cut.isPathTrusted(path), equalTo(true)) + } +}
src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt+6 −0 modified@@ -30,6 +30,8 @@ import io.snyk.plugin.snykcode.core.RunUtils import io.snyk.plugin.ui.SnykBalloonNotifications import org.jetbrains.annotations.TestOnly import snyk.common.SnykError +import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded +import java.nio.file.Paths @Service class SnykTaskQueueService(val project: Project) { @@ -73,6 +75,10 @@ class SnykTaskQueueService(val project: Project) { fun scan() { taskQueue.run(object : Task.Backgroundable(project, "Snyk wait for changed files to be saved on disk", true) { override fun run(indicator: ProgressIndicator) { + project.basePath?.let { + if (!confirmScanningAndSetWorkspaceTrustedStateIfNeeded(Paths.get(it))) return + } + ApplicationManager.getApplication().invokeAndWait { FileDocumentManager.getInstance().saveAllDocuments() }
src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykAuthPanel.kt+19 −3 modified@@ -2,6 +2,7 @@ package io.snyk.plugin.ui.toolwindow.panels import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.uiDesigner.core.GridConstraints.ANCHOR_EAST import com.intellij.uiDesigner.core.GridConstraints.ANCHOR_NORTHWEST @@ -24,12 +25,15 @@ import io.snyk.plugin.ui.baseGridConstraints import io.snyk.plugin.ui.boldLabel import io.snyk.plugin.ui.getReadOnlyClickableHtmlJEditorPaneFixedSize import io.snyk.plugin.ui.getStandardLayout +import snyk.SnykBundle import snyk.amplitude.api.ExperimentUser import snyk.analytics.AuthenticateButtonIsClicked import snyk.analytics.AuthenticateButtonIsClicked.EventSource import snyk.analytics.AuthenticateButtonIsClicked.Ide import snyk.analytics.AuthenticateButtonIsClicked.builder +import snyk.trust.WorkspaceTrustService import java.awt.event.ActionEvent +import java.nio.file.Paths import javax.swing.AbstractAction import javax.swing.JButton import javax.swing.JLabel @@ -39,7 +43,7 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable { init { name = "authPanel" - val authButton = JButton(object : AbstractAction(AUTHENTICATE_BUTTON_TEXT) { + val authButton = JButton(object : AbstractAction(TRUST_AND_SCAN_BUTTON_TEXT) { override fun actionPerformed(e: ActionEvent?) { val analytics = getSnykAnalyticsService() analytics.logAuthenticateButtonIsClicked(authenticateEvent()) @@ -49,6 +53,12 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable { pluginSettings().token = token SnykCodeParams.instance.sessionToken = token + // explicitly add the project to workspace trusted paths, because + // scan can be auto-triggered depending on "settings.pluginFirstRun" value + project.basePath?.let { + service<WorkspaceTrustService>().addTrustedPath(Paths.get(it)) + } + val userId = analytics.obtainUserId(token) if (userId.isNotBlank()) { analytics.setUserId(userId) @@ -102,20 +112,26 @@ class SnykAuthPanel(val project: Project) : JPanel(), Disposable { } private fun descriptionLabelText(): String { + val trustWarningDescription = SnykBundle.message("snyk.panel.auth.trust.warning.text") return """ - |<html><ol> + |<html> + |<ol> | <li align="left">Authenticate to Snyk.io</li> | <li align="left">Analyze code for issues and vulnerabilities</li> | <li align="left">Improve your code and upgrade dependencies</li> |</ol> + |<br> + |$trustWarningDescription + |<br> + |<br> |</html> """.trimMargin() } override fun dispose() {} companion object { - const val AUTHENTICATE_BUTTON_TEXT = "Test code now" + const val TRUST_AND_SCAN_BUTTON_TEXT = "Trust project and scan" val messagePolicyAndTermsHtml = """ <br>
src/main/kotlin/snyk/trust/TrustedProjects.kt+73 −0 added@@ -0,0 +1,73 @@ +@file:JvmName("TrustedProjectsKt") +package snyk.trust + +import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.ui.MessageDialogBuilder +import com.intellij.openapi.ui.Messages +import snyk.SnykBundle +import java.nio.file.Files +import java.nio.file.Path + +private val LOG = Logger.getInstance("snyk.trust.TrustedProjects") + +/** + * Shows the "Trust and Scan Project?" dialog, if the user wasn't asked yet if they trust this project, + * and sets the project trusted state according to the user choice. + * + * @return `false` if the user chose not to scan the project at all; `true` otherwise + */ +fun confirmScanningAndSetWorkspaceTrustedStateIfNeeded(projectFileOrDir: Path): Boolean { + val projectDir = if (Files.isDirectory(projectFileOrDir)) projectFileOrDir else projectFileOrDir.parent + + val trustService = service<WorkspaceTrustService>() + val trustedState = trustService.isPathTrusted(projectDir) + if (!trustedState) { + LOG.info("Asking user to trust the project ${projectDir.fileName}") + return when (confirmScanningUntrustedProject(projectDir)) { + ScanUntrustedProjectChoice.TRUST_AND_SCAN -> { + trustService.addTrustedPath(projectDir) + true + } + + ScanUntrustedProjectChoice.CANCEL -> false + } + } + + return true +} + +private fun confirmScanningUntrustedProject(projectDir: Path): ScanUntrustedProjectChoice { + val fileName = projectDir.fileName ?: projectDir.toString() + val title = SnykBundle.message("snyk.trust.dialog.warning.title", fileName) + val message = SnykBundle.message("snyk.trust.dialog.warning.text") + val trustButton = SnykBundle.message("snyk.trust.dialog.warning.button.trust") + val distrustButton = SnykBundle.message("snyk.trust.dialog.warning.button.distrust") + + var choice = ScanUntrustedProjectChoice.CANCEL + + invokeAndWaitIfNeeded { + val result = MessageDialogBuilder + .yesNo(title, message) + .icon(Messages.getWarningIcon()) + .yesText(trustButton) + .noText(distrustButton) + .show() + + choice = if (result == Messages.YES) { + LOG.info("User trusts the project $fileName for scans") + ScanUntrustedProjectChoice.TRUST_AND_SCAN + } else { + LOG.info("User doesn't trust the project $fileName for scans") + ScanUntrustedProjectChoice.CANCEL + } + } + + return choice +} + +enum class ScanUntrustedProjectChoice { + TRUST_AND_SCAN, + CANCEL; +}
src/main/kotlin/snyk/trust/WorkspaceTrustService.kt+38 −0 added@@ -0,0 +1,38 @@ +package snyk.trust + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.logger +import java.nio.file.Path +import java.nio.file.Paths + +private val LOG = logger<WorkspaceTrustService>() + +@Service +class WorkspaceTrustService { + + val settings + get() = service<WorkspaceTrustSettings>() + + fun addTrustedPath(path: Path) { + LOG.debug("Adding trusted path: $path") + settings.addTrustedPath(path.toString()) + } + + fun isPathTrusted(path: Path): Boolean { + LOG.debug("Verifying if path is trusted: $path") + return settings.getTrustedPaths().asSequence().mapNotNull { + try { + Paths.get(it) + } catch (e: Exception) { + LOG.warn(e) + null + } + }.any { + LOG.debug("Checking if the $it is an ancestor $path") + it.isAncestor(path) + } + } +} + +internal fun Path.isAncestor(child: Path): Boolean = child.startsWith(this)
src/main/kotlin/snyk/trust/WorkspaceTrustSettings.kt+29 −0 added@@ -0,0 +1,29 @@ +package snyk.trust + +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.RoamingType +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.util.xmlb.annotations.OptionTag +import java.util.Collections + +@Service +@State( + name = "Workspace.Trust.Settings", + storages = [Storage("snyk.settings.xml", roamingType = RoamingType.DISABLED)] +) +class WorkspaceTrustSettings : SimplePersistentStateComponent<WorkspaceTrustSettings.State>(State()) { + class State : BaseState() { + @get:OptionTag("TRUSTED_PATHS") + var trustedPaths by list<String>() + } + + fun addTrustedPath(path: String) { + state.trustedPaths.add(path) + } + + fun getTrustedPaths(): List<String> = Collections.unmodifiableList(state.trustedPaths) +} +
src/main/resources/SnykBundle.properties+7 −0 modified@@ -1,3 +1,10 @@ +snyk.panel.auth.trust.warning.text=When scanning project files, Snyk may automatically execute code<br>such as invoking the package manager to get dependency information.<br>You should only scan projects you trust. <a href="https://docs.snyk.io/ide-tools/jetbrains-plugins/folder-trust">More info</a> + snyk.settings.organization.tooltip.description=<p>Specify an organization slug name to run tests for that organization.</p><p>It must match the URL slug as displayed in the URL of your org in the Snyk UI: <code>https://app.snyk.io/org/<b>[orgslugname]</b></code></p> snyk.settings.organization.tooltip.linkText=Learn more about organization snyk.settings.organization.tooltip.link=https://docs.snyk.io/integrations/ide-tools/jetbrains-plugins#organization-setting + +snyk.trust.dialog.warning.button.distrust=Don't scan +snyk.trust.dialog.warning.button.trust=Trust project and continue +snyk.trust.dialog.warning.text=When scanning project files for vulnerabilities, Snyk may automatically execute code such as invoking the package manager to get dependency information.<br><br>You should only scan projects you trust.<br><br><a href="https://docs.snyk.io/ide-tools/jetbrains-plugins/folder-trust">More info</a> +snyk.trust.dialog.warning.title=Trust and Scan Project ''{0}''?
src/test/kotlin/snyk/InMemoryFsRule.kt+33 −0 added@@ -0,0 +1,33 @@ +package snyk + +import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import org.junit.rules.ExternalResource +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.net.URLEncoder +import java.nio.file.FileSystem +import kotlin.properties.Delegates + +class InMemoryFsRule : ExternalResource() { + private var _fs: FileSystem? = null + private var sanitizedName: String by Delegates.notNull() + + override fun apply(base: Statement, description: Description): Statement { + sanitizedName = URLEncoder.encode(description.methodName, Charsets.UTF_8.name()) + return super.apply(base, description) + } + + val fs: FileSystem + get() { + if (_fs == null) { + _fs = Jimfs.newFileSystem(Configuration.unix()) + } + return _fs!! + } + + override fun after() { + _fs?.close() + _fs = null + } +}
src/test/kotlin/snyk/trust/WorkspaceTrustServiceTest.kt+69 −0 added@@ -0,0 +1,69 @@ +package snyk.trust + +import org.hamcrest.core.IsEqual.equalTo +import org.junit.Assert.assertThat +import org.junit.Rule +import org.junit.Test +import snyk.InMemoryFsRule + +class WorkspaceTrustServiceTest { + + @JvmField + @Rule + val memoryFs = InMemoryFsRule() + + @Test + fun `isAncestor should return true for itself`() { + val absoluteSimpleDir = memoryFs.fs.getPath("/opt/projects/simple") + val relativeSimpleDir = memoryFs.fs.getPath("projects/simple") + + assertThat(absoluteSimpleDir.isAncestor(absoluteSimpleDir), equalTo(true)) + assertThat(relativeSimpleDir.isAncestor(relativeSimpleDir), equalTo(true)) + } + + @Test + fun `isAncestor should return true for inner folder inside of outer`() { + val absoluteOuterDir = memoryFs.fs.getPath("/opt/projects/outer") + val absoluteInnerDir = memoryFs.fs.getPath("/opt/projects/outer/inner") + val relativeOuterDir = memoryFs.fs.getPath("projects/outer") + val relativeInnerDir = memoryFs.fs.getPath("projects/outer/inner") + + assertThat(absoluteOuterDir.isAncestor(absoluteInnerDir), equalTo(true)) + assertThat(relativeOuterDir.isAncestor(relativeInnerDir), equalTo(true)) + } + + @Test + fun `isAncestor should return true for inner folder with more than one level inside of outer`() { + val absoluteOuterDir = memoryFs.fs.getPath("/opt/projects/outer") + val absoluteInnerDir = memoryFs.fs.getPath("/opt/projects/outer/level1/level2/level3/inner") + val relativeOuterDir = memoryFs.fs.getPath("projects/outer") + val relativeInnerDir = memoryFs.fs.getPath("projects/outer/level1/level2/level3/inner") + + assertThat(absoluteOuterDir.isAncestor(absoluteInnerDir), equalTo(true)) + assertThat(relativeOuterDir.isAncestor(relativeInnerDir), equalTo(true)) + } + + @Test + fun `isAncestor should return false for outer folder`() { + val absoluteOuterDir = memoryFs.fs.getPath("/opt/projects/outer") + val absoluteInnerDir = memoryFs.fs.getPath("/opt/projects/outer/inner") + val relativeOuterDir = memoryFs.fs.getPath("projects/outer") + val relativeInnerDir = memoryFs.fs.getPath("projects/outer/inner") + + assertThat(absoluteInnerDir.isAncestor(absoluteOuterDir), equalTo(false)) + assertThat(relativeInnerDir.isAncestor(relativeOuterDir), equalTo(false)) + } + + @Test + fun `isAncestor should return false for folders on different levels`() { + val absoluteFirstDir = memoryFs.fs.getPath("/opt/projects/first") + val absoluteSecondDir = memoryFs.fs.getPath("/opt/projects/second") + val relativeFirstDir = memoryFs.fs.getPath("projects/first") + val relativeSecondDir = memoryFs.fs.getPath("projects/second") + + assertThat(absoluteFirstDir.isAncestor(absoluteSecondDir), equalTo(false)) + assertThat(absoluteSecondDir.isAncestor(absoluteFirstDir), equalTo(false)) + assertThat(relativeFirstDir.isAncestor(relativeSecondDir), equalTo(false)) + assertThat(relativeSecondDir.isAncestor(relativeFirstDir), equalTo(false)) + } +}
0b53dbbd4a31feat: add workspace trust (#217)
19 files changed · +638 −38
CHANGELOG.md+6 −1 modified@@ -1,5 +1,10 @@ # Snyk Changelog +## [1.1.31] + +### Added +- Adds workspace trust mechanism to ensure scans are run on the trusted projects. + ## [1.1.30] ### Changed @@ -48,7 +53,7 @@ ### Added - Organization description information in settings. - + ### Fixed - Changing custom endpoint settings leads to authentication errors.
Snyk.VisualStudio.Extension.2022/Snyk.VisualStudio.Extension.2022.csproj+9 −0 modified@@ -49,6 +49,9 @@ </PropertyGroup> <ItemGroup> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="TrustDialogWindow.xaml.cs"> + <DependentUpon>TrustDialogWindow.xaml</DependentUpon> + </Compile> </ItemGroup> <ItemGroup> <None Include="source.extension.vsixmanifest"> @@ -120,6 +123,12 @@ <Name>Snyk.Common</Name> </ProjectReference> </ItemGroup> + <ItemGroup> + <Page Include="TrustDialogWindow.xaml"> + <Generator>MSBuild:Compile</Generator> + <SubType>Designer</SubType> + </Page> + </ItemGroup> <Import Project="..\Snyk.VisualStudio.Extension.Shared\Snyk.VisualStudio.Extension.Shared.projitems" Label="Shared" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(VSToolsPath)\VSSDK\Microsoft.VsSDK.targets" Condition="'$(VSToolsPath)' != ''" />
Snyk.VisualStudio.Extension.2022/TrustDialogWindow.xaml+59 −0 added@@ -0,0 +1,59 @@ +<ui:DialogWindow x:Class="Snyk.VisualStudio.Extension.TrustDialogWindow" + x:Name="TrustWindow" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:toolkit="clr-namespace:Community.VisualStudio.Toolkit;assembly=Community.VisualStudio.Toolkit" + xmlns:ui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0" + mc:Ignorable="d" + WindowStartupLocation="CenterScreen" + IsCloseButtonEnabled="True" + HasHelpButton="False" + MinHeight="290" Height="290" + MinWidth="500" Width="500" + BorderBrush="{x:Static SystemColors.WindowFrameBrush}" BorderThickness="1" + WindowStyle="None" ResizeMode="NoResize" AllowsTransparency="True" + xmlns:catalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog" + xmlns:imaging="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.Imaging" + toolkit:Themes.UseVsTheme="True" + Title="Snyk - This folder has not been trusted" + MouseDown="TrustDialogWindow_OnMouseDown"> + <DockPanel Margin="10"> + <Button DockPanel.Dock="Top" HorizontalAlignment="Right" Click="DoNotTrustButton_OnClick" MinWidth="1" MinHeight="1" Width="35" Margin="0" Padding="0"> + <imaging:CrispImage Moniker="{x:Static catalog:KnownMonikers.Close}"/> + </Button> + <StackPanel HorizontalAlignment="Right" DockPanel.Dock="Bottom" Orientation="Horizontal"> + <Button x:Name="TrustButton" Margin="5, 5" Content="Trust folder and continue" Click="TrustButton_OnClick"/> + <Button x:Name="DoNotTrustButton" Margin="5, 5" Content="Don't scan" Click="DoNotTrustButton_OnClick"/> + </StackPanel> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="auto"/> + <RowDefinition Height="auto"/> + </Grid.RowDefinitions> + <Grid Grid.Row="0"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*"/> + <ColumnDefinition Width="5*"/> + </Grid.ColumnDefinitions> + <imaging:CrispImage Grid.Column="0" Width="50" Moniker="{x:Static catalog:KnownMonikers.StatusSecurityWarning}"/> + <StackPanel VerticalAlignment="Center" Grid.Column="1" Margin="0, 0, 5, 0"> + <TextBlock FontSize="14">This folder has not been trusted:</TextBlock> + <TextBlock FontSize="14" FontWeight="Bold" TextWrapping="Wrap" Text="{Binding ElementName=TrustWindow, Path=FolderPath}"/> + </StackPanel> + </Grid> + <StackPanel Grid.Row="1" Margin="5"> + <TextBlock TextWrapping="Wrap"> + When scanning folder files for vulnerabilities, Snyk may automatically execute code such as invoking the package manager to get dependency information. You should only scan folders you trust. + </TextBlock> + <TextBlock> + <LineBreak/> + <Hyperlink NavigateUri="https://docs.snyk.io/ide-tools/visual-studio-extension/workspace-trust" RequestNavigate="Hyperlink_OnRequestNavigate"> + More information + </Hyperlink> + </TextBlock> + </StackPanel> + </Grid> + </DockPanel> +</ui:DialogWindow> \ No newline at end of file
Snyk.VisualStudio.Extension.2022/TrustDialogWindow.xaml.cs+49 −0 added@@ -0,0 +1,49 @@ + +namespace Snyk.VisualStudio.Extension +{ + using System.Diagnostics; + using System.Windows; + using System.Windows.Input; + using System.Windows.Navigation; + using Microsoft.VisualStudio.PlatformUI; + + /// <summary> + /// Trusted dialog window for Visual Studio 2022. + /// </summary> + public partial class TrustDialogWindow : DialogWindow + { + public TrustDialogWindow(string folderPath) + { + this.FolderPath = folderPath; + this.InitializeComponent(); + } + + public string FolderPath { get; } + + private void DoNotTrustButton_OnClick(object sender, RoutedEventArgs e) + { + this.DialogResult = false; + this.Close(); + } + + private void TrustButton_OnClick(object sender, RoutedEventArgs e) + { + this.DialogResult = true; + this.Close(); + } + + private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); + e.Handled = true; + } + + private void TrustDialogWindow_OnMouseDown(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Left) + { + this.DragMove(); + } + } + } +}
Snyk.VisualStudio.Extension.Shared/Service/ISnykServiceProvider.cs+2 −0 modified@@ -38,6 +38,8 @@ public interface ISnykServiceProvider /// </summary> ISolutionService SolutionService { get; } + IWorkspaceTrustService WorkspaceTrustService { get; } + /// <summary> /// Gets Tasks service instance. /// </summary>
Snyk.VisualStudio.Extension.Shared/Service/IWorkspaceTrustService.cs+9 −0 added@@ -0,0 +1,9 @@ +namespace Snyk.VisualStudio.Extension.Shared.Service +{ + public interface IWorkspaceTrustService + { + bool IsFolderTrusted(string absoluteFolderPath); + + void AddFolderToTrusted(string absoluteFolderPath); + } +}
Snyk.VisualStudio.Extension.Shared/Service/SnykService.cs+8 −0 modified@@ -52,6 +52,8 @@ public class SnykService : ISnykServiceProvider, ISnykService private ISentryService sentryService; + private IWorkspaceTrustService workspaceTrustService; + /// <summary> /// Initializes a new instance of the <see cref="SnykService"/> class. /// </summary> @@ -68,6 +70,11 @@ public class SnykService : ISnykServiceProvider, ISnykService /// </summary> public ISolutionService SolutionService => SnykSolutionService.Instance; + /// <summary> + /// Gets solution service. + /// </summary> + public IWorkspaceTrustService WorkspaceTrustService => this.workspaceTrustService; + /// <summary> /// Gets Tasks service. /// </summary> @@ -241,6 +248,7 @@ public async Task InitializeAsync(CancellationToken cancellationToken) this.dte = await this.serviceProvider.GetServiceAsync(typeof(DTE)) as DTE2; await SnykSolutionService.Instance.InitializeAsync(this); this.tasksService = SnykTasksService.Instance; + this.workspaceTrustService = new WorkspaceTrustService(this.UserStorageSettingsService); NotificationService.Initialize(this); VsStatusBar.Initialize(this);
Snyk.VisualStudio.Extension.Shared/Service/SnykTasksService.cs+45 −1 modified@@ -4,14 +4,17 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Community.VisualStudio.Toolkit; using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Shell.Interop; using Serilog; using Snyk.Analytics; using Snyk.Code.Library.Domain.Analysis; using Snyk.Common; using Snyk.VisualStudio.Extension.Shared.CLI; using Snyk.VisualStudio.Extension.Shared.CLI.Download; using Snyk.VisualStudio.Extension.Shared.Service.Domain; + using Snyk.VisualStudio.Extension.Shared.UI; using static Snyk.VisualStudio.Extension.Shared.CLI.Download.SnykCliDownloader; using Task = System.Threading.Tasks.Task; @@ -198,7 +201,6 @@ public void CancelTasks() public async Task ScanAsync() { Logger.Information("Enter Scan method"); - try { var selectedFeatures = await this.GetFeaturesSettingsAsync(); @@ -212,6 +214,13 @@ public async Task ScanAsync() return; } + var isFolderTrusted = await this.IsFolderTrustedAsync(); + if (!isFolderTrusted) + { + Logger.Information("Workspace folder was not trusted for scanning."); + return; + } + this.serviceProvider.AnalyticsService.LogAnalysisIsTriggeredEvent(this.GetSelectedFeatures(selectedFeatures)); var ossScanTask = this.ScanOssAsync(selectedFeatures); @@ -225,6 +234,41 @@ public async Task ScanAsync() } } + /// <summary> + /// Checks if opened solution folder is trusted. If not, prompts a user with trust permission. + /// </summary> + /// <returns>Folder is trusted or not.</returns> + public async Task<bool> IsFolderTrustedAsync() + { + var solutionFolderPath = await this.serviceProvider.SolutionService.GetSolutionFolderAsync(); + var isFolderTrusted = this.serviceProvider.WorkspaceTrustService.IsFolderTrusted(solutionFolderPath); + + if (string.IsNullOrEmpty(solutionFolderPath) || isFolderTrusted) + { + return true; + } + + var trustDialog = new TrustDialogWindow(solutionFolderPath); + var trusted = trustDialog.ShowModal(); + + if (trusted != true) + { + return false; + } + + try + { + this.serviceProvider.WorkspaceTrustService.AddFolderToTrusted(solutionFolderPath); + Logger.Information("Workspace folder was trusted: {SolutionFolderPath}", solutionFolderPath); + return true; + } + catch (ArgumentException e) + { + Logger.Error(e, "Failed to add folder to trusted list."); + throw; + } + } + /// <summary> /// Start a CLI download task in background thread. /// </summary>
Snyk.VisualStudio.Extension.Shared/Service/WorkspaceTrustService.cs+89 −0 added@@ -0,0 +1,89 @@ +namespace Snyk.VisualStudio.Extension.Shared.Service +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Serilog; + using Snyk.Common; + using Snyk.VisualStudio.Extension.Shared.Settings; + + public class WorkspaceTrustService : IWorkspaceTrustService + { + private static readonly ILogger Logger = LogManager.ForContext<WorkspaceTrustService>(); + + private readonly IUserStorageSettingsService settingsService; + + public WorkspaceTrustService(IUserStorageSettingsService settingsService) + { + this.settingsService = settingsService; + } + + public void AddFolderToTrusted(string absoluteFolderPath) + { + if (!Path.IsPathRooted(absoluteFolderPath)) + { + throw new ArgumentException("Trusted folder path provided is not absolute."); + } + + if (!Directory.Exists(absoluteFolderPath)) + { + throw new ArgumentException("Trusted folder doesn't exist."); + } + + try + { + var trustedFolders = this.settingsService.TrustedFolders; + trustedFolders.Add(absoluteFolderPath); + this.settingsService.TrustedFolders = trustedFolders; + } + catch (Exception e) + { + Logger.Error(e, "Failed to add a folder to trusted."); + } + } + + public bool IsFolderTrusted(string absoluteFolderPath) + { + var trustedFolders = this.settingsService.TrustedFolders; + + foreach (var trustedFolder in trustedFolders) + { + if (this.IsSubFolderOrEqual(trustedFolder, absoluteFolderPath)) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Verify if subfolder is rooted at parent path. + /// </summary> + /// <param name="parentPath">Parent path to check against.</param> + /// <param name="childPath">Subfolder path to verify.</param> + /// <returns>Returns true if childPath is subfolder of parentPath, or equal to it.</returns> + private bool IsSubFolderOrEqual(string parentPath, string childPath) + { + var parentUri = new Uri(parentPath); + if (new Uri(childPath).Equals(parentUri)) + { + return true; + } + + var childUri = new DirectoryInfo(childPath).Parent; + while (childUri != null) + { + if (new Uri(childUri.FullName).Equals(parentUri)) + { + return true; + } + + childUri = childUri.Parent; + } + + return false; + } + } +}
Snyk.VisualStudio.Extension.Shared/Settings/IUserStorageSettingsService.cs+9 −0 added@@ -0,0 +1,9 @@ +namespace Snyk.VisualStudio.Extension.Shared.Settings +{ + using System.Collections.Generic; + + public interface IUserStorageSettingsService + { + ISet<string> TrustedFolders { get; set; } + } +}
Snyk.VisualStudio.Extension.Shared/Settings/SnykSettings.cs+6 −1 modified@@ -61,8 +61,13 @@ public SnykSettings() public bool BinariesAutoUpdateEnabled { get; set; } = true; /// <summary> - /// Gets or sets the value of the custom CLI path + /// Gets or sets the value of the custom CLI path. /// </summary> public string CustomCliPath { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets an array of workspace trusted folders. + /// </summary> + public ISet<string> TrustedFolders { get; set; } = new HashSet<string>(); } } \ No newline at end of file
Snyk.VisualStudio.Extension.Shared/Settings/SnykUserStorageSettingsService.cs+16 −1 modified@@ -1,6 +1,7 @@ namespace Snyk.VisualStudio.Extension.Shared.Settings { using System; + using System.Collections.Generic; using System.Threading.Tasks; using Serilog; using Snyk.Common; @@ -9,7 +10,7 @@ /// <summary> /// Service for solution settings. /// </summary> - public class SnykUserStorageSettingsService + public class SnykUserStorageSettingsService : IUserStorageSettingsService { private static readonly ILogger Logger = LogManager.ForContext<SnykUserStorageSettingsService>(); @@ -51,6 +52,20 @@ public string CliCustomPath } } + /// <summary> + /// Gets or sets trusted folders list. + /// </summary> + public ISet<string> TrustedFolders + { + get => this.LoadSettings().TrustedFolders; + set + { + var settings = this.LoadSettings(); + settings.TrustedFolders = value; + this.settingsLoader.Save(settings); + } + } + /// <summary> /// Get CLI additional options string. /// </summary>
Snyk.VisualStudio.Extension.Shared/Snyk.VisualStudio.Extension.Shared.projitems+3 −0 modified@@ -31,13 +31,16 @@ <Compile Include="$(MSBuildThisFileDirectory)Service\ApiEndpointResolver.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Service\ISentryService.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Service\ISnykApiService.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)Service\IWorkspaceTrustService.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Service\LocalCodeEngine.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Service\SentryService.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Service\SolutionType.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Service\IOssService.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Service\OssScanException.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Service\OssService.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)Service\WorkspaceTrustService.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Settings\ISnykOptions.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)Settings\IUserStorageSettingsService.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Settings\SnykGeneralOptionsDialogPage.cs"> <SubType>Component</SubType> </Compile>
Snyk.VisualStudio.Extension.Shared/UI/Toolwindow/MessagePanel.xaml+15 −9 modified@@ -1,12 +1,12 @@ <UserControl x:Class="Snyk.VisualStudio.Extension.Shared.UI.Toolwindow.MessagePanel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:c="clr-namespace:Snyk.VisualStudio.Extension.Shared.UI.Controls" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:c="clr-namespace:Snyk.VisualStudio.Extension.Shared.UI.Controls" xmlns:toolkit="clr-namespace:Community.VisualStudio.Toolkit;assembly=Community.VisualStudio.Toolkit" toolkit:Themes.UseVsTheme="True" - mc:Ignorable="d" + mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Grid VerticalAlignment="Center"> <Grid.Resources> @@ -32,11 +32,11 @@ <Button x:Name="runScanButton" Content="Run scan" IsDefault="True" Click="RunButton_Click" HorizontalAlignment="Center" MinWidth="80"/> </StackPanel> </StackPanel> - + <StackPanel Name="messagePanel" Visibility="Collapsed" HorizontalAlignment="Center"> <c:TextField x:Name="message" FontWeight="Bold"/> </StackPanel> - + <StackPanel Name="overviewPanel" Visibility="Collapsed" HorizontalAlignment="Center" Orientation="Horizontal"> <Image Source="{StaticResource SnykDogLogo}" Width="48" Margin="0,-30,0,30"/> @@ -48,26 +48,32 @@ 1. Authenticate to Snyk.io </TextBlock> <TextBlock> - 2. Analyze code for issues and vulnerabilities + 2. Analyze code for issues and vulnerabilities </TextBlock> <TextBlock> 3. Improve your code and upgrade dependencies </TextBlock> + <TextBlock Margin="0,10,0,0" TextWrapping="Wrap" Width="550"> + When scanning project files, Snyk may automatically execute code such as invoking the package manager to get dependency information. You should only scan projects you trust. + <Hyperlink NavigateUri="https://docs.snyk.io/ide-tools/visual-studio-extension/workspace-trust" RequestNavigate="Hyperlink_RequestNavigate"> + More info + </Hyperlink> + </TextBlock> <StackPanel HorizontalAlignment="Left" Margin="0,25,0,0"> <Button Name="testCodeNowButton" Click="TestCodeNow_Click" Padding="20,6,20,8"> <Button.Resources> <Style TargetType="Border"> <Setter Property="CornerRadius" Value="3"/> </Style> </Button.Resources> - Test code now + Trust project and scan </Button> </StackPanel> <ProgressBar Name="authenticateSnykProgressBar" Visibility="Collapsed" IsIndeterminate="True" Height="3" Padding="0,2" Margin="0, 5, 0, 0"/> <TextBlock Margin="0, 20, 0, 0" FontSize="11"> By connecting your account with Snyk, you agree </TextBlock> - <TextBlock FontSize="11"> + <TextBlock FontSize="11"> to the Snyk <Hyperlink NavigateUri="https://snyk.io/policies/privacy/" RequestNavigate="Hyperlink_RequestNavigate"> Privacy Policy
Snyk.VisualStudio.Extension.Shared/UI/Toolwindow/MessagePanel.xaml.cs+37 −0 modified@@ -1,20 +1,27 @@ namespace Snyk.VisualStudio.Extension.Shared.UI.Toolwindow { + using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; + using Community.VisualStudio.Toolkit; using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Threading; + using Serilog; + using Serilog.Core; + using Snyk.Common; using Snyk.VisualStudio.Extension.Shared.Service; /// <summary> /// Interaction logic for MessagePanel.xaml. /// </summary> public partial class MessagePanel : UserControl { + private static readonly ILogger Logger = LogManager.ForContext<MessagePanel>(); private readonly IList<StackPanel> panels; /// <summary> @@ -120,6 +127,36 @@ private async void TestCodeNow_Click(object sender, RoutedEventArgs e) this.authenticateSnykProgressBar.Visibility = Visibility.Collapsed; this.testCodeNowButton.IsEnabled = true; + // Add folder to trusted + var solutionFolderPath = await this.ServiceProvider.SolutionService.GetSolutionFolderAsync(); + if (!string.IsNullOrEmpty(solutionFolderPath)) + { + try + { + this.ServiceProvider.WorkspaceTrustService.AddFolderToTrusted(solutionFolderPath); + Logger.Information("Workspace folder was trusted: {SolutionFolderPath}", solutionFolderPath); + } + catch (ArgumentException ex) + { + Logger.Error(ex, "Failed to add folder to trusted list."); + throw ex; + } + } + + // Issue scan + if (authenticationSucceeded) + { + var uiShell = Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider.GetService(typeof(SVsUIShell)) as IVsUIShell; + if (uiShell != null) + { + uiShell.PostExecCommand( + SnykGuids.SnykVSPackageCommandSet, + SnykGuids.RunScanCommandId, + 0, + null); + } + } + var nextPanel = authenticationSucceeded ? (ToolWindowState)RunScanState.Instance : OverviewState.Instance; this.Context.TransitionTo(nextPanel); }
Snyk.VisualStudio.Extension/Snyk.VisualStudio.Extension.csproj+20 −25 modified@@ -26,7 +26,7 @@ <StartAction>Program</StartAction> <StartProgram Condition="'$(DevEnvDir)' != ''">$(DevEnvDir)devenv.exe</StartProgram> <StartArguments>/rootsuffix Exp</StartArguments> - <DeployVsixExtensionFilesDependsOn>$(DeployVsixExtensionFilesDependsOn);SaveSettingsJsonFile</DeployVsixExtensionFilesDependsOn> + <DeployVsixExtensionFilesDependsOn>$(DeployVsixExtensionFilesDependsOn);SaveSettingsJsonFile</DeployVsixExtensionFilesDependsOn> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -121,9 +121,17 @@ <Version>4.5.4</Version> </PackageReference> </ItemGroup> - <ItemGroup /> + <ItemGroup> + <Page Include="TrustDialogWindow.xaml"> + <Generator>MSBuild:Compile</Generator> + <SubType>Designer</SubType> + </Page> + </ItemGroup> <ItemGroup> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="TrustDialogWindow.xaml.cs"> + <DependentUpon>TrustDialogWindow.xaml</DependentUpon> + </Compile> </ItemGroup> <Import Project="..\Snyk.VisualStudio.Extension.Shared\Snyk.VisualStudio.Extension.Shared.projitems" Label="Shared" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> @@ -144,30 +152,17 @@ </Target> --> <Target Name="IncludePackageReferenceDependencies" AfterTargets="GetVsixSourceItems"> - <ItemGroup> - <VSIXSourceItem Include="@(ReferencePath)" /> - </ItemGroup> + <ItemGroup> + <VSIXSourceItem Include="@(ReferencePath)" /> + </ItemGroup> </Target> - <Target Name="SaveSettingsJsonFile" - DependsOnTargets="GetVsixDeploymentPath"> - <Message Condition="!Exists('$(VsixDeploymentPath)settings.json')" - Importance="High" - Text="settings.json does not exist, skipping step"/> - <Message Condition="Exists('$(VsixDeploymentPath)settings.json')" - Importance="High" - Text="Saving settings.json file from $(VsixDeploymentPath)settings.json"/> - <Move Condition="Exists('$(VsixDeploymentPath)settings.json')" - SourceFiles="$(VsixDeploymentPath)settings.json" - DestinationFiles="$(IntermediateOutputPath)settings.json"/> + <Target Name="SaveSettingsJsonFile" DependsOnTargets="GetVsixDeploymentPath"> + <Message Condition="!Exists('$(VsixDeploymentPath)settings.json')" Importance="High" Text="settings.json does not exist, skipping step" /> + <Message Condition="Exists('$(VsixDeploymentPath)settings.json')" Importance="High" Text="Saving settings.json file from $(VsixDeploymentPath)settings.json" /> + <Move Condition="Exists('$(VsixDeploymentPath)settings.json')" SourceFiles="$(VsixDeploymentPath)settings.json" DestinationFiles="$(IntermediateOutputPath)settings.json" /> </Target> - <Target Name="AfterBuild" - DependsOnTargets="GetVsixDeploymentPath"> - <Message - Condition="Exists('$(IntermediateOutputPath)settings.json')" - Text="Copying settings.json back to $(VsixDeploymentPath)" - Importance="High"/> - <Move Condition="Exists('$(IntermediateOutputPath)settings.json')" - DestinationFiles="$(VsixDeploymentPath)settings.json" - SourceFiles="$(IntermediateOutputPath)settings.json"/> + <Target Name="AfterBuild" DependsOnTargets="GetVsixDeploymentPath"> + <Message Condition="Exists('$(IntermediateOutputPath)settings.json')" Text="Copying settings.json back to $(VsixDeploymentPath)" Importance="High" /> + <Move Condition="Exists('$(IntermediateOutputPath)settings.json')" DestinationFiles="$(VsixDeploymentPath)settings.json" SourceFiles="$(IntermediateOutputPath)settings.json" /> </Target> </Project> \ No newline at end of file
Snyk.VisualStudio.Extension.Tests/Service/WorkspaceTrustServiceTest.cs+149 −0 added@@ -0,0 +1,149 @@ +namespace Snyk.VisualStudio.Extension.Tests.Service +{ + using System; + using System.Collections.Generic; + using System.IO; + using Moq; + using Snyk.VisualStudio.Extension.Shared.Service; + using Snyk.VisualStudio.Extension.Shared.Settings; + using Xunit; + + public class WorkspaceTrustServiceTest + { + [Fact] + public void WorkspaceTrustServiceTest_IsFolderTrusted_NotTrusted() + { + var trustedFolders = new HashSet<string>(); + var settingsServiceMock = new Mock<IUserStorageSettingsService>(); + settingsServiceMock.Setup(s => s.TrustedFolders).Returns(trustedFolders); + + var service = new WorkspaceTrustService(settingsServiceMock.Object); + var folderPath = "C:\\Users\\Project"; + + Assert.False(service.IsFolderTrusted(folderPath)); + } + + [Fact] + public void WorkspaceTrustServiceTest_IsFolderTrusted_Trusted() + { + var trustedFolders = new HashSet<string>(); + trustedFolders.Add("C:\\Users\\Project"); + var settingsServiceMock = new Mock<IUserStorageSettingsService>(); + settingsServiceMock.Setup(s => s.TrustedFolders).Returns(trustedFolders); + + var service = new WorkspaceTrustService(settingsServiceMock.Object); + var folderPath = "C:\\Users\\Project"; + + Assert.True(service.IsFolderTrusted(folderPath)); + } + + [Fact] + public void WorkspaceTrustServiceTest_IsFolderTrusted_SubfolderTrusted() + { + var trustedFolders = new HashSet<string>(); + trustedFolders.Add("C:\\Users\\Project"); + + var settingsServiceMock = new Mock<IUserStorageSettingsService>(); + settingsServiceMock.Setup(s => s.TrustedFolders).Returns(trustedFolders); + + var service = new WorkspaceTrustService(settingsServiceMock.Object); + var folderPath = "C:\\Users\\Project\\subfolder"; + + Assert.True(service.IsFolderTrusted(folderPath)); + } + + [Fact] + public void WorkspaceTrustServiceTest_IsFolderTrusted_ParentFolderNotTrusted() + { + var trustedFolders = new HashSet<string>(); + trustedFolders.Add("C:\\Users\\Project\\subfolder"); + + var settingsServiceMock = new Mock<IUserStorageSettingsService>(); + settingsServiceMock.Setup(s => s.TrustedFolders).Returns(trustedFolders); + + var service = new WorkspaceTrustService(settingsServiceMock.Object); + var folderPath = "C:\\Users\\Project"; + + Assert.False(service.IsFolderTrusted(folderPath)); + } + + [Fact] + public void WorkspaceTrustServiceTest_AddFolderToTrusted_NonExistingFolder() + { + var settingsServiceMock = new Mock<IUserStorageSettingsService>(); + + var service = new WorkspaceTrustService(settingsServiceMock.Object); + var folderPath = "C:\\Users\\Project"; + + Assert.Throws<ArgumentException>(() => service.AddFolderToTrusted(folderPath)); + } + + [Fact] + public void WorkspaceTrustServiceTest_AddFolderToTrusted_RelativeFolder() + { + var settingsServiceMock = new Mock<IUserStorageSettingsService>(); + + var service = new WorkspaceTrustService(settingsServiceMock.Object); + var folderPath = "\\Users\\Project"; + + Assert.Throws<ArgumentException>(() => service.AddFolderToTrusted(folderPath)); + } + + [Fact] + public void WorkspaceTrustServiceTest_AddFolderToTrusted_ExistingFolder() + { + var settingsServiceMock = new Mock<IUserStorageSettingsService>(); + settingsServiceMock.Setup(s => s.TrustedFolders).Returns(new HashSet<string>()); + + var service = new WorkspaceTrustService(settingsServiceMock.Object); + var folderPath = Path.GetDirectoryName(Path.GetTempFileName()); + + service.AddFolderToTrusted(folderPath); + + settingsServiceMock.VerifySet(s => s.TrustedFolders = new HashSet<string> { folderPath }, Times.Once); + } + + [Fact] + public void WorkspaceTrustServiceTest_AddFolderToTrusted_MultipleFolders() + { + var settingsServiceMock = new Mock<IUserStorageSettingsService>(); + var presentFolder = "C:\\Users\\Project"; + settingsServiceMock.Setup(s => s.TrustedFolders).Returns(new HashSet<string> { presentFolder }); + + var service = new WorkspaceTrustService(settingsServiceMock.Object); + + var newFolderPath = this.CreateTempDirectory(); + + service.AddFolderToTrusted(newFolderPath); + + settingsServiceMock.VerifySet(s => s.TrustedFolders = new HashSet<string> { presentFolder, newFolderPath }); + } + + [Fact] + public void WorkspaceTrustServiceTest_AddFolderToTrusted_SameFolderTwice() + { + var settingsServiceMock = new Mock<IUserStorageSettingsService>(); + settingsServiceMock.Setup(s => s.TrustedFolders).Returns(new HashSet<string>()); + var service = new WorkspaceTrustService(settingsServiceMock.Object); + + var folderPath1 = this.CreateTempDirectory(); + var folderPath2 = folderPath1; + + service.AddFolderToTrusted(folderPath1); + settingsServiceMock.VerifySet(s => s.TrustedFolders = new HashSet<string> { folderPath1 }, Times.Once); + + service.AddFolderToTrusted(folderPath2); + + // Must not append new entry to collection + settingsServiceMock.VerifySet(s => s.TrustedFolders = new HashSet<string> { folderPath1 }, Times.Exactly(2)); + } + + private string CreateTempDirectory() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + + return tempDirectory; + } + } +}
Snyk.VisualStudio.Extension/TrustDialogWindow.xaml+59 −0 added@@ -0,0 +1,59 @@ +<ui:DialogWindow x:Class="Snyk.VisualStudio.Extension.TrustDialogWindow" + x:Name="trustDialogWindow" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:toolkit="clr-namespace:Community.VisualStudio.Toolkit;assembly=Community.VisualStudio.Toolkit" + xmlns:ui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.14.0" + mc:Ignorable="d" + WindowStartupLocation="CenterScreen" + IsCloseButtonEnabled="True" + HasHelpButton="False" + MinHeight="290" Height="290" + MinWidth="500" Width="500" + BorderBrush="{x:Static SystemColors.WindowFrameBrush}" BorderThickness="1" + WindowStyle="None" ResizeMode="NoResize" AllowsTransparency="True" + xmlns:catalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog" + xmlns:imaging="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.Imaging" + toolkit:Themes.UseVsTheme="True" + Title="Snyk - This folder has not been trusted" + MouseDown="TrustDialogWindow_OnMouseDown"> + <DockPanel Margin="10"> + <Button DockPanel.Dock="Top" HorizontalAlignment="Right" Click="DoNotTrustButton_OnClick" MinWidth="1" MinHeight="1" Width="35" Margin="0" Padding="0"> + <imaging:CrispImage Moniker="{x:Static catalog:KnownMonikers.Close}"/> + </Button> + <StackPanel HorizontalAlignment="Right" DockPanel.Dock="Bottom" Orientation="Horizontal"> + <Button x:Name="TrustButton" Margin="5, 5" Content="Trust folder and continue" Click="TrustButton_OnClick"/> + <Button x:Name="DoNotTrustButton" Margin="5, 5" Content="Don't scan" Click="DoNotTrustButton_OnClick"/> + </StackPanel> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="auto"/> + <RowDefinition Height="auto"/> + </Grid.RowDefinitions> + <Grid Grid.Row="0"> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*"/> + <ColumnDefinition Width="5*"/> + </Grid.ColumnDefinitions> + <imaging:CrispImage Grid.Column="0" Width="50" Moniker="{x:Static catalog:KnownMonikers.StatusSecurityWarning}"/> + <StackPanel VerticalAlignment="Center" Grid.Column="1" Margin="0, 0, 5, 0"> + <TextBlock FontSize="14">This folder has not been trusted:</TextBlock> + <TextBlock FontSize="14" FontWeight="Bold" TextWrapping="Wrap" Text="{Binding ElementName=TrustWindow, Path=FolderPath}"/> + </StackPanel> + </Grid> + <StackPanel Grid.Row="1" Margin="5"> + <TextBlock TextWrapping="Wrap"> + When scanning folder files for vulnerabilities, Snyk may automatically execute code such as invoking the package manager to get dependency information. You should only scan folders you trust. + </TextBlock> + <TextBlock> + <LineBreak/> + <Hyperlink NavigateUri="https://docs.snyk.io/ide-tools/visual-studio-extension/workspace-trust" RequestNavigate="Hyperlink_OnRequestNavigate"> + More information + </Hyperlink> + </TextBlock> + </StackPanel> + </Grid> + </DockPanel> +</ui:DialogWindow> \ No newline at end of file
Snyk.VisualStudio.Extension/TrustDialogWindow.xaml.cs+48 −0 added@@ -0,0 +1,48 @@ +namespace Snyk.VisualStudio.Extension +{ + using System.Diagnostics; + using System.Windows; + using System.Windows.Input; + using System.Windows.Navigation; + using Microsoft.VisualStudio.PlatformUI; + + /// <summary> + /// Trusted dialog window for Visual Studio versions less than 2022. + /// </summary> + public partial class TrustDialogWindow : DialogWindow + { + public TrustDialogWindow(string folderPath) + { + this.FolderPath = folderPath; + this.InitializeComponent(); + } + + public string FolderPath { get; } + + private void DoNotTrustButton_OnClick(object sender, RoutedEventArgs e) + { + this.DialogResult = false; + this.Close(); + } + + private void TrustButton_OnClick(object sender, RoutedEventArgs e) + { + this.DialogResult = true; + this.Close(); + } + + private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); + e.Handled = true; + } + + private void TrustDialogWindow_OnMouseDown(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Left) + { + this.DragMove(); + } + } + } +}
b5a8bce25a35Merge pull request #106 from snyk/feat/trust-feature
22 files changed · +518 −163
CHANGELOG.md+4 −0 modified@@ -3,6 +3,10 @@ ## [2.0.0] - Unreleased ### Changes +- add folder trust feature + +## [2.0.0] - v20221115.132308 +### Changes - adds configuration wizard for custom endpoints ## [2.0.0] - v20221007.135736
plugin/build.properties+1 −1 modified@@ -14,7 +14,7 @@ bin.includes = plugin.xml,\ target/dependency/httpcore-4.4.15.jar,\ target/dependency/jackson-annotations-2.13.4.jar,\ target/dependency/jackson-core-2.13.4.jar,\ - target/dependency/jackson-databind-2.13.4.jar,\ + target/dependency/jackson-databind-2.13.4.2.jar,\ target/dependency/javax.inject-1.jar src.includes =src/,\ icons/
plugin/io.snyk.eclipse.plugin.eml+7 −7 modified@@ -19,22 +19,22 @@ <lib name="httpcore-4.4.15.jar" scope="COMPILE"> <relative-module-cls project-related="jar://$PROJECT_DIR$/plugin/target/dependency/httpcore-4.4.15.jar!/"/> </lib> - <lib name="jackson-annotations-2.13.4.jar" scope="COMPILE"> + <lib name="jackson-annotations-2.13.4.2.jar" scope="COMPILE"> <relative-module-cls project-related="jar://$PROJECT_DIR$/plugin/target/dependency/jackson-annotations-2.13.4.jar!/"/> </lib> - <lib name="jackson-core-2.13.4.jar" scope="COMPILE"> + <lib name="jackson-core-2.13.4.2.jar" scope="COMPILE"> <relative-module-cls project-related="jar://$PROJECT_DIR$/plugin/target/dependency/jackson-core-2.13.4.jar!/"/> </lib> - <lib name="jackson-databind-2.13.4.jar" scope="COMPILE"> - <relative-module-cls project-related="jar://$PROJECT_DIR$/plugin/target/dependency/jackson-databind-2.13.4.jar!/"/> + <lib name="jackson-databind-2.13.4.2.jar" scope="COMPILE"> + <relative-module-cls project-related="jar://$PROJECT_DIR$/plugin/target/dependency/jackson-databind-2.13.4.2.jar!/"/> </lib> <lib name="javax.inject-1.jar" scope="COMPILE"> <relative-module-cls project-related="jar://$PROJECT_DIR$/plugin/target/dependency/javax.inject-1.jar!/"/> </lib> <levels> - <level name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.13.4" value="project"/> - <level name="Maven: com.fasterxml.jackson.core:jackson-core:2.13.4" value="project"/> - <level name="Maven: com.fasterxml.jackson.core:jackson-databind:2.13.4" value="project"/> + <level name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.13.4.2" value="project"/> + <level name="Maven: com.fasterxml.jackson.core:jackson-core:2.13.4.2" value="project"/> + <level name="Maven: com.fasterxml.jackson.core:jackson-databind:2.13.4.2" value="project"/> <level name="Maven: org.apache.commons:commons-lang3:3.12.0" value="project"/> <level name="Maven: org.apache.httpcomponents:httpcore:4.4.15" value="project"/> <level name="Maven: org.apache.httpcomponents:httpclient:4.5.13" value="project"/>
plugin/META-INF/MANIFEST.MF+2 −2 modified@@ -10,7 +10,7 @@ Require-Bundle: org.eclipse.ui, org.eclipse.core.runtime, org.eclipse.jdt.core, org.eclipse.core.resources, - org.eclipse.lsp4e;bundle-version="0.13.9", + org.eclipse.lsp4e;bundle-version="[0.13.9,0.14.0.qualifier]", org.eclipse.lsp4e.jdt;bundle-version="0.10.1", org.eclipse.equinox.security, org.eclipse.equinox.security.ui, @@ -33,5 +33,5 @@ Bundle-ClassPath: ., target/dependency/httpcore-4.4.15.jar, target/dependency/jackson-annotations-2.13.4.jar, target/dependency/jackson-core-2.13.4.jar, - target/dependency/jackson-databind-2.13.4.jar, + target/dependency/jackson-databind-2.13.4.2.jar, target/dependency/javax.inject-1.jar
plugin/pom.xml+1 −1 modified@@ -29,7 +29,7 @@ <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> - <version>2.13.4</version> + <version>2.13.4.2</version> <type>jar</type> </dependency> <dependency>
plugin/src/main/java/io/snyk/eclipse/plugin/properties/PreferencesPage.java+106 −92 modified@@ -8,6 +8,9 @@ import io.snyk.languageserver.LsRuntimeEnvironment; import io.snyk.languageserver.download.HttpClientFactory; import io.snyk.languageserver.download.LsBinaries; + +import java.io.File; + import org.eclipse.core.net.proxy.IProxyData; import org.eclipse.jface.preference.BooleanFieldEditor; import org.eclipse.jface.preference.FieldEditor; @@ -19,103 +22,114 @@ import org.eclipse.ui.IWorkbenchPreferencePage; public class PreferencesPage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { - private BooleanFieldEditor snykCodeCheckbox; - - public PreferencesPage() { - super(GRID); - } - - @Override - public void init(IWorkbench workbench) { - setPreferenceStore(io.snyk.eclipse.plugin.properties.preferences.Preferences.getInstance().getStore()); - setMessage("Snyk Preferences"); - } - - @Override - protected void createFieldEditors() { - TokenFieldEditor tokenField = new TokenFieldEditor( - io.snyk.eclipse.plugin.properties.preferences.Preferences.getInstance(), - io.snyk.eclipse.plugin.properties.preferences.Preferences.AUTH_TOKEN_KEY, "Snyk API Token:", - getFieldEditorParent()); - addField(tokenField); - addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.PATH_KEY, "Path:", - getFieldEditorParent())); - addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ENDPOINT_KEY, - "Custom Endpoint:", getFieldEditorParent())); - addField(new BooleanFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.INSECURE_KEY, - "Allow unknown certificate authorities", getFieldEditorParent())); - - addField(space()); + private BooleanFieldEditor snykCodeCheckbox; + + public PreferencesPage() { + super(GRID); + } + + @Override + public void init(IWorkbench workbench) { + setPreferenceStore(io.snyk.eclipse.plugin.properties.preferences.Preferences.getInstance().getStore()); + setMessage("Snyk Preferences"); + } + + @Override + protected void createFieldEditors() { + TokenFieldEditor tokenField = new TokenFieldEditor( + io.snyk.eclipse.plugin.properties.preferences.Preferences.getInstance(), + io.snyk.eclipse.plugin.properties.preferences.Preferences.AUTH_TOKEN_KEY, "Snyk API Token:", + getFieldEditorParent()); + addField(tokenField); + addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.PATH_KEY, "Path:", + getFieldEditorParent())); + addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ENDPOINT_KEY, + "Custom Endpoint:", getFieldEditorParent())); + addField(new BooleanFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.INSECURE_KEY, + "Allow unknown certificate authorities", getFieldEditorParent())); + + addField(space()); addField(new LabelFieldEditor("The following options involve the Snyk Language Server.", getFieldEditorParent())); - addField(new LabelFieldEditor( - "Activating Snyk Code will cause upload of source code to Snyk or the given endpoint address.", - getFieldEditorParent())); - addField(space()); + addField(new LabelFieldEditor( + "Activating Snyk Code will cause upload of source code to Snyk or the given endpoint address.", + getFieldEditorParent())); + addField(space()); addField(new BooleanFieldEditor( io.snyk.eclipse.plugin.properties.preferences.Preferences.ACTIVATE_SNYK_OPEN_SOURCE, - "Snyk Open Source enabled", getFieldEditorParent())); - snykCodeCheckbox = new BooleanFieldEditor( - io.snyk.eclipse.plugin.properties.preferences.Preferences.ACTIVATE_SNYK_CODE, "Snyk Code enable" + "d", - getFieldEditorParent()); - - addField(snykCodeCheckbox); - addField(new BooleanFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ACTIVATE_SNYK_IAC, - "Snyk Infrastructure-as-Code enabled", getFieldEditorParent())); - - addField(space()); - addField(new LabelFieldEditor("Advanced options:", getFieldEditorParent())); - addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ORGANIZATION_KEY, - "Organization:", getFieldEditorParent())); - addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ADDITIONAL_PARAMETERS, - "Additional Parameters:", getFieldEditorParent())); - addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ADDITIONAL_ENVIRONMENT, - "Additional Environment:", getFieldEditorParent())); - - addField(space()); - BooleanFieldEditor manageBinaries = new BooleanFieldEditor(Preferences.MANAGE_BINARIES_AUTOMATICALLY, - "Update and install Snyk binaries automatically", getFieldEditorParent()); - manageBinaries.setPropertyChangeListener((PropertyChangeEvent propertyChangeEvent) -> { - System.out.println("managed bionaries changed"); - }); - addField(manageBinaries); - addField(new FileFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.LS_BINARY_KEY, - "Snyk Language Server:", getFieldEditorParent())); - addField(new FileFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.CLI_PATH, "Snyk CLI:", - getFieldEditorParent())); - - addField(space()); - - addField(new BooleanFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.SEND_ERROR_REPORTS, - "Send error reports to Snyk", getFieldEditorParent())); + "Snyk Open Source enabled", getFieldEditorParent())); + snykCodeCheckbox = new BooleanFieldEditor( + io.snyk.eclipse.plugin.properties.preferences.Preferences.ACTIVATE_SNYK_CODE, "Snyk Code enable" + "d", + getFieldEditorParent()); + + addField(snykCodeCheckbox); + addField(new BooleanFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ACTIVATE_SNYK_IAC, + "Snyk Infrastructure-as-Code enabled", getFieldEditorParent())); + + addField(space()); + addField(new LabelFieldEditor("Advanced options:", getFieldEditorParent())); + addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ORGANIZATION_KEY, + "Organization:", getFieldEditorParent())); + addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ADDITIONAL_PARAMETERS, + "Additional Parameters:", getFieldEditorParent())); + addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.ADDITIONAL_ENVIRONMENT, + "Additional Environment:", getFieldEditorParent())); + + addField(space()); + BooleanFieldEditor manageBinaries = new BooleanFieldEditor(Preferences.MANAGE_BINARIES_AUTOMATICALLY, + "Update and install Snyk binaries automatically", getFieldEditorParent()); + manageBinaries.setPropertyChangeListener((PropertyChangeEvent propertyChangeEvent) -> { + System.out.println("managed bionaries changed"); + }); + addField(manageBinaries); + addField(new FileFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.LS_BINARY_KEY, + "Snyk Language Server:", getFieldEditorParent())); + addField(new FileFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.CLI_PATH, "Snyk CLI:", + getFieldEditorParent())); + + addField(space()); + + addField(new BooleanFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.SEND_ERROR_REPORTS, + "Send error reports to Snyk", getFieldEditorParent())); addField(new BooleanFieldEditor(Preferences.ENABLE_TELEMETRY, "Send usage statistics to Snyk", getFieldEditorParent())); - disableSnykCodeIfOrgDisabled(); - } - - private FieldEditor space() { - return new LabelFieldEditor("", getFieldEditorParent()); - } - - @Override - public boolean performOk() { - boolean superOK = super.performOk(); - var snykView = SnykStartup.getSnykView(); - snykView.disableRunAbortActions(); - snykView.toggleRunActionEnablement(); - disableSnykCodeIfOrgDisabled(); - - new LsConfigurationUpdater().configurationChanged(); - return superOK; - } - - private void disableSnykCodeIfOrgDisabled() { - var apiClient = new ApiClient(); - if (snykCodeCheckbox.getBooleanValue() && !apiClient.checkSnykCodeEnablement()) { - String message = "Snyk Code disabled, because it is not enabled for your organization. After you close this preference page, it will stay disabled."; - snykCodeCheckbox.setLabelText(snykCodeCheckbox.getLabelText()+" ("+message+")"); - SnykLogger.logInfo(message); - } - } + + addField(space()); + + addField(new LabelFieldEditor( + "Only trusted paths are scanned by Snyk. The Trusted Folders setting allows to specify, which \n" + + "paths are safe to scan. Every path below a given path is considered safe to scan. \n" + + "Please separate entries with \"" + File.pathSeparator + "\".", + getFieldEditorParent())); + addField(new StringFieldEditor(io.snyk.eclipse.plugin.properties.preferences.Preferences.TRUSTED_FOLDERS, + "Trusted Folders:", getFieldEditorParent())); + + disableSnykCodeIfOrgDisabled(); + } + + private FieldEditor space() { + return new LabelFieldEditor("", getFieldEditorParent()); + } + + @Override + public boolean performOk() { + boolean superOK = super.performOk(); + var snykView = SnykStartup.getSnykView(); + snykView.disableRunAbortActions(); + snykView.toggleRunActionEnablement(); + disableSnykCodeIfOrgDisabled(); + + new LsConfigurationUpdater().configurationChanged(); + return superOK; + } + + private void disableSnykCodeIfOrgDisabled() { + var apiClient = new ApiClient(); + if (snykCodeCheckbox.getBooleanValue() && !apiClient.checkSnykCodeEnablement()) { + String message = "Snyk Code disabled, because it is not enabled for your organization. After you close this preference page, it will stay disabled."; + snykCodeCheckbox.setLabelText(snykCodeCheckbox.getLabelText() + " (" + message + ")"); + SnykLogger.logInfo(message); + } + } }
plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/Preferences.java+1 −0 modified@@ -26,6 +26,7 @@ public static synchronized Preferences getInstance(PreferenceStore store) { return preferences; } + public static final String TRUSTED_FOLDERS = "trustedFolders"; public static final String AUTH_TOKEN_KEY = "authtoken"; public static final String PATH_KEY = "path"; public static final String ENDPOINT_KEY = "endpoint";
plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/SecurePreferenceStore.java+20 −1 modified@@ -4,7 +4,12 @@ import org.eclipse.equinox.security.storage.ISecurePreferences; import org.eclipse.equinox.security.storage.SecurePreferencesFactory; import org.eclipse.equinox.security.storage.StorageException; +import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; import org.eclipse.ui.preferences.ScopedPreferenceStore; public class SecurePreferenceStore extends ScopedPreferenceStore implements PreferenceStore { @@ -14,7 +19,21 @@ public class SecurePreferenceStore extends ScopedPreferenceStore implements Pref public SecurePreferenceStore() { super(InstanceScope.INSTANCE, QUALIFIER); - node = SecurePreferencesFactory.getDefault().node(QUALIFIER); + ISecurePreferences secureStorage = SecurePreferencesFactory.getDefault(); + if (secureStorage == null) { + PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { + Display display = PlatformUI.getWorkbench().getDisplay(); + Shell activeShell = display.getActiveShell(); + String message = "Eclipse was unable to create or access the Secure Storage mechanism. " + + "Please check your Secure Storage in Eclipse preferences under " + + "General -> Security -> Secure Storage. " + + "The Snyk plugin will not be able to work reliably and save preferences " + + "or the authentication token until Secure Storage can be used."; + String title = "Error accessing Eclipse Secure Storage (Snyk)"; + MessageDialog.openError(activeShell, title, message); + }); + } + node = secureStorage.node(QUALIFIER); } @Override
plugin/src/main/java/io/snyk/eclipse/plugin/runner/SnykCliRunner.java+8 −2 modified@@ -17,8 +17,6 @@ public class SnykCliRunner { private static final String TEST_PARAMS = "test"; private static final String FILE_PARAM = "--file="; - private static final String INSECURE = "--insecure"; - private static final String MONITOR_PARAM = "monitor"; // private static final String AUTH_PARAM = "auth"; @@ -59,12 +57,20 @@ private ProcessResult snykRun(List<String> arguments) { private ProcessResult snykRun(List<String> arguments, Optional<File> navigatePath) { try { + checkIfTrusted(navigatePath.get()); ProcessBuilder processBuilder = createProcessBuilderByOS(arguments, Preferences.getInstance().getPath()); return processRunner.run(processBuilder, navigatePath); } catch (Exception e) { return ProcessResult.error(e.getMessage()); } } + + private void checkIfTrusted(File file) { + var trustedPaths = Preferences.getInstance().getPref(Preferences.TRUSTED_FOLDERS, ""); + if (!trustedPaths.contains(file.getAbsolutePath())) { + throw new UntrustedScanRequestedException(file.getAbsolutePath() + " is not trusted."); + } + } private ProcessBuilder createProcessBuilderByOS(List<String> params, Optional<String> path) throws Exception { ProcessBuilder processbuilder;
plugin/src/main/java/io/snyk/eclipse/plugin/runner/UntrustedScanRequestedException.java+27 −0 added@@ -0,0 +1,27 @@ +package io.snyk.eclipse.plugin.runner; + +public class UntrustedScanRequestedException extends RuntimeException { + + private static final long serialVersionUID = 4849361078384083852L; + + public UntrustedScanRequestedException() { + } + + public UntrustedScanRequestedException(String message) { + super(message); + } + + public UntrustedScanRequestedException(Throwable cause) { + super(cause); + } + + public UntrustedScanRequestedException(String message, Throwable cause) { + super(message, cause); + } + + public UntrustedScanRequestedException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +}
plugin/src/main/java/io/snyk/eclipse/plugin/wizards/SnykWizardAuthenticatePage.java+34 −27 modified@@ -3,21 +3,27 @@ import org.eclipse.jface.wizard.WizardPage; import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Text; import io.snyk.eclipse.plugin.properties.preferences.Preferences; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Listener; public class SnykWizardAuthenticatePage extends WizardPage implements Listener { private Text endpoint; private Button unknownCerts; + private String trustMessage = "⚠️ When scanning folder files, Snyk may automatically execute code such as invoking the package manager to get dependency information. " + + "You should only scan projects you trust. <a href=\"https://docs.snyk.io/ide-tools/eclipse-plugin/folder-trust\">More Info</a>" + + "\n\nOn finishing the wizard, the plugin will open a browser to authenticate you, trust the current workspace projects and trigger a scan."; public SnykWizardAuthenticatePage() { super("Snyk Wizard"); @@ -27,37 +33,45 @@ public SnykWizardAuthenticatePage() { @Override public void createControl(Composite parent) { - Composite composite = new Composite(parent, SWT.NONE); - GridLayout gl = new GridLayout(); - GridData gd = new GridData(GridData.FILL_HORIZONTAL); + Composite composite = new Composite(parent, SWT.NONE); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); - int ncol = 2; - gl.numColumns = ncol; - composite.setLayout(gl); + GridLayout gl = new GridLayout(); + int ncol = 2; + gl.numColumns = ncol; + composite.setLayout(gl); - Label endpointLabel = new Label(composite, SWT.NONE); - endpointLabel.setText("Endpoint:"); + Group endpointGroup = SWTWidgetHelper.createGroup(composite, ""); - endpoint = new Text(composite, SWT.BORDER | SWT.READ_ONLY); - endpoint.setLayoutData(gd); - - createLine(composite, ncol); + Label endpointLabel = new Label(endpointGroup, SWT.NONE); + endpointLabel.setText("Endpoint:"); + endpoint = new Text(endpointGroup, SWT.BORDER | SWT.READ_ONLY); + endpoint.setLayoutData(gd); - Label unknownCertsLabel = new Label(composite, SWT.NONE); - unknownCertsLabel.setText("Allow unknown certificate authorities:"); + Label unknownCertsLabel = new Label(endpointGroup, SWT.NONE); + unknownCertsLabel.setText("Allow unknown certificate authorities:"); - unknownCerts = new Button(composite, SWT.CHECK); - unknownCerts.setLayoutData(gd); + unknownCerts = new Button(endpointGroup, SWT.CHECK); + unknownCerts.setLayoutData(gd); - // required to avoid an error in the system - setControl(composite); - setPageComplete(false); + Group trustGroup = SWTWidgetHelper.createGroup(composite, ""); + + Link trustText = new Link(trustGroup, SWT.NONE); + trustText.setText(trustMessage); + gd = new GridData(GridData.FILL_BOTH); + trustText.setLayoutData(gd); + trustText.setBackground(new Color(0, 0, 0, 0)); + trustText.addListener(SWT.Selection, event -> org.eclipse.swt.program.Program.launch(event.text)); + + // required to avoid an error in the system + setControl(composite); + setPageComplete(false); } public void handleEvent(Event e) { getWizard().getContainer().updateButtons(); } - + public boolean isPageComplete() { return true; } @@ -66,11 +80,4 @@ void onEnterPage() { endpoint.setText(Preferences.getInstance().getEndpoint()); unknownCerts.setSelection(Preferences.getInstance().getBooleanPref(Preferences.INSECURE_KEY)); } - - private void createLine(Composite parent, int ncol) { - Label line = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL | SWT.BOLD); - GridData gridData = new GridData(GridData.FILL_HORIZONTAL); - gridData.horizontalSpan = ncol; - line.setLayoutData(gridData); - } }
plugin/src/main/java/io/snyk/eclipse/plugin/wizards/SnykWizard.java+11 −10 modified@@ -11,9 +11,9 @@ public class SnykWizard extends Wizard implements INewWizard { protected SnykWizardConfigureAPIPage configureAPIPage; protected SnykWizardAuthenticatePage authenticatePage; - + protected SnykWizardModel model; - + protected IWorkbench workbench; protected IStructuredSelection selection; @@ -22,41 +22,42 @@ public SnykWizard() { model = new SnykWizardModel(); setNeedsProgressMonitor(true); } - + @Override public String getWindowTitle() { return "Snyk Wizard"; } - + @Override public void addPages() { - configureAPIPage = new SnykWizardConfigureAPIPage(); + configureAPIPage = new SnykWizardConfigureAPIPage(); addPage(configureAPIPage); - - authenticatePage = new SnykWizardAuthenticatePage(); + + authenticatePage = new SnykWizardAuthenticatePage(); addPage(authenticatePage); } public void init(IWorkbench workbench, IStructuredSelection selection) { this.workbench = workbench; this.selection = selection; } - + public boolean canFinish() { if (this.getContainer().getCurrentPage() == authenticatePage) { return true; } return false; } - + public boolean performCancel() { model.resetPreferences(); return true; } - public boolean performFinish() { + public boolean performFinish() { new LsConfigurationUpdater().configurationChanged(); SnykExtendedLanguageClient.getInstance().triggerAuthentication(); + SnykExtendedLanguageClient.getInstance().trustWorkspaceFolders(); return true; } }
plugin/src/main/java/io/snyk/eclipse/plugin/wizards/SWTWidgetHelper.java+91 −0 added@@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2005-2008 VecTrace (Zingo Andersen) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * bastian implementation + *******************************************************************************/ + + +package io.snyk.eclipse.plugin.wizards; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; + +/** + * @author bastian + * https://foss.heptapod.net/mercurial/mercurialeclipse/-/blob/branch/default/plugin/src/com/vectrace/MercurialEclipse/ui/SWTWidgetHelper.java + */ +public final class SWTWidgetHelper { + public static final int LABEL_WIDTH_HINT = 400; + public static final int LABEL_INDENT_WIDTH = 32; + public static final int LIST_HEIGHT_HINT = 100; + public static final int SPACER_HEIGHT = 8; + + private SWTWidgetHelper() { + // hide constructor of utility class. + } + + /** + * Creates a group that <b>has <u>and</u> spans</b> the given number of columns in its parent and which has the + * given style. + * + * @param parent + * the parent control. + * @param text + * the title of the group. + * @param span + * the number of columns (in the parent's layout) that this group will span, which is also the number of + * columns that this group has. + * @param style + * the chosen style of the grid layout for this group. + * @return a new group + */ + public static Group createGroup(Composite parent, String text, int span, int style) { + Group group = new Group(parent, SWT.NULL); + group.setText(text); + GridData data = new GridData(style); + data.horizontalSpan = span; + // data.widthHint = GROUP_WIDTH; + + group.setLayoutData(data); + GridLayout layout = new GridLayout(); + layout.numColumns = span; + group.setLayout(layout); + return group; + } + + /** + * Creates a group that spans two columns. + * + * @param parent + * the parent control + * @param text + * the title of the group + * @param style + * the chosen style for this group + * @return a new group + */ + public static Group createGroup(Composite parent, String text, int style) { + return createGroup(parent, text, 2, style); + } + + /** + * Creates a group that has two columns and which style is horizontal fill. + * + * @param parent + * the parent control + * @param text + * the title of the group + * @return a new group + */ + public static Group createGroup(Composite parent, String text) { + return createGroup(parent, text, GridData.FILL_HORIZONTAL); + } +}
plugin/src/main/java/io/snyk/languageserver/download/LsBinaries.java+1 −1 modified@@ -4,7 +4,7 @@ public class LsBinaries { private static final String LS_DOWNLOAD_BASE_URL = "https://static.snyk.io/snyk-ls"; - public static final String REQUIRED_LS_PROTOCOL_VERSION = "3"; + public static final String REQUIRED_LS_PROTOCOL_VERSION = "4"; public static URI getBaseUri() { return URI.create(String.format("%s/%s", LS_DOWNLOAD_BASE_URL, REQUIRED_LS_PROTOCOL_VERSION));
plugin/src/main/java/io/snyk/languageserver/LsConfigurationUpdater.java+33 −9 modified@@ -4,6 +4,7 @@ import io.snyk.eclipse.plugin.properties.preferences.Preferences; import io.snyk.eclipse.plugin.utils.SnykLogger; +import java.io.File; import java.util.Collections; import org.eclipse.core.resources.IProject; @@ -45,23 +46,30 @@ public void configurationChanged() { Settings getCurrentSettings() { Preferences preferences = Preferences.getInstance(); - String activateSnykOpenSource = preferences.getPref(Preferences.ACTIVATE_SNYK_OPEN_SOURCE, "true"); - String activateSnykCode = preferences.getPref(Preferences.ACTIVATE_SNYK_CODE, "false"); - String activateSnykIac = preferences.getPref(Preferences.ACTIVATE_SNYK_IAC, "true"); - String insecure = preferences.getPref(Preferences.INSECURE_KEY, "false"); + String activateSnykOpenSource = preferences.getPref(Preferences.ACTIVATE_SNYK_OPEN_SOURCE, + Boolean.TRUE.toString()); + String activateSnykCode = preferences.getPref(Preferences.ACTIVATE_SNYK_CODE, Boolean.FALSE.toString()); + String activateSnykIac = preferences.getPref(Preferences.ACTIVATE_SNYK_IAC, Boolean.TRUE.toString()); + String insecure = preferences.getPref(Preferences.INSECURE_KEY, Boolean.FALSE.toString()); String endpoint = preferences.getPref(Preferences.ENDPOINT_KEY, ""); String additionalParams = preferences.getPref(Preferences.ADDITIONAL_PARAMETERS, ""); String additionalEnv = preferences.getPref(Preferences.ADDITIONAL_ENVIRONMENT, ""); String path = preferences.getPref(Preferences.PATH_KEY, ""); String sendErrorReports = preferences.getPref(Preferences.SEND_ERROR_REPORTS, ""); - String enableTelemetry = preferences.getPref(Preferences.ENABLE_TELEMETRY, "false"); + String enableTelemetry = preferences.getPref(Preferences.ENABLE_TELEMETRY, Boolean.FALSE.toString()); String organization = preferences.getPref(Preferences.ORGANIZATION_KEY, ""); - String manageBinariesAutomatically = preferences.getPref(Preferences.MANAGE_BINARIES_AUTOMATICALLY, "true"); + String manageBinariesAutomatically = preferences.getPref(Preferences.MANAGE_BINARIES_AUTOMATICALLY, Boolean.TRUE.toString()); String cliPath = preferences.getPref(Preferences.CLI_PATH, ""); String token = preferences.getPref(Preferences.AUTH_TOKEN_KEY, ""); String integrationName = Activator.INTEGRATION_NAME; String integrationVersion = Activator.PLUGIN_VERSION; String automaticAuthentication = "false"; + String trustedFoldersString = preferences.getPref(Preferences.TRUSTED_FOLDERS); + String[] trustedFolders = new String[0]; + if (trustedFoldersString != null && !trustedFoldersString.isBlank()) { + trustedFolders = trustedFoldersString.split(File.pathSeparator); + } + String enableTrustedFolderFeature = Boolean.TRUE.toString(); return new Settings(activateSnykOpenSource, activateSnykCode, activateSnykIac, @@ -78,7 +86,9 @@ Settings getCurrentSettings() { token, integrationName, integrationVersion, - automaticAuthentication + automaticAuthentication, + trustedFolders, + enableTrustedFolderFeature ); } @@ -101,6 +111,8 @@ static class Settings { private final String integrationName; private final String integrationVersion; private final String automaticAuthentication; + private final String[] trustedFolders; + private final String enableTrustedFoldersFeature; public Settings(String activateSnykOpenSource, String activateSnykCode, @@ -118,7 +130,9 @@ public Settings(String activateSnykOpenSource, String token, String integrationName, String integrationVersion, - String automaticAuthentication + String automaticAuthentication, + String[] trustedFolders, + String enableTrustedFoldersFeature ) { this.activateSnykOpenSource = activateSnykOpenSource; this.activateSnykCode = activateSnykCode; @@ -137,6 +151,8 @@ public Settings(String activateSnykOpenSource, this.integrationName = integrationName; this.integrationVersion = integrationVersion; this.automaticAuthentication = automaticAuthentication; + this.trustedFolders = trustedFolders; + this.enableTrustedFoldersFeature = enableTrustedFoldersFeature; } public String getPath() { @@ -202,9 +218,17 @@ public String getIntegrationName() { public String getIntegrationVersion() { return integrationVersion; } - + public String getAutomaticAuthentication() { return automaticAuthentication; } + + public String[] getTrustedFolders() { + return trustedFolders; + } + + public String getEnableTrustedFoldersFeature() { + return enableTrustedFoldersFeature; + } } }
plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/SnykTrustedFoldersParams.java+13 −0 added@@ -0,0 +1,13 @@ +package io.snyk.languageserver.protocolextension.messageObjects; + +public class SnykTrustedFoldersParams { + private String[] trustedFolders; + + public String[] getTrustedFolders() { + return trustedFolders; + } + + public void setTrustedFolders(String[] trustedFolders) { + this.trustedFolders = trustedFolders; + } +}
plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java+40 −8 modified@@ -1,7 +1,12 @@ package io.snyk.languageserver.protocolextension; +import java.io.File; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.eclipse.core.resources.IProject; import org.eclipse.jdt.internal.core.JavaProject; @@ -19,6 +24,7 @@ import org.eclipse.lsp4j.ShowDocumentResult; import org.eclipse.lsp4j.WorkDoneProgressCreateParams; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; import org.eclipse.ui.ISelectionService; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; @@ -30,6 +36,7 @@ import io.snyk.eclipse.plugin.wizards.SnykWizard; import io.snyk.languageserver.protocolextension.messageObjects.HasAuthenticatedParam; import io.snyk.languageserver.protocolextension.messageObjects.SnykIsAvailableCliParams; +import io.snyk.languageserver.protocolextension.messageObjects.SnykTrustedFoldersParams; @SuppressWarnings("restriction") public class SnykExtendedLanguageClient extends LanguageClientImpl { @@ -50,9 +57,8 @@ public void triggerScan(IWorkbenchWindow window) { if (Preferences.getInstance().getAuthToken().isBlank()) { runSnykWizard(); } else { - ExecuteCommandParams params = new ExecuteCommandParams("snyk.workspace.scan", new ArrayList<>()); try { - getLanguageServer().getWorkspaceService().executeCommand(params); + executeCommand("snyk.workspace.scan", new ArrayList<>()); if (window == null) { window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); @@ -82,12 +88,11 @@ public void triggerScan(IWorkbenchWindow window) { } public void triggerAuthentication() { - ExecuteCommandParams params = new ExecuteCommandParams("snyk.login", new ArrayList<>()); - try { - getLanguageServer().getWorkspaceService().executeCommand(params); - } catch (Exception e) { - SnykLogger.logError(e); - } + executeCommand("snyk.login", new ArrayList<>()); + } + + public void trustWorkspaceFolders() { + executeCommand("snyk.trustWorkspaceFolders", new ArrayList<>()); } @JsonNotification(value = "$/snyk.hasAuthenticated") @@ -106,6 +111,21 @@ public void isAvailableCli(SnykIsAvailableCliParams param) { enableSnykViewRunActions(); } + @JsonNotification(value = "$/snyk.addTrustedFolders") + public void addTrustedPaths(SnykTrustedFoldersParams param) { + var prefs = Preferences.getInstance(); + var storedTrustedPaths = prefs.getPref(Preferences.TRUSTED_FOLDERS, ""); + var trustedPaths = storedTrustedPaths.split(File.pathSeparator); + var pathSet = new HashSet<>(Arrays.asList(trustedPaths)); + pathSet.addAll(Arrays.asList(param.getTrustedFolders())); + Preferences.getInstance().store(Preferences.TRUSTED_FOLDERS, + pathSet.stream() + .filter(s -> !s.isBlank()) + .map(s -> s.trim()) + .distinct() + .collect(Collectors.joining(File.pathSeparator))); + } + @Override public CompletableFuture<Void> createProgress(WorkDoneProgressCreateParams params) { return progressMgr.createProgress(params); @@ -147,6 +167,15 @@ private void runForProject(String projectName) { } } + private void executeCommand(@NonNull String command, List<Object> arguments) { + ExecuteCommandParams params = new ExecuteCommandParams(command, arguments); + try { + getLanguageServer().getWorkspaceService().executeCommand(params); + } catch (Exception e) { + SnykLogger.logError(e); + } + } + // TODO: remove once LSP4e supports `showDocument` in its next release (it's // been merged to it already) @Override @@ -163,4 +192,7 @@ public CompletableFuture<ShowDocumentResult> showDocument(ShowDocumentParams par return new ShowDocumentResult(true); }); } + + + }
target-platform/target-platform.target+1 −1 modified@@ -43,7 +43,7 @@ <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> - <version>2.13.4</version> + <version>2.13.4.2</version> <type>jar</type> </dependency> <dependency>
tests/pom.xml+1 −1 modified@@ -25,7 +25,7 @@ <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> - <version>2.13.4</version> + <version>2.13.4.2</version> <type>jar</type> </dependency> <dependency>
tests/src/test/java/io/snyk/eclipse/plugin/runner/SnykCliRunnerTest.java+38 −0 added@@ -0,0 +1,38 @@ +package io.snyk.eclipse.plugin.runner; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +import io.snyk.eclipse.plugin.properties.preferences.InMemoryPreferenceStore; +import io.snyk.eclipse.plugin.properties.preferences.Preferences; +import io.snyk.eclipse.plugin.properties.preferences.PreferencesUtils; + +class SnykCliRunnerTest { + + @Test + void testRunDoesntAllowScansOfUntrustedPath() { + PreferencesUtils.setPreferences(Preferences.getInstance(new InMemoryPreferenceStore())); + SnykCliRunner cut = new SnykCliRunner(); + File navigatePath = new File("untrusted/path"); + + ProcessResult result = cut.snykTest(navigatePath); + + assertTrue(result.getError().endsWith(navigatePath.getAbsolutePath() + " is not trusted.")); + } + + @Test + void testRunAllowsScanOfTrustedPath() { + File navigatePath = new File("trusted/path"); + InMemoryPreferenceStore store = new InMemoryPreferenceStore(); + store.put(Preferences.TRUSTED_FOLDERS, "a" + File.pathSeparator + navigatePath.getAbsolutePath() + File.pathSeparator + "b"); + PreferencesUtils.setPreferences(Preferences.getInstance(store)); + + SnykCliRunner cut = new SnykCliRunner(); + ProcessResult result = cut.snykTest(navigatePath); + + assertFalse(result.getError().endsWith(navigatePath.getAbsolutePath() + " is not trusted.")); + } +}
tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java+46 −0 added@@ -0,0 +1,46 @@ +package io.snyk.languageserver.protocolextension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.snyk.eclipse.plugin.properties.preferences.InMemoryPreferenceStore; +import io.snyk.eclipse.plugin.properties.preferences.Preferences; +import io.snyk.eclipse.plugin.properties.preferences.PreferencesUtils; +import io.snyk.languageserver.protocolextension.messageObjects.HasAuthenticatedParam; +import io.snyk.languageserver.protocolextension.messageObjects.SnykIsAvailableCliParams; +import io.snyk.languageserver.protocolextension.messageObjects.SnykTrustedFoldersParams; + +class SnykExtendedLanguageClientTest { + private InMemoryPreferenceStore store = new InMemoryPreferenceStore(); + private SnykExtendedLanguageClient cut = new SnykExtendedLanguageClient(); + + @BeforeEach + void setUp() { + store = new InMemoryPreferenceStore(); + PreferencesUtils.setPreferences(Preferences.getInstance(store)); + } + + @Test + void testAddTrustedPathsAddsPathToPreferenceStore() { + SnykTrustedFoldersParams param = new SnykTrustedFoldersParams(); + param.setTrustedFolders(new String[] {"trusted/path "}); + + cut.addTrustedPaths(param); + + assertEquals("trusted/path", store.getString(Preferences.TRUSTED_FOLDERS, "")); + } + + @Test + void testAddTrustedPathsDeduplicatesAndTrims() { + SnykTrustedFoldersParams param = new SnykTrustedFoldersParams(); + param.setTrustedFolders(new String[] {"trusted/path", "trusted/path", " trusted/path "}); + + cut.addTrustedPaths(param); + + assertEquals("trusted/path", store.getString(Preferences.TRUSTED_FOLDERS, "")); + } + +}
tests/src/test/java/io/snyk/languageserver/SnykLanguageServerTest.java+32 −0 modified@@ -7,6 +7,8 @@ import static org.junit.jupiter.api.Assertions.*; +import java.io.File; + class SnykLanguageServerTest { @Test @@ -18,4 +20,34 @@ void getInitializationOptions() { assertInstanceOf(LsConfigurationUpdater.Settings.class, output); } + + @Test + void getInitializationOptionsContainsTrustedPaths() { + InMemoryPreferenceStore store = new InMemoryPreferenceStore(); + String trustedPaths = "a" + File.pathSeparatorChar + "b/c"; + store.put(Preferences.TRUSTED_FOLDERS, trustedPaths); + PreferencesUtils.setPreferences(Preferences.getInstance(store)); + SnykLanguageServer snykStreamConnectionProvider = new SnykLanguageServer(); + + Object output = snykStreamConnectionProvider.getInitializationOptions(null); + + assertInstanceOf(LsConfigurationUpdater.Settings.class, output); + LsConfigurationUpdater.Settings settings = (LsConfigurationUpdater.Settings) output; + assertEquals("a", settings.getTrustedFolders()[0]); + assertEquals("b/c", settings.getTrustedFolders()[1]); + } + + @Test + void getInitializationOptionsDoesNotContainsTrustedPathsIfNoneKnown() { + InMemoryPreferenceStore store = new InMemoryPreferenceStore(); + PreferencesUtils.setPreferences(Preferences.getInstance(store)); + SnykLanguageServer snykStreamConnectionProvider = new SnykLanguageServer(); + + Object output = snykStreamConnectionProvider.getInitializationOptions(null); + + assertInstanceOf(LsConfigurationUpdater.Settings.class, output); + LsConfigurationUpdater.Settings settings = (LsConfigurationUpdater.Settings) output; + assertNotNull(settings.getTrustedFolders()); + assertEquals(0, settings.getTrustedFolders().length); + } }
b3229f0142f7feat: add trust management mechanism (#187)
22 files changed · +922 −104
application/config/config.go+52 −27 modified@@ -119,33 +119,35 @@ func (c *CliSettings) DefaultBinaryInstallPath() string { } type Config struct { - configLoaded concurrency.AtomicBool - cliSettings *CliSettings - configFile string - format string - isErrorReportingEnabled concurrency.AtomicBool - isSnykCodeEnabled concurrency.AtomicBool - isSnykOssEnabled concurrency.AtomicBool - isSnykIacEnabled concurrency.AtomicBool - isSnykContainerEnabled concurrency.AtomicBool - isSnykAdvisorEnabled concurrency.AtomicBool - isTelemetryEnabled concurrency.AtomicBool - manageBinariesAutomatically concurrency.AtomicBool - logPath string - organization string - snykCodeAnalysisTimeout time.Duration - snykApiUrl string - snykCodeApiUrl string - token string - deviceId string - clientCapabilities lsp.ClientCapabilities - m sync.Mutex - path string - defaultDirs []string - integrationName string - integrationVersion string - automaticAuthentication bool - tokenChangeChannels []chan string + configLoaded concurrency.AtomicBool + cliSettings *CliSettings + configFile string + format string + isErrorReportingEnabled concurrency.AtomicBool + isSnykCodeEnabled concurrency.AtomicBool + isSnykOssEnabled concurrency.AtomicBool + isSnykIacEnabled concurrency.AtomicBool + isSnykContainerEnabled concurrency.AtomicBool + isSnykAdvisorEnabled concurrency.AtomicBool + isTelemetryEnabled concurrency.AtomicBool + manageBinariesAutomatically concurrency.AtomicBool + logPath string + organization string + snykCodeAnalysisTimeout time.Duration + snykApiUrl string + snykCodeApiUrl string + token string + deviceId string + clientCapabilities lsp.ClientCapabilities + m sync.Mutex + path string + defaultDirs []string + integrationName string + integrationVersion string + automaticAuthentication bool + tokenChangeChannels []chan string + trustedFolders []string + trustedFoldersFeatureEnabled bool } func CurrentConfig() *Config { @@ -185,6 +187,7 @@ func New() *Config { c.snykCodeApiUrl = defaultDeeproxyApiUrl c.snykCodeAnalysisTimeout = snykCodeAnalysisTimeoutFromEnv() c.token = "" + c.trustedFoldersFeatureEnabled = true c.clientSettingsFromEnv() c.deviceId = c.determineDeviceId() c.addDefaults() @@ -211,6 +214,16 @@ func (c *Config) determineDeviceId() string { } } +func (c *Config) IsTrustedFolderFeatureEnabled() bool { + return c.trustedFoldersFeatureEnabled +} + +func (c *Config) SetTrustedFolderFeatureEnabled(enabled bool) { + c.m.Lock() + defer c.m.Unlock() + c.trustedFoldersFeatureEnabled = enabled +} + func (c *Config) Load() { files := c.configFiles() for _, fileName := range files { @@ -538,3 +551,15 @@ func (c *Config) SetIntegrationName(integrationName string) { func (c *Config) SetIntegrationVersion(integrationVersion string) { c.integrationVersion = integrationVersion } + +func (c *Config) TrustedFolders() []string { + c.m.Lock() + defer c.m.Unlock() + return c.trustedFolders +} + +func (c *Config) SetTrustedFolders(folderPaths []string) { + c.m.Lock() + defer c.m.Unlock() + c.trustedFolders = folderPaths +}
application/config/config_test.go+1 −0 modified@@ -45,6 +45,7 @@ func TestConfigDefaults(t *testing.T) { assert.True(t, c.IsSnykIacEnabled(), "Snyk IaC should be enabled by default") assert.Equal(t, "", c.LogPath(), "Logpath should be empty by default") assert.Equal(t, "md", c.Format(), "Output format should be md by default") + assert.Empty(t, c.trustedFolders) } func Test_TokenChanged_ChannelsInformed(t *testing.T) {
application/server/configuration.go+18 −3 modified@@ -19,6 +19,7 @@ package server import ( "context" "os" + "reflect" "strconv" "strings" @@ -37,7 +38,7 @@ func WorkspaceDidChangeConfiguration(srv *jrpc2.Server) jrpc2.Handler { defer log.Info().Str("method", "WorkspaceDidChangeConfiguration").Interface("params", params).Msg("DONE") emptySettings := lsp.Settings{} - if params.Settings != emptySettings { + if !reflect.DeepEqual(params.Settings, emptySettings) { // client used settings push UpdateSettings(ctx, params.Settings) return true, nil @@ -65,7 +66,7 @@ func WorkspaceDidChangeConfiguration(srv *jrpc2.Server) jrpc2.Handler { return false, err } - if fetchedSettings[0] != emptySettings { + if !reflect.DeepEqual(fetchedSettings[0], emptySettings) { UpdateSettings(ctx, fetchedSettings[0]) return true, nil } @@ -86,7 +87,7 @@ func UpdateSettings(ctx context.Context, settings lsp.Settings) { func writeSettings(ctx context.Context, settings lsp.Settings, initialize bool) { emptySettings := lsp.Settings{} - if settings == emptySettings { + if reflect.DeepEqual(settings, emptySettings) { return } updateToken(settings.Token) @@ -98,6 +99,20 @@ func writeSettings(ctx context.Context, settings lsp.Settings, initialize bool) updateTelemetry(settings) updateOrganization(settings) manageBinariesAutomatically(settings) + updateTrustedFolders(settings) +} + +func updateTrustedFolders(settings lsp.Settings) { + trustedFoldersFeatureEnabled, err := strconv.ParseBool(settings.EnableTrustedFoldersFeature) + if err == nil { + config.CurrentConfig().SetTrustedFolderFeatureEnabled(trustedFoldersFeatureEnabled) + } else { + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + } + + if settings.TrustedFolders != nil { + config.CurrentConfig().SetTrustedFolders(settings.TrustedFolders) + } } func updateAutoAuthentication(settings lsp.Settings) {
application/server/configuration_test.go+13 −1 modified@@ -71,7 +71,7 @@ func Test_WorkspaceDidChangeConfiguration_Push(t *testing.T) { assert.Equal(t, "token", config.CurrentConfig().Token()) } -func callBackMock(ctx context.Context, request *jrpc2.Request) (interface{}, error) { +func callBackMock(_ context.Context, request *jrpc2.Request) (interface{}, error) { jsonRPCRecorder.Record(*request) if request.Method() == "workspace/configuration" { return []lsp.Settings{sampleSettings}, nil @@ -151,6 +151,7 @@ func Test_UpdateSettings(t *testing.T) { ManageBinariesAutomatically: "false", CliPath: "C:\\Users\\CliPath\\snyk-ls.exe", Token: "a fancy token", + TrustedFolders: []string{"trustedPath1", "trustedPath2"}, } UpdateSettings(context.Background(), settings) @@ -171,6 +172,8 @@ func Test_UpdateSettings(t *testing.T) { assert.False(t, c.ManageBinariesAutomatically()) assert.Equal(t, "C:\\Users\\CliPath\\snyk-ls.exe", c.CliSettings().Path()) assert.Equal(t, "a fancy token", c.Token()) + assert.Contains(t, c.TrustedFolders(), "trustedPath1") + assert.Contains(t, c.TrustedFolders(), "trustedPath2") }) t.Run("blank organisation is ignored", func(t *testing.T) { @@ -209,6 +212,15 @@ func Test_UpdateSettings(t *testing.T) { assert.Empty(t, os.Getenv("b")) assert.Empty(t, os.Getenv(";")) }) + t.Run("trusted folders", func(t *testing.T) { + config.SetCurrentConfig(config.New()) + + UpdateSettings(context.Background(), lsp.Settings{TrustedFolders: []string{"/a/b", "/b/c"}}) + + c := config.CurrentConfig() + assert.Contains(t, c.TrustedFolders(), "/a/b") + assert.Contains(t, c.TrustedFolders(), "/b/c") + }) t.Run("manage binaries automatically", func(t *testing.T) { t.Run("true", func(t *testing.T) {
application/server/execute_command.go+28 −2 modified@@ -25,7 +25,9 @@ import ( "github.com/rs/zerolog/log" sglsp "github.com/sourcegraph/go-lsp" + "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/application/di" + "github.com/snyk/snyk-ls/application/server/lsp" "github.com/snyk/snyk-ls/domain/ide/command" "github.com/snyk/snyk-ls/domain/ide/workspace" "github.com/snyk/snyk-ls/domain/snyk" @@ -49,10 +51,18 @@ func ExecuteCommandHandler(srv *jrpc2.Server) jrpc2.Handler { } navigateToLocation(srv, args) case snyk.WorkspaceScanCommand: - workspace.Get().ClearIssues(bgCtx) - workspace.Get().ScanWorkspace(bgCtx) + w := workspace.Get() + w.ClearIssues(bgCtx) + w.ScanWorkspace(bgCtx) + handleUntrustedFolders(bgCtx, srv) case snyk.OpenBrowserCommand: command.OpenBrowser(params.Arguments[0].(string)) + case snyk.TrustWorkspaceFoldersCommand: + err := TrustWorkspaceFolders() + if err != nil { + log.Err(err).Msgf("Error on %s command", snyk.TrustWorkspaceFoldersCommand) + notification.SendError(err) + } case snyk.LoginCommand: authenticator := di.Authenticator() _, err := authenticator.Authenticate(context.Background()) @@ -75,3 +85,19 @@ func ExecuteCommandHandler(srv *jrpc2.Server) jrpc2.Handler { return nil, nil }) } + +func TrustWorkspaceFolders() error { + if !config.CurrentConfig().IsTrustedFolderFeatureEnabled() { + return nil + } + + trustedFolderPaths := config.CurrentConfig().TrustedFolders() + _, untrusted := workspace.Get().GetFolderTrust() + for _, folder := range untrusted { + trustedFolderPaths = append(trustedFolderPaths, folder.Path()) + } + + config.CurrentConfig().SetTrustedFolders(trustedFolderPaths) + notification.Send(lsp.SnykTrustedFoldersParams{TrustedFolders: trustedFolderPaths}) + return nil +}
application/server/execute_command_test.go+66 −0 modified@@ -25,6 +25,7 @@ import ( "github.com/atotto/clipboard" + "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/application/di" "github.com/snyk/snyk-ls/domain/ide/workspace" "github.com/snyk/snyk-ls/domain/snyk" @@ -47,6 +48,24 @@ func Test_executeWorkspaceScanCommand_shouldStartWorkspaceScanOnCommandReceipt(t }, 2*time.Second, time.Millisecond) } +func Test_executeWorkspaceScanCommand_shouldAskForTrust(t *testing.T) { + loc := setupServer(t) + + scanner := &snyk.TestScanner{} + workspace.Get().AddFolder(workspace.NewFolder("dummy", "dummy", scanner, di.HoverService())) + // explicitly enable folder trust which is disabled by default in tests + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + + params := lsp.ExecuteCommandParams{Command: snyk.WorkspaceScanCommand} + _, err := loc.Client.Call(ctx, "workspace/executeCommand", params) + if err != nil { + t.Fatal(err) + } + assert.Eventually(t, func() bool { + return scanner.Calls() == 0 && checkTrustMessageRequest() + }, 2*time.Second, time.Millisecond) +} + func Test_loginCommand_StartsAuthentication(t *testing.T) { // Arrange loc := setupServer(t) @@ -84,3 +103,50 @@ func Test_executeCommand_shouldCopyAuthURLToClipboard(t *testing.T) { assert.Equal(t, authenticationMock.ExpectedAuthURL, actualURL) } + +func Test_TrustWorkspaceFolders(t *testing.T) { + t.Run("Doesn't mutate trusted folders, if trusted folders disabled", func(t *testing.T) { + loc := setupServer(t) + workspace.Get().AddFolder(workspace.NewFolder("/path/to/folder1", "dummy", nil, di.HoverService())) + + params := lsp.ExecuteCommandParams{Command: snyk.TrustWorkspaceFoldersCommand} + _, err := loc.Client.Call(ctx, "workspace/executeCommand", params) + if err != nil { + t.Fatal(err) + } + + assert.Len(t, config.CurrentConfig().TrustedFolders(), 0) + }) + + t.Run("Updates trusted workspace folders", func(t *testing.T) { + loc := setupServer(t) + workspace.Get().AddFolder(workspace.NewFolder("/path/to/folder1", "dummy", nil, di.HoverService())) + workspace.Get().AddFolder(workspace.NewFolder("/path/to/folder2", "dummy", nil, di.HoverService())) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + + params := lsp.ExecuteCommandParams{Command: snyk.TrustWorkspaceFoldersCommand} + _, err := loc.Client.Call(ctx, "workspace/executeCommand", params) + if err != nil { + t.Fatal(err) + } + + assert.Len(t, config.CurrentConfig().TrustedFolders(), 2) + assert.Contains(t, config.CurrentConfig().TrustedFolders(), "/path/to/folder1", "/path/to/folder2") + }) + + t.Run("Existing trusted workspace folders are not removed", func(t *testing.T) { + loc := setupServer(t) + workspace.Get().AddFolder(workspace.NewFolder("/path/to/folder1", "dummy", nil, di.HoverService())) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + config.CurrentConfig().SetTrustedFolders([]string{"/path/to/folder2"}) + + params := lsp.ExecuteCommandParams{Command: snyk.TrustWorkspaceFoldersCommand} + _, err := loc.Client.Call(ctx, "workspace/executeCommand", params) + if err != nil { + t.Fatal(err) + } + + assert.Len(t, config.CurrentConfig().TrustedFolders(), 2) + assert.Contains(t, config.CurrentConfig().TrustedFolders(), "/path/to/folder1", "/path/to/folder2") + }) +}
application/server/lsp/message_types.go+41 −18 modified@@ -233,24 +233,26 @@ type WorkspaceFoldersChangeEvent struct { // Settings is the struct that is parsed from the InitializationParams.InitializationOptions field type Settings struct { - ActivateSnykOpenSource string `json:"activateSnykOpenSource,omitempty"` - ActivateSnykCode string `json:"activateSnykCode,omitempty"` - ActivateSnykIac string `json:"activateSnykIac,omitempty"` - Insecure string `json:"insecure,omitempty"` - Endpoint string `json:"endpoint,omitempty"` - AdditionalParams string `json:"additionalParams,omitempty"` - AdditionalEnv string `json:"additionalEnv,omitempty"` - Path string `json:"path,omitempty"` - SendErrorReports string `json:"sendErrorReports,omitempty"` - Organization string `json:"organization,omitempty"` - EnableTelemetry string `json:"enableTelemetry,omitempty"` - ManageBinariesAutomatically string `json:"manageBinariesAutomatically,omitempty"` - CliPath string `json:"cliPath,omitempty"` - Token string `json:"token,omitempty"` - IntegrationName string `json:"integrationName,omitempty"` - IntegrationVersion string `json:"integrationVersion,omitempty"` - AutomaticAuthentication string `json:"automaticAuthentication,omitempty"` - DeviceId string `json:"deviceId,omitempty"` + ActivateSnykOpenSource string `json:"activateSnykOpenSource,omitempty"` + ActivateSnykCode string `json:"activateSnykCode,omitempty"` + ActivateSnykIac string `json:"activateSnykIac,omitempty"` + Insecure string `json:"insecure,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + AdditionalParams string `json:"additionalParams,omitempty"` + AdditionalEnv string `json:"additionalEnv,omitempty"` + Path string `json:"path,omitempty"` + SendErrorReports string `json:"sendErrorReports,omitempty"` + Organization string `json:"organization,omitempty"` + EnableTelemetry string `json:"enableTelemetry,omitempty"` + ManageBinariesAutomatically string `json:"manageBinariesAutomatically,omitempty"` + CliPath string `json:"cliPath,omitempty"` + Token string `json:"token,omitempty"` + IntegrationName string `json:"integrationName,omitempty"` + IntegrationVersion string `json:"integrationVersion,omitempty"` + AutomaticAuthentication string `json:"automaticAuthentication,omitempty"` + DeviceId string `json:"deviceId,omitempty"` + EnableTrustedFoldersFeature string `json:"enableTrustedFoldersFeature,omitempty"` + TrustedFolders []string `json:"trustedFolders,omitempty"` } type DidChangeConfigurationParams struct { @@ -607,3 +609,24 @@ type ShowDocumentParams struct { */ Selection sglsp.Range `json:"selection"` } + +type MessageActionItem struct { + Title string `json:"title"` +} + +type ShowMessageRequestParams struct { + Type MessageType `json:"type"` + Message string `json:"message"` + Actions []MessageActionItem `json:"actions"` +} + +type MessageType int + +const Error MessageType = 1 +const Warning MessageType = 2 +const Info MessageType = 3 +const Log MessageType = 4 + +type SnykTrustedFoldersParams struct { + TrustedFolders []string `json:"trustedFolders"` +}
application/server/server.go+39 −21 modified@@ -90,7 +90,7 @@ func initHandlers(srv *jrpc2.Server, handlers *handler.Map) { (*handlers)["textDocument/willSaveWaitUntil"] = NoOpHandler() (*handlers)["shutdown"] = Shutdown() (*handlers)["exit"] = Exit(srv) - (*handlers)["workspace/didChangeWorkspaceFolders"] = WorkspaceDidChangeWorkspaceFoldersHandler() + (*handlers)["workspace/didChangeWorkspaceFolders"] = WorkspaceDidChangeWorkspaceFoldersHandler(srv) (*handlers)["workspace/didChangeConfiguration"] = WorkspaceDidChangeConfiguration(srv) (*handlers)["window/workDoneProgress/cancel"] = WindowWorkDoneProgressCancelHandler() (*handlers)["workspace/executeCommand"] = ExecuteCommandHandler(srv) @@ -145,15 +145,16 @@ func CodeActionHandler() jrpc2.Handler { }) } -func WorkspaceDidChangeWorkspaceFoldersHandler() jrpc2.Handler { +func WorkspaceDidChangeWorkspaceFoldersHandler(srv *jrpc2.Server) jrpc2.Handler { return handler.New(func(ctx context.Context, params lsp.DidChangeWorkspaceFoldersParams) (interface{}, error) { // The context provided by the JSON-RPC server is cancelled once a new message is being processed, // so we don't want to propagate it to functions that start background operations bgCtx := context.Background() log.Info().Str("method", "WorkspaceDidChangeWorkspaceFoldersHandler").Msg("RECEIVING") defer log.Info().Str("method", "WorkspaceDidChangeWorkspaceFoldersHandler").Msg("SENDING") - workspace.Get().ProcessFolderChange(bgCtx, params) + workspace.Get().AddAndRemoveFoldersAndTriggerScan(bgCtx, params) + handleUntrustedFolders(bgCtx, srv) return nil, nil }) } @@ -183,24 +184,7 @@ func InitializeHandler(srv *jrpc2.Server) handler.Func { os.Exit(0) }() - if len(params.WorkspaceFolders) > 0 { - for _, workspaceFolder := range params.WorkspaceFolders { - log.Info().Str("method", method).Msgf("Adding workspaceFolder %v", workspaceFolder) - f := workspace.NewFolder( - uri.PathFromUri(workspaceFolder.Uri), - workspaceFolder.Name, - di.Scanner(), - di.HoverService(), - ) - w.AddFolder(f) - } - } else { - if params.RootURI != "" { - w.AddFolder(workspace.NewFolder(uri.PathFromUri(params.RootURI), params.ClientInfo.Name, di.Scanner(), di.HoverService())) - } else if params.RootPath != "" { - w.AddFolder(workspace.NewFolder(params.RootPath, params.ClientInfo.Name, di.Scanner(), di.HoverService())) - } - } + addWorkspaceFolders(params, w) return lsp.InitializeResult{ ServerInfo: lsp.ServerInfo{ @@ -232,6 +216,7 @@ func InitializeHandler(srv *jrpc2.Server) handler.Func { snyk.LoginCommand, snyk.CopyAuthLinkCommand, snyk.LogoutCommand, + snyk.TrustWorkspaceFoldersCommand, }, }, }, @@ -241,10 +226,37 @@ func InitializeHandler(srv *jrpc2.Server) handler.Func { func InitializedHandler(srv *jrpc2.Server) handler.Func { return handler.New(func(ctx context.Context, params lsp.InitializedParams) (interface{}, error) { workspace.Get().ScanWorkspace(context.Background()) + if config.CurrentConfig().AutomaticAuthentication() || config.CurrentConfig().NonEmptyToken() { + go handleUntrustedFolders(context.Background(), srv) + } return nil, nil }) } +func addWorkspaceFolders(params lsp.InitializeParams, w *workspace.Workspace) { + const method = "addWorkspaceFolders" + if len(params.WorkspaceFolders) > 0 { + for _, workspaceFolder := range params.WorkspaceFolders { + log.Info().Str("method", method).Msgf("Adding workspaceFolder %v", workspaceFolder) + f := workspace.NewFolder( + uri.PathFromUri(workspaceFolder.Uri), + workspaceFolder.Name, + di.Scanner(), + di.HoverService(), + ) + w.AddFolder(f) + } + } else { + if params.RootURI != "" { + f := workspace.NewFolder(uri.PathFromUri(params.RootURI), params.ClientInfo.Name, di.Scanner(), di.HoverService()) + w.AddFolder(f) + } else if params.RootPath != "" { + f := workspace.NewFolder(params.RootPath, params.ClientInfo.Name, di.Scanner(), di.HoverService()) + w.AddFolder(f) + } + } +} + func setClientInformation(initParams lsp.InitializeParams) { var integrationName, integrationVersion string if initParams.InitializationOptions.IntegrationName != "" { @@ -400,6 +412,12 @@ func registerNotifier(srv *jrpc2.Server) { Interface("source", source). Interface("diagnosticCount", len(params.Diagnostics)). Msg("publishing diagnostics") + case lsp.SnykTrustedFoldersParams: + notifier(srv, "$/snyk.addTrustedFolders", params) + log.Info(). + Str("method", "registerNotifier"). + Interface("trustedPaths", params.TrustedFolders). + Msg("sending trusted Folders to client") default: log.Warn(). Str("method", "registerNotifier").
application/server/server_test.go+77 −4 modified@@ -230,7 +230,6 @@ func Test_initialize_shouldSupportCodeLenses(t *testing.T) { func Test_TextDocumentCodeLenses_shouldReturnCodeLenses(t *testing.T) { loc := setupServer(t) - didOpenParams, dir := didOpenTextParams(t) clientParams := lsp.InitializeParams{ @@ -243,6 +242,7 @@ func Test_TextDocumentCodeLenses_shouldReturnCodeLenses(t *testing.T) { Token: "xxx", ManageBinariesAutomatically: "true", CliPath: "", + EnableTrustedFoldersFeature: "false", }, } _, err := loc.Client.Call(ctx, "initialize", clientParams) @@ -468,6 +468,72 @@ func Test_initialize_autoAuthenticateSetCorrectly(t *testing.T) { }) } +func Test_initialize_handlesUntrustedFoldersWhenAutomaticAuthentication(t *testing.T) { + loc := setupServer(t) + initializationOptions := lsp.Settings{ + EnableTrustedFoldersFeature: "true", + } + params := lsp.InitializeParams{ + InitializationOptions: initializationOptions, + WorkspaceFolders: []lsp.WorkspaceFolder{{Uri: uri.PathToUri("/untrusted/dummy"), Name: "dummy"}}} + _, err := loc.Client.Call(ctx, "initialize", params) + if err != nil { + t.Fatal(err, "couldn't send initialized") + } + + _, err = loc.Client.Call(ctx, "initialized", nil) + if err != nil { + t.Fatal(err, "couldn't send initialized") + } + + assert.Nil(t, err) + assert.Eventually(t, func() bool { return checkTrustMessageRequest() }, time.Second, time.Millisecond) +} + +func Test_initialize_handlesUntrustedFoldersWhenAuthenticated(t *testing.T) { + loc := setupServer(t) + initializationOptions := lsp.Settings{ + EnableTrustedFoldersFeature: "true", + Token: "token", + } + params := lsp.InitializeParams{ + InitializationOptions: initializationOptions, + WorkspaceFolders: []lsp.WorkspaceFolder{{Uri: uri.PathToUri("/untrusted/dummy"), Name: "dummy"}}} + _, err := loc.Client.Call(ctx, "initialize", params) + if err != nil { + t.Fatal(err, "couldn't send initialized") + } + + _, err = loc.Client.Call(ctx, "initialized", nil) + if err != nil { + t.Fatal(err, "couldn't send initialized") + } + + assert.Nil(t, err) + assert.Eventually(t, func() bool { return checkTrustMessageRequest() }, time.Second, time.Millisecond) +} + +func Test_initialize_doesnotHandleUntrustedFolders(t *testing.T) { + loc := setupServer(t) + initializationOptions := lsp.Settings{ + EnableTrustedFoldersFeature: "true", + } + params := lsp.InitializeParams{ + InitializationOptions: initializationOptions, + WorkspaceFolders: []lsp.WorkspaceFolder{{Uri: uri.PathToUri("/untrusted/dummy"), Name: "dummy"}}} + _, err := loc.Client.Call(ctx, "initialize", params) + if err != nil { + t.Fatal(err, "couldn't send initialized") + } + _, err = loc.Client.Call(ctx, "initialized", nil) + if err != nil { + t.Fatal(err, "couldn't send initialized") + } + + assert.Nil(t, err) + assert.Eventually(t, func() bool { return checkTrustMessageRequest() }, time.Second, time.Millisecond) +} + func Test_textDocumentDidOpenHandler_shouldAcceptDocumentItemAndPublishDiagnostics(t *testing.T) { loc := setupServer(t) didOpenParams, dir := didOpenTextParams(t) @@ -480,15 +546,21 @@ func Test_textDocumentDidOpenHandler_shouldAcceptDocumentItemAndPublishDiagnosti ActivateSnykIac: "false", Organization: "fancy org", Token: "xxx", - ManageBinariesAutomatically: "true", + ManageBinariesAutomatically: "false", CliPath: "", + EnableTrustedFoldersFeature: "false", }, } _, err := loc.Client.Call(ctx, "initialize", clientParams) if err != nil { t.Fatal(err, "couldn't initialize") } + _, err = loc.Client.Call(ctx, "initialized", nil) + if err != nil { + t.Fatal(err, "couldn't send initialized") + } + _, err = loc.Client.Call(ctx, "textDocument/didOpen", didOpenParams) if err != nil { t.Fatal(err) @@ -688,8 +760,9 @@ func runSmokeTest(repo string, commit string, file1 string, file2 string, t *tes clientParams := lsp.InitializeParams{ WorkspaceFolders: []lsp.WorkspaceFolder{folder}, InitializationOptions: lsp.Settings{ - Endpoint: os.Getenv("SNYK_API"), - Token: os.Getenv("SNYK_TOKEN"), + Endpoint: os.Getenv("SNYK_API"), + Token: os.Getenv("SNYK_TOKEN"), + EnableTrustedFoldersFeature: "false", }, }
application/server/trust.go+88 −0 added@@ -0,0 +1,88 @@ +/* + * Copyright 2022 Snyk Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package server + +import ( + "context" + "fmt" + + "github.com/creachadair/jrpc2" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/snyk/snyk-ls/application/server/lsp" + "github.com/snyk/snyk-ls/domain/ide/workspace" +) + +const doTrust = "Trust folders and continue" +const dontTrust = "Don't trust folders" + +func handleUntrustedFolders(ctx context.Context, srv *jrpc2.Server) { + w := workspace.Get() + // debounce requests from overzealous clients (Eclipse, I'm looking at you) + if w.IsTrustRequestOngoing() { + return + } + w.StartRequestTrustCommunication() + defer w.EndRequestTrustCommunication() + + _, untrusted := w.GetFolderTrust() + if len(untrusted) > 0 { + + decision, err := showTrustDialog(srv, untrusted, doTrust, dontTrust) + if err != nil { + return + } + + if decision.Title == doTrust { + w.TrustFoldersAndScan(ctx, untrusted) + } + } +} + +func showTrustDialog(srv *jrpc2.Server, untrusted []*workspace.Folder, dontTrust string, doTrust string) (lsp.MessageActionItem, error) { + method := "showTrustDialog" + result, err := srv.Callback(context.Background(), "window/showMessageRequest", lsp.ShowMessageRequestParams{ + Type: lsp.Warning, + Message: getTrustMessage(untrusted), + Actions: []lsp.MessageActionItem{{Title: dontTrust}, {Title: doTrust}}, + }) + if err != nil { + log.Err(errors.Wrap(err, "couldn't show trust message")).Str("method", method).Send() + return lsp.MessageActionItem{Title: dontTrust}, err + } + + var trust lsp.MessageActionItem + if result != nil { + err = result.UnmarshalResult(&trust) + if err != nil { + log.Err(errors.Wrap(err, "couldn't unmarshal trust message")).Str("method", method).Send() + return lsp.MessageActionItem{Title: dontTrust}, err + } + } + return trust, err +} + +func getTrustMessage(untrusted []*workspace.Folder) string { + var untrustedFolderString string + for _, folder := range untrusted { + untrustedFolderString += folder.Path() + "\n" + } + return fmt.Sprintf("When scanning for vulnerabilities, Snyk may automatically execute code such as invoking "+ + "the package manager to get dependency information. You should only scan folders you trust."+ + "\n\nUntrusted Folders: \n%s\n\n", untrustedFolderString) +}
application/server/trust_test.go+145 −0 added@@ -0,0 +1,145 @@ +/* + * Copyright 2022 Snyk Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package server + +import ( + "context" + "testing" + "time" + + "github.com/creachadair/jrpc2" + "github.com/stretchr/testify/assert" + + "github.com/snyk/snyk-ls/application/config" + "github.com/snyk/snyk-ls/application/di" + "github.com/snyk/snyk-ls/application/server/lsp" + "github.com/snyk/snyk-ls/domain/ide/workspace" + "github.com/snyk/snyk-ls/domain/snyk" + "github.com/snyk/snyk-ls/internal/uri" +) + +func Test_handleUntrustedFolders_shouldTriggerTrustRequestAndNotScan(t *testing.T) { + loc := setupServer(t) + w := workspace.Get() + scanner := &snyk.TestScanner{} + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + w.AddFolder(workspace.NewFolder("dummy", "dummy", scanner, di.HoverService())) + handleUntrustedFolders(context.Background(), loc.Server) + + assert.True(t, checkTrustMessageRequest()) + assert.Equal(t, scanner.Calls(), 0) +} + +func Test_handleUntrustedFolders_shouldNotTriggerTrustRequestWhenAlreadyRequesting(t *testing.T) { + loc := setupServer(t) + w := workspace.Get() + scanner := &snyk.TestScanner{} + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + w.AddFolder(workspace.NewFolder("dummy", "dummy", scanner, di.HoverService())) + w.StartRequestTrustCommunication() + + handleUntrustedFolders(context.Background(), loc.Server) + + assert.Len(t, jsonRPCRecorder.FindCallbacksByMethod("window/showMessageRequest"), 0) + assert.Equal(t, scanner.Calls(), 0) +} + +func Test_handleUntrustedFolders_shouldTriggerTrustRequestAndScanAfterConfirmation(t *testing.T) { + loc := setupCustomServer(t, func(_ context.Context, _ *jrpc2.Request) (interface{}, error) { + return lsp.MessageActionItem{ + Title: doTrust, + }, nil + }) + registerNotifier(loc.Server) + + w := workspace.Get() + scanner := &snyk.TestScanner{} + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + w.AddFolder(workspace.NewFolder("/trusted/dummy", "dummy", scanner, di.HoverService())) + + handleUntrustedFolders(context.Background(), loc.Server) + + assert.Eventually(t, func() bool { + addTrustedSent := len(jsonRPCRecorder.FindNotificationsByMethod("$/snyk.addTrustedFolders")) == 1 + return scanner.Calls() == 1 && addTrustedSent + }, time.Second, time.Millisecond) +} + +func Test_handleUntrustedFolders_shouldTriggerTrustRequestAndNotScanAfterNegativeConfirmation(t *testing.T) { + loc := setupCustomServer(t, func(_ context.Context, _ *jrpc2.Request) (interface{}, error) { + return lsp.MessageActionItem{ + Title: dontTrust, + }, nil + }) + registerNotifier(loc.Server) + w := workspace.Get() + scanner := &snyk.TestScanner{} + w.AddFolder(workspace.NewFolder("/trusted/dummy", "dummy", scanner, di.HoverService())) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + + handleUntrustedFolders(context.Background(), loc.Server) + + assert.Equal(t, scanner.Calls(), 0) +} + +func Test_initializeHandler_shouldCallHandleUntrustedFolders(t *testing.T) { + loc := setupServer(t) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + + _, err := loc.Client.Call(context.Background(), "initialize", lsp.InitializeParams{ + RootURI: uri.PathToUri("/untrusted/dummy"), + }) + if err != nil { + t.Fatal(err, "couldn't send initialized") + } + + _, err = loc.Client.Call(ctx, "initialized", nil) + if err != nil { + t.Fatal(err, "couldn't send initialized") + } + + assert.NoError(t, err) + assert.Eventually(t, func() bool { return checkTrustMessageRequest() }, time.Second, time.Millisecond) +} + +func Test_DidWorkspaceFolderChange_shouldCallHandleUntrustedFolders(t *testing.T) { + loc := setupServer(t) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + + _, err := loc.Client.Call(context.Background(), "workspace/didChangeWorkspaceFolders", lsp.DidChangeWorkspaceFoldersParams{ + Event: lsp.WorkspaceFoldersChangeEvent{ + Added: []lsp.WorkspaceFolder{ + {Uri: uri.PathToUri("/untrusted/dummy"), Name: "dummy"}, + }, + Removed: []lsp.WorkspaceFolder{}, + }, + }) + + assert.NoError(t, err) + assert.Eventually(t, func() bool { return checkTrustMessageRequest() }, time.Second, time.Millisecond) +} + +func checkTrustMessageRequest() bool { + callbacks := jsonRPCRecorder.FindCallbacksByMethod("window/showMessageRequest") + if len(callbacks) == 0 { + return false + } + var params lsp.ShowMessageRequestParams + _ = callbacks[0].UnmarshalParams(¶ms) + _, untrusted := workspace.Get().GetFolderTrust() + return params.Type == lsp.Warning && params.Message == getTrustMessage(untrusted) +}
domain/ide/workspace/folder.go+22 −1 modified@@ -18,10 +18,12 @@ package workspace import ( "context" + "strings" "sync" "github.com/rs/zerolog/log" + "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/application/server/lsp" "github.com/snyk/snyk-ls/domain/ide/converter" "github.com/snyk/snyk-ls/domain/ide/hover" @@ -64,6 +66,7 @@ func NewFolder(path string, name string, scanner snyk.Scanner, hoverService hove folder.productAttributes[snyk.ProductInfrastructureAsCode] = snyk.ProductAttributes{} folder.productAttributes[snyk.ProductOpenSource] = snyk.ProductAttributes{} folder.documentDiagnosticCache = concurrency.AtomicMap{} + return &folder } @@ -115,9 +118,14 @@ func (f *Folder) ClearDiagnosticsCache(filePath string) { } func (f *Folder) scan(ctx context.Context, path string) { + const method = "domain.ide.workspace.folder.scan" + if !f.IsTrusted() { + log.Warn().Str("path", path).Str("method", method).Msg("skipping scan of untrusted path") + return + } issuesSlice := f.DocumentDiagnosticsFromCache(path) if issuesSlice != nil { - log.Info().Str("method", "domain.ide.workspace.folder.scan").Msgf("Cached results found: Skipping scan for %s", path) + log.Info().Str("method", method).Msgf("Cached results found: Skipping scan for %s", path) f.processResults(issuesSlice) return } @@ -228,3 +236,16 @@ func (f *Folder) ClearDiagnostics() { f.documentDiagnosticCache.ClearAll() } + +func (f *Folder) IsTrusted() bool { + if !config.CurrentConfig().IsTrustedFolderFeatureEnabled() { + return true + } + + for _, path := range config.CurrentConfig().TrustedFolders() { + if strings.HasPrefix(f.path, path) { + return true + } + } + return false +}
domain/ide/workspace/folder_test.go+54 −6 modified@@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/application/server/lsp" "github.com/snyk/snyk-ls/domain/ide/hover" "github.com/snyk/snyk-ls/domain/snyk" @@ -71,7 +72,7 @@ func Test_Scan_WhenCachedResultsButNoIssues_shouldNotReScan(t *testing.T) { assert.Equal(t, 1, scannerRecorder.Calls()) } -func TestProcessResults_SendsDiagnosticsAndHovers(t *testing.T) { +func Test_ProcessResults_SendsDiagnosticsAndHovers(t *testing.T) { t.Skipf("test this once we have uniform abstractions for hover & diagnostics") testutil.UnitTest(t) hoverService := hover.NewFakeHoverService() @@ -86,7 +87,7 @@ func TestProcessResults_SendsDiagnosticsAndHovers(t *testing.T) { // assert.hoverService.GetAll() } -func TestProcessResults_whenDifferentPaths_AddsToCache(t *testing.T) { +func Test_ProcessResults_whenDifferentPaths_AddsToCache(t *testing.T) { testutil.UnitTest(t) f := NewFolder("dummy", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) @@ -102,7 +103,7 @@ func TestProcessResults_whenDifferentPaths_AddsToCache(t *testing.T) { assert.Len(t, f.documentDiagnosticCache.Get("path2"), 1) } -func TestProcessResults_whenSamePaths_AddsToCache(t *testing.T) { +func Test_ProcessResults_whenSamePaths_AddsToCache(t *testing.T) { testutil.UnitTest(t) f := NewFolder("dummy", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) @@ -116,7 +117,7 @@ func TestProcessResults_whenSamePaths_AddsToCache(t *testing.T) { assert.Len(t, f.documentDiagnosticCache.Get("path1"), 2) } -func TestProcessResults_whenDifferentPaths_AccumulatesIssues(t *testing.T) { +func Test_ProcessResults_whenDifferentPaths_AccumulatesIssues(t *testing.T) { testutil.UnitTest(t) f := NewFolder("dummy", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) @@ -132,7 +133,7 @@ func TestProcessResults_whenDifferentPaths_AccumulatesIssues(t *testing.T) { assert.NotNil(t, f.documentDiagnosticCache.Get("path3")) } -func TestProcessResults_whenSamePaths_AccumulatesIssues(t *testing.T) { +func Test_ProcessResults_whenSamePaths_AccumulatesIssues(t *testing.T) { testutil.UnitTest(t) f := NewFolder("dummy", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) @@ -147,7 +148,7 @@ func TestProcessResults_whenSamePaths_AccumulatesIssues(t *testing.T) { assert.Len(t, f.documentDiagnosticCache.Get("path1"), 3) } -func TestProcessResults_whenSamePathsAndDuplicateIssues_DeDuplicates(t *testing.T) { +func Test_ProcessResults_whenSamePathsAndDuplicateIssues_DeDuplicates(t *testing.T) { testutil.UnitTest(t) f := NewFolder("dummy", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) @@ -200,3 +201,50 @@ func Test_ClearDiagnostics(t *testing.T) { 10*time.Millisecond, ) } + +func Test_IsTrusted_shouldReturnFalseByDefault(t *testing.T) { + testutil.UnitTest(t) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + f := NewFolder("dummy", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) + assert.False(t, f.IsTrusted()) +} + +func Test_IsTrusted_shouldReturnTrueForPathContainedInTrustedFolders(t *testing.T) { + testutil.UnitTest(t) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + config.CurrentConfig().SetTrustedFolders([]string{"dummy"}) + f := NewFolder("dummy", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) + assert.True(t, f.IsTrusted()) +} + +func Test_IsTrusted_shouldReturnTrueForSubfolderOfTrustedFolders_Linux(t *testing.T) { + testutil.IntegTest(t) + testutil.NotOnWindows(t, "Unix/macOS file paths are incompatible with Windows") + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + config.CurrentConfig().SetTrustedFolders([]string{"/dummy"}) + f := NewFolder("/dummy/dummyF", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) + assert.True(t, f.IsTrusted()) +} + +func Test_IsTrusted_shouldReturnFalseForDifferentFolder(t *testing.T) { + testutil.UnitTest(t) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + config.CurrentConfig().SetTrustedFolders([]string{"/dummy"}) + f := NewFolder("/UntrustedPath", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) + assert.False(t, f.IsTrusted()) +} + +func Test_IsTrusted_shouldReturnTrueForSubfolderOfTrustedFolders(t *testing.T) { + testutil.IntegTest(t) + testutil.OnlyOnWindows(t, "Windows specific test") + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + config.CurrentConfig().SetTrustedFolders([]string{"c:\\dummy"}) + f := NewFolder("c:\\dummy\\dummyF", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) + assert.True(t, f.IsTrusted()) +} + +func Test_IsTrusted_shouldReturnTrueIfTrustFeatureDisabled(t *testing.T) { + testutil.UnitTest(t) // disables trust feature + f := NewFolder("c:\\dummy\\dummyF", "dummy", snyk.NewTestScanner(), hover.NewFakeHoverService()) + assert.True(t, f.IsTrusted()) +}
domain/ide/workspace/workspace.go+43 −10 modified@@ -20,10 +20,14 @@ import ( "context" "sync" + "github.com/rs/zerolog/log" + + "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/application/server/lsp" "github.com/snyk/snyk-ls/domain/ide/hover" "github.com/snyk/snyk-ls/domain/observability/performance" "github.com/snyk/snyk-ls/domain/snyk" + "github.com/snyk/snyk-ls/internal/notification" "github.com/snyk/snyk-ls/internal/uri" ) @@ -33,11 +37,13 @@ var mutex = &sync.Mutex{} // Workspace represents the highest entity in an IDE that contains code. A workspace may contain multiple folders type Workspace struct { - mutex sync.Mutex - folders map[string]*Folder - instrumentor performance.Instrumentor - scanner snyk.Scanner - hoverService hover.Service + mutex sync.Mutex + folders map[string]*Folder + instrumentor performance.Instrumentor + scanner snyk.Scanner + hoverService hover.Service + trustMutex sync.Mutex + trustRequestOngoing bool // for debouncing } func New(instrumentor performance.Instrumentor, scanner snyk.Scanner, hoverService hover.Service) *Workspace { @@ -62,7 +68,7 @@ func Set(w *Workspace) { instance = w } -func (w *Workspace) DeleteFolder(folder string) { +func (w *Workspace) RemoveFolder(folder string) { w.mutex.Lock() defer w.mutex.Unlock() delete(w.folders, folder) @@ -87,14 +93,16 @@ func (w *Workspace) GetFolderContaining(path string) (folder *Folder) { } func (w *Workspace) ScanWorkspace(ctx context.Context) { - for _, folder := range w.folders { + trusted, _ := w.GetFolderTrust() + + for _, folder := range trusted { go folder.ScanFolder(ctx) } } -func (w *Workspace) ProcessFolderChange(ctx context.Context, params lsp.DidChangeWorkspaceFoldersParams) { +func (w *Workspace) AddAndRemoveFoldersAndTriggerScan(ctx context.Context, params lsp.DidChangeWorkspaceFoldersParams) { for _, folder := range params.Event.Removed { - w.DeleteFolder(uri.PathFromUri(folder.Uri)) + w.RemoveFolder(uri.PathFromUri(folder.Uri)) // TODO: check if we need to clean up the reported diagnostics, if folder was removed? } for _, folder := range params.Event.Added { @@ -104,11 +112,36 @@ func (w *Workspace) ProcessFolderChange(ctx context.Context, params lsp.DidChang w.ScanWorkspace(ctx) } -func (w *Workspace) ClearIssues(ctx context.Context) { +func (w *Workspace) ClearIssues(_ context.Context) { for _, folder := range w.folders { folder.ClearScannedStatus() folder.ClearDiagnostics() } w.hoverService.ClearAllHovers() } + +func (w *Workspace) TrustFoldersAndScan(ctx context.Context, foldersToBeTrusted []*Folder) { + currentConfig := config.CurrentConfig() + trustedFolderPaths := currentConfig.TrustedFolders() + for _, f := range foldersToBeTrusted { + // we need to append and set the trusted path to the config before the scan, as the scan is checking for trust + trustedFolderPaths = append(trustedFolderPaths, f.Path()) + currentConfig.SetTrustedFolders(trustedFolderPaths) + go f.ScanFolder(ctx) + } + notification.Send(lsp.SnykTrustedFoldersParams{TrustedFolders: trustedFolderPaths}) +} + +func (w *Workspace) GetFolderTrust() (trusted []*Folder, untrusted []*Folder) { + for _, folder := range w.folders { + if folder.IsTrusted() { + trusted = append(trusted, folder) + log.Info().Str("folder", folder.Path()).Msg("Trusted folder") + } else { + untrusted = append(untrusted, folder) + log.Info().Str("folder", folder.Path()).Msg("Untrusted folder") + } + } + return trusted, untrusted +}
domain/ide/workspace/workspace_test.go+117 −0 added@@ -0,0 +1,117 @@ +/* + * Copyright 2022 Snyk Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package workspace + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/snyk/snyk-ls/application/config" + "github.com/snyk/snyk-ls/application/server/lsp" + "github.com/snyk/snyk-ls/domain/observability/performance" + "github.com/snyk/snyk-ls/domain/snyk" + "github.com/snyk/snyk-ls/internal/testutil" + "github.com/snyk/snyk-ls/internal/uri" +) + +func Test_GetFolderTrust_shouldReturnTrustedAndUntrustedFolders(t *testing.T) { + testutil.UnitTest(t) + const trustedDummy = "trustedDummy" + const untrustedDummy = "untrustedDummy" + scanner := &snyk.TestScanner{} + w := New(performance.NewTestInstrumentor(), scanner, nil) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + config.CurrentConfig().SetTrustedFolders([]string{trustedDummy}) + w.AddFolder(NewFolder(trustedDummy, trustedDummy, scanner, nil)) + w.AddFolder(NewFolder(untrustedDummy, untrustedDummy, scanner, nil)) + + trusted, untrusted := w.GetFolderTrust() + + assert.Equal(t, trustedDummy, trusted[0].path) + assert.Equal(t, untrustedDummy, untrusted[0].path) +} + +func Test_TrustFoldersAndScan_shouldAddFoldersToTrustedFoldersAndTriggerScan(t *testing.T) { + testutil.UnitTest(t) + const trustedDummy = "trustedDummy" + const untrustedDummy = "untrustedDummy" + scanner := &snyk.TestScanner{} + w := New(performance.NewTestInstrumentor(), scanner, nil) + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + trustedFolder := NewFolder(trustedDummy, trustedDummy, scanner, nil) + w.AddFolder(trustedFolder) + untrustedFolder := NewFolder(untrustedDummy, untrustedDummy, scanner, nil) + w.AddFolder(untrustedFolder) + + w.TrustFoldersAndScan(context.Background(), []*Folder{trustedFolder}) + + assert.Contains(t, config.CurrentConfig().TrustedFolders(), trustedFolder.path) + assert.NotContains(t, config.CurrentConfig().TrustedFolders(), untrustedFolder.path) + assert.Eventually(t, func() bool { + return scanner.Calls() == 1 + }, time.Second, time.Millisecond, "scanner should be called after trust is granted") +} + +func Test_AddAndRemoveFoldersAndTriggerScan(t *testing.T) { + testutil.UnitTest(t) + const trustedDummy = "trustedDummy" + const untrustedDummy = "untrustedDummy" + const toBeRemoved = "toBeRemoved" + trustedPathAfterConversions := uri.PathFromUri(uri.PathToUri(trustedDummy)) + toBeRemovedAbsolutePathAfterConversions := uri.PathFromUri(uri.PathToUri(toBeRemoved)) + + scanner := &snyk.TestScanner{} + w := New(performance.NewTestInstrumentor(), scanner, nil) + toBeRemovedFolder := NewFolder(toBeRemovedAbsolutePathAfterConversions, toBeRemoved, scanner, nil) + w.AddFolder(toBeRemovedFolder) + + config.CurrentConfig().SetTrustedFolderFeatureEnabled(true) + config.CurrentConfig().SetTrustedFolders([]string{trustedPathAfterConversions}) + + params := lsp.DidChangeWorkspaceFoldersParams{Event: lsp.WorkspaceFoldersChangeEvent{ + Added: []lsp.WorkspaceFolder{ + {Name: trustedDummy, Uri: uri.PathToUri(trustedDummy)}, + {Name: untrustedDummy, Uri: uri.PathToUri(untrustedDummy)}, + }, + Removed: []lsp.WorkspaceFolder{ + {Name: toBeRemoved, Uri: uri.PathToUri(toBeRemoved)}, + }, + }} + + w.AddAndRemoveFoldersAndTriggerScan(context.Background(), params) + + assert.Nil(t, w.GetFolderContaining(toBeRemoved)) + + // one call for one trusted folder + assert.Eventually(t, func() bool { + return scanner.Calls() == 1 + }, time.Second, time.Millisecond, "scanner should be called after trust is granted") +} + +func Test_Get(t *testing.T) { + New(nil, nil, nil) + assert.Equal(t, instance, Get()) +} + +func Test_Set(t *testing.T) { + w := New(nil, nil, nil) + Set(w) + assert.Equal(t, w, instance) +}
domain/ide/workspace/workspace_trust.go+35 −0 added@@ -0,0 +1,35 @@ +/* + * Copyright 2022 Snyk Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package workspace + +func (w *Workspace) StartRequestTrustCommunication() { + w.trustMutex.Lock() + w.trustRequestOngoing = true + w.trustMutex.Unlock() +} + +func (w *Workspace) EndRequestTrustCommunication() { + w.trustMutex.Lock() + w.trustRequestOngoing = false + w.trustMutex.Unlock() +} + +func (w *Workspace) IsTrustRequestOngoing() bool { + w.trustMutex.Lock() + defer w.trustMutex.Unlock() + return w.trustRequestOngoing +}
domain/ide/workspace/workspace_trust_test.go+35 −0 added@@ -0,0 +1,35 @@ +/* + * Copyright 2022 Snyk Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package workspace + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/snyk/snyk-ls/internal/testutil" +) + +func TestWorkspace_TrustRequests(t *testing.T) { + testutil.UnitTest(t) + w := New(nil, nil, nil) + w.StartRequestTrustCommunication() + w.IsTrustRequestOngoing() + assert.True(t, w.IsTrustRequestOngoing()) + w.EndRequestTrustCommunication() + assert.False(t, w.IsTrustRequestOngoing()) +}
domain/snyk/command.go+7 −6 modified@@ -17,12 +17,13 @@ package snyk const ( - NavigateToRangeCommand = "snyk.navigateToRange" - WorkspaceScanCommand = "snyk.workspace.scan" - OpenBrowserCommand = "snyk.openBrowser" - LoginCommand = "snyk.login" - CopyAuthLinkCommand = "snyk.copyAuthLink" - LogoutCommand = "snyk.logout" + NavigateToRangeCommand = "snyk.navigateToRange" + WorkspaceScanCommand = "snyk.workspace.scan" + OpenBrowserCommand = "snyk.openBrowser" + LoginCommand = "snyk.login" + CopyAuthLinkCommand = "snyk.copyAuthLink" + LogoutCommand = "snyk.logout" + TrustWorkspaceFoldersCommand = "snyk.trustWorkspaceFolders" ) type Command struct {
.goreleaser.yaml+1 −1 modified@@ -60,4 +60,4 @@ dist: build env: - GO111MODULE=on - CGO_ENABLED=0 - - LS_PROTOCOL_VERSION=3 + - LS_PROTOCOL_VERSION=4
internal/testutil/test_setup.go+2 −0 modified@@ -45,6 +45,7 @@ func UnitTest(t *testing.T) { c := config.New() c.SetManageBinariesAutomatically(false) c.SetToken("00000000-0000-0000-0000-000000000001") + c.SetTrustedFolderFeatureEnabled(false) config.SetCurrentConfig(c) CLIDownloadLockFileCleanUp(t) } @@ -123,6 +124,7 @@ func prepareTestHelper(t *testing.T, envVar string) { c.SetToken(GetEnvironmentToken()) c.SetErrorReportingEnabled(false) c.SetTelemetryEnabled(false) + c.SetTrustedFolderFeatureEnabled(false) config.SetCurrentConfig(c) CLIDownloadLockFileCleanUp(t)
Makefile+1 −1 modified@@ -37,7 +37,7 @@ tools: @echo "==> Installing go-licenses" @go install github.com/google/go-licenses@latest ifeq (,$(wildcard ./.bin/golangci-lint*)) - @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .bin/ v1.48.0 + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .bin/ v1.50.0 else @echo "==> golangci-lint is already installed" endif
README.md+37 −3 modified@@ -75,6 +75,15 @@ Right now the language server supports the following actions: } ``` +- Trust Notification + - method: `$/snyk.addTrustedFolders` + - payload: + ```json + { + "trustedFolders": ["/a/path/to/trust"] + } + ``` + ## Installation ### Download @@ -132,12 +141,37 @@ within `initializationOptions?: LSPAny;` we support the following settings: "organization": "a string", // The name of your organization, e.g. the output of: curl -H "Authorization: token $(snyk config get api)" https://snyk.io/api/cli-config/settings/sast | jq .org "enableTelemetry": "true", // Whether or not user analytics can be tracked "manageBinariesAutomatically": "true", // Whether or not CLI/LS binaries will be downloaded & updated automatically - "cliPath": "/a/patch/snyk-cli" // The path where the CLI can be found, or where it should be downloaded to - "token": "secret-token" // The Snyk token, e.g.: snyk config get api - "automaticAuthentication": "true" // Whether or not LS will automatically authenticate on scan start (default: true) + "cliPath": "/a/patch/snyk-cli", // The path where the CLI can be found, or where it should be downloaded to + "token": "secret-token", // The Snyk token, e.g.: snyk config get api + "automaticAuthentication": "true", // Whether or not LS will automatically authenticate on scan start (default: true) + "enableTrustedFoldersFeature": "true", // Whether or not LS will prompt to trust a folder (default: true) + "trustedFolders": ["/a/trusted/path", "/another/trusted/path"], // An array of folder that should be trusted } ``` +#### Workspace Trust + +As part of examining the codebase for vulnerabilities, Snyk may automatically execute code on your computer to obtain +additional data for analysis. For example, this includes invoking the package manager (e.g., pip, gradle, maven, yarn, +npm, etc.) +to get dependency information for Snyk Open Source. Invoking these programs on untrusted code that has malicious +configurations may expose your system to malicious code execution and exploits. + +To safeguard from using the language server on untrusted folders, our language server will ask for folder trust +before running scans against these folders. When in doubt, do not grant trust. + +The trust feature is enabled by default. When a folder is trusted, all sub-folders are also trusted. After a folder +is trusted, Snyk Language Server notifies the Language Server Client with the custom `$/snyk.addTrustedFolders` +notification, +which contains a list of currently trusted folder paths. Based on this, a client can then implement logic to intercept +this notification and persist the decision and trust in the IDE or Editor storage mechanism. + +Trust dialogs can be disabled by setting `enableTrustedFoldersFeature` to `false` in the initialization options. This +will disable all trust prompts and checks. + +An initial set of trusted folders can be provided by setting `trustedFolders` to an array of paths in the +`initializationOptions`. These folders will be trusted on startup and will not prompt the user to trust them. + #### Environment variables Snyk LS and Snyk CLI support and need certain environment variables to function:
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-4vrv-93c7-m92jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-24441ghsaADVISORY
- github.com/snyk/snyk-eclipse-plugin/commit/b5a8bce25a359ced75f83a729fc6b2393fc9a495ghsaWEB
- github.com/snyk/snyk-intellij-plugin/commit/56682f4ba6081ce1d95cb980cbfacd3809a826f4ghsaWEB
- github.com/snyk/snyk-ls/commit/b3229f0142f782871aa72d1a7dcf417546d568edghsaWEB
- github.com/snyk/snyk-visual-studio-plugin/commit/0b53dbbd4a3153c3ef9aaf797af3b5caad0f731aghsaWEB
- github.com/snyk/vscode-extension/commit/0db3b4240be0db6a0a5c6d02c0d4231a2c4ba708ghsaWEB
- security.snyk.io/vuln/SNYK-JS-SNYK-3111871ghsaWEB
- www.imperva.com/blog/how-scanning-your-projects-for-security-issues-can-lead-to-remote-code-executionghsaWEB
- www.imperva.com/blog/how-scanning-your-projects-for-security-issues-can-lead-to-remote-code-execution/mitre
News mentions
0No linked articles in our index yet.