Skip to content

Commit

Permalink
feat: workspace trust (#306)
Browse files Browse the repository at this point in the history
  • Loading branch information
michelkaporin authored Nov 29, 2022
1 parent 883dcb0 commit 0db3b42
Show file tree
Hide file tree
Showing 23 changed files with 213 additions and 45 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down Expand Up @@ -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"
},
{
Expand Down
4 changes: 2 additions & 2 deletions src/snyk/base/modules/snykLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
31 changes: 25 additions & 6 deletions src/snyk/cli/services/cliService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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());
Expand All @@ -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);
Expand All @@ -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);

Expand Down Expand Up @@ -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 = [];

Expand Down
3 changes: 2 additions & 1 deletion src/snyk/common/commands/commandController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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> {
Expand Down
19 changes: 19 additions & 0 deletions src/snyk/common/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions src/snyk/common/configuration/trustedFolders.ts
Original file line number Diff line number Diff line change
@@ -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));
}
2 changes: 2 additions & 0 deletions src/snyk/common/constants/languageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions src/snyk/common/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
76 changes: 45 additions & 31 deletions src/snyk/common/languageServer/languageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));

Expand All @@ -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> {
Expand Down
4 changes: 4 additions & 0 deletions src/snyk/common/languageServer/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export type ServerSettings = {
manageBinariesAutomatically?: string;
cliPath?: string;
token?: string;
enableTrustedFoldersFeature?: string;
trustedFolders?: string[];
};

export class LanguageServerSettings {
Expand All @@ -36,6 +38,8 @@ export class LanguageServerSettings {
organization: configuration.organization,
token: await configuration.getToken(),
manageBinariesAutomatically: `${configuration.isAutomaticDependencyManagementEnabled()}`,
enableTrustedFoldersFeature: 'true',
trustedFolders: configuration.getTrustedFolders(),
};
}
}
3 changes: 3 additions & 0 deletions src/snyk/common/messages/analysisMessages.ts
Original file line number Diff line number Diff line change
@@ -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.',
};
10 changes: 10 additions & 0 deletions src/snyk/common/views/analysisTreeNodeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
2 changes: 2 additions & 0 deletions src/snyk/common/watchers/configurationWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 0db3b42

Please sign in to comment.