VYPR
High severityNVD Advisory· Published Nov 30, 2022· Updated Apr 24, 2025

Code Injection

CVE-2022-24441

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.

PackageAffected versionsPatched versions
snyknpm
< 1.1064.01.1064.0

Affected products

2
  • snyk/snykdescription
  • ghsa-coords
    Range: < 1.1064.0

Patches

5
0db3b4240be0

feat: workspace trust (#306)

https://github.com/snyk/vscode-extensionMichel KaporinNov 29, 2022via ghsa
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,
    
56682f4ba608

Merge pull request #417 from snyk/feat/trust

https://github.com/snyk/snyk-intellij-pluginMichel KaporinNov 29, 2022via ghsa
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))
    +    }
    +}
    
0b53dbbd4a31

feat: add workspace trust (#217)

https://github.com/snyk/snyk-visual-studio-pluginMichel KaporinNov 29, 2022via ghsa
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();
    +            }
    +        }
    +    }
    +}
    
b5a8bce25a35

Merge pull request #106 from snyk/feat/trust-feature

https://github.com/snyk/snyk-eclipse-pluginMichel KaporinNov 29, 2022via ghsa
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);
    +  }
     }
    
b3229f0142f7

feat: add trust management mechanism (#187)

https://github.com/snyk/snyk-lsMichel KaporinNov 29, 2022via ghsa
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(&params)
    +	_, 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

News mentions

0

No linked articles in our index yet.