diff --git a/package.json b/package.json index 628aed4a3..8b8224d3b 100644 --- a/package.json +++ b/package.json @@ -346,6 +346,13 @@ "name": "Snyk", "when": "!snyk:loggedIn || snyk:error || !snyk:workspaceFound || snyk:authenticationChanged" }, + { + "type": "webview", + "id": "snyk.views.summary", + "name": "SUMMARY", + "when": "snyk:initialized && snyk:loggedIn && snyk:workspaceFound && !snyk:error", + "content": "${scanSummaryHtml}" + }, { "id": "snyk.views.analysis.oss", "name": "Open Source Security", @@ -417,12 +424,12 @@ "view/title": [ { "command": "snyk.start", - "when": "view == 'snyk.views.analysis.code.security' || view == 'snyk.views.analysis.code.security.delta' || view == 'snyk.views.analysis.code.quality' || view == 'snyk.views.analysis.code.quality.delta' || view == 'snyk.views.analysis.oss' || view == 'snyk.views.analysis.configuration'", + "when": "view == 'snyk.views.summary' || view == 'snyk.views.analysis.code.security' || view == 'snyk.views.analysis.code.security.delta' || view == 'snyk.views.analysis.code.quality' || view == 'snyk.views.analysis.code.quality.delta' || view == 'snyk.views.analysis.oss' || view == 'snyk.views.analysis.configuration'", "group": "navigation" }, { "command": "snyk.settings", - "when": "view == 'snyk.views.analysis.code.security' || view == 'snyk.views.analysis.code.security.delta' || view == 'snyk.views.analysis.code.quality' || view == 'snyk.views.analysis.code.quality.delta' || view == 'snyk.views.analysis.oss' || view == 'snyk.views.welcome' || view == 'snyk.views.analysis.configuration'", + "when": "view == 'snyk.views.summary' || view == 'snyk.views.analysis.code.security' || view == 'snyk.views.analysis.code.security.delta' || view == 'snyk.views.analysis.code.quality' || view == 'snyk.views.analysis.code.quality.delta' || view == 'snyk.views.analysis.oss' || view == 'snyk.views.welcome' || view == 'snyk.views.analysis.configuration'", "group": "navigation" } ], diff --git a/src/snyk/base/modules/baseSnykModule.ts b/src/snyk/base/modules/baseSnykModule.ts index 3ddf7d2ee..a069f30ca 100644 --- a/src/snyk/base/modules/baseSnykModule.ts +++ b/src/snyk/base/modules/baseSnykModule.ts @@ -25,6 +25,7 @@ import { OssVulnerabilityCountService } from '../../snykOss/services/vulnerabili import { IAuthenticationService } from '../services/authenticationService'; import { ScanModeService } from '../services/scanModeService'; import SnykStatusBarItem, { IStatusBarItem } from '../statusBarItem/statusBarItem'; +import { ISummaryProviderService } from '../summary/summaryProviderService'; import { ILoadingBadge, LoadingBadge } from '../views/loadingBadge'; import { IBaseSnykModule } from './interfaces'; @@ -35,7 +36,7 @@ export default abstract class BaseSnykModule implements IBaseSnykModule { protected readonly editorsWatcher: IWatcher; protected configurationWatcher: IWatcher; - + protected summaryProviderService: ISummaryProviderService; readonly contextService: IContextService; cacheService: IClearCacheService; readonly openerService: IOpenerService; diff --git a/src/snyk/base/summary/summaryProviderService.ts b/src/snyk/base/summary/summaryProviderService.ts new file mode 100644 index 000000000..1747bfc47 --- /dev/null +++ b/src/snyk/base/summary/summaryProviderService.ts @@ -0,0 +1,24 @@ +import { ILog } from '../../common/logger/interfaces'; +import { SummaryWebviewViewProvider } from '../../common/views/summaryWebviewProvider'; + +export interface ISummaryProviderService { + updateSummaryPanel(scanSummary: string): void; +} + +export class SummaryProviderService implements ISummaryProviderService { + constructor( + private readonly logger: ILog, + private readonly summaryWebviewViewProvider: SummaryWebviewViewProvider | undefined, + ) {} + public updateSummaryPanel(scanSummary: string) { + if (!this.summaryWebviewViewProvider) { + this.logger.error('Summary Webview Provider was not initialized.'); + return; + } + try { + this.summaryWebviewViewProvider.updateWebviewContent(scanSummary); + } catch (error) { + this.logger.error('Failed to update Summary panel'); + } + } +} diff --git a/src/snyk/common/commands/commandController.ts b/src/snyk/common/commands/commandController.ts index 596976f03..f07ab2831 100644 --- a/src/snyk/common/commands/commandController.ts +++ b/src/snyk/common/commands/commandController.ts @@ -84,6 +84,10 @@ export class CommandController { await this.folderConfigs.setBranch(this.window, this.configuration, folderPath); } + async toggleDelta(isEnabled: boolean): Promise { + await this.configuration.setDeltaFindingsEnabled(isEnabled); + } + openSettings(): void { void this.commands.executeCommand(VSCODE_GO_TO_SETTINGS_COMMAND, `@ext:${this.configuration.getExtensionId()}`); } diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 187d8e68b..e5a318590 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -35,6 +35,7 @@ import { IVSCodeWorkspace } from '../vscode/workspace'; import { CliExecutable } from '../../cli/cliExecutable'; const NEWISSUES = 'Net new issues'; +const ALLISSUES = 'All issues'; export type FeaturesConfiguration = { ossEnabled: boolean | undefined; @@ -143,6 +144,7 @@ export interface IConfiguration { setEndpoint(endpoint: string): Promise; getDeltaFindingsEnabled(): boolean; + setDeltaFindingsEnabled(isEnabled: boolean): Promise; getOssQuickFixCodeActionsEnabled(): boolean; @@ -361,6 +363,19 @@ export class Configuration implements IConfiguration { ); } + async setDeltaFindingsEnabled(isEnabled: boolean): Promise { + let deltaValue = NEWISSUES; + if (!isEnabled) { + deltaValue = ALLISSUES; + } + await this.workspace.updateConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(DELTA_FINDINGS), + deltaValue, + true, + ); + } + async clearToken(): Promise { return new Promise((resolve, reject) => { SecretStorageAdapter.instance diff --git a/src/snyk/common/constants/commands.ts b/src/snyk/common/constants/commands.ts index b49959089..316ba5eed 100644 --- a/src/snyk/common/constants/commands.ts +++ b/src/snyk/common/constants/commands.ts @@ -20,6 +20,7 @@ export const SNYK_SHOW_ERROR_FROM_CONTEXT_COMMAND = 'snyk.showErrorFromContext'; export const SNYK_GET_LESSON_COMMAND = 'snyk.getLearnLesson'; export const SNYK_GET_SETTINGS_SAST_ENABLED = 'snyk.getSettingsSastEnabled'; export const SNYK_SET_BASE_BRANCH_COMMAND = 'snyk.setBaseBranch'; +export const SNYK_TOGGLE_DELTA = 'snyk.toggleDelta'; 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'; diff --git a/src/snyk/common/constants/languageServer.ts b/src/snyk/common/constants/languageServer.ts index a4187beaa..6efc5aa66 100644 --- a/src/snyk/common/constants/languageServer.ts +++ b/src/snyk/common/constants/languageServer.ts @@ -12,3 +12,4 @@ export const SNYK_HAS_AUTHENTICATED = '$/snyk.hasAuthenticated'; export const SNYK_ADD_TRUSTED_FOLDERS = '$/snyk.addTrustedFolders'; export const SNYK_SCAN = '$/snyk.scan'; export const SNYK_FOLDERCONFIG = '$/snyk.folderConfigs'; +export const SNYK_SCANSUMMARY = '$/snyk.scanSummary'; diff --git a/src/snyk/common/constants/views.ts b/src/snyk/common/constants/views.ts index 438827751..da3bed911 100644 --- a/src/snyk/common/constants/views.ts +++ b/src/snyk/common/constants/views.ts @@ -1,6 +1,7 @@ // see https://code.visualstudio.com/api/references/contribution-points#contributes.viewsWelcome export const SNYK_VIEW_WELCOME = 'snyk.views.welcome'; +export const SNYK_VIEW_SUMMARY = 'snyk.views.summary'; export const SNYK_VIEW_ANALYSIS_CODE_ENABLEMENT = 'snyk.views.analysis.code.enablement'; export const SNYK_VIEW_ANALYSIS_CODE_SECURITY = 'snyk.views.analysis.code.security'; export const SNYK_VIEW_ANALYSIS_CODE_QUALITY = 'snyk.views.analysis.code.quality'; @@ -25,6 +26,7 @@ export const SNYK_CONTEXT = { MODE: 'mode', ADVANCED: 'advanced', DELTA_FINDINGS_ENABLED: 'deltaFindingsEnabled', + SCANSUMMARY: 'scanSummaryHtml', }; export const SNYK_ANALYSIS_STATUS = { diff --git a/src/snyk/common/languageServer/languageServer.ts b/src/snyk/common/languageServer/languageServer.ts index 11ee70504..45a2fc13c 100644 --- a/src/snyk/common/languageServer/languageServer.ts +++ b/src/snyk/common/languageServer/languageServer.ts @@ -8,6 +8,7 @@ import { SNYK_HAS_AUTHENTICATED, SNYK_LANGUAGE_SERVER_NAME, SNYK_SCAN, + SNYK_SCANSUMMARY, } from '../constants/languageServer'; import { CONFIGURATION_IDENTIFIER } from '../constants/settings'; import { ErrorHandler } from '../error/errorHandler'; @@ -19,11 +20,11 @@ import { ILanguageClientAdapter } from '../vscode/languageClient'; import { LanguageClient, LanguageClientOptions, ServerOptions } from '../vscode/types'; import { IVSCodeWindow } from '../vscode/window'; import { IVSCodeWorkspace } from '../vscode/workspace'; -import { CliExecutable } from '../../cli/cliExecutable'; import { LanguageClientMiddleware } from './middleware'; import { LanguageServerSettings, ServerSettings } from './settings'; import { CodeIssueData, IacIssueData, OssIssueData, Scan } from './types'; import { ExtensionContext } from '../vscode/extensionContext'; +import { ISummaryProviderService } from '../../base/summary/summaryProviderService'; export interface ILanguageServer { start(): Promise; @@ -51,6 +52,7 @@ export class LanguageServer implements ILanguageServer { private readonly logger: ILog, private downloadService: DownloadService, private extensionContext: ExtensionContext, + private summaryProvider: ISummaryProviderService, ) { this.downloadService = downloadService; } @@ -156,6 +158,10 @@ export class LanguageServer implements ILanguageServer { this.logger.info(`${_.capitalize(scan.product)} scan for ${scan.folderPath}: ${scan.status}.`); this.scan$.next(scan); }); + + client.onNotification(SNYK_SCANSUMMARY, ({ scanSummary }: { scanSummary: string }) => { + this.summaryProvider.updateSummaryPanel(scanSummary); + }); } // Initialization options are not semantically equal to server settings, thus separated here diff --git a/src/snyk/common/languageServer/types.ts b/src/snyk/common/languageServer/types.ts index cc53c2f9e..e3f17bf20 100644 --- a/src/snyk/common/languageServer/types.ts +++ b/src/snyk/common/languageServer/types.ts @@ -135,3 +135,14 @@ export type AutofixUnifiedDiffSuggestion = { fixId: string; unifiedDiffsPerFile: { [key: string]: string }; }; + +export type Summary = { + toggleDelta: boolean; +}; + +export type SummaryMessage = { + type: string; + args: { + summary: Summary; + }; +}; diff --git a/src/snyk/common/services/downloadService.ts b/src/snyk/common/services/downloadService.ts index b7e547a3b..2dbe538d1 100644 --- a/src/snyk/common/services/downloadService.ts +++ b/src/snyk/common/services/downloadService.ts @@ -90,7 +90,7 @@ export class DownloadService { } async isCliInstalled() { - const cliExecutableExists = await CliExecutable.exists(this.extensionContext.extensionPath); + const cliExecutableExists = await CliExecutable.exists(await this.configuration.getCliPath()); const cliChecksumWritten = !!this.getCliChecksum(); return cliExecutableExists && cliChecksumWritten; diff --git a/src/snyk/common/views/summaryWebviewProvider.ts b/src/snyk/common/views/summaryWebviewProvider.ts new file mode 100644 index 000000000..7c22c6aaf --- /dev/null +++ b/src/snyk/common/views/summaryWebviewProvider.ts @@ -0,0 +1,71 @@ +import * as vscode from 'vscode'; +import { readFileSync } from 'fs'; +import { getNonce } from './nonce'; +import { SummaryMessage } from '../languageServer/types'; +import { SNYK_TOGGLE_DELTA } from '../constants/commands'; +import { Logger } from '../logger/logger'; +export class SummaryWebviewViewProvider implements vscode.WebviewViewProvider { + private static instance: SummaryWebviewViewProvider; + private webviewView: vscode.WebviewView | undefined; + private context: vscode.ExtensionContext; + + private constructor(context: vscode.ExtensionContext) { + this.context = context; + } + + public static getInstance(extensionContext?: vscode.ExtensionContext): SummaryWebviewViewProvider | undefined { + if (!SummaryWebviewViewProvider.instance) { + if (!extensionContext) { + console.log('ExtensionContext is required for the first initialization of SnykDiagnosticsWebviewViewProvider'); + return undefined; + } else { + SummaryWebviewViewProvider.instance = new SummaryWebviewViewProvider(extensionContext); + } + } + return SummaryWebviewViewProvider.instance; + } + + resolveWebviewView(webviewView: vscode.WebviewView) { + this.webviewView = webviewView; + webviewView.webview.options = { + enableScripts: true, + }; + this.webviewView.webview.onDidReceiveMessage((msg: SummaryMessage) => this.handleMessage(msg)); + } + + private async handleMessage(message: SummaryMessage) { + try { + switch (message.type) { + case 'sendSummaryParams': { + const { summary } = message.args; + await vscode.commands.executeCommand(SNYK_TOGGLE_DELTA, summary.toggleDelta); + break; + } + } + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Logger.error(error); + } + } + + public updateWebviewContent(html: string) { + if (this.webviewView) { + const nonce = getNonce(); + const ideScriptPath = vscode.Uri.joinPath( + vscode.Uri.file(this.context.extensionPath), + 'out', + 'snyk', + 'common', + 'views', + 'summaryWebviewScript.js', + ); + const ideScript = readFileSync(ideScriptPath.fsPath, 'utf8'); + + html = html.replace('${ideStyle}', `'); + html = html.replace('${ideFunc}', ideScript); + html = html.replace(/\${nonce}/g, nonce); + + this.webviewView.webview.html = html; + } + } +} diff --git a/src/snyk/common/views/summaryWebviewScript.ts b/src/snyk/common/views/summaryWebviewScript.ts new file mode 100644 index 000000000..45f37e4e5 --- /dev/null +++ b/src/snyk/common/views/summaryWebviewScript.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +type SummaryMessage = { + type: 'sendSummaryParams'; + args: { + summary: Summary; + }; +}; + +type Summary = { + toggleDelta: boolean; +}; +const vscode = acquireVsCodeApi(); + +const summary: Summary = { + // @ts-expect-error this will be injected in a func coming from LS that has isEnabled as arg. + toggleDelta: isEnabled, +}; + +const message: SummaryMessage = { + type: 'sendSummaryParams', + args: { summary }, +}; +vscode.postMessage(message); diff --git a/src/snyk/common/watchers/configurationWatcher.ts b/src/snyk/common/watchers/configurationWatcher.ts index c3846daaa..0e18d898f 100644 --- a/src/snyk/common/watchers/configurationWatcher.ts +++ b/src/snyk/common/watchers/configurationWatcher.ts @@ -60,7 +60,7 @@ class ConfigurationWatcher implements IWatcher { extension.initDependencyDownload(); return; } else if (key === FOLDER_CONFIGS || key == DELTA_FINDINGS) { - extension.viewManagerService.refreshAllViews(); + return extension.viewManagerService.refreshAllViews(); } else if (key === TRUSTED_FOLDERS) { extension.workspaceTrust.resetTrustedFoldersCache(); extension.viewManagerService.refreshAllViews(); diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index cb455a315..dfb641d53 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -25,6 +25,7 @@ import { SNYK_SHOW_LS_OUTPUT_COMMAND, SNYK_SHOW_OUTPUT_COMMAND, SNYK_START_COMMAND, + SNYK_TOGGLE_DELTA, SNYK_WORKSPACE_SCAN_COMMAND, } from './common/constants/commands'; import { @@ -34,6 +35,7 @@ import { SNYK_VIEW_ANALYSIS_CODE_SECURITY, SNYK_VIEW_ANALYSIS_IAC, SNYK_VIEW_ANALYSIS_OSS, + SNYK_VIEW_SUMMARY, SNYK_VIEW_SUPPORT, SNYK_VIEW_WELCOME, } from './common/constants/views'; @@ -83,9 +85,22 @@ import { GitAPI, GitExtension, Repository } from './common/git'; import { AnalyticsSender } from './common/analytics/AnalyticsSender'; import { MEMENTO_ANALYTICS_PLUGIN_INSTALLED_SENT } from './common/constants/globalState'; import { AnalyticsEvent } from './common/analytics/AnalyticsEvent'; +import { SummaryWebviewViewProvider } from './common/views/summaryWebviewProvider'; +import { SummaryProviderService } from './base/summary/summaryProviderService'; class SnykExtension extends SnykLib implements IExtension { public async activate(vscodeContext: vscode.ExtensionContext): Promise { + const summaryWebviewViewProvider = SummaryWebviewViewProvider.getInstance(vscodeContext); + if (!summaryWebviewViewProvider) { + console.log('Summary panel not initialized.'); + } else { + this.summaryProviderService = new SummaryProviderService(Logger, summaryWebviewViewProvider); + vscodeContext.subscriptions.push( + vscode.window.registerWebviewViewProvider(SNYK_VIEW_SUMMARY, summaryWebviewViewProvider), + ); + } + + SummaryWebviewViewProvider.getInstance(vscodeContext); extensionContext.setContext(vscodeContext); this.context = extensionContext; const snykConfiguration = await this.getSnykConfiguration(); @@ -199,6 +214,7 @@ class SnykExtension extends SnykLib implements IExtension { Logger, this.downloadService, this.context, + this.summaryProviderService, ); const codeSuggestionProvider = new CodeSuggestionWebviewProvider( @@ -498,6 +514,7 @@ class SnykExtension extends SnykLib implements IExtension { ), vscode.commands.registerCommand(SNYK_START_COMMAND, async () => { await vscode.commands.executeCommand(SNYK_WORKSPACE_SCAN_COMMAND); + await vscode.commands.executeCommand('setContext', 'scanSummaryHtml', 'scanSummary'); }), vscode.commands.registerCommand(SNYK_SETTINGS_COMMAND, () => this.commandController.openSettings()), vscode.commands.registerCommand(SNYK_DCIGNORE_COMMAND, (custom: boolean, path?: string) => @@ -512,6 +529,9 @@ class SnykExtension extends SnykLib implements IExtension { vscode.commands.registerCommand(SNYK_SET_BASE_BRANCH_COMMAND, (folderPath: string) => this.commandController.setBaseBranch(folderPath), ), + vscode.commands.registerCommand(SNYK_TOGGLE_DELTA, (isEnabled: boolean) => + this.commandController.toggleDelta(isEnabled), + ), vscode.commands.registerCommand(SNYK_SHOW_ERROR_FROM_CONTEXT_COMMAND, () => { const err = this.contextService.viewContext[SNYK_CONTEXT.ERROR] as Error; void this.notificationService.showErrorNotification(err.message); diff --git a/src/test/unit/common/languageServer/languageServer.test.ts b/src/test/unit/common/languageServer/languageServer.test.ts index ddce3a8c5..b82f24d80 100644 --- a/src/test/unit/common/languageServer/languageServer.test.ts +++ b/src/test/unit/common/languageServer/languageServer.test.ts @@ -18,6 +18,7 @@ import { windowMock } from '../../mocks/window.mock'; import { stubWorkspaceConfiguration } from '../../mocks/workspace.mock'; import { PROTOCOL_VERSION } from '../../../../snyk/common/constants/languageServer'; import { ExtensionContext } from '../../../../snyk/common/vscode/extensionContext'; +import { ISummaryProviderService } from '../../../../snyk/base/summary/summaryProviderService'; suite('Language Server', () => { const authServiceMock = {} as IAuthenticationService; @@ -142,6 +143,7 @@ suite('Language Server', () => { logger, downloadServiceMock, extensionContextMock, + {} as ISummaryProviderService, ); downloadServiceMock.downloadReady$.next(); @@ -192,6 +194,7 @@ suite('Language Server', () => { new LoggerMock(), downloadServiceMock, extensionContextMock, + {} as ISummaryProviderService, ); downloadServiceMock.downloadReady$.next(); await languageServer.start(); @@ -218,6 +221,7 @@ suite('Language Server', () => { new LoggerMock(), downloadServiceMock, extensionContextMock, + {} as ISummaryProviderService, ); }); @@ -265,6 +269,7 @@ suite('Language Server', () => { new LoggerMock(), downloadServiceMock, extensionContextMock, + {} as ISummaryProviderService, ); const initOptions = await languageServer.getInitializationOptions();