diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5fc5678d7..e9e98adf1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,7 +2,7 @@ name: CI on: pull_request: branches: - - main + - '**' workflow_call: secrets: ITERATIVELY_KEY: diff --git a/.github/workflows/release-preview.yaml b/.github/workflows/release-preview.yaml index fa7ad1a51..4fd5195f2 100644 --- a/.github/workflows/release-preview.yaml +++ b/.github/workflows/release-preview.yaml @@ -28,6 +28,7 @@ jobs: - name: Verify analytics events run: npm run ampli:verify -- -t ${{ secrets.ITERATIVELY_KEY }} + # Naming convention for the preview version means we can only release one preview per hour - name: Patch to preview version run: npm run patch-preview env: diff --git a/CHANGELOG.md b/CHANGELOG.md index e893b4352..c76ade112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Snyk Security - Code and Open Source Dependencies Changelog +## [2.1.0] + +### Added + +- Snyk LS: Snyk Open Source Security features now use Language Server backend +- Snyk OSS: Squiggly warning underlines for direct and transitive vulnerabilities +- Snyk OSS: Squiggly underlines colour coded based on severity +- Snyk OSS: Vulnerability count text includes transitive vulnerabilities +- Snyk OSS: Vulnerability count text includes breakdown of vulnerabilities by severity +- Snyk OSS: Hovers lists vulnerabilities and shows summary (without typo) +- Snyk OSS: Hovers show information from security.snyk.io/vuln database +- Snyk OSS: CodeActions shows actions available for all vulnerabilities + ## [1.26.1] ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 16faa4f5f..9d32814bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ ## Run extension and debug -Clone the repository, then run `npm install` in the directory. +Clone the repository, then run `npm install && npm run build` in the directory. - Open repository directory in VS Code and press `F5` to run extension in a new VS Code window. - This allows extension debugging within VS Code. diff --git a/package-lock.json b/package-lock.json index a4fb2f853..6d6f52a0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "analytics-node": "^4.0.1", "axios": "^0.27.2", "glob": "^7.2.0", + "he": "^1.2.0", "htmlparser2": "^7.2.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", @@ -41,6 +42,7 @@ "@types/babel__traverse": "^7.12.2", "@types/find-package-json": "^1.2.2", "@types/glob": "^7.1.3", + "@types/he": "^1.2.3", "@types/lodash": "^4.14.161", "@types/marked": "^3.0.0", "@types/mocha": "^8.0.3", @@ -1673,6 +1675,12 @@ "@types/node": "*" } }, + "node_modules/@types/he": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz", + "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==", + "dev": true + }, "node_modules/@types/inquirer": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.5.tgz", @@ -4739,7 +4747,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, "bin": { "he": "bin/he" } @@ -6958,9 +6965,9 @@ } }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -9781,6 +9788,12 @@ "@types/node": "*" } }, + "@types/he": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz", + "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==", + "dev": true + }, "@types/inquirer": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.5.tgz", @@ -12063,8 +12076,7 @@ "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "htmlparser2": { "version": "7.2.0", @@ -13736,9 +13748,9 @@ } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "qs": { diff --git a/package.json b/package.json index fb321914a..29f70e09e 100644 --- a/package.json +++ b/package.json @@ -399,6 +399,7 @@ "@types/babel__traverse": "^7.12.2", "@types/find-package-json": "^1.2.2", "@types/glob": "^7.1.3", + "@types/he": "^1.2.3", "@types/lodash": "^4.14.161", "@types/marked": "^3.0.0", "@types/mocha": "^8.0.3", @@ -441,6 +442,7 @@ "analytics-node": "^4.0.1", "axios": "^0.27.2", "glob": "^7.2.0", + "he": "^1.2.0", "htmlparser2": "^7.2.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", diff --git a/src/snyk/base/modules/baseSnykModule.ts b/src/snyk/base/modules/baseSnykModule.ts index 5a3bee102..14ed435b2 100644 --- a/src/snyk/base/modules/baseSnykModule.ts +++ b/src/snyk/base/modules/baseSnykModule.ts @@ -23,7 +23,7 @@ import { IMarkdownStringAdapter, MarkdownStringAdapter } from '../../common/vsco import { IWatcher } from '../../common/watchers/interfaces'; import { ICodeSettings } from '../../snykCode/codeSettings'; import SnykEditorsWatcher from '../../snykCode/watchers/editorsWatcher'; -import { OssService } from '../../snykOss/services/ossService'; +import { OssService } from '../../snykOss/ossService'; import { OssVulnerabilityCountService } from '../../snykOss/services/vulnerabilityCount/ossVulnerabilityCountService'; import { IAuthenticationService } from '../services/authenticationService'; import { ScanModeService } from '../services/scanModeService'; @@ -85,6 +85,4 @@ export default abstract class BaseSnykModule implements IBaseSnykModule { } abstract runScan(): Promise; - - abstract runOssScan(): Promise; } diff --git a/src/snyk/base/modules/interfaces.ts b/src/snyk/base/modules/interfaces.ts index b1748810e..e43446fbb 100644 --- a/src/snyk/base/modules/interfaces.ts +++ b/src/snyk/base/modules/interfaces.ts @@ -17,7 +17,6 @@ export interface IBaseSnykModule { // Abstract methods runScan(): Promise; - runOssScan(manual?: boolean): Promise; } export interface ISnykLib { diff --git a/src/snyk/base/modules/snykLib.ts b/src/snyk/base/modules/snykLib.ts index 070350d5d..640b8e2fc 100644 --- a/src/snyk/base/modules/snykLib.ts +++ b/src/snyk/base/modules/snykLib.ts @@ -1,9 +1,7 @@ import * as _ from 'lodash'; -import { firstValueFrom } from 'rxjs'; -import { CliError } from '../../cli/services/cliService'; import { SupportedAnalysisProperties } from '../../common/analytics/itly'; import { configuration } from '../../common/configuration/instance'; -import { DEFAULT_SCAN_DEBOUNCE_INTERVAL, IDE_NAME, OSS_SCAN_DEBOUNCE_INTERVAL } from '../../common/constants/general'; +import { DEFAULT_SCAN_DEBOUNCE_INTERVAL, IDE_NAME } from '../../common/constants/general'; import { SNYK_CONTEXT } from '../../common/constants/views'; import { ErrorHandler } from '../../common/error/errorHandler'; import { Logger } from '../../common/logger/logger'; @@ -23,7 +21,6 @@ export default class SnykLib extends BaseSnykModule implements ISnykLib { return; } - // Only starts OSS scan. Code & IaC scans are managed by LS Logger.info('Starting full scan'); await this.contextService.setContext(SNYK_CONTEXT.AUTHENTICATING, false); @@ -39,7 +36,6 @@ export default class SnykLib extends BaseSnykModule implements ISnykLib { const workspacePaths = vsCodeWorkspace.getWorkspaceFolders(); if (workspacePaths.length) { this.logFullAnalysisIsTriggered(manual); - void this.startOssAnalysis(manual, false); } } catch (err) { await ErrorHandler.handleGlobal(err, Logger, this.contextService, this.loadingBadge); @@ -48,11 +44,8 @@ export default class SnykLib extends BaseSnykModule implements ISnykLib { // This function is called by commands, error handlers, etc. // We should avoid having duplicate parallel executions. - // Only starts OSS scan. Code & IaC scans are managed by LS public runScan = _.debounce(this.runFullScan_.bind(this), DEFAULT_SCAN_DEBOUNCE_INTERVAL, { leading: true }); - public runOssScan = _.debounce(this.startOssAnalysis.bind(this), OSS_SCAN_DEBOUNCE_INTERVAL, { leading: true }); - async enableCode(): Promise { Logger.info('Enabling Snyk Code'); const wasEnabled = await this.codeSettings.enable(); @@ -66,12 +59,6 @@ export default class SnykLib extends BaseSnykModule implements ISnykLib { } } - onDidChangeOssTreeVisibility(visible: boolean): void { - if (this.ossService) { - this.ossService.setVulnerabilityTreeVisibility(visible); - } - } - async checkAdvancedMode(): Promise { await this.contextService.setContext(SNYK_CONTEXT.ADVANCED, configuration.shouldShowAdvancedView); } @@ -81,25 +68,6 @@ export default class SnykLib extends BaseSnykModule implements ISnykLib { await this.contextService.setContext(SNYK_CONTEXT.WORKSPACE_FOUND, workspaceFound); } - private async startOssAnalysis(manual = false, reportTriggeredEvent = true): Promise { - if (!configuration.getFeaturesConfiguration()?.ossEnabled) return; - if (!this.ossService) throw new Error('OSS service is not initialized.'); - - // wait until Snyk Language Server is downloaded - await firstValueFrom(this.downloadService.downloadReady$); - - try { - const result = await this.ossService.test(manual, reportTriggeredEvent); - - if (result instanceof CliError || !result) { - return; - } - } catch (err) { - // catch unhandled error cases by reporting test failure - this.ossService.finalizeTest(new CliError(err)); - } - } - private isSnykCodeAutoscanSuspended(manual: boolean) { return !manual && !this.scanModeService.isCodeAutoScanAllowed(); } diff --git a/src/snyk/cli/process.ts b/src/snyk/cli/process.ts index d49553ff0..793335386 100644 --- a/src/snyk/cli/process.ts +++ b/src/snyk/cli/process.ts @@ -1,11 +1,11 @@ import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; +import { OAuthToken } from '../base/services/authenticationService'; import { Configuration, IConfiguration } from '../common/configuration/configuration'; import { ILog } from '../common/logger/interfaces'; import { getVsCodeProxy } from '../common/proxy'; import { IVSCodeWorkspace } from '../common/vscode/workspace'; import { CLI_INTEGRATION_NAME } from './contants/integration'; import { CliError } from './services/cliService'; -import { OAuthToken } from '../base/services/authenticationService'; export class CliProcess { private runningProcess: ChildProcessWithoutNullStreams | null; @@ -25,6 +25,7 @@ export class CliProcess { return new Promise((resolve, reject) => { let output = ''; + // file deepcode ignore ArrayMethodOnNonArray: readonly string[] is an array of strings this.logger.info(`Running "${cliPath} ${args.join(' ')}".`); this.runningProcess = spawn(cliPath, args, { env: { ...process.env, ...processEnv }, cwd }); diff --git a/src/snyk/common/commands/commandController.ts b/src/snyk/common/commands/commandController.ts index cb2f31a28..630ec0e26 100644 --- a/src/snyk/common/commands/commandController.ts +++ b/src/snyk/common/commands/commandController.ts @@ -2,13 +2,11 @@ import _ from 'lodash'; import { IAuthenticationService } from '../../base/services/authenticationService'; import { ScanModeService } from '../../base/services/scanModeService'; -import { createDCIgnore } from '../../snykCode/utils/ignoreFileUtils'; +import { createDCIgnore as createDCIgnoreUtil } from '../../snykCode/utils/ignoreFileUtils'; import { IssueUtils } from '../../snykCode/utils/issueUtils'; import { CodeIssueCommandArg } from '../../snykCode/views/interfaces'; import { IacIssueCommandArg } from '../../snykIac/views/interfaces'; -import { capitalizeOssSeverity } from '../../snykOss/ossResult'; -import { OssService } from '../../snykOss/services/ossService'; -import { OssIssueCommandArg } from '../../snykOss/views/ossVulnerabilityTreeProvider'; +import { OssService } from '../../snykOss/ossService'; import { IAnalytics } from '../analytics/itly'; import { SNYK_INITIATE_LOGIN_COMMAND, @@ -90,11 +88,11 @@ export class CommandController { const paths = this.workspace.getWorkspaceFolders(); const promises = []; for (const p of paths) { - promises.push(createDCIgnore(p, custom, this.workspace, this.window, uriAdapter)); + promises.push(createDCIgnoreUtil(p, custom, this.workspace, this.window, uriAdapter)); } await Promise.all(promises); } else { - await createDCIgnore(path, custom, this.workspace, this.window, uriAdapter); + await createDCIgnoreUtil(path, custom, this.workspace, this.window, uriAdapter); } } @@ -122,14 +120,28 @@ export class CommandController { severity: IssueUtils.issueSeverityAsText(issue.severity), }); } else if (arg.issueType == OpenCommandIssueType.OssVulnerability) { - const issue = arg.issue as OssIssueCommandArg; - void this.ossService.showSuggestionProvider(issue); + const issueArgs = arg.issue as CodeIssueCommandArg; + const folderPath = issueArgs.folderPath; + const issue = this.ossService.getIssue(folderPath, issueArgs.id); + + if (!issue) { + this.logger.warn(`Failed to find the issue ${issueArgs.id}.`); + return; + } + + await this.openLocalFile(issue.filePath, issueArgs.range); + + try { + await this.ossService.showSuggestionProvider(folderPath, issueArgs.id); + } catch (e) { + ErrorHandler.handle(e, this.logger); + } this.analytics.logIssueInTreeIsClicked({ ide: IDE_NAME, issueId: issue.id, issueType: 'Open Source Vulnerability', - severity: capitalizeOssSeverity(issue.severity), + severity: IssueUtils.issueSeverityAsText(issue.severity), }); } else if (arg.issueType == OpenCommandIssueType.IacIssue) { const issueArgs = arg.issue as IacIssueCommandArg; diff --git a/src/snyk/common/commands/types.ts b/src/snyk/common/commands/types.ts index 5c61f0596..2fee394d9 100644 --- a/src/snyk/common/commands/types.ts +++ b/src/snyk/common/commands/types.ts @@ -1,7 +1,7 @@ import { completeFileSuggestionType } from '../../snykCode/interfaces'; import { CodeIssueCommandArg } from '../../snykCode/views/interfaces'; import { IacIssueCommandArg } from '../../snykIac/views/interfaces'; -import { OssIssueCommandArg } from '../../snykOss/views/ossVulnerabilityTreeProvider'; +import { OssIssueCommandArg } from '../../snykOss/interfaces'; import { CodeIssueData, Issue } from '../languageServer/types'; export enum OpenCommandIssueType { @@ -11,7 +11,7 @@ export enum OpenCommandIssueType { } export type OpenIssueCommandArg = { - issue: CodeIssueCommandArg | OssIssueCommandArg | IacIssueCommandArg; + issue: CodeIssueCommandArg | IacIssueCommandArg | OssIssueCommandArg; issueType: OpenCommandIssueType; }; @@ -21,10 +21,3 @@ export const isCodeIssue = ( ): _issue is Issue => { return issueType === OpenCommandIssueType.CodeIssue; }; - -export const isOssIssue = ( - _issue: completeFileSuggestionType | Issue | OssIssueCommandArg, - issueType: OpenCommandIssueType, -): _issue is OssIssueCommandArg => { - return issueType === OpenCommandIssueType.OssVulnerability; -}; diff --git a/src/snyk/common/constants/general.ts b/src/snyk/common/constants/general.ts index d6e685c4e..17ac8fe7a 100644 --- a/src/snyk/common/constants/general.ts +++ b/src/snyk/common/constants/general.ts @@ -10,7 +10,6 @@ export const IDE_NAME_SHORT = 'vscode'; export const COMMAND_DEBOUNCE_INTERVAL = 200; // 200 milliseconds export const DEFAULT_SCAN_DEBOUNCE_INTERVAL = 1000; // 1 second export const DEFAULT_LS_DEBOUNCE_INTERVAL = 1000; // 1 second -export const OSS_SCAN_DEBOUNCE_INTERVAL = 10000; // 10 seconds export const EXECUTION_THROTTLING_INTERVAL = 1000 * 10; // * 60 * 30; // 30 minutes export const EXECUTION_PAUSE_INTERVAL = 1000 * 60 * 30; // 30 minutes export const REFRESH_VIEW_DEBOUNCE_INTERVAL = 200; // 200 milliseconds diff --git a/src/snyk/common/editor/codeActionsProvider.ts b/src/snyk/common/editor/codeActionsProvider.ts index 74a38b4b9..8181c39b3 100644 --- a/src/snyk/common/editor/codeActionsProvider.ts +++ b/src/snyk/common/editor/codeActionsProvider.ts @@ -2,19 +2,26 @@ import { IAnalytics, SupportedQuickFixProperties } from '../../common/analytics/ import { IDE_NAME } from '../../common/constants/general'; import { Issue } from '../../common/languageServer/types'; import { ICodeActionKindAdapter } from '../../common/vscode/codeAction'; -import { CodeAction, CodeActionKind, CodeActionProvider, Range, TextDocument } from '../../common/vscode/types'; +import { + CodeAction, + CodeActionContext, + CodeActionKind, + CodeActionProvider, + Range, + TextDocument, +} from '../../common/vscode/types'; import { ProductResult } from '../services/productService'; export abstract class CodeActionsProvider implements CodeActionProvider { protected readonly providedCodeActionKinds = [this.codeActionKindAdapter.getQuickFix()]; constructor( - private readonly issues: ProductResult, + protected readonly issues: ProductResult, private readonly codeActionKindAdapter: ICodeActionKindAdapter, - private readonly analytics: IAnalytics, + protected readonly analytics: IAnalytics, ) {} - abstract getActions(folderPath: string, document: TextDocument, issue: Issue, issueRange: Range): CodeAction[]; + abstract getActions(folderPath: string, document: TextDocument, issue: Issue, issueRange?: Range): CodeAction[]; abstract getAnalyticsActionTypes(): [string, ...string[]] & [SupportedQuickFixProperties, ...SupportedQuickFixProperties[]]; @@ -25,7 +32,11 @@ export abstract class CodeActionsProvider implements CodeActionProvider { return this.providedCodeActionKinds; } - public provideCodeActions(document: TextDocument, clickedRange: Range): CodeAction[] | undefined { + public provideCodeActions( + document: TextDocument, + clickedRange: Range, + _context: CodeActionContext, + ): CodeAction[] | undefined { if (this.issues.size === 0) { return undefined; } @@ -57,7 +68,7 @@ export abstract class CodeActionsProvider implements CodeActionProvider { return undefined; } - private findIssueWithRange( + protected findIssueWithRange( result: Issue[], document: TextDocument, clickedRange: Range, diff --git a/src/snyk/common/languageServer/lsExecutable.ts b/src/snyk/common/languageServer/lsExecutable.ts index 8a7c0ca8e..341e1e04a 100644 --- a/src/snyk/common/languageServer/lsExecutable.ts +++ b/src/snyk/common/languageServer/lsExecutable.ts @@ -43,10 +43,10 @@ export class LsExecutable { return customPath; } - const platform = this.getCurrentWithArch(); + const platform = LsExecutable.getCurrentWithArch(); const homeDir = Platform.getHomeDir(); - const lsFilename = this.getFilename(platform); + const lsFilename = LsExecutable.getFilename(platform); const defaultPath = this.defaultPaths[platform]; const lsDir = path.join(homeDir, defaultPath, 'snyk-ls'); return path.join(lsDir, lsFilename); diff --git a/src/snyk/common/languageServer/settings.ts b/src/snyk/common/languageServer/settings.ts index 7fbe9024b..af55f094c 100644 --- a/src/snyk/common/languageServer/settings.ts +++ b/src/snyk/common/languageServer/settings.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { CLI_INTEGRATION_NAME } from '../../cli/contants/integration'; import { Configuration, IConfiguration, SeverityFilter } from '../configuration/configuration'; import { User } from '../user'; @@ -57,35 +58,35 @@ export class LanguageServerSettings { static async fromConfiguration(configuration: IConfiguration, user: User): Promise { const featuresConfiguration = configuration.getFeaturesConfiguration(); - const iacEnabled = defaultToTrue(featuresConfiguration.iacEnabled); - const codeSecurityEnabled = defaultToTrue(featuresConfiguration.codeSecurityEnabled); - const codeQualityEnabled = defaultToTrue(featuresConfiguration.codeQualityEnabled); + const ossEnabled = _.isUndefined(featuresConfiguration.ossEnabled) ? true : featuresConfiguration.ossEnabled; - return { - activateSnykCodeSecurity: codeSecurityEnabled, - activateSnykCodeQuality: codeQualityEnabled, - activateSnykOpenSource: 'false', - activateSnykIac: iacEnabled, + const iacEnabled = _.isUndefined(featuresConfiguration.iacEnabled) ? true : featuresConfiguration.iacEnabled; + const codeSecurityEnabled = _.isUndefined(featuresConfiguration.codeSecurityEnabled) + ? true + : featuresConfiguration.codeSecurityEnabled; + const codeQualityEnabled = _.isUndefined(featuresConfiguration.codeQualityEnabled) + ? true + : featuresConfiguration.codeQualityEnabled; + return { + activateSnykCodeSecurity: `${codeSecurityEnabled}`, + activateSnykCodeQuality: `${codeQualityEnabled}`, + activateSnykOpenSource: `${ossEnabled}`, + activateSnykIac: `${iacEnabled}`, + enableTelemetry: `${configuration.shouldReportEvents}`, + sendErrorReports: `${configuration.shouldReportErrors}`, cliPath: configuration.getCliPath(), endpoint: configuration.snykOssApiEndpoint, organization: configuration.organization, - token: await configuration.getToken(), automaticAuthentication: 'false', additionalParams: configuration.getAdditionalCliParameters(), manageBinariesAutomatically: `${configuration.isAutomaticDependencyManagementEnabled()}`, - - sendErrorReports: `${configuration.shouldReportErrors}`, - enableTelemetry: `${configuration.shouldReportEvents}`, - filterSeverity: configuration.severityFilter, scanningMode: configuration.scanningMode, insecure: `${configuration.getInsecure()}`, - enableTrustedFoldersFeature: 'true', trustedFolders: configuration.getTrustedFolders(), - integrationName: CLI_INTEGRATION_NAME, integrationVersion: await Configuration.getVersion(), deviceId: user.anonymousId, diff --git a/src/snyk/common/languageServer/types.ts b/src/snyk/common/languageServer/types.ts index 9dbe9a125..1a7d150d7 100644 --- a/src/snyk/common/languageServer/types.ts +++ b/src/snyk/common/languageServer/types.ts @@ -95,6 +95,8 @@ export type OssIssueData = { projectName: string; displayTargetFile: string; + + details: string; }; export type Identifiers = { CWE: string[]; diff --git a/src/snyk/common/services/learnService.ts b/src/snyk/common/services/learnService.ts index df3bd9964..fde6fd070 100644 --- a/src/snyk/common/services/learnService.ts +++ b/src/snyk/common/services/learnService.ts @@ -1,4 +1,3 @@ -import { OssIssueCommandArg } from '../../snykOss/views/ossVulnerabilityTreeProvider'; import { SNYK_GET_LESSON_COMMAND } from '../constants/commands'; import { CodeIssueData, Issue } from '../languageServer/types'; import { IVSCodeCommands } from '../vscode/commands'; @@ -11,28 +10,6 @@ export type Lesson = { export class LearnService { constructor(private commandExecutor: IVSCodeCommands) {} - async getOssLesson(vulnerability: OssIssueCommandArg): Promise { - const cwe = vulnerability.identifiers?.CWE; - let cweElement = ''; - if (cwe && cwe.length > 0) { - cweElement = cwe[0]; - } - - const cve = vulnerability.identifiers?.CWE; - let cveElement = ''; - if (cve && cve.length > 0) { - cveElement = cve[0]; - } - return this.commandExecutor.executeCommand( - SNYK_GET_LESSON_COMMAND, - vulnerability.id, - vulnerability.packageManager, - cweElement, - cveElement, - 4, - ); - } - async getCodeLesson(issue: Issue): Promise { const ruleSplit = issue.additionalData.ruleId.split('/'); const rule = ruleSplit[ruleSplit.length - 1]; diff --git a/src/snyk/common/services/productService.ts b/src/snyk/common/services/productService.ts index 62b6c69b3..efb4f06b1 100644 --- a/src/snyk/common/services/productService.ts +++ b/src/snyk/common/services/productService.ts @@ -1,4 +1,4 @@ -import { Subscription } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { AnalysisStatusProvider } from '../analysis/statusProvider'; import { IConfiguration } from '../configuration/configuration'; import { IWorkspaceTrust } from '../configuration/trustedFolders'; @@ -30,6 +30,7 @@ export interface IProductService extends AnalysisStatusProvider, Disposable { export abstract class ProductService extends AnalysisStatusProvider implements IProductService { private _result: ProductResult; + readonly newResultAvailable$ = new Subject(); // Track running scan count. Assumption: server sends N success/error messages for N scans in progress. private runningScanCount = 0; @@ -168,6 +169,7 @@ export abstract class ProductService extends AnalysisStatusProvider implement this.analysisFinished(); this.runningScanCount = 0; + this.newResultAvailable$.next(); this.refreshTreeView(); } } diff --git a/src/snyk/common/types.ts b/src/snyk/common/types.ts index 6b3fd99e6..7a5a667c4 100644 --- a/src/snyk/common/types.ts +++ b/src/snyk/common/types.ts @@ -1,3 +1,5 @@ +import { JAVASCRIPT, TYPESCRIPT, HTML, PJSON } from './constants/languageConsts'; + export enum Language { TypeScript, JavaScript, @@ -22,3 +24,16 @@ export type ImportedModule = { string: string; version?: string; }; + +export function languageToString(language: Language): string { + switch (language) { + case Language.TypeScript: + return TYPESCRIPT; + case Language.JavaScript: + return JAVASCRIPT; + case Language.HTML: + return HTML; + case Language.PJSON: + return PJSON; + } +} diff --git a/src/snyk/common/views/issueTreeProvider.ts b/src/snyk/common/views/issueTreeProvider.ts index 122f94966..b55fe4b01 100644 --- a/src/snyk/common/views/issueTreeProvider.ts +++ b/src/snyk/common/views/issueTreeProvider.ts @@ -41,8 +41,13 @@ export abstract class ProductIssueTreeProvider extends AnalysisTreeNodeProvid abstract getRunTestMessage(): string; abstract getIssueTitle(issue: Issue): string; - abstract getIssueRange(issue: Issue): Range; - abstract getOpenIssueCommand(issue: Issue, folderPath: string, filePath: string): Command; + abstract getIssueRange(issue?: Issue): Range | undefined; + abstract getOpenIssueCommand( + issue: Issue, + folderPath: string, + filePath: string, + filteredIssues?: Issue[], + ): Command; getRootChildren(): TreeNode[] { const nodes: TreeNode[] = []; @@ -229,7 +234,7 @@ export abstract class ProductIssueTreeProvider extends AnalysisTreeNodeProvid return IssueSeverity.Low; } - private initSeverityCounts(): ISeverityCounts { + protected initSeverityCounts(): ISeverityCounts { return { [IssueSeverity.Critical]: 0, [IssueSeverity.High]: 0, diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index a3688f750..669c942b2 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -21,8 +21,8 @@ import { SNYK_OPEN_BROWSER_COMMAND, SNYK_OPEN_ISSUE_COMMAND, SNYK_OPEN_LOCAL_COMMAND, - SNYK_SET_TOKEN_COMMAND, SNYK_SETTINGS_COMMAND, + SNYK_SET_TOKEN_COMMAND, SNYK_SHOW_LS_OUTPUT_COMMAND, SNYK_SHOW_OUTPUT_COMMAND, SNYK_START_COMMAND, @@ -49,7 +49,7 @@ import { DownloadService } from './common/services/downloadService'; import { LearnService } from './common/services/learnService'; import { NotificationService } from './common/services/notificationService'; import { User } from './common/user'; -import { CodeActionAdapter, CodeActionKindAdapter } from './common/vscode/codeAction'; +import { CodeActionAdapter } from './common/vscode/codeAction'; import { vsCodeCommands } from './common/vscode/commands'; import { vsCodeEnv } from './common/vscode/env'; import { extensionContext } from './common/vscode/extensionContext'; @@ -74,12 +74,11 @@ import { IacService } from './snykIac/iacService'; import IacIssueTreeProvider from './snykIac/views/iacIssueTreeProvider'; import { IacSuggestionWebviewProvider } from './snykIac/views/suggestion/iacSuggestionWebviewProvider'; import { EditorDecorator } from './snykOss/editor/editorDecorator'; -import { OssService } from './snykOss/services/ossService'; +import { OssService } from './snykOss/ossService'; +import { OssDetailPanelProvider } from './snykOss/providers/ossDetailPanelProvider'; +import { OssVulnerabilityCountProvider } from './snykOss/providers/ossVulnerabilityCountProvider'; +import OssIssueTreeProvider from './snykOss/providers/ossVulnerabilityTreeProvider'; import { OssVulnerabilityCountService } from './snykOss/services/vulnerabilityCount/ossVulnerabilityCountService'; -import { ModuleVulnerabilityCountProvider } from './snykOss/services/vulnerabilityCount/vulnerabilityCountProvider'; -import { OssVulnerabilityTreeProvider } from './snykOss/views/ossVulnerabilityTreeProvider'; -import { OssSuggestionWebviewProvider } from './snykOss/views/suggestion/ossSuggestionWebviewProvider'; -import { DailyScanJob } from './snykOss/watchers/dailyScanJob'; class SnykExtension extends SnykLib implements IExtension { public async activate(vscodeContext: vscode.ExtensionContext): Promise { @@ -208,19 +207,27 @@ class SnykExtension extends SnykLib implements IExtension { this.analytics, ); - this.ossService = new OssService( - this.context, + const ossSuggestionProvider = new OssDetailPanelProvider( + vsCodeWindow, + extensionContext, Logger, - configuration, - new OssSuggestionWebviewProvider(this.context, vsCodeWindow, Logger, this.learnService), + vsCodeLanguages, vsCodeWorkspace, + ); + + this.ossService = new OssService( + extensionContext, + configuration, + ossSuggestionProvider, + new CodeActionAdapter(), + this.codeActionKindAdapter, this.viewManagerService, - this.downloadService, - new DailyScanJob(this), - this.notificationService, - this.analytics, - this.languageServer, + vsCodeWorkspace, this.workspaceTrust, + this.languageServer, + vsCodeLanguages, + Logger, + this.analytics, ); const iacSuggestionProvider = new IacSuggestionWebviewProvider( @@ -263,19 +270,20 @@ class SnykExtension extends SnykLib implements IExtension { this.registerCommands(vscodeContext); const codeSecurityIssueProvider = new CodeSecurityIssueTreeProvider( - this.viewManagerService, - this.contextService, - this.snykCode, - configuration, - vsCodeLanguages, - ), - codeQualityIssueProvider = new CodeQualityIssueTreeProvider( - this.viewManagerService, - this.contextService, - this.snykCode, - configuration, - vsCodeLanguages, - ); + this.viewManagerService, + this.contextService, + this.snykCode, + configuration, + vsCodeLanguages, + ); + + const codeQualityIssueProvider = new CodeQualityIssueTreeProvider( + this.viewManagerService, + this.contextService, + this.snykCode, + configuration, + vsCodeLanguages, + ); const codeSecurityTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_CODE_SECURITY, { treeDataProvider: codeSecurityIssueProvider, @@ -291,17 +299,7 @@ class SnykExtension extends SnykLib implements IExtension { codeQualityTree, ); - const ossVulnerabilityProvider = new OssVulnerabilityTreeProvider( - this.viewManagerService, - this.contextService, - this.ossService, - configuration, - ); - - vscodeContext.subscriptions.push( - vscode.window.registerTreeDataProvider(SNYK_VIEW_ANALYSIS_OSS, ossVulnerabilityProvider), - vscode.window.registerTreeDataProvider(SNYK_VIEW_SUPPORT, new SupportProvider()), - ); + vscodeContext.subscriptions.push(vscode.window.registerTreeDataProvider(SNYK_VIEW_SUPPORT, new SupportProvider())); const welcomeTree = vscode.window.createTreeView(SNYK_VIEW_WELCOME, { treeDataProvider: new EmptyTreeDataProvider(), @@ -310,16 +308,28 @@ class SnykExtension extends SnykLib implements IExtension { treeDataProvider: new EmptyTreeDataProvider(), }); - const ossTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_OSS, { - treeDataProvider: ossVulnerabilityProvider, - }); - vscodeContext.subscriptions.push( - ossTree.onDidChangeVisibility(e => this.onDidChangeOssTreeVisibility(e.visible)), welcomeTree.onDidChangeVisibility(e => this.onDidChangeWelcomeViewVisibility(e.visible)), codeEnablementTree, ); + const ossIssueProvider = new OssIssueTreeProvider( + this.viewManagerService, + this.contextService, + this.ossService, + configuration, + vsCodeLanguages, + ); + + const ossSecurityTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_OSS, { + treeDataProvider: ossIssueProvider, + }); + + vscodeContext.subscriptions.push( + vscode.window.registerTreeDataProvider(SNYK_VIEW_ANALYSIS_OSS, ossIssueProvider), + ossSecurityTree, + ); + const iacIssueProvider = new IacIssueTreeProvider( this.viewManagerService, this.contextService, @@ -352,9 +362,8 @@ class SnykExtension extends SnykLib implements IExtension { this.editorsWatcher.activate(this); this.configurationWatcher.activate(this); this.snykCode.activateWebviewProviders(); - this.ossService.activateSuggestionProvider(); - this.ossService.activateManifestFileWatcher(this); this.iacService.activateWebviewProviders(); + this.ossService.activateWebviewProviders(); // noinspection ES6MissingAwait void this.notificationService.init(); @@ -372,7 +381,7 @@ class SnykExtension extends SnykLib implements IExtension { vsCodeWorkspace, vsCodeWindow, vsCodeLanguages, - new ModuleVulnerabilityCountProvider( + new OssVulnerabilityCountProvider( this.ossService, languageClientAdapter, new UriAdapter(), @@ -381,7 +390,6 @@ class SnykExtension extends SnykLib implements IExtension { this.ossService, Logger, new EditorDecorator(vsCodeWindow, vsCodeLanguages, new ThemeColorAdapter()), - new CodeActionKindAdapter(), this.analytics, configuration, ); @@ -438,7 +446,6 @@ class SnykExtension extends SnykLib implements IExtension { private initDependencyDownload(): DownloadService { this.downloadService.downloadOrUpdate().catch(err => { Logger.error(`${messages.lsDownloadFailed} ${ErrorHandler.stringifyError(err)}`); - this.ossService?.handleLsDownloadFailure(); }); return this.downloadService; @@ -459,8 +466,6 @@ class SnykExtension extends SnykLib implements IExtension { ), vscode.commands.registerCommand(SNYK_START_COMMAND, async () => { await vscode.commands.executeCommand(SNYK_WORKSPACE_SCAN_COMMAND); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - await this.commandController.executeCommand(SNYK_START_COMMAND, () => this.runScan(true)); // todo: remove once OSS scans replaced with LS }), vscode.commands.registerCommand(SNYK_SETTINGS_COMMAND, () => this.commandController.openSettings()), vscode.commands.registerCommand(SNYK_DCIGNORE_COMMAND, (custom: boolean, path?: string) => diff --git a/src/snyk/snykCode/constants/analysis.ts b/src/snyk/snykCode/constants/analysis.ts index a9dc2baf4..814b590c9 100644 --- a/src/snyk/snykCode/constants/analysis.ts +++ b/src/snyk/snykCode/constants/analysis.ts @@ -24,6 +24,7 @@ export const ISSUES_MARKERS_DECORATION_TYPE: { [key: string]: string } = { export const DIAGNOSTICS_CODE_SECURITY_COLLECTION_NAME = 'Snyk Code Security'; export const DIAGNOSTICS_CODE_QUALITY_COLLECTION_NAME = 'Snyk Code Quality'; export const DIAGNOSTICS_OSS_COLLECTION_NAME = 'Snyk Open Source Security'; +export const DIAGNOSTICS_OSS_COLLECTION_NAME_LS = 'Snyk Open Source'; export const WEBVIEW_PANEL_SECURITY_TITLE = 'Snyk Code Vulnerability'; export const WEBVIEW_PANEL_QUALITY_TITLE = 'Snyk Code Issue'; diff --git a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts index f12f5b37a..616ef14f0 100644 --- a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts +++ b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts @@ -1,3 +1,4 @@ +import he from 'he'; import _ from 'lodash'; import * as vscode from 'vscode'; import { @@ -111,6 +112,14 @@ export class CodeSuggestionWebviewProvider ); this.registerListeners(); } + + issue.additionalData.exampleCommitFixes.map(ecf => { + return ecf.lines.map(l => { + l.line = he.encode(l.line); + return l; + }); + }); + this.panel.webview.html = this.getHtmlForWebview(this.panel.webview); this.panel.iconPath = vscode.Uri.joinPath( vscode.Uri.file(this.context.extensionPath), diff --git a/src/snyk/snykIac/views/suggestion/iacSuggestionWebviewScript.ts b/src/snyk/snykIac/views/suggestion/iacSuggestionWebviewScript.ts index bcab7587e..1ca18f758 100644 --- a/src/snyk/snykIac/views/suggestion/iacSuggestionWebviewScript.ts +++ b/src/snyk/snykIac/views/suggestion/iacSuggestionWebviewScript.ts @@ -143,6 +143,7 @@ } } + // file deepcode ignore InsufficientPostmessageValidation: Content Security Policy applied in provider window.addEventListener('message', event => { const { type, args } = event.data; switch (type) { diff --git a/src/snyk/snykOss/codeActions/vulnerabilityCodeActionProvider.ts b/src/snyk/snykOss/codeActions/vulnerabilityCodeActionProvider.ts deleted file mode 100644 index 68b1b3535..000000000 --- a/src/snyk/snykOss/codeActions/vulnerabilityCodeActionProvider.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { IAnalytics } from '../../common/analytics/itly'; -import { OpenCommandIssueType, OpenIssueCommandArg } from '../../common/commands/types'; -import { SNYK_OPEN_ISSUE_COMMAND } from '../../common/constants/commands'; -import { IDE_NAME } from '../../common/constants/general'; -import { ICodeActionKindAdapter } from '../../common/vscode/codeAction'; -import { - CodeAction, - CodeActionContext, - CodeActionKind, - CodeActionProvider, - Command, - ProviderResult, - Range, - Selection, - TextDocument, -} from '../../common/vscode/types'; -import { DIAGNOSTICS_OSS_COLLECTION_NAME } from '../../snykCode/constants/analysis'; -import { messages } from '../messages/vulnerabilityCount'; -import { isResultCliError } from '../ossResult'; -import { OssService } from '../services/ossService'; -import { ModuleVulnerabilityCountProvider } from '../services/vulnerabilityCount/vulnerabilityCountProvider'; - -export class VulnerabilityCodeActionProvider implements CodeActionProvider { - public codeActionKinds: ReadonlyArray = [this.codeActionKindProvider.getQuickFix()]; - - constructor( - private readonly ossService: OssService, - private readonly vulnerabilityCountProvider: ModuleVulnerabilityCountProvider, - private readonly codeActionKindProvider: ICodeActionKindAdapter, - private readonly analytics: IAnalytics, - ) {} - - async provideCodeActions( - document: TextDocument, - _: Range | Selection, - context: CodeActionContext, - ): Promise> { - const ossDiagnostics = context.diagnostics.filter(d => d.source === DIAGNOSTICS_OSS_COLLECTION_NAME); - if (!ossDiagnostics.length) { - return; - } - - const ossResult = this.ossService.getResultArray(); - if (!ossResult) { - return; - } - - const fileResult = ossResult.find( - res => !isResultCliError(res) && this.vulnerabilityCountProvider.isFilePartOfOssTest(document.fileName, res), - ); - - if (!fileResult || isResultCliError(fileResult)) { - return; - } - - for (const diagnostic of ossDiagnostics) { - const vulnerability = fileResult.vulnerabilities.find(vuln => vuln.id === diagnostic.code); - if (!vulnerability) { - continue; - } - - const command: Command = { - command: SNYK_OPEN_ISSUE_COMMAND, - title: messages.showMostSevereVulnerability, - arguments: [ - { - issueType: OpenCommandIssueType.OssVulnerability, - issue: await this.ossService.getOssIssueCommandArg(vulnerability, fileResult.vulnerabilities), - } as OpenIssueCommandArg, - ], - }; - - this.analytics.logQuickFixIsDisplayed({ - quickFixType: ['Show Most Severe Vulnerability'], - ide: IDE_NAME, - }); - - return [command]; - } - } -} diff --git a/src/snyk/snykOss/constants/messages.ts b/src/snyk/snykOss/constants/messages.ts new file mode 100644 index 000000000..cee0bfd21 --- /dev/null +++ b/src/snyk/snykOss/constants/messages.ts @@ -0,0 +1,34 @@ +import { ModuleVulnerabilityCount } from '../services/vulnerabilityCount/importedModule'; + +export const messages = { + analysis: { + scanFailed: 'Scan failed', + noWorkspaceTrust: 'No workspace folder was granted trust', + clickToProblem: 'Click here to see the problem.', + scanRunning: 'Scanning...', + 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.', + }, + errors: { + suggestionViewShowFailed: 'Failed to show Snyk OSS suggestion view', + }, + test: { + testFailed: 'Open Source Security test failed.', + testStarted: 'Open Source Security test started.', + viewResults: 'View results', + hide: "Don't show again", + testFailedForPath: (path: string): string => `Open Source Security test failed for "${path}".`, + testFinished: (projectName: string): string => `Open Source Security test finished for "${projectName}".`, + }, + treeView: { + cookingDependencies: 'Scanning...', + runTest: 'Run scan for Open Source security vulnerabilities.', + noVulnerabilitiesFound: ' ✅ Congrats! Snyk found no vulnerabilities.', + singleVulnerabilityFound: 'Snyk found 1 vulnerability', + vulnerability: 'vulnerability', + vulnerabilities: 'vulnerabilities', + multipleVulnerabilitiesFound: (issueCount: number): string => `Snyk found ${issueCount} vulnerabilities`, + }, +}; diff --git a/src/snyk/snykOss/editor/editorDecorator.ts b/src/snyk/snykOss/editor/editorDecorator.ts index 398a5d888..dedec4fef 100644 --- a/src/snyk/snykOss/editor/editorDecorator.ts +++ b/src/snyk/snykOss/editor/editorDecorator.ts @@ -4,7 +4,6 @@ import { IVSCodeLanguages } from '../../common/vscode/languages'; import { IThemeColorAdapter } from '../../common/vscode/theme'; import { TextEditorDecorationType } from '../../common/vscode/types'; import { IVSCodeWindow } from '../../common/vscode/window'; -import { messages } from '../messages/vulnerabilityCount'; import { ImportedModule, ModuleVulnerabilityCount } from '../services/vulnerabilityCount/importedModule'; export class EditorDecorator { @@ -58,7 +57,7 @@ export class EditorDecorator { module.line - 1, this.editorLastCharacterIndex, ), - renderOptions: getRenderOptions(messages.fetchingVulnerabilities, this.themeColorAdapter), + renderOptions: getRenderOptions('Fetching vulnerabilities...', this.themeColorAdapter), }; } @@ -92,16 +91,16 @@ export class EditorDecorator { this.fileDecorationMap.set(filePath, lineDecorations); // set map, if no decoration was set before } - const text = vulnerabilityCount.count ? messages.decoratorMessage(vulnerabilityCount.count) : ''; + const vulnerabilityCountMessage = vulnerabilityCount.count ?? ''; lineDecorations[vulnerabilityCount.line] = { range: this.languages.createRange( - vulnerabilityCount.line - 1, + vulnerabilityCount.line - 1, // start line, index is 0 based this.editorLastCharacterIndex, - vulnerabilityCount.line - 1, + vulnerabilityCount.line - 1, // end line, index is 0 based this.editorLastCharacterIndex, ), - renderOptions: getRenderOptions(text, this.themeColorAdapter), + renderOptions: getRenderOptions(vulnerabilityCountMessage, this.themeColorAdapter), }; if (triggerUpdate) { diff --git a/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts b/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts deleted file mode 100644 index 8fc56f11d..000000000 --- a/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { IAnalytics } from '../../common/analytics/itly'; -import { IDE_NAME } from '../../common/constants/general'; -import { SupportedLanguageIds } from '../../common/constants/languageConsts'; -import { IVSCodeLanguages } from '../../common/vscode/languages'; -import { DiagnosticCollection, Disposable, Hover, Position, TextDocument } from '../../common/vscode/types'; -import { IssueUtils } from '../../snykCode/utils/issueUtils'; - -export class VulnerabilityCountHoverProvider implements Disposable { - private hoverProvider: Disposable | undefined; - - constructor(private readonly vscodeLanguages: IVSCodeLanguages, private readonly analytics: IAnalytics) {} - - register(diagnostics: DiagnosticCollection | undefined): Disposable { - const documentFilter = SupportedLanguageIds.map(id => ({ scheme: 'file', language: id })); - - this.hoverProvider = this.vscodeLanguages.registerHoverProvider(documentFilter, { - provideHover: this.getHover(diagnostics), - }); - - return this; - } - - getHover(diagnostics: DiagnosticCollection | undefined) { - return (document: TextDocument, position: Position): Hover | undefined => { - if (!diagnostics || !diagnostics.has(document.uri)) { - return undefined; - } - - const currentFileReviewIssues = diagnostics.get(document.uri); - const issue = IssueUtils.findIssueWithRange(position, currentFileReviewIssues); - if (issue) { - this.logIssueHoverIsDisplayed(); - } - }; - } - - private logIssueHoverIsDisplayed(): void { - this.analytics.logIssueHoverIsDisplayed({ - issueType: 'Open Source Vulnerability', - ide: IDE_NAME, - }); - } - - dispose(): void { - if (this.hoverProvider) { - this.hoverProvider.dispose(); - } - } -} diff --git a/src/snyk/snykOss/interfaces.ts b/src/snyk/snykOss/interfaces.ts new file mode 100644 index 000000000..fde681716 --- /dev/null +++ b/src/snyk/snykOss/interfaces.ts @@ -0,0 +1,114 @@ +import _ from 'lodash'; +import { Issue, IssueSeverity, OssIssueData } from '../common/languageServer/types'; +import { IWebViewProvider } from '../common/views/webviewProvider'; +import { CliError } from '../cli/services/cliService'; + +export interface IOssSuggestionWebviewProvider extends IWebViewProvider> { + openIssueId: string | undefined; +} + +export type OssIssueCommandArg = Issue & { + matchingIdVulnerabilities: Issue[]; + overviewHtml: string; + folderPath: string; +}; + +export type OssResult = OssFileResult[] | OssFileResult; + +export type OssFileResult = OssResultBody | CliError; + +export type OssResultBody = { + vulnerabilities: OssVulnerability[]; + projectName: string; + displayTargetFile: string; + packageManager: string; + path: string; +}; + +export type OssVulnerability = { + id: string; + license?: string; + identifiers?: Identifiers; + title: string; + description: string; + language: string; + packageManager: string; + packageName: string; + severity: OssSeverity; + name: string; + version: string; + exploit?: string; + + CVSSv3?: string; + cvssScore?: string; + + fixedIn?: Array; + from: Array; + upgradePath: Array; + isPatchable: boolean; + isUpgradable: boolean; +}; + +export type Identifiers = { + CWE: string[]; + CVE: string[]; +}; + +export enum OssSeverity { + Low = 'low', + Medium = 'medium', + High = 'high', + Critical = 'critical', +} + +export function capitalizeOssSeverity(ossSeverity: OssSeverity): Capitalize { + return _.capitalize(ossSeverity) as Capitalize; +} + +export function isResultCliError(fileResult: OssFileResult): fileResult is CliError { + return (fileResult as CliError).error !== undefined; +} + +export function convertSeverity(severity: IssueSeverity): OssSeverity { + switch (severity) { + case IssueSeverity.Low: + return OssSeverity.Low; + case IssueSeverity.Medium: + return OssSeverity.Medium; + case IssueSeverity.High: + return OssSeverity.High; + default: + return OssSeverity.Critical; + } +} + +export function convertIssue(issue: Issue): OssVulnerability { + const tempVuln: OssVulnerability = { + id: issue.id, + identifiers: issue.additionalData.identifiers, + title: issue.title, + description: issue.additionalData.description, + language: issue.additionalData.language, + packageManager: issue.additionalData.packageManager, + packageName: issue.additionalData.packageName, + severity: convertSeverity(issue.severity), + name: issue.additionalData.name, + version: issue.additionalData.version, + exploit: issue.additionalData.exploit, + + CVSSv3: issue.additionalData.CVSSv3, + cvssScore: issue.additionalData.cvssScore, + + fixedIn: issue.additionalData.fixedIn === undefined ? [] : issue.additionalData.fixedIn, + from: issue.additionalData.from, + upgradePath: issue.additionalData.upgradePath, + isPatchable: issue.additionalData.isPatchable, + isUpgradable: issue.additionalData.isUpgradable, + }; + + if (issue.additionalData.license !== undefined) { + tempVuln.license = issue.additionalData.license; + } + + return tempVuln; +} diff --git a/src/snyk/snykOss/messages/error.ts b/src/snyk/snykOss/messages/error.ts deleted file mode 100644 index f8f215e17..000000000 --- a/src/snyk/snykOss/messages/error.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const messages = { - suggestionViewShowFailed: 'Failed to show Snyk OSS suggestion view', -}; diff --git a/src/snyk/snykOss/messages/test.ts b/src/snyk/snykOss/messages/test.ts deleted file mode 100644 index d41d6d05e..000000000 --- a/src/snyk/snykOss/messages/test.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const messages = { - testFailed: 'Open Source Security test failed.', - testStarted: 'Open Source Security test started.', - viewResults: 'View results', - hide: "Don't show again", - - testFailedForPath: (path: string): string => `Open Source Security test failed for "${path}".`, - testFinished: (projectName: string): string => `Open Source Security test finished for "${projectName}".`, -}; diff --git a/src/snyk/snykOss/messages/treeView.ts b/src/snyk/snykOss/messages/treeView.ts deleted file mode 100644 index e01a041bc..000000000 --- a/src/snyk/snykOss/messages/treeView.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const messages = { - cookingDependencies: 'Scanning...', - - runTest: 'Run scan for Open Source security vulnerabilities.', - noVulnerabilitiesFound: ' ✅ Congrats! Snyk found no vulnerabilities.', - singleVulnerabilityFound: 'Snyk found 1 vulnerability', - vulnerability: 'vulnerability', - vulnerabilities: 'vulnerabilities', - - multipleVulnerabilitiesFound: (issueCount: number): string => `Snyk found ${issueCount} vulnerabilities`, -}; diff --git a/src/snyk/snykOss/messages/vulnerabilityCount.ts b/src/snyk/snykOss/messages/vulnerabilityCount.ts deleted file mode 100644 index f328ebd22..000000000 --- a/src/snyk/snykOss/messages/vulnerabilityCount.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ModuleVulnerabilityCount } from '../services/vulnerabilityCount/importedModule'; - -export const messages = { - fetchingVulnerabilities: 'Fetching vulnerabilities...', - vulnerability: 'vulnerability', - vulnerabilities: 'vulnerabilities', - showMostSevereVulnerability: 'Show the most severe vulnerability (Snyk)', - - decoratorMessage: (vulnerabilityCount: string): string => { - const vulnerabilityCountNumber = Number.parseInt(vulnerabilityCount, 10); - if (isNaN(vulnerabilityCountNumber)) { - return vulnerabilityCount; - } - return `${vulnerabilityCountNumber} ${vulnerabilityCountNumber > 1 ? 'vulnerabilities' : 'vulnerability'}`; - }, - - diagnosticMessagePrefix: (module: ModuleVulnerabilityCount): string => - `Dependency ${module.name}${module.version ? `@${module.version}` : ''} has `, -}; diff --git a/src/snyk/snykOss/ossResult.ts b/src/snyk/snykOss/ossResult.ts deleted file mode 100644 index 625019a49..000000000 --- a/src/snyk/snykOss/ossResult.ts +++ /dev/null @@ -1,58 +0,0 @@ -import _ from 'lodash'; -import { CliError } from '../cli/services/cliService'; - -export type OssResult = OssFileResult[] | OssFileResult; - -export type OssFileResult = OssResultBody | CliError; - -export type OssResultBody = { - vulnerabilities: OssVulnerability[]; - projectName: string; - displayTargetFile: string; - packageManager: string; - path: string; -}; - -export type OssVulnerability = { - id: string; - license?: string; - identifiers?: Identifiers; - title: string; - description: string; - language: string; - packageManager: string; - packageName: string; - severity: OssSeverity; - name: string; - version: string; - exploit?: string; - - CVSSv3?: string; - cvssScore?: string; - - fixedIn?: Array; - from: Array; - upgradePath: Array; - isPatchable: boolean; - isUpgradable: boolean; -}; - -export type Identifiers = { - CWE: string[]; - CVE: string[]; -}; - -export enum OssSeverity { - Low = 'low', - Medium = 'medium', - High = 'high', - Critical = 'critical', -} - -export function capitalizeOssSeverity(ossSeverity: OssSeverity): Capitalize { - return _.capitalize(ossSeverity) as Capitalize; -} - -export function isResultCliError(fileResult: OssFileResult): fileResult is CliError { - return (fileResult as CliError).error !== undefined; -} diff --git a/src/snyk/snykOss/ossService.ts b/src/snyk/snykOss/ossService.ts new file mode 100644 index 000000000..b0394567f --- /dev/null +++ b/src/snyk/snykOss/ossService.ts @@ -0,0 +1,62 @@ +import { Subscription } from 'rxjs'; +import { IAnalytics } from '../common/analytics/itly'; +import { IConfiguration } from '../common/configuration/configuration'; +import { IWorkspaceTrust } from '../common/configuration/trustedFolders'; +import { ILanguageServer } from '../common/languageServer/languageServer'; +import { OssIssueData, Scan, ScanProduct } from '../common/languageServer/types'; +import { ILog } from '../common/logger/interfaces'; +import { ProductService } from '../common/services/productService'; +import { IViewManagerService } from '../common/services/viewManagerService'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../common/vscode/codeAction'; +import { ExtensionContext } from '../common/vscode/extensionContext'; +import { IVSCodeLanguages } from '../common/vscode/languages'; +import { IVSCodeWorkspace } from '../common/vscode/workspace'; +import { IOssSuggestionWebviewProvider } from './interfaces'; +import { OssCodeActionsProvider } from './providers/ossCodeActionsProvider'; + +export class OssService extends ProductService { + constructor( + extensionContext: ExtensionContext, + config: IConfiguration, + suggestionProvider: IOssSuggestionWebviewProvider, + codeActionAdapter: ICodeActionAdapter, + codeActionKindAdapter: ICodeActionKindAdapter, + viewManagerService: IViewManagerService, + workspace: IVSCodeWorkspace, + workspaceTrust: IWorkspaceTrust, + languageServer: ILanguageServer, + languages: IVSCodeLanguages, + logger: ILog, + readonly analytics: IAnalytics, + ) { + super( + extensionContext, + config, + suggestionProvider, + viewManagerService, + workspace, + workspaceTrust, + languageServer, + languages, + logger, + ); + + this.registerCodeActionsProvider( + new OssCodeActionsProvider(languages, codeActionAdapter, codeActionKindAdapter, this.result, analytics), + ); + } + + subscribeToLsScanMessages(): Subscription { + return this.languageServer.scan$.subscribe((scan: Scan) => { + if (scan.product !== ScanProduct.OpenSource) { + return; + } + + super.handleLsScanMessage(scan); + }); + } + + refreshTreeView() { + this.viewManagerService.refreshOssView(); + } +} diff --git a/src/snyk/snykOss/providers/ossCodeActionsProvider.ts b/src/snyk/snykOss/providers/ossCodeActionsProvider.ts new file mode 100644 index 000000000..7c0141db2 --- /dev/null +++ b/src/snyk/snykOss/providers/ossCodeActionsProvider.ts @@ -0,0 +1,168 @@ +import { CodeAction, Range, TextDocument, Uri } from 'vscode'; +import { IAnalytics, SupportedQuickFixProperties } from '../../common/analytics/itly'; +import { OpenCommandIssueType, OpenIssueCommandArg } from '../../common/commands/types'; +import { SNYK_OPEN_ISSUE_COMMAND } from '../../common/constants/commands'; +import { IDE_NAME } from '../../common/constants/general'; +import { CodeActionsProvider } from '../../common/editor/codeActionsProvider'; +import { Issue, IssueSeverity, OssIssueData } from '../../common/languageServer/types'; +import { ProductResult } from '../../common/services/productService'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../common/vscode/codeAction'; +import { IVSCodeLanguages } from '../../common/vscode/languages'; +import { CodeActionContext } from '../../common/vscode/types'; +import { DIAGNOSTICS_OSS_COLLECTION_NAME_LS } from '../../snykCode/constants/analysis'; +import { getOssIssueCommandArg } from './ossIssueCommandHelper'; + +export class OssCodeActionsProvider extends CodeActionsProvider { + constructor( + private readonly languages: IVSCodeLanguages, + private readonly codeActionAdapter: ICodeActionAdapter, + codeActionKindAdapter: ICodeActionKindAdapter, + issues: Readonly>, + analytics: IAnalytics, + ) { + super(issues, codeActionKindAdapter, analytics); + } + + override provideCodeActions( + document: TextDocument, + _clickedRange: Range, + context: CodeActionContext, + ): CodeAction[] | undefined { + // is there a better way to get the folder path? + const folderPath = document.uri.fsPath.split('/').slice(0, -1).join('/'); + if (!folderPath) { + return; + } + + const vulnerabilities = this.getVulnerabilities(folderPath, context); + if (!vulnerabilities) { + return; + } + + const mostSevereVulnerability = this.getMostSevereVulnerability(vulnerabilities, folderPath); + if (!mostSevereVulnerability) { + return; + } + + const codeActions = this.getActions( + folderPath, + document, + mostSevereVulnerability, + this.getIssueRange(mostSevereVulnerability), + ); + const analyticsType = this.getAnalyticsActionTypes(); + + this.analytics.logQuickFixIsDisplayed({ + quickFixType: analyticsType, + ide: IDE_NAME, + }); + + return codeActions; + } + + getActions( + _folderPath: string, + _document: TextDocument, + mostSevereVulnerability: Issue, + _issueRange?: Range, + ): CodeAction[] { + const openIssueAction = this.createMostSevereVulnerabilityAction(mostSevereVulnerability); + + // returns list of actions, all new actions should be added to this list + return [openIssueAction]; + } + + getAnalyticsActionTypes(): [string, ...string[]] & [SupportedQuickFixProperties, ...SupportedQuickFixProperties[]] { + return ['Show Suggestion']; + } + + // noop + getIssueRange(_issue: Issue): Range { + return this.languages.createRange(0, 0, 0, 0); + } + + private createMostSevereVulnerabilityAction(mostSevereVulnerability: Issue): CodeAction { + // create the CodeAction + const openIssueAction = this.codeActionAdapter.create( + `Show the most severe vulnerability [${mostSevereVulnerability.id}] (Snyk)`, + this.providedCodeActionKinds[0], + ); + + openIssueAction.command = { + command: SNYK_OPEN_ISSUE_COMMAND, + title: SNYK_OPEN_ISSUE_COMMAND, + arguments: [ + { + issueType: OpenCommandIssueType.OssVulnerability, + issue: mostSevereVulnerability, + } as OpenIssueCommandArg, + ], + }; + + return openIssueAction; + } + + private issueSeverityToRanking(severity: IssueSeverity): number { + switch (severity) { + case IssueSeverity.Critical: + return 3; + case IssueSeverity.High: + return 2; + case IssueSeverity.Medium: + return 1; + default: + return 0; + } + } + + private getVulnerabilities(folderPath: string, context: CodeActionContext): Issue[] | undefined { + // get all OSS vulnerabilities for the folder + const ossResult = this.issues.get(folderPath); + if (!ossResult || ossResult instanceof Error) { + return; + } + + // get all OSS diagnostics; these contain the relevant vulnerabilities + const ossDiagnostics = context.diagnostics.filter(d => d.source === DIAGNOSTICS_OSS_COLLECTION_NAME_LS); + if (!ossDiagnostics.length) { + return; + } + + // find the corresponding Issue objects from ossDiagnostics + const vulnerabilities: Issue[] = []; + for (const diagnostic of ossDiagnostics) { + const vulnerability = ossResult.find( + ossIssue => ossIssue.id === (diagnostic.code as { value: string | number; target: Uri }).value, + ); + if (!vulnerability) { + continue; + } + vulnerabilities.push(vulnerability); + } + + return vulnerabilities; + } + + private getMostSevereVulnerability( + vulnerabilities: Issue[], + folderPath: string, + ): Issue | undefined { + // iterate vulnerabilities and get the most severe one + // if there are multiple of the same severity, get the first one + let highestSeverity = this.issueSeverityToRanking(IssueSeverity.Low); + let mostSevereVulnerability: Issue | undefined; + + for (const vulnerability of vulnerabilities) { + if (this.issueSeverityToRanking(vulnerability.severity) > highestSeverity) { + highestSeverity = this.issueSeverityToRanking(vulnerability.severity); + mostSevereVulnerability = vulnerability; + } + } + + if (!mostSevereVulnerability) { + return; + } + + return getOssIssueCommandArg(mostSevereVulnerability, folderPath, vulnerabilities); + } +} diff --git a/src/snyk/snykOss/providers/ossDetailPanelProvider.ts b/src/snyk/snykOss/providers/ossDetailPanelProvider.ts new file mode 100644 index 000000000..1b5946a06 --- /dev/null +++ b/src/snyk/snykOss/providers/ossDetailPanelProvider.ts @@ -0,0 +1,121 @@ +import * as vscode from 'vscode'; +import { SNYK_VIEW_SUGGESTION_OSS } from '../../common/constants/views'; +import { ErrorHandler } from '../../common/error/errorHandler'; +import { Issue, OssIssueData } from '../../common/languageServer/types'; +import { ILog } from '../../common/logger/interfaces'; +import { getNonce } from '../../common/views/nonce'; +import { WebviewPanelSerializer } from '../../common/views/webviewPanelSerializer'; +import { IWebViewProvider, WebviewProvider } from '../../common/views/webviewProvider'; +import { ExtensionContext } from '../../common/vscode/extensionContext'; +import { IVSCodeLanguages } from '../../common/vscode/languages'; +import { IVSCodeWindow } from '../../common/vscode/window'; +import { IVSCodeWorkspace } from '../../common/vscode/workspace'; +import { messages } from '../constants/messages'; + +export class OssDetailPanelProvider + extends WebviewProvider> + implements IWebViewProvider> +{ + protected getHtmlForWebview(_webview: vscode.Webview): string { + throw new Error('Method not implemented.'); + } + + // For consistency reasons, the single source of truth for the current suggestion is the + // panel state. The following field is only used in + private issue: Issue | undefined; + + constructor( + private readonly window: IVSCodeWindow, + protected readonly context: ExtensionContext, + protected readonly logger: ILog, + private readonly languages: IVSCodeLanguages, + private readonly workspace: IVSCodeWorkspace, + ) { + super(context, logger); + } + + activate(): void { + this.context.addDisposables( + this.window.registerWebviewPanelSerializer(SNYK_VIEW_SUGGESTION_OSS, new WebviewPanelSerializer(this)), + ); + } + + get openIssueId(): string | undefined { + return this.issue?.id; + } + + async showPanel(issue: Issue): Promise { + try { + await this.focusSecondEditorGroup(); + + if (this.panel) { + this.panel.reveal(vscode.ViewColumn.Two, true); + } else { + this.panel = vscode.window.createWebviewPanel( + SNYK_VIEW_SUGGESTION_OSS, + 'Snyk OSS Vulnerability', + { + viewColumn: vscode.ViewColumn.Two, + preserveFocus: true, + }, + this.getWebviewOptions(), + ); + this.registerListeners(); + } + + const images: Record = [ + ['icon-code', 'svg'], + ['dark-critical-severity', 'svg'], + ['dark-high-severity', 'svg'], + ['dark-medium-severity', 'svg'], + ['dark-low-severity', 'svg'], + ['learn-icon', 'svg'], + ].reduce((accumulator: Record, [name, ext]) => { + const uri = this.getWebViewUri('media', 'images', `${name}.${ext}`); + if (!uri) throw new Error('Image missing.'); + accumulator[name] = uri.toString(); + return accumulator; + }, {}); + + const displayMode = 'dark'; + const styleUri = this.getWebViewUri('media', 'views', 'oss', 'suggestion', 'suggestion.css'); + const headerEndValue = ``; + const serverityIconName = `${displayMode}-${issue.severity}-severity`; + const nonce = getNonce(); + + let html = issue.additionalData.details; + html = html.replace('${headerEnd}', headerEndValue); + html = html.replaceAll('${cspSource}', this.panel.webview.cspSource); + html = html.replaceAll('${nonce}', nonce); + html = html.replace('${severityIcon}', images[serverityIconName]); + html = html.replace('${learnIcon}', images['learn-icon']); + html = html.replaceAll(/\$\{\w+\}/g, ''); + this.panel.webview.html = html; + this.panel.iconPath = vscode.Uri.joinPath( + vscode.Uri.file(this.context.extensionPath), + 'media', + 'images', + 'snyk-oss.svg', + ); + + this.issue = issue; + } catch (e) { + ErrorHandler.handle(e, this.logger, messages.errors.suggestionViewShowFailed); + } + } + + protected registerListeners(): void { + if (!this.panel) return; + + this.panel.onDidDispose(() => this.onPanelDispose(), null, this.disposables); + this.panel.onDidChangeViewState(() => this.checkVisibility(), undefined, this.disposables); + } + + disposePanel(): void { + super.disposePanel(); + } + + protected onPanelDispose(): void { + super.onPanelDispose(); + } +} diff --git a/src/snyk/snykOss/providers/ossIssueCommandHelper.ts b/src/snyk/snykOss/providers/ossIssueCommandHelper.ts new file mode 100644 index 000000000..6319e41fb --- /dev/null +++ b/src/snyk/snykOss/providers/ossIssueCommandHelper.ts @@ -0,0 +1,26 @@ +import marked from 'marked'; +import { Issue, OssIssueData } from '../../common/languageServer/types'; +import { OssIssueCommandArg } from '../interfaces'; + +export function getOssIssueCommandArg( + vuln: Issue, + folderPath: string, + filteredVulns: Issue[], +): OssIssueCommandArg { + const matchingIdVulnerabilities = filteredVulns.filter(v => v.id === vuln.id); + let overviewHtml = ''; + + try { + // TODO: marked.parse does not sanitize the HTML. See: https://marked.js.org/#usage + overviewHtml = marked.parse(vuln.additionalData.description); + } catch (error) { + overviewHtml = '

There was a problem rendering the vulnerability overview

'; + } + + return { + ...vuln, + matchingIdVulnerabilities, + overviewHtml, + folderPath, + }; +} diff --git a/src/snyk/snykOss/providers/ossVulnerabilityCountProvider.ts b/src/snyk/snykOss/providers/ossVulnerabilityCountProvider.ts new file mode 100644 index 000000000..0ecd40aa7 --- /dev/null +++ b/src/snyk/snykOss/providers/ossVulnerabilityCountProvider.ts @@ -0,0 +1,132 @@ +import { CliError } from '../../cli/services/cliService'; +import { Language, languageToString } from '../../common/types'; +import { ILanguageClientAdapter } from '../../common/vscode/languageClient'; +import { ITextDocumentAdapter } from '../../common/vscode/textdocument'; +import { InlineValueText, LSPTextDocument } from '../../common/vscode/types'; +import { IUriAdapter } from '../../common/vscode/uri'; +import { convertIssue, isResultCliError, OssFileResult, OssResultBody } from '../interfaces'; +import { OssService } from '../ossService'; +import { ImportedModule, ModuleVulnerabilityCount } from '../services/vulnerabilityCount/importedModule'; +import { VulnerabilityCountEmitter } from '../services/vulnerabilityCount/vulnerabilityCountEmitter'; + +export class OssVulnerabilityCountProvider { + constructor( + private readonly ossService: OssService, + private readonly languageClientAdapter: ILanguageClientAdapter, + private readonly uriAdapter: IUriAdapter, + private readonly textDocumentAdapter: ITextDocumentAdapter, + ) {} + + async getVulnerabilityCount( + fileName: string, + module: ImportedModule, + language: Language, + emitter: VulnerabilityCountEmitter, + ): Promise { + let moduleVulnerabilityCount: ModuleVulnerabilityCount = { + name: module.name, + fileName: module.fileName, + line: module.line, + range: module.loc, + hasCount: false, + }; + + const processFile = [Language.TypeScript, Language.JavaScript, Language.PJSON, Language.HTML].includes(language); + if (processFile) { + const uri = this.uriAdapter.file(fileName).toString(); + const doc: LSPTextDocument = this.textDocumentAdapter.create(uri, languageToString(language), 1, ''); + + let firstLine = 0; + let lastLine = doc.lineCount; + let firstCharacter = 0; + let lastCharacter = Number.MAX_SAFE_INTEGER; + + if (module.loc) { + firstLine = module.loc.start.line - 1; + lastLine = module.loc.end.line - 1; + firstCharacter = module.loc.start.column; + lastCharacter = module.loc.end.column; + } + + const param = { + textDocument: { uri: doc.uri }, + range: { + start: { line: firstLine, character: firstCharacter }, + end: { line: lastLine, character: lastCharacter }, + }, + }; + + const inlineValues: InlineValueText[] = await this.languageClientAdapter + .getLanguageClient() + .sendRequest('textDocument/inlineValue', param); + + if (inlineValues?.length > 0) { + moduleVulnerabilityCount = { + name: module.name, + version: module.version, + fileName: module.fileName, + line: module.line, + range: module.loc, + count: inlineValues[0].text, + hasCount: true, + }; + } + } + + emitter?.scanned(moduleVulnerabilityCount); + return moduleVulnerabilityCount; + } + + isFilePartOfOssTest(filePath: string, ossFileResult: OssFileResult): boolean { + if (isResultCliError(ossFileResult)) { + return false; + } + + // File is considered to be part of OSS test if it has common root directory between OSS result path and filename path. + // This is since package.json always lies in the root directory folder of a project. + return filePath.startsWith(ossFileResult.path); + } + + public getResultArray = (): ReadonlyArray | undefined => { + if (!this.ossService.result) { + return undefined; + } + + const tempResultArray: OssFileResult[] = []; + const resultCache = new Map(); + + for (const [, value] of this.ossService.result) { + // value is Error + if (value instanceof Error) { + tempResultArray.push(new CliError(value)); + } + // value is Issue[] + else { + for (const issue of value) { + // try to access list of vulns for the current file + let res = resultCache.get(issue.filePath); + + // add list of vulns to local cache if not there yet + if (res === undefined) { + res = { + path: issue.filePath, + vulnerabilities: [], + projectName: issue.additionalData.projectName, + displayTargetFile: issue.additionalData.displayTargetFile, + packageManager: issue.additionalData.packageManager, + }; + resultCache.set(issue.filePath, res); + } + + const tempVuln = convertIssue(issue); + res.vulnerabilities.push(tempVuln); + } + } + } + + // copy cached results to final result array + resultCache.forEach(value => tempResultArray.push(value)); + + return tempResultArray; + }; +} diff --git a/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts b/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts new file mode 100644 index 000000000..f3aeba284 --- /dev/null +++ b/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts @@ -0,0 +1,202 @@ +import _ from 'lodash'; +import { Command, Uri } from 'vscode'; +import { OpenCommandIssueType, OpenIssueCommandArg } from '../../common/commands/types'; +import { IConfiguration } from '../../common/configuration/configuration'; +import { configuration } from '../../common/configuration/instance'; +import { SNYK_OPEN_ISSUE_COMMAND } from '../../common/constants/commands'; +import { SNYK_ANALYSIS_STATUS } from '../../common/constants/views'; +import { Issue, IssueSeverity, OssIssueData } from '../../common/languageServer/types'; +import { IContextService } from '../../common/services/contextService'; +import { IProductService } from '../../common/services/productService'; +import { IViewManagerService } from '../../common/services/viewManagerService'; +import { ProductIssueTreeProvider } from '../../common/views/issueTreeProvider'; +import { TreeNode } from '../../common/views/treeNode'; +import { IVSCodeLanguages } from '../../common/vscode/languages'; +import { messages } from '../constants/messages'; +import { getOssIssueCommandArg } from './ossIssueCommandHelper'; + +export default class OssIssueTreeProvider extends ProductIssueTreeProvider { + constructor( + protected viewManagerService: IViewManagerService, + protected contextService: IContextService, + protected ossService: IProductService, + protected configuration: IConfiguration, + protected languages: IVSCodeLanguages, + ) { + super(contextService, ossService, configuration, languages); + } + + getRootChildren(): TreeNode[] { + if (!configuration.getFeaturesConfiguration()?.ossEnabled) { + return [ + new TreeNode({ + text: SNYK_ANALYSIS_STATUS.OSS_DISABLED, + }), + ]; + } + + return super.getRootChildren(); + } + + override getResultNodes(): [TreeNode[], number] { + const nodes: TreeNode[] = []; + let totalVulnCount = 0; + + for (const result of this.productService.result.entries()) { + const folderPath = result[0]; + const folderResult = result[1]; + + const uri = Uri.file(folderPath); + const shortFolderPath = uri.path.split('/'); + const folderName = shortFolderPath.pop() || uri.path; + + let folderVulnCount = 0; + if (folderResult instanceof Error) { + nodes.push(this.getErrorEncounteredTreeNode(folderName)); + continue; + } + + const folderSeverityCounts = this.initSeverityCounts(); + const fileNodes: TreeNode[] = []; + + const fileVulns = _.groupBy(folderResult, v => v.filePath); + + for (const file in fileVulns) { + const fileIssues = fileVulns[file]; + const uri = Uri.file(file); + const filePath = uri.path.split('/'); + const filename = filePath.pop() || uri.path; + const dir = filePath.pop(); + + const fileSeverityCounts = this.initSeverityCounts(); + + const uniqueIssues = fileIssues.filter( + (issue, index, self) => index === self.findIndex(t => t.id === issue.id), + ); + + const filteredIssues = this.filterIssues(uniqueIssues); + + const vulnerabilityNodes: TreeNode[] = filteredIssues.map((issue: Issue) => { + fileSeverityCounts[issue.severity] += 1; + totalVulnCount++; + folderVulnCount++; + + return new TreeNode({ + text: `${issue.additionalData.packageName}@${issue.additionalData.version} - ${issue.title}`, + icon: ProductIssueTreeProvider.getSeverityIcon(issue.severity), + internal: { + severity: ProductIssueTreeProvider.getSeverityComparatorIndex(issue.severity), + }, + command: this.getOpenIssueCommand(issue, folderPath, '', filteredIssues), + }); + }); + + if (vulnerabilityNodes.length === 0) { + continue; + } + + vulnerabilityNodes.sort(this.compareNodes); + + const fileSeverity = ProductIssueTreeProvider.getHighestSeverity(fileSeverityCounts); + folderSeverityCounts[fileSeverity] += 1; + + // append file node + const fileNode = new TreeNode({ + text: filename, + description: this.getIssueDescriptionText(dir, vulnerabilityNodes.length), + icon: ProductIssueTreeProvider.getSeverityIcon(fileSeverity), + children: vulnerabilityNodes, + internal: { + nIssues: vulnerabilityNodes.length, + severity: ProductIssueTreeProvider.getSeverityComparatorIndex(fileSeverity), + }, + }); + fileNodes.push(fileNode); + } + + fileNodes.sort(this.compareNodes); + + const folderSeverity = ProductIssueTreeProvider.getHighestSeverity(folderSeverityCounts); + + if (folderVulnCount == 0) { + continue; + } + + // flatten results if single workspace folder + if (this.productService.result.size == 1) { + nodes.push(...fileNodes); + } else { + const folderNode = new TreeNode({ + text: folderName, + description: this.getIssueDescriptionText(folderName, folderVulnCount), + icon: ProductIssueTreeProvider.getSeverityIcon(folderSeverity), + children: fileNodes, + internal: { + nIssues: folderVulnCount, + severity: ProductIssueTreeProvider.getSeverityComparatorIndex(folderSeverity), + }, + }); + nodes.push(folderNode); + } + } + + return [nodes, totalVulnCount]; + } + + onDidChangeTreeData = this.viewManagerService.refreshOssViewEmitter.event; + + shouldShowTree(): boolean { + return this.contextService.shouldShowOssAnalysis; + } + + getIssueDescriptionText(dir: string | undefined, issueCount: number): string | undefined { + return `${dir} - ${issueCount} ${issueCount === 1 ? 'vulnerability' : 'vulnerabilities'}`; + } + + getIssueFoundText(nIssues: number): string { + return `Snyk found ${ + !nIssues ? 'no vulnerabilities! ✅' : `${nIssues} ${nIssues === 1 ? 'vulnerability' : 'vulnerabilities'}` + }`; + } + + filterIssues(issues: Issue[]): Issue[] { + return issues.filter(vuln => { + switch (vuln.severity.toLowerCase()) { + case IssueSeverity.Critical: + return this.configuration.severityFilter.critical; + case IssueSeverity.High: + return this.configuration.severityFilter.high; + case IssueSeverity.Medium: + return this.configuration.severityFilter.medium; + case IssueSeverity.Low: + return this.configuration.severityFilter.low; + default: + return true; + } + }); + } + + getRunTestMessage = () => messages.treeView.runTest; + + getIssueTitle = (issue: Issue) => issue.title; + + getIssueRange = () => undefined; + + getOpenIssueCommand( + issue: Issue, + folderPath: string, + _filePath: string, + filteredIssues: Issue[], + ): Command { + return { + command: SNYK_OPEN_ISSUE_COMMAND, + title: '', + arguments: [ + { + issueType: OpenCommandIssueType.OssVulnerability, + issue: getOssIssueCommandArg(issue, folderPath, filteredIssues), + } as OpenIssueCommandArg, + ], + }; + } +} diff --git a/src/snyk/snykOss/services/ossService.ts b/src/snyk/snykOss/services/ossService.ts deleted file mode 100644 index 0a1cda3d7..000000000 --- a/src/snyk/snykOss/services/ossService.ts +++ /dev/null @@ -1,224 +0,0 @@ -import * as marked from 'marked'; -import { Subject } from 'rxjs'; -import { IExtension } from '../../base/modules/interfaces'; -import { CliError, CliService } from '../../cli/services/cliService'; -import { IAnalytics } from '../../common/analytics/itly'; -import { IConfiguration } from '../../common/configuration/configuration'; -import { IWorkspaceTrust } from '../../common/configuration/trustedFolders'; -import { IDE_NAME } from '../../common/constants/general'; -import { ILanguageServer } from '../../common/languageServer/languageServer'; -import { ILog } from '../../common/logger/interfaces'; -import { DownloadService } from '../../common/services/downloadService'; -import { INotificationService } from '../../common/services/notificationService'; -import { IViewManagerService } from '../../common/services/viewManagerService'; -import { IWebViewProvider } from '../../common/views/webviewProvider'; -import { ExtensionContext } from '../../common/vscode/extensionContext'; -import { IVSCodeWorkspace } from '../../common/vscode/workspace'; -import { messages } from '../messages/test'; -import { isResultCliError, OssFileResult, OssResult, OssSeverity, OssVulnerability } from '../ossResult'; -import { OssIssueCommandArg } from '../views/ossVulnerabilityTreeProvider'; -import { DailyScanJob } from '../watchers/dailyScanJob'; -import createManifestFileWatcher from '../watchers/manifestFileWatcher'; - -export class OssService extends CliService { - protected readonly command: string[] = ['test']; - - private isVulnerabilityTreeVisible = false; - - readonly scanFinished$ = new Subject(); - - constructor( - protected readonly extensionContext: ExtensionContext, - protected readonly logger: ILog, - protected readonly config: IConfiguration, - private readonly suggestionProvider: IWebViewProvider, - protected readonly workspace: IVSCodeWorkspace, - private readonly viewManagerService: IViewManagerService, - protected readonly downloadService: DownloadService, - private readonly dailyScanJob: DailyScanJob, - private readonly notificationService: INotificationService, - private readonly analytics: IAnalytics, - protected readonly languageServer: ILanguageServer, - protected readonly workspaceTrust: IWorkspaceTrust, - ) { - super(extensionContext, logger, config, workspace, downloadService, languageServer, workspaceTrust); - } - - public getResultArray = (): ReadonlyArray | undefined => { - if (!this.result) { - return undefined; - } - - return Array.isArray(this.result) ? this.result : [this.result]; - }; - - protected mapToResultType(rawCliResult: string): OssResult { - if (rawCliResult.length == 0) { - throw new Error('CLI returned empty output result.'); - } - - let result: OssResult; - try { - result = JSON.parse(rawCliResult) as OssResult; - } catch (err) { - throw new Error(`Failed to parse JSON result. Unparsed: ${rawCliResult}`); - } - - return result; - } - - protected ensureDependencies(): void { - this.viewManagerService.refreshOssView(); - this.logger.info('Waiting for Open Source scan CLI readiness'); - } - - protected beforeTest(manualTrigger: boolean, reportTriggeredEvent: boolean): void { - this.logger.info(messages.testStarted); - this.viewManagerService.refreshOssView(); - - if (reportTriggeredEvent) { - this.analytics.logAnalysisIsTriggered({ - analysisType: ['Snyk Open Source'], - ide: IDE_NAME, - triggeredByUser: manualTrigger, - }); - } - } - - protected afterTest(result: OssResult | CliError): void { - if (result instanceof CliError) { - this.logger.error(`${messages.testFailed} ${result.error}`); - this.logAnalysisIsReady('Error'); - } else { - this.logOssResult(result); - - if (this.config.shouldAutoScanOss) { - this.dailyScanJob.schedule(); - } - } - - this.scanFinished$.next(); - this.viewManagerService.refreshOssView(); - } - - override handleLsDownloadFailure(): void { - super.handleLsDownloadFailure(); - this.viewManagerService.refreshOssView(); - } - - override handleNoTrustedFolders(): void { - super.handleNoTrustedFolders(); - this.viewManagerService.refreshOssView(); - } - - activateSuggestionProvider(): void { - this.suggestionProvider.activate(); - } - - showSuggestionProvider(vulnerability: OssIssueCommandArg): Promise { - return this.suggestionProvider.showPanel(vulnerability); - } - - activateManifestFileWatcher(extension: IExtension): void { - const manifestWatcher = createManifestFileWatcher(extension, this.workspace, this.config); - this.extensionContext.addDisposables(manifestWatcher); - } - - setVulnerabilityTreeVisibility(visible: boolean): void { - this.isVulnerabilityTreeVisible = visible; - } - - getUniqueVulnerabilities(vulnerabilities: OssVulnerability[]): OssVulnerability[] { - return vulnerabilities.filter((val, i, arr) => arr.findIndex(el => el.id === val.id) == i); - } - - getNewCriticalVulnerabilitiesCount(currentResult: OssResult, otherResult: OssResult): number { - if (Array.isArray(currentResult) && Array.isArray(otherResult)) { - let newVulnerabilityCount = 0; - for (let i = 0; i < otherResult.length; i++) { - newVulnerabilityCount += this.getNewCriticalVulnerabilitiesCount(currentResult[i], otherResult[i]); - } - - return newVulnerabilityCount; - } - - // if only one of results is an array, no count possible - if (Array.isArray(currentResult) || Array.isArray(otherResult)) { - throw new Error('Result types mismatch for new vulnerabilities calculation.'); - } - - if (!currentResult || isResultCliError(currentResult)) { - return 0; - } - - const currentVulnSet = this.getUniqueVulnerabilities(currentResult.vulnerabilities).filter( - v => v.severity === OssSeverity.Critical, - ); - - if (isResultCliError(otherResult)) { - return currentVulnSet.length; - } - - const otherVulnSet = this.getUniqueVulnerabilities(otherResult.vulnerabilities).filter( - v => v.severity === OssSeverity.Critical, - ); - - if (currentVulnSet.length > otherVulnSet.length) { - return currentVulnSet.length - otherVulnSet.length; - } - - return 0; - } - - getOssIssueCommandArg( - vulnerability: OssVulnerability, - allVulnerabilities: OssVulnerability[], - ): Promise { - return new Promise((resolve, reject) => { - const matchingIdVulnerabilities = allVulnerabilities.filter(v => v.id === vulnerability.id); - marked.parse(vulnerability.description, (err, overviewHtml) => { - if (err) { - return reject(err); - } - - return resolve({ - ...vulnerability, - matchingIdVulnerabilities: matchingIdVulnerabilities, - overviewHtml, - }); - }); - }); - } - - private logOssResult(result: OssResult) { - const fileResults = Array.isArray(result) ? result : [result]; - - for (const fileResult of fileResults) { - if (isResultCliError(fileResult)) { - this.logger.error(this.getTestErrorMessage(fileResult)); - this.logAnalysisIsReady('Error'); - } else { - this.logger.info(messages.testFinished(fileResult.projectName)); - this.logAnalysisIsReady('Success'); - } - } - } - - private getTestErrorMessage(fileResult: CliError): string { - let errorMessage: string; - if (fileResult.path) { - errorMessage = `${messages.testFailedForPath(fileResult.path)} ${fileResult.error}`; - } else { - errorMessage = `${messages.testFailed} ${fileResult.error}`; - } - return errorMessage; - } - - private logAnalysisIsReady(result: 'Error' | 'Success'): void { - this.analytics.logAnalysisIsReady({ - ide: IDE_NAME, - analysisType: 'Snyk Open Source', - result, - }); - } -} diff --git a/src/snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.ts b/src/snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.ts index 787d0f36c..844c0339a 100644 --- a/src/snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.ts +++ b/src/snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.ts @@ -1,25 +1,27 @@ import { Subscription } from 'rxjs'; import { IAnalytics } from '../../../common/analytics/itly'; import { IConfiguration } from '../../../common/configuration/configuration'; -import { HTML, JAVASCRIPT, PJSON, TYPESCRIPT } from '../../../common/constants/languageConsts'; import { ILog } from '../../../common/logger/interfaces'; import { getSupportedLanguage, isValidModuleName } from '../../../common/parsing'; import { ModuleParserProvider } from '../../../common/services/moduleParserProvider'; import { Language } from '../../../common/types'; -import { ICodeActionKindAdapter } from '../../../common/vscode/codeAction'; import { IVSCodeLanguages } from '../../../common/vscode/languages'; -import { Diagnostic, DiagnosticCollection, Disposable, TextDocument } from '../../../common/vscode/types'; +import { + Diagnostic, + DiagnosticCollection, + Disposable, + TextDocument, + TextDocumentChangeEvent, + TextEditor, +} from '../../../common/vscode/types'; import { IVSCodeWindow } from '../../../common/vscode/window'; import { IVSCodeWorkspace } from '../../../common/vscode/workspace'; import { DIAGNOSTICS_OSS_COLLECTION_NAME } from '../../../snykCode/constants/analysis'; -import { VulnerabilityCodeActionProvider } from '../../codeActions/vulnerabilityCodeActionProvider'; import { EditorDecorator } from '../../editor/editorDecorator'; -import { VulnerabilityCountHoverProvider } from '../../hoverProvider/vulnerabilityCountHoverProvider'; -import { messages } from '../../messages/vulnerabilityCount'; -import { VulnerabilityCountEmitter, VulnerabilityCountEvents } from '../../vulnerabilityCountEmitter'; -import { OssService } from '../ossService'; +import { OssService } from '../../ossService'; +import { OssVulnerabilityCountProvider } from '../../providers/ossVulnerabilityCountProvider'; import { ImportedModule, ModuleVulnerabilityCount, ModuleVulnerabilityCountSeverity } from './importedModule'; -import { ModuleVulnerabilityCountProvider } from './vulnerabilityCountProvider'; +import { VulnerabilityCountEmitter, VulnerabilityCountEvents } from './vulnerabilityCountEmitter'; export enum SupportedLanguage { TypeScript, @@ -39,57 +41,39 @@ export class OssVulnerabilityCountService implements Disposable { private readonly workspace: IVSCodeWorkspace, private readonly window: IVSCodeWindow, private readonly languages: IVSCodeLanguages, - private readonly vulnerabilityCountProvider: ModuleVulnerabilityCountProvider, + private readonly vulnerabilityCountProvider: OssVulnerabilityCountProvider, private readonly ossService: OssService, private readonly logger: ILog, private readonly editorDecorator: EditorDecorator, - private readonly codeActionKindProvider: ICodeActionKindAdapter, private readonly analytics: IAnalytics, private readonly configuration: IConfiguration, ) {} - activate(): boolean { this.disposables.push( (this.diagnostics = this.languages.createDiagnosticCollection(DIAGNOSTICS_OSS_COLLECTION_NAME)), - this.workspace.onDidChangeTextDocument(ev => { - if (ev.contentChanges.length) { - this.processFile(ev.document); + this.workspace.onDidChangeTextDocument((ev: TextDocumentChangeEvent) => { + if (ev?.contentChanges.length) { + // TODO: this feature is buggy if implemented; reset decorations instead as a compromise + // this.processFile(ev.document); + this.editorDecorator.resetDecorations(ev.document.fileName); } }), - this.window.onDidChangeActiveTextEditor(ev => { + this.window.onDidChangeActiveTextEditor((ev: TextEditor | undefined) => { if (ev) { this.processFile(ev.document); } }), - - // register hover provider - new VulnerabilityCountHoverProvider(this.languages, this.analytics).register(this.diagnostics), ); // Subscribe to OSS scan finished updates - this.ossScanFinishedSubscription = this.ossService.scanFinished$.subscribe(() => this.processActiveEditor()); - - [JAVASCRIPT, TYPESCRIPT, PJSON, HTML].forEach(language => { - const provider = new VulnerabilityCodeActionProvider( - this.ossService, - this.vulnerabilityCountProvider, - this.codeActionKindProvider, - this.analytics, - ); - this.disposables.push( - this.languages.registerCodeActionsProvider(language, provider, { - providedCodeActionKinds: provider.codeActionKinds, - }), - ); - }); - - this.processActiveEditor(); + this.ossScanFinishedSubscription = this.ossService.newResultAvailable$.subscribe(() => this.processActiveEditor()); return true; } processActiveEditor(): void { const activeEditor = this.window.getActiveTextEditor(); + if (activeEditor) { this.processFile(activeEditor.document); } @@ -138,13 +122,18 @@ export class OssVulnerabilityCountService implements Disposable { emitter.on(VulnerabilityCountEvents.Start, (modules: ImportedModule[]) => { this.editorDecorator.setScanStartDecorations(fileName, modules); }); + emitter.on(VulnerabilityCountEvents.Scanned, (vulnerabilityCount: ModuleVulnerabilityCount) => { this.editorDecorator.setScannedDecoration(vulnerabilityCount, true); }); emitter.on(VulnerabilityCountEvents.Done, (modules: ModuleVulnerabilityCount[]) => { this.editorDecorator.setScanDoneDecorations(fileName, modules); - this.updateDiagnostics(document, modules); + // TODO: delete this and related code if we move HTML diagnostics to Language Server + // Update diagnostics only for HTML files; for other files, diagnostics are provided by Language Server + if (getSupportedLanguage(fileName, languageId) === Language.HTML) { + this.updateDiagnostics(document, modules); + } }); // Start @@ -188,7 +177,7 @@ export class OssVulnerabilityCountService implements Disposable { private shouldProcessFile(fileName: string, language: Language): boolean { if ([Language.TypeScript, Language.JavaScript, Language.PJSON].includes(language)) { - const ossResult = this.ossService.getResultArray(); + const ossResult = this.vulnerabilityCountProvider.getResultArray(); if (!ossResult) { return false; } @@ -215,16 +204,13 @@ export class OssVulnerabilityCountService implements Disposable { const modules = this.getModules(fileName, content, language).filter(isValidModuleName); emitter.startScanning(modules); - const promises = modules - .map(module => this.vulnerabilityCountProvider.getVulnerabilityCount(fileName, module, language)) - .map(promise => - promise.then(module => { - emitter.scanned(module); - return module; - }), - ); - const testedModules = await Promise.all(promises); - emitter.done(testedModules); + const vulnerabilityCountPromises = modules.map(module => + this.vulnerabilityCountProvider.getVulnerabilityCount(fileName, module, language, emitter), + ); + + const vulnerabilityCount = await Promise.all(vulnerabilityCountPromises); + + emitter.done(vulnerabilityCount); } catch (e) { emitter.error(e); } @@ -244,7 +230,7 @@ export class OssVulnerabilityCountService implements Disposable { return ''; } - let message = messages.diagnosticMessagePrefix(module); + let message = `Dependency ${module.name}${module.version ? `@${module.version}` : ''} has `; message += this.getSeverityCountMessage( [ ModuleVulnerabilityCountSeverity.Critical, @@ -254,7 +240,7 @@ export class OssVulnerabilityCountService implements Disposable { ], module, ); - message += messages.decoratorMessage(module.count); + return message; } diff --git a/src/snyk/snykOss/vulnerabilityCountEmitter.ts b/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountEmitter.ts similarity index 88% rename from src/snyk/snykOss/vulnerabilityCountEmitter.ts rename to src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountEmitter.ts index 4564f0742..dcde88813 100644 --- a/src/snyk/snykOss/vulnerabilityCountEmitter.ts +++ b/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountEmitter.ts @@ -1,5 +1,5 @@ import EventEmitter from 'events'; -import { ImportedModule, ModuleVulnerabilityCount } from './services/vulnerabilityCount/importedModule'; +import { ImportedModule, ModuleVulnerabilityCount } from './importedModule'; export enum VulnerabilityCountEvents { PackageJsonFound = 'packageJsonFound', diff --git a/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.ts b/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.ts deleted file mode 100644 index 3c852ae33..000000000 --- a/src/snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { Language } from '../../../common/types'; -import { ILanguageClientAdapter } from '../../../common/vscode/languageClient'; -import { ITextDocumentAdapter } from '../../../common/vscode/textdocument'; -import { InlineValueText, LSPTextDocument } from '../../../common/vscode/types'; -import { IUriAdapter } from '../../../common/vscode/uri'; -import { OssFileResult, OssResultBody, OssVulnerability, isResultCliError } from '../../ossResult'; -import { OssService } from '../ossService'; -import { ImportedModule, ModuleVulnerabilityCount, SeverityCounts } from './importedModule'; - -export class ModuleVulnerabilityCountProvider { - constructor( - private readonly ossService: OssService, - private readonly lca: ILanguageClientAdapter, - private readonly uriAdapter: IUriAdapter, - private readonly textDocumentAdapter: ITextDocumentAdapter, - ) {} - - async getVulnerabilityCount( - fileName: string, - module: ImportedModule, - language: Language, - ): Promise { - const notCalculated = { - name: module.name, - fileName: module.fileName, - line: null, - range: null, - hasCount: false, - }; - - if ([Language.TypeScript, Language.JavaScript, Language.PJSON].includes(language)) { - // TODO use LS when OSS is moved to LS - const ossResult = this.ossService.getResultArray(); - if (!ossResult) { - return notCalculated; - } - - return this.mapOssResult(module, ossResult); - } else if (language == Language.HTML && module.loc) { - const uri = this.uriAdapter.file(fileName).toString(); - const doc: LSPTextDocument = this.textDocumentAdapter.create(uri, 'HTML', 1, ''); - const line = module.loc.start.line - 1; - const param = { - textDocument: { uri: doc.uri }, - range: { - start: { line: line, character: module.loc.start.column }, - end: { line: line, character: module.loc.end.column }, - }, - }; - const inlineValues: InlineValueText[] = await this.lca - .getLanguageClient() - .sendRequest('textDocument/inlineValue', param); - - if (inlineValues.length > 0) { - return { - name: module.name, - version: module.version, - fileName: module.fileName, - line: module.line, - range: module.loc, - count: inlineValues[0].text, - hasCount: true, - } as ModuleVulnerabilityCount; - } - } - - return notCalculated; - } - - isFilePartOfOssTest(filePath: string, ossFileResult: OssFileResult): boolean { - if (isResultCliError(ossFileResult)) { - return false; - } - - // File is considered to be part of OSS test if it has common root directory between OSS result path and filename path. - // This is since package.json always lies in the root directory folder of a project. - return filePath.startsWith(ossFileResult.path); - } - - private mapOssResult(module: ImportedModule, ossResult: ReadonlyArray): ModuleVulnerabilityCount { - const notCalculated = { - name: module.name, - fileName: module.fileName, - line: null, - hasCount: false, - range: null, - }; - - for (const fileResult of ossResult) { - if (!this.isFilePartOfOssTest(module.fileName, fileResult)) { - continue; - } - - const vulnerabilities = this.ossService.getUniqueVulnerabilities((fileResult as OssResultBody).vulnerabilities); - - // Sum up all vulnerabilities detected in first-level dependencies by OSS matching the imported module name. - // Ideally we want to use the same mechanism as NPM for determining the version used within users code. For now we stick with direct-vulnerability surfacing only. - const directVulnerabilities = vulnerabilities - .filter(v => v.name === module.name) - .filter(v => v.from.length == 2 && v.from[1].startsWith(module.name)); - const vulnerabilityCount = directVulnerabilities.length; - - // NPM allows declaration of the same direct dependency with multiple versions of it (e.g. {"dependencies": "webpack": "^4.44.1", "webpack": "^4.44.2",}). Thus, we should account for vulnerabilities that can be in different versions of the same package. - const hasSingleVersionVulnerability = directVulnerabilities.every( - vuln => vuln.version == directVulnerabilities[0].version, - ); - - let moduleVersion; - if (directVulnerabilities.length && hasSingleVersionVulnerability) { - moduleVersion = directVulnerabilities[0].version; - } - - const severityCounts = this.getSeverityCounts(directVulnerabilities); - const mostSevereVulnerability = this.getMostSevereVulnerability(directVulnerabilities); - - return { - name: module.name, - version: moduleVersion, - fileName: module.fileName, - count: `${vulnerabilityCount}`, - line: module.line, - range: module.loc, - hasCount: vulnerabilityCount > 0, - severityCounts, - mostSevereVulnerabilityId: mostSevereVulnerability?.id, - }; - } - - return notCalculated; - } - - private getSeverityCounts(directVulnerabilities: OssVulnerability[]): SeverityCounts { - return directVulnerabilities - .map(v => v.severity) - .reduce( - (arr, severity) => ({ - ...arr, - [severity]: directVulnerabilities.filter(v => v.severity == severity).length, - }), - {} as SeverityCounts, - ); - } - - private getMostSevereVulnerability(vulnerabilities: OssVulnerability[]): OssVulnerability | null { - return vulnerabilities.sort((a, b) => { - if (!a.cvssScore && !b.cvssScore) return 0; - if (!a.cvssScore) return 1; - if (!b.cvssScore) return -1; - - const cvssScore1 = parseFloat(a.cvssScore); - const cvssScore2 = parseFloat(b.cvssScore); - if (cvssScore1 > cvssScore2) { - return -1; - } else if (cvssScore1 < cvssScore2) { - return 1; - } - - return 0; - })?.[0]; - } -} diff --git a/src/snyk/snykOss/views/ossVulnerabilityTreeProvider.ts b/src/snyk/snykOss/views/ossVulnerabilityTreeProvider.ts deleted file mode 100644 index 17b9037b2..000000000 --- a/src/snyk/snykOss/views/ossVulnerabilityTreeProvider.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { OpenCommandIssueType, OpenIssueCommandArg } from '../../common/commands/types'; -import { IConfiguration } from '../../common/configuration/configuration'; -import { SNYK_OPEN_ISSUE_COMMAND } from '../../common/constants/commands'; -import { SNYK_ANALYSIS_STATUS } from '../../common/constants/views'; -import { messages as commonMessages } from '../../common/messages/analysisMessages'; -import { IContextService } from '../../common/services/contextService'; -import { IViewManagerService } from '../../common/services/viewManagerService'; -import { AnalysisTreeNodeProvider } from '../../common/views/analysisTreeNodeProvider'; -import { INodeIcon, NODE_ICONS, TreeNode } from '../../common/views/treeNode'; -import { messages } from '../messages/treeView'; -import { isResultCliError, OssFileResult, OssSeverity, OssVulnerability } from '../ossResult'; -import { OssService } from '../services/ossService'; - -type ISeverityCounts = { - [key in OssSeverity]: number; -}; - -export type OssIssueCommandArg = OssVulnerability & { - matchingIdVulnerabilities: OssVulnerability[]; - overviewHtml: string; -}; - -export class OssVulnerabilityTreeProvider extends AnalysisTreeNodeProvider { - constructor( - protected readonly viewManagerService: IViewManagerService, - protected readonly contextService: IContextService, - protected readonly ossService: OssService, - protected readonly configuration: IConfiguration, - ) { - super(configuration, ossService); - } - - async getRootChildren(): Promise { - if (!this.configuration.getFeaturesConfiguration()?.ossEnabled) { - return [ - new TreeNode({ - text: SNYK_ANALYSIS_STATUS.OSS_DISABLED, - }), - ]; - } - - if (!this.contextService.shouldShowOssAnalysis) return []; - - if (!this.ossService.isLsDownloadSuccessful) { - return [this.getErrorEncounteredTreeNode()]; - } - - if (!this.ossService.isCliReady) { - return [ - new TreeNode({ - text: messages.cookingDependencies, - }), - ]; - } else if (!this.ossService.isAnyWorkspaceFolderTrusted) { - return [this.getNoWorkspaceTrustTreeNode()]; - } - - if (this.ossService.isAnalysisRunning) { - return [ - new TreeNode({ - text: commonMessages.scanRunning, - }), - ]; - } - - const ossResults = this.ossService.getResultArray(); - if (!ossResults) { - return [ - new TreeNode({ - text: messages.runTest, - }), - ]; - } - - const nodes: TreeNode[] = []; - const [resultNodes, totalVulnCount] = await this.getResultNodes(ossResults); - nodes.push(...resultNodes); - - if (ossResults.length == 1 && isResultCliError(ossResults[0])) { - return nodes; - } - - nodes.sort(this.compareNodes); - - const topNodes = [ - new TreeNode({ - text: this.getIssueFoundText(totalVulnCount), - }), - this.getDurationTreeNode(), - this.getNoSeverityFiltersSelectedTreeNode(), - ]; - nodes.unshift(...topNodes.filter((n): n is TreeNode => n !== null)); - - return nodes; - } - - protected getIssueFoundText(nIssues: number): string { - switch (nIssues) { - case 0: - return messages.noVulnerabilitiesFound; - case 1: - return messages.singleVulnerabilityFound; - default: - return messages.multipleVulnerabilitiesFound(nIssues); - } - } - - protected getIssueDescriptionText( - dir: string | undefined, - vulnerabilities: readonly OssVulnerability[], - ): string | undefined { - return `${dir} - ${vulnerabilities.length} ${ - vulnerabilities.length === 1 ? messages.vulnerability : messages.vulnerabilities - }`; - } - - static getSeverityIcon(severity: OssSeverity | string): INodeIcon { - return ( - { - [OssSeverity.Critical]: NODE_ICONS.critical, - [OssSeverity.High]: NODE_ICONS.high, - [OssSeverity.Medium]: NODE_ICONS.medium, - [OssSeverity.Low]: NODE_ICONS.low, - }[severity] || NODE_ICONS.low - ); - } - - static getFileSeverity(counts: ISeverityCounts): OssSeverity { - for (const s of [OssSeverity.Critical, OssSeverity.High, OssSeverity.Medium, OssSeverity.Low]) { - if (counts[s]) return s; - } - - return OssSeverity.Low; - } - - /** Returns severity significance index. The higher, the more significant severity is. */ - static getSeverityComparatorIndex(severity: OssSeverity): number { - return Object.values(OssSeverity).indexOf(severity); - } - - onDidChangeTreeData = this.viewManagerService.refreshOssViewEmitter.event; - - private initFileSeverityCounts(): ISeverityCounts { - return { - [OssSeverity.Critical]: 0, - [OssSeverity.High]: 0, - [OssSeverity.Medium]: 0, - [OssSeverity.Low]: 0, - }; - } - - protected getFilteredIssues(uniqueVulnerabilities: OssVulnerability[]): OssVulnerability[] { - return uniqueVulnerabilities.filter(vuln => { - switch (vuln.severity.toLowerCase()) { - case OssSeverity.Critical: - return this.configuration.severityFilter.critical; - case OssSeverity.High: - return this.configuration.severityFilter.high; - case OssSeverity.Medium: - return this.configuration.severityFilter.medium; - case OssSeverity.Low: - return this.configuration.severityFilter.low; - default: - return true; - } - }); - } - - private async getResultNodes(ossResults: ReadonlyArray): Promise<[TreeNode[], number]> { - const nodes: TreeNode[] = []; - let totalVulnCount = 0; - - for (const fileResult of ossResults) { - if (isResultCliError(fileResult)) { - nodes.push(this.getErrorEncounteredTreeNode(fileResult.path)); - continue; - } - - const counts: ISeverityCounts = this.initFileSeverityCounts(); - const vulnerabilityNodes: TreeNode[] = []; - - const uniqueVulns = this.ossService.getUniqueVulnerabilities(fileResult.vulnerabilities); - totalVulnCount += uniqueVulns.length; - - const fileVulnerabilities = this.getFilteredIssues(uniqueVulns); - if (fileVulnerabilities.length == 0) continue; - - for (const vuln of fileVulnerabilities) { - counts[vuln.severity]++; - vulnerabilityNodes.push( - new TreeNode({ - text: `${vuln.packageName}@${vuln.version} - ${vuln.title}`, - icon: OssVulnerabilityTreeProvider.getSeverityIcon(vuln.severity), - internal: { - severity: OssVulnerabilityTreeProvider.getSeverityComparatorIndex(vuln.severity), - }, - command: { - command: SNYK_OPEN_ISSUE_COMMAND, - title: '', - arguments: [ - { - issueType: OpenCommandIssueType.OssVulnerability, - // eslint-disable-next-line no-await-in-loop - issue: await this.ossService.getOssIssueCommandArg(vuln, fileResult.vulnerabilities), - } as OpenIssueCommandArg, - ], - }, - }), - ); - } - - vulnerabilityNodes.sort(this.compareNodes); - const fileSeverity = OssVulnerabilityTreeProvider.getFileSeverity(counts); - - const fileNode = new TreeNode({ - text: fileResult.displayTargetFile, - description: this.getIssueDescriptionText(fileResult.projectName, fileVulnerabilities), - icon: OssVulnerabilityTreeProvider.getSeverityIcon(fileSeverity), - children: vulnerabilityNodes, - internal: { - nIssues: vulnerabilityNodes.length, - severity: OssVulnerabilityTreeProvider.getSeverityComparatorIndex(fileSeverity), - }, - }); - nodes.push(fileNode); - } - - return [nodes, totalVulnCount]; - } -} diff --git a/src/snyk/snykOss/views/suggestion/ossSuggestionWebviewProvider.ts b/src/snyk/snykOss/views/suggestion/ossSuggestionWebviewProvider.ts deleted file mode 100644 index 8d9ad8b86..000000000 --- a/src/snyk/snykOss/views/suggestion/ossSuggestionWebviewProvider.ts +++ /dev/null @@ -1,210 +0,0 @@ -import * as vscode from 'vscode'; -import { SNYK_OPEN_BROWSER_COMMAND } from '../../../common/constants/commands'; -import { SNYK_VIEW_SUGGESTION_OSS } from '../../../common/constants/views'; -import { ErrorHandler } from '../../../common/error/errorHandler'; -import { ILog } from '../../../common/logger/interfaces'; -import { messages as learnMessages } from '../../../common/messages/learn'; -import { LearnService } from '../../../common/services/learnService'; -import { getNonce } from '../../../common/views/nonce'; -import { WebviewPanelSerializer } from '../../../common/views/webviewPanelSerializer'; -import { WebviewProvider } from '../../../common/views/webviewProvider'; -import { ExtensionContext } from '../../../common/vscode/extensionContext'; -import { IVSCodeWindow } from '../../../common/vscode/window'; -import { messages as errorMessages } from '../../messages/error'; -import { OssIssueCommandArg } from '../ossVulnerabilityTreeProvider'; - -enum OssSuggestionsViewEventMessageType { - OpenBrowser = 'openBrowser', -} - -type OssSuggestionViewEventMessage = { - type: OssSuggestionsViewEventMessageType; - value: unknown; -}; - -export class OssSuggestionWebviewProvider extends WebviewProvider { - constructor( - protected readonly context: ExtensionContext, - private readonly window: IVSCodeWindow, - protected readonly logger: ILog, - private readonly learnService: LearnService, - ) { - super(context, logger); - } - - activate(): void { - this.context.addDisposables( - this.window.registerWebviewPanelSerializer(SNYK_VIEW_SUGGESTION_OSS, new WebviewPanelSerializer(this)), - ); - } - - async postLearnLessonMessage(vulnerability: OssIssueCommandArg): Promise { - try { - if (this.panel) { - const lesson = await this.learnService.getOssLesson(vulnerability); - if (lesson) { - void this.panel.webview.postMessage({ - type: 'setLesson', - args: { url: lesson.url, title: learnMessages.lessonButtonTitle }, - }); - } else { - void this.panel.webview.postMessage({ - type: 'setLesson', - args: null, - }); - } - } - } catch (e) { - ErrorHandler.handle(e, this.logger, learnMessages.getLessonError); - } - } - - async showPanel(vulnerability: OssIssueCommandArg): Promise { - try { - await this.focusSecondEditorGroup(); - - if (this.panel) { - this.panel.reveal(vscode.ViewColumn.Two, true); - } else { - this.panel = vscode.window.createWebviewPanel( - SNYK_VIEW_SUGGESTION_OSS, - 'Snyk OSS Vulnerability', - { - viewColumn: vscode.ViewColumn.Two, - preserveFocus: true, - }, - this.getWebviewOptions(), - ); - this.registerListeners(); - } - - this.panel.webview.html = this.getHtmlForWebview(this.panel.webview); - this.panel.iconPath = vscode.Uri.joinPath( - vscode.Uri.file(this.context.extensionPath), - 'media', - 'images', - 'snyk-oss.svg', - ); - - void this.panel.webview.postMessage({ type: 'set', args: vulnerability }); - void this.postLearnLessonMessage(vulnerability); - } catch (e) { - ErrorHandler.handle(e, this.logger, errorMessages.suggestionViewShowFailed); - } - } - - protected registerListeners(): void { - if (!this.panel) return; - - this.panel.onDidDispose(() => this.onPanelDispose(), null, this.disposables); - this.panel.webview.onDidReceiveMessage( - (data: OssSuggestionViewEventMessage) => { - switch (data.type) { - case OssSuggestionsViewEventMessageType.OpenBrowser: - void vscode.commands.executeCommand(SNYK_OPEN_BROWSER_COMMAND, data.value); - break; - default: - break; - } - }, - null, - this.disposables, - ); - this.panel.onDidChangeViewState(() => this.checkVisibility(), null, this.disposables); - } - - protected getHtmlForWebview(webview: vscode.Webview): string { - const images: Record = [ - ['icon-code', 'svg'], - ['dark-critical-severity', 'svg'], - ['dark-high-severity', 'svg'], - ['dark-medium-severity', 'svg'], - ['dark-low-severity', 'svg'], - ['learn-icon', 'svg'], - ].reduce((accumulator: Record, [name, ext]) => { - const uri = this.getWebViewUri('media', 'images', `${name}.${ext}`); - if (!uri) throw new Error('Image missing.'); - accumulator[name] = uri.toString(); - return accumulator; - }, {}); - - const scriptUri = this.getWebViewUri( - 'out', - 'snyk', - 'snykOss', - 'views', - 'suggestion', - 'ossSuggestionWebviewScript.js', - ); - const styleUri = this.getWebViewUri('media', 'views', 'oss', 'suggestion', 'suggestion.css'); - const learnStyleUri = this.getWebViewUri('media', 'views', 'common', 'learn.css'); - - const nonce = getNonce(); - - return ` - - - - - - - - - - - - -
-
-
- - - - - - - - - -
-
-
-
- - -
-
-
-
-
Vulnerable module
-
-
-
-
Introduced through
-
-
-
-
Fixed in
-
-
-
-
Exploit maturity
-
-
-
-
-

Detailed paths

-
-
-
-
-
-
- - - `; - } -} diff --git a/src/snyk/snykOss/views/suggestion/ossSuggestionWebviewScript.ts b/src/snyk/snykOss/views/suggestion/ossSuggestionWebviewScript.ts deleted file mode 100644 index 5dce4e32d..000000000 --- a/src/snyk/snykOss/views/suggestion/ossSuggestionWebviewScript.ts +++ /dev/null @@ -1,293 +0,0 @@ -/* 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-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/// -// This script will be run within the webview itself -// It cannot access the main VS Code APIs directly. -(function () { - // TODO: Redefine types until bundling is introduced into extension - // https://stackoverflow.com/a/56938089/1713082 - type Vulnerability = { - id: string; - license?: string; - identifiers?: Identifiers; - title: string; - description: string; - language: string; - packageManager: string; - packageName: string; - severity: string; - name: string; - version: string; - exploit?: string; - - CVSSv3?: string; - cvssScore?: string; - - fixedIn?: Array; - from: Array; - upgradePath: Array; - isPatchable: boolean; - isUpgradable: boolean; - - matchingIdVulnerabilities: Vulnerability[]; - overviewHtml: string; - }; - - type Lesson = { - url: string; - title: string; - }; - - type Identifiers = { - CWE: string[]; - CVE: string[]; - }; - - const vscode = acquireVsCodeApi(); - let vulnerability = {} as Vulnerability; - - let lesson: Lesson | null; - - function navigateToUrl(url: string) { - vscode.postMessage({ - type: 'openBrowser', - value: url, - }); - } - - function fillLearnLink() { - const learnWrapper = document.querySelector('.learn')!; - learnWrapper.className = 'learn'; - - if (lesson) { - const learnLink = document.querySelector('.learn--link')!; - learnLink.innerText = lesson.title; - const lessonUrl = lesson.url; - learnLink.onclick = () => navigateToUrl(lessonUrl); - learnWrapper.className = 'learn show'; - } - } - - function showCurrentSuggestion() { - const severity = document.querySelector('.severity')!; - const title = document.querySelector('.suggestion .suggestion-text')!; - - // Set title - title.innerHTML = vulnerability.title; - - // Set severity icon - setSeverityIcon(); - - // Fill identifiers line - fillIdentifiers(); - - // Fill summary - fillSummary(); - - // Fill detailed paths - fillDetailedPaths(); - - // Fill overview - fillOverview(); - - function setSeverityIcon() { - if (vulnerability.severity) { - severity.querySelectorAll('img').forEach(n => { - if (n.id.slice(-1) === 'l') { - if (n.id.includes(vulnerability.severity)) n.className = 'icon light-only'; - else n.className = 'icon light-only hidden'; - } else { - if (n.id.includes(vulnerability.severity)) n.className = 'icon dark-only'; - else n.className = 'icon dark-only hidden'; - } - }); - } else { - severity.querySelectorAll('img').forEach(n => (n.className = 'icon hidden')); - } - } - - function fillIdentifiers() { - const identifiers = document.querySelector('.identifiers')!; - identifiers.innerHTML = ''; // reset node - const type = vulnerability.license ? 'License' : 'Vulnerability'; - const typeNode = document.createTextNode(type); - identifiers.appendChild(typeNode); - - vulnerability.identifiers?.CVE.forEach(cve => - appendIdentifierSpan(identifiers, cve, `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${cve}`), - ); - vulnerability.identifiers?.CWE.forEach(cwe => appendIdentifierSpan(identifiers, cwe, getCweLink(cwe))); - if (vulnerability.cvssScore) appendIdentifierSpan(identifiers, `CVSS ${vulnerability.cvssScore}`); - appendIdentifierSpan(identifiers, vulnerability.id.toUpperCase(), `https://snyk.io/vuln/${vulnerability.id}`); - } - - function fillSummary() { - const module = document.querySelector('.module > .content')!; - module.textContent = vulnerability.name; - - const maturity = document.querySelector('.maturity > .content')!; - if (!vulnerability.exploit) { - maturity.classList.add('hidden'); - } else { - maturity.textContent = vulnerability.exploit; - } - - const introducedThrough = document.querySelector('.introduced-through > .content')!; - introducedThrough.innerHTML = ''; // reset node - if (vulnerability.from.length == 0) { - introducedThrough.classList.add('hidden'); - } else { - let modules = vulnerability.matchingIdVulnerabilities - .filter(vuln => vuln.from.length > 1) - .map(vuln => vuln.from[1]); - modules = [...new Set(modules)]; // obtain distinct only - - modules.forEach((module, i, arr) => { - appendIntroducedThroughSpan(introducedThrough, module, vulnerability.packageManager); - if (i != arr.length - 1) introducedThrough.append(document.createTextNode(', ')); - }); - } - - const fixedIn = document.querySelector('.fixed-in > .content')!; - fixedIn.innerHTML = ''; // reset node - if (!vulnerability.fixedIn || vulnerability.fixedIn.length == 0) { - fixedIn.append('Not fixed'); - } else { - fixedIn.append(vulnerability.name + '@'); - vulnerability.fixedIn.forEach((version, i, arr) => { - let versionStr = version; - if (i != arr.length - 1) versionStr = versionStr + ', '; - fixedIn.append(versionStr); - }); - } - } - - function fillDetailedPaths() { - const paths = document.querySelector('.detailed-paths')!; - paths.innerHTML = ''; // reset node - - vulnerability.matchingIdVulnerabilities.forEach(vuln => { - const introducedThrough = vuln.from.join(' > '); - - const isOutdated = vuln.upgradePath && vuln.upgradePath[1] === vuln.from[1]; - - // The logic as in registry - // https://github.com/snyk/registry/blob/5fe141a3c5eeb6b2c5e62cfa2b5a8643df29403d/frontend/src/components/IssueCardVulnerablePath/IssueCardVulnerablePath.vue#L109 - let remediationAdvice: string; - const upgradeMessage = `Upgrade to ${vuln.upgradePath[1]}`; - - if (vuln.isUpgradable || vuln.isPatchable) { - if (isOutdated) { - remediationAdvice = vuln.isPatchable ? upgradeMessage : getOutdatedDependencyMessage(vuln); - } else { - remediationAdvice = upgradeMessage; - } - } else { - remediationAdvice = 'none'; - } - - const html = ` -
-
Introduced through
-
${introducedThrough}
-
-
-
Remediation
-
${remediationAdvice}
-
`; - - const path = document.createElement('div'); - path.className = 'detailed-path'; - path.innerHTML = html; - paths.append(path); - }); - } - - function fillOverview() { - const overview = document.getElementById('overview')!; - overview.innerHTML = vulnerability.overviewHtml; - } - } - - function getCweLink(cwe: string) { - const id = cwe.toUpperCase().replace('CWE-', ''); - return `https://cwe.mitre.org/data/definitions/${id}.html`; - } - - function appendIdentifierSpan(identifiers: Element, id: string, link?: string) { - const delimiter = document.createElement('span'); - // delimiter.innerText = ' | '; - delimiter.className = 'delimiter'; - identifiers.appendChild(delimiter); - - let cveNode: HTMLElement; - if (link) { - cveNode = document.createElement('a'); - cveNode.onclick = () => navigateToUrl(link); - } else { - cveNode = document.createElement('span'); - } - - cveNode.innerText = id; - - identifiers.appendChild(cveNode); - } - - function appendIntroducedThroughSpan(introducedThrough: Element, module: string, packageManager: string) { - const supportedPackageManagers = ['npm']; - - let node: HTMLElement; - // replicate app.snyk.io linking logic from https://github.com/snyk/registry/blob/c78f0ae84dc20f25146880b3d3d5661f3d3e4db2/frontend/src/lib/issue-utils.ts#L547 - if (supportedPackageManagers.includes(packageManager.toLowerCase())) { - node = document.createElement('a'); - node.onclick = () => navigateToUrl(`https://app.snyk.io/test/${packageManager}/${module}`); - } else { - node = document.createElement('span'); - } - - node.innerText = module; - introducedThrough.appendChild(node); - } - - function getOutdatedDependencyMessage(vulnerability: Vulnerability) { - return `Your dependencies are out of date, otherwise you would be using a newer ${vulnerability.name} than ${ - vulnerability.name - }@${vulnerability.version}. - ${ - ['npm', 'yarn', 'yarn-workspace'].includes(vulnerability.packageManager) - ? `Try relocking your lockfile or deleting node_modules and reinstalling your dependencies. If the problem persists, one of your dependencies may be bundling outdated modules.` - : 'Try reinstalling your dependencies. If the problem persists, one of your dependencies may be bundling outdated modules.' - }`; - } - - window.addEventListener('message', event => { - const { type, args } = event.data; - switch (type) { - case 'set': { - vulnerability = args; - vscode.setState({ ...vscode.getState(), vulnerability }); - showCurrentSuggestion(); - break; - } - case 'get': { - vulnerability = vscode.getState()?.vulnerability || {}; - showCurrentSuggestion(); - break; - } - case 'setLesson': { - lesson = args; - vscode.setState({ ...vscode.getState(), lesson }); - fillLearnLink(); - break; - } - case 'getLesson': { - lesson = vscode.getState()?.lesson || null; - fillLearnLink(); - break; - } - } - }); -})(); diff --git a/src/snyk/snykOss/watchers/dailyScanJob.ts b/src/snyk/snykOss/watchers/dailyScanJob.ts deleted file mode 100644 index 6b21793b8..000000000 --- a/src/snyk/snykOss/watchers/dailyScanJob.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IExtension } from '../../base/modules/interfaces'; - -export class DailyScanJob { - private readonly dayInMs = 86400000; - private job: NodeJS.Timeout; - - constructor(private readonly extension: IExtension) {} - - schedule(): void { - if (this.job) { - this.job.refresh(); - return; - } - - this.job = setTimeout(() => { - void this.extension.runOssScan(false); - }, this.dayInMs); - } -} diff --git a/src/snyk/snykOss/watchers/manifestFileWatcher.ts b/src/snyk/snykOss/watchers/manifestFileWatcher.ts index 9a13c05c1..d022125c3 100644 --- a/src/snyk/snykOss/watchers/manifestFileWatcher.ts +++ b/src/snyk/snykOss/watchers/manifestFileWatcher.ts @@ -45,13 +45,13 @@ export default function createManifestFileWatcher( const globPattern = `**/{${Object.values(SUPPORTED_MANIFEST_FILES).join(',')}}`; const watcher = workspace.createFileSystemWatcher(globPattern); - watcher.onDidChange(() => runOssScan()); - watcher.onDidDelete(() => runOssScan()); - watcher.onDidCreate(() => runOssScan()); + watcher.onDidChange(() => runScan()); + watcher.onDidDelete(() => runScan()); + watcher.onDidCreate(() => runScan()); - function runOssScan() { + function runScan() { if (configuration.shouldAutoScanOss) { - void extension.runOssScan(); + void extension.runScan(); } } diff --git a/src/test/unit/common/commands/commandController.test.ts b/src/test/unit/common/commands/commandController.test.ts index f0b8ef8c6..4a13ede58 100644 --- a/src/test/unit/common/commands/commandController.test.ts +++ b/src/test/unit/common/commands/commandController.test.ts @@ -5,15 +5,15 @@ import { ScanModeService } from '../../../../snyk/base/services/scanModeService' import { IAnalytics } from '../../../../snyk/common/analytics/itly'; import { CommandController } from '../../../../snyk/common/commands/commandController'; import { COMMAND_DEBOUNCE_INTERVAL } from '../../../../snyk/common/constants/general'; +import { CodeIssueData, IacIssueData } from '../../../../snyk/common/languageServer/types'; import { IOpenerService } from '../../../../snyk/common/services/openerService'; +import { IProductService } from '../../../../snyk/common/services/productService'; import { IVSCodeCommands } from '../../../../snyk/common/vscode/commands'; import { IVSCodeWorkspace } from '../../../../snyk/common/vscode/workspace'; -import { OssService } from '../../../../snyk/snykOss/services/ossService'; +import { OssService } from '../../../../snyk/snykOss/ossService'; import { LanguageServerMock } from '../../mocks/languageServer.mock'; import { LoggerMock } from '../../mocks/logger.mock'; import { windowMock } from '../../mocks/window.mock'; -import { IProductService } from '../../../../snyk/common/services/productService'; -import { CodeIssueData, IacIssueData } from '../../../../snyk/common/languageServer/types'; suite('CommandController', () => { const sleep = util.promisify(setTimeout); diff --git a/src/test/unit/common/editor/codeActionsProvider.test.ts b/src/test/unit/common/editor/codeActionsProvider.test.ts index c30574e7a..e6a2fe81b 100644 --- a/src/test/unit/common/editor/codeActionsProvider.test.ts +++ b/src/test/unit/common/editor/codeActionsProvider.test.ts @@ -6,7 +6,7 @@ import { CodeActionsProvider } from '../../../../snyk/common/editor/codeActionsP import { Issue } from '../../../../snyk/common/languageServer/types'; import { WorkspaceFolderResult } from '../../../../snyk/common/services/productService'; import { ICodeActionKindAdapter } from '../../../../snyk/common/vscode/codeAction'; -import { CodeAction, Range, TextDocument } from '../../../../snyk/common/vscode/types'; +import { CodeAction, CodeActionContext, Range, TextDocument } from '../../../../snyk/common/vscode/types'; type ProductData = { issueType: string; @@ -84,7 +84,7 @@ suite('Code Actions Provider', () => { } as unknown as TextDocument; // act - const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range); + const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(codeActions?.length, 2); @@ -101,7 +101,7 @@ suite('Code Actions Provider', () => { } as unknown as TextDocument; // act - issuesActionsProvider.provideCodeActions(document, {} as Range); + issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(logQuickFixIsDisplayed.calledOnce, true); diff --git a/src/test/unit/common/services/learnService.test.ts b/src/test/unit/common/services/learnService.test.ts index e5715a87d..60804dcd2 100644 --- a/src/test/unit/common/services/learnService.test.ts +++ b/src/test/unit/common/services/learnService.test.ts @@ -1,8 +1,7 @@ -import { strictEqual } from 'assert'; import sinon from 'sinon'; +import { strictEqual } from 'assert'; import { IVSCodeCommands } from '../../../../snyk/common/vscode/commands'; import { LearnService } from '../../../../snyk/common/services/learnService'; -import { OssIssueCommandArg } from '../../../../snyk/snykOss/views/ossVulnerabilityTreeProvider'; import { CodeIssueData, Issue, IssueSeverity } from '../../../../snyk/common/languageServer/types'; import { SNYK_GET_LESSON_COMMAND } from '../../../../snyk/common/constants/commands'; @@ -20,21 +19,6 @@ suite('LearnService', () => { sinon.restore(); }); - test('getOssLesson executes correct command', async () => { - const learnService = new LearnService(commands); - - const issue: OssIssueCommandArg = { - id: 'id', - packageManager: 'packageManager', - } as OssIssueCommandArg; - - await learnService.getOssLesson(issue); - strictEqual(executeCommandFake.calledOnce, true); - strictEqual( - executeCommandFake.calledWith(SNYK_GET_LESSON_COMMAND, issue.id, issue.packageManager, '', '', 4), - true, - ); - }); test('getCodeLesson executes correct command', async () => { const learnService = new LearnService(commands); const issue: Issue = { diff --git a/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts b/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts index 0a5870c19..d36ae1504 100644 --- a/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts +++ b/src/test/unit/snykCode/codeActions/codeIssuesActionsProvider.test.ts @@ -6,7 +6,7 @@ import { CodeIssueData, Issue } from '../../../../snyk/common/languageServer/typ import { WorkspaceFolderResult } from '../../../../snyk/common/services/productService'; import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../../../snyk/common/vscode/codeAction'; import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages'; -import { CodeActionKind, Range, TextDocument } from '../../../../snyk/common/vscode/types'; +import { CodeActionContext, CodeActionKind, Range, TextDocument } from '../../../../snyk/common/vscode/types'; import { SnykCodeActionsProvider } from '../../../../snyk/snykCode/codeActions/codeIssuesActionsProvider'; import { IssueUtils } from '../../../../snyk/snykCode/utils/issueUtils'; @@ -68,7 +68,7 @@ suite('Snyk Code actions provider', () => { } as unknown as TextDocument; // act - const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range); + const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(codeActions?.length, 3); @@ -86,7 +86,7 @@ suite('Snyk Code actions provider', () => { } as unknown as TextDocument; // act - issuesActionsProvider.provideCodeActions(document, {} as Range); + issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(logQuickFixIsDisplayed.calledOnce, true); diff --git a/src/test/unit/snykIac/codeActions/iacCodeActionsProvider.test.ts b/src/test/unit/snykIac/codeActions/iacCodeActionsProvider.test.ts index 8d59650a0..201207332 100644 --- a/src/test/unit/snykIac/codeActions/iacCodeActionsProvider.test.ts +++ b/src/test/unit/snykIac/codeActions/iacCodeActionsProvider.test.ts @@ -6,7 +6,7 @@ import { IacIssueData, Issue } from '../../../../snyk/common/languageServer/type import { WorkspaceFolderResult } from '../../../../snyk/common/services/productService'; import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../../../snyk/common/vscode/codeAction'; import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages'; -import { CodeActionKind, Range, TextDocument } from '../../../../snyk/common/vscode/types'; +import { CodeActionContext, CodeActionKind, Range, TextDocument } from '../../../../snyk/common/vscode/types'; import { IacCodeActionsProvider } from '../../../../snyk/snykIac/codeActions/iacCodeActionsProvider'; import { IacIssue } from '../../../../snyk/snykIac/issue'; @@ -66,7 +66,7 @@ suite('IaC code actions provider', () => { } as unknown as TextDocument; // act - const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range); + const codeActions = issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(codeActions?.length, 1); @@ -82,7 +82,7 @@ suite('IaC code actions provider', () => { } as unknown as TextDocument; // act - issuesActionsProvider.provideCodeActions(document, {} as Range); + issuesActionsProvider.provideCodeActions(document, {} as Range, {} as CodeActionContext); // verify strictEqual(logQuickFixIsDisplayed.calledOnce, true); diff --git a/src/test/unit/snykOss/ossService.test.ts b/src/test/unit/snykOss/ossService.test.ts new file mode 100644 index 000000000..46f3e13db --- /dev/null +++ b/src/test/unit/snykOss/ossService.test.ts @@ -0,0 +1,79 @@ +import { strictEqual } from 'assert'; +import sinon from 'sinon'; +import { IAnalytics } from '../../../snyk/common/analytics/itly'; +import { IConfiguration } from '../../../snyk/common/configuration/configuration'; +import { WorkspaceTrust } from '../../../snyk/common/configuration/trustedFolders'; +import { ILanguageServer } from '../../../snyk/common/languageServer/languageServer'; +import { OssIssueData, ScanProduct, ScanStatus } from '../../../snyk/common/languageServer/types'; +import { IProductService } from '../../../snyk/common/services/productService'; +import { IViewManagerService } from '../../../snyk/common/services/viewManagerService'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../../snyk/common/vscode/codeAction'; +import { ExtensionContext } from '../../../snyk/common/vscode/extensionContext'; +import { IVSCodeLanguages } from '../../../snyk/common/vscode/languages'; +import { IVSCodeWorkspace } from '../../../snyk/common/vscode/workspace'; +import { IOssSuggestionWebviewProvider } from '../../../snyk/snykOss/interfaces'; +import { OssService } from '../../../snyk/snykOss/ossService'; +import { LanguageServerMock } from '../mocks/languageServer.mock'; +import { LoggerMock } from '../mocks/logger.mock'; + +suite('OSS Service', () => { + let ls: ILanguageServer; + let service: IProductService; + let refreshViewFake: sinon.SinonSpy; + + setup(() => { + ls = new LanguageServerMock(); + refreshViewFake = sinon.fake(); + + const viewManagerService = { + refreshOssView: refreshViewFake, + } as unknown as IViewManagerService; + + service = new OssService( + {} as ExtensionContext, + {} as IConfiguration, + {} as IOssSuggestionWebviewProvider, + {} as ICodeActionAdapter, + { getQuickFix: sinon.fake() } as ICodeActionKindAdapter, + viewManagerService, + { + getWorkspaceFolders: () => [''], + } as IVSCodeWorkspace, + new WorkspaceTrust(), + ls, + { + registerCodeActionsProvider: sinon.fake(), + } as unknown as IVSCodeLanguages, + new LoggerMock(), + {} as IAnalytics, + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Scan returned for OSS product', () => { + ls.scan$.next({ + product: ScanProduct.OpenSource, + folderPath: 'test/path', + issues: [], + status: ScanStatus.InProgress, + }); + + strictEqual(service.isAnalysisRunning, true); + sinon.assert.calledOnce(refreshViewFake); + }); + + test('Scan not returned for non-OSS product', () => { + ls.scan$.next({ + product: ScanProduct.Code, + folderPath: 'test/path', + issues: [], + status: ScanStatus.InProgress, + }); + + strictEqual(service.isAnalysisRunning, false); + sinon.assert.notCalled(refreshViewFake); + }); +}); diff --git a/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts b/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts new file mode 100644 index 000000000..5a6ad2c26 --- /dev/null +++ b/src/test/unit/snykOss/providers/ossCodeActionsProvider.test.ts @@ -0,0 +1,118 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import { CodeAction, CodeActionContext, CodeActionKind, Range, TextDocument } from 'vscode'; +import { IAnalytics } from '../../../../snyk/common/analytics/itly'; +import { OpenCommandIssueType, OpenIssueCommandArg } from '../../../../snyk/common/commands/types'; +import { SNYK_OPEN_ISSUE_COMMAND } from '../../../../snyk/common/constants/commands'; +import { Issue, IssueSeverity, OssIssueData } from '../../../../snyk/common/languageServer/types'; +import { WorkspaceFolderResult } from '../../../../snyk/common/services/productService'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../../../snyk/common/vscode/codeAction'; +import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages'; +import { OssCodeActionsProvider } from '../../../../snyk/snykOss/providers/ossCodeActionsProvider'; + +suite('OSS code actions provider', () => { + let ossActionsProvider: OssCodeActionsProvider; + let logQuickFixIsDisplayed: sinon.SinonSpy; + let rangeMock: Range; + + setup(() => { + const ossResults = new Map>(); + ossResults.set('folderName', [ + { + filePath: '//folderName//package.json', + additionalData: {}, + } as unknown as Issue, + ]); + + logQuickFixIsDisplayed = sinon.fake(); + const analytics = { + logQuickFixIsDisplayed, + } as unknown as IAnalytics; + + const codeActionAdapter = { + create: (_: string, _kind?: CodeActionKind) => ({ + command: {}, + }), + } as ICodeActionAdapter; + + const codeActionKindAdapter = { + getQuickFix: sinon.fake(), + } as ICodeActionKindAdapter; + + rangeMock = { + contains: () => true, + } as unknown as Range; + + ossActionsProvider = new OssCodeActionsProvider( + {} as IVSCodeLanguages, + codeActionAdapter, + codeActionKindAdapter, + ossResults, + analytics, + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Provides the most severe vulnerability CodeAction', () => { + // arrange + const document = { + uri: { + fsPath: '//folderName//package.json', + }, + } as unknown as TextDocument; + + const clickedRange = {} as Range; + const context = {} as CodeActionContext; + + const vulnerabilities = [ + { + id: 'vulnerability1', + severity: IssueSeverity.High, + }, + { + id: 'vulnerability2', + severity: IssueSeverity.Medium, + }, + { + id: 'vulnerability3', + severity: IssueSeverity.Critical, + }, + ] as Issue[]; + + const mostSevereVulnerability = { + id: 'vulnerability3', + severity: IssueSeverity.Critical, + } as Issue; + + const codeActions = [ + { + command: { + command: SNYK_OPEN_ISSUE_COMMAND, + title: SNYK_OPEN_ISSUE_COMMAND, + arguments: [ + { + issueType: OpenCommandIssueType.OssVulnerability, + issue: mostSevereVulnerability, + } as OpenIssueCommandArg, + ], + }, + }, + ] as CodeAction[]; + + sinon.stub(ossActionsProvider, 'getIssueRange').returns(rangeMock); + // stubbing private methods workaround is to cast to any + sinon.stub(ossActionsProvider, 'getVulnerabilities').returns(vulnerabilities); + sinon.stub(ossActionsProvider, 'getMostSevereVulnerability').returns(mostSevereVulnerability); + sinon.stub(ossActionsProvider, 'getActions').returns(codeActions); + + // act + const result = ossActionsProvider.provideCodeActions(document, clickedRange, context); + + // assert + sinon.assert.calledOnce(logQuickFixIsDisplayed); + assert.deepStrictEqual(result, codeActions); + }); +}); diff --git a/src/test/unit/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.test.ts b/src/test/unit/snykOss/providers/vulnerabilityCountProvider.test.ts similarity index 51% rename from src/test/unit/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.test.ts rename to src/test/unit/snykOss/providers/vulnerabilityCountProvider.test.ts index fb5eb9ca4..92cd60afa 100644 --- a/src/test/unit/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider.test.ts +++ b/src/test/unit/snykOss/providers/vulnerabilityCountProvider.test.ts @@ -1,18 +1,21 @@ import { deepStrictEqual, strictEqual } from 'assert'; import sinon from 'sinon'; -import { CliError } from '../../../../../snyk/cli/services/cliService'; -import { Language } from '../../../../../snyk/common/types'; -import { OssResultBody, OssVulnerability } from '../../../../../snyk/snykOss/ossResult'; -import { OssService } from '../../../../../snyk/snykOss/services/ossService'; -import { ImportedModule } from '../../../../../snyk/snykOss/services/vulnerabilityCount/importedModule'; -import { ModuleVulnerabilityCountProvider } from '../../../../../snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider'; -import { ILanguageClientAdapter } from '../../../../../snyk/common/vscode/languageClient'; -import { IUriAdapter } from '../../../../../snyk/common/vscode/uri'; -import { ITextDocumentAdapter } from '../../../../../snyk/common/vscode/textdocument'; - -suite('OSS ModuleVulnerabilityCountProvider', () => { +import { InlineValueText } from 'vscode'; +import { CliError } from '../../../../snyk/cli/services/cliService'; +import { Language } from '../../../../snyk/common/types'; +import { ILanguageClientAdapter } from '../../../../snyk/common/vscode/languageClient'; +import { ITextDocumentAdapter } from '../../../../snyk/common/vscode/textdocument'; +import { IUriAdapter } from '../../../../snyk/common/vscode/uri'; +import { OssResultBody, OssVulnerability } from '../../../../snyk/snykOss/interfaces'; +import { OssService } from '../../../../snyk/snykOss/ossService'; +import { OssVulnerabilityCountProvider } from '../../../../snyk/snykOss/providers/ossVulnerabilityCountProvider'; +import { ImportedModule } from '../../../../snyk/snykOss/services/vulnerabilityCount/importedModule'; +import { VulnerabilityCountEmitter } from '../../../../snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountEmitter'; + +suite('OSS VulnerabilityCountProvider', () => { let ossService: OssService; - let vulnerabilityCountProvider: ModuleVulnerabilityCountProvider; + let vulnerabilityCountProvider: OssVulnerabilityCountProvider; + let vulnerabilityCountEmitterStub: VulnerabilityCountEmitter; const sampleFilePath = 'C:\\git\\project\\test.js'; const sampleModuleName = 'mongo-express'; @@ -53,14 +56,38 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { }, ]; + let sampleInlineValueText = [] as InlineValueText[]; + let sampleFileName = 'package.json'; + const sameplUri = `file:///Users/some.user/Documents/some-project/${sampleFileName}`; + + let languageClientStub: { sendRequest: any }; + let uriStub; + setup(() => { + uriStub = sinon.stub().returns(sameplUri); + languageClientStub = { + sendRequest: sinon.stub().resolves(sampleInlineValueText), + }; + ossService = {} as OssService; - vulnerabilityCountProvider = new ModuleVulnerabilityCountProvider( + vulnerabilityCountProvider = new OssVulnerabilityCountProvider( ossService, - {} as ILanguageClientAdapter, - {} as IUriAdapter, - {} as ITextDocumentAdapter, + { + create: sinon.spy(), + getLanguageClient: sinon.stub().returns(languageClientStub), + } as ILanguageClientAdapter, + { + file: uriStub, + parse: sinon.spy(), + } as IUriAdapter, + { + create: sinon.stub().returns({ + uri: uriStub, + lineCount: 1, + }), + } as ITextDocumentAdapter, ); + vulnerabilityCountEmitterStub = sinon.createStubInstance(VulnerabilityCountEmitter); }); teardown(() => { @@ -68,12 +95,13 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { }); test('Not calculated if JS/TS results are not provided', async () => { - ossService.getResultArray = () => undefined; + vulnerabilityCountProvider.getResultArray = () => undefined; const tsCount = await vulnerabilityCountProvider.getVulnerabilityCount( 'test.ts', sampleImportedModule, Language.TypeScript, + vulnerabilityCountEmitterStub, ); const jsCount = await vulnerabilityCountProvider.getVulnerabilityCount( 'test.ts', @@ -82,6 +110,7 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { fileName: 'test.ts', }, Language.JavaScript, + vulnerabilityCountEmitterStub, ); strictEqual(jsCount.hasCount, false); @@ -89,18 +118,36 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { }); test('Gets TS/JS imported module vulnerability results correctly', async () => { - ossService.getResultArray = () => sampleOssResults; - ossService.getUniqueVulnerabilities = () => sampleOssResults[0].vulnerabilities; + const text = 'Vulnerabilities: 2 | Critical: 1, High 1, Medium: 0, Low: 0 | Most Severe: npm:adm-zip:20180415'; + sampleInlineValueText = [ + { + text, + range: { + start: { + line: 1, + character: 16, + } as unknown as InlineValueText['range']['start'], + end: { + line: 1, + character: 29, + } as unknown as InlineValueText['range']['end'], + } as unknown as InlineValueText['range'], + }, + ]; + + sampleFileName = 'test.ts'; + languageClientStub.sendRequest = sinon.stub().resolves(sampleInlineValueText); const count = await vulnerabilityCountProvider.getVulnerabilityCount( - 'test.ts', + sampleFileName, sampleImportedModule, Language.TypeScript, + vulnerabilityCountEmitterStub, ); - strictEqual(count.hasCount, true); - strictEqual(count.count, '2'); - strictEqual(count.line, 1); + strictEqual(count.hasCount, true, 'hasCount is not true'); + strictEqual(count.count, text, `count is not: "${text}"`); + strictEqual(count.line, 1, 'line is not 1'); deepStrictEqual(count.range, { start: { line: 1, @@ -111,84 +158,21 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { column: 29, }, }); - strictEqual(count.severityCounts?.low, 1); - strictEqual(count.severityCounts?.medium, 1); }); test('Gets package.json dependency vulnerability results correctly', async () => { - ossService.getResultArray = () => sampleOssResults; - ossService.getUniqueVulnerabilities = () => sampleOssResults[0].vulnerabilities; + vulnerabilityCountProvider.getResultArray = sinon.stub().returns(sampleOssResults); const count = await vulnerabilityCountProvider.getVulnerabilityCount( 'test.ts', sampleImportedModule, Language.PJSON, + vulnerabilityCountEmitterStub, ); strictEqual(count.hasCount, true); }); - test('Gets only direct dependency vulnerability count', async () => { - const indirectDependency = '@indirect/dependency'; - const ossResultsWithIndirectVulnerability = [ - { - ...sampleOssResults[0], - vulnerabilities: [ - { - name: sampleModuleName, - from: ['goof', sampleModuleName], - } as unknown as OssVulnerability, - { - name: sampleModuleName, - from: ['goof', indirectDependency, sampleModuleName], - } as unknown as OssVulnerability, - ], - }, - ]; - ossService.getResultArray = () => ossResultsWithIndirectVulnerability; - ossService.getUniqueVulnerabilities = () => ossResultsWithIndirectVulnerability[0].vulnerabilities; - - const count = await vulnerabilityCountProvider.getVulnerabilityCount( - 'test.ts', - sampleImportedModule, - Language.TypeScript, - ); - - strictEqual(count.hasCount, true); - strictEqual(count.count, '1'); - }); - - test('Provides a version if same direct dependency has single vulnerable version', async () => { - const version = '1.0.0'; - const ossResultsWithMultipleVersionsVulnerability = [ - { - ...sampleOssResults[0], - vulnerabilities: [ - { - name: sampleModuleName, - from: ['goof', sampleModuleName], - version: version, - } as unknown as OssVulnerability, - { - name: sampleModuleName, - from: ['goof', sampleModuleName], - version: version, - } as unknown as OssVulnerability, - ], - }, - ]; - ossService.getResultArray = () => ossResultsWithMultipleVersionsVulnerability; - ossService.getUniqueVulnerabilities = () => ossResultsWithMultipleVersionsVulnerability[0].vulnerabilities; - - const count = await vulnerabilityCountProvider.getVulnerabilityCount( - 'test.ts', - sampleImportedModule, - Language.TypeScript, - ); - - strictEqual(count.version, version); - }); - test("Doesn't provide a version if same direct dependency has multiple vulnerable versions", async () => { const ossResultsWithMultipleVersionsVulnerability = [ { @@ -207,13 +191,13 @@ suite('OSS ModuleVulnerabilityCountProvider', () => { ], }, ]; - ossService.getResultArray = () => ossResultsWithMultipleVersionsVulnerability; - ossService.getUniqueVulnerabilities = () => ossResultsWithMultipleVersionsVulnerability[0].vulnerabilities; + vulnerabilityCountProvider.getResultArray = sinon.stub().returns(ossResultsWithMultipleVersionsVulnerability); const count = await vulnerabilityCountProvider.getVulnerabilityCount( 'test.ts', sampleImportedModule, Language.TypeScript, + vulnerabilityCountEmitterStub, ); strictEqual(count.version, undefined); diff --git a/src/test/unit/snykOss/services/ossService.test.ts b/src/test/unit/snykOss/services/ossService.test.ts deleted file mode 100644 index de0817279..000000000 --- a/src/test/unit/snykOss/services/ossService.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { deepStrictEqual, rejects, strictEqual } from 'assert'; -import * as fs from 'fs/promises'; -import _ from 'lodash'; -import sinon from 'sinon'; -import { CliProcess } from '../../../../snyk/cli/process'; -import { IAnalytics } from '../../../../snyk/common/analytics/itly'; -import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; -import { WorkspaceTrust } from '../../../../snyk/common/configuration/trustedFolders'; -import { ILog } from '../../../../snyk/common/logger/interfaces'; -import { DownloadService } from '../../../../snyk/common/services/downloadService'; -import { INotificationService } from '../../../../snyk/common/services/notificationService'; -import { IViewManagerService } from '../../../../snyk/common/services/viewManagerService'; -import { IWebViewProvider } from '../../../../snyk/common/views/webviewProvider'; -import { ExtensionContext } from '../../../../snyk/common/vscode/extensionContext'; -import { IVSCodeWorkspace } from '../../../../snyk/common/vscode/workspace'; -import { OssFileResult, OssResult, OssSeverity } from '../../../../snyk/snykOss/ossResult'; -import { OssService } from '../../../../snyk/snykOss/services/ossService'; -import { OssIssueCommandArg } from '../../../../snyk/snykOss/views/ossVulnerabilityTreeProvider'; -import { DailyScanJob } from '../../../../snyk/snykOss/watchers/dailyScanJob'; -import { LanguageServerMock } from '../../mocks/languageServer.mock'; -import { LoggerMock } from '../../mocks/logger.mock'; - -suite('OssService', () => { - const extensionPath = 'test/path'; - let logger: ILog; - let ossService: OssService; - - setup(() => { - logger = new LoggerMock(); - - const ls = new LanguageServerMock(); - ls.cliReady$.next(''); - - const testFolderPath = ''; - ossService = new OssService( - { - extensionPath, - } as ExtensionContext, - logger, - { - getAdditionalCliParameters: () => '', - getCliPath: () => undefined, - isAutomaticDependencyManagementEnabled: () => true, - getTrustedFolders: () => [testFolderPath], - } as unknown as IConfiguration, - {} as IWebViewProvider, - { - getWorkspaceFolders: () => [testFolderPath], - } as IVSCodeWorkspace, - { - refreshOssView: () => undefined, - } as IViewManagerService, - {} as DownloadService, - { - schedule: sinon.fake(), - } as unknown as DailyScanJob, - {} as INotificationService, - { - logAnalysisIsReady: sinon.fake(), - } as unknown as IAnalytics, - ls, - new WorkspaceTrust(), - ); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Maps single project result correctly', async () => { - const cliOutput = await fs.readFile('mocked_data/snykOss/single-project-vulnerabilities.json', 'utf-8'); - sinon.stub(CliProcess.prototype, 'spawn').resolves(cliOutput); - - const result = await ossService.test(false, false); - const expected = JSON.parse(cliOutput) as OssResult; - deepStrictEqual(result, expected); - }); - - test('Maps multiple project results correctly', async () => { - const cliOutput = await fs.readFile('mocked_data/snykOss/multi-project-vulnerabilities.json', 'utf-8'); - sinon.stub(CliProcess.prototype, 'spawn').resolves(cliOutput); - - const result = await ossService.test(false, false); - const expected = JSON.parse(cliOutput) as OssResult; - deepStrictEqual(result, expected); - }); - - test('Empty result output throws an error', async () => { - sinon.stub(CliProcess.prototype, 'spawn').resolves(''); - await rejects(async () => await ossService.test(false, false)); - }); - - test('Invalid JSON output throws an error', async () => { - sinon.stub(CliProcess.prototype, 'spawn').resolves('{'); - await rejects(async () => await ossService.test(false, false)); - }); - - test('Gets new critical vulns count correctly for single project', () => { - const oldOssResult = { - vulnerabilities: [ - { - id: '1', - severity: OssSeverity.Critical, - }, - { - id: '2', - severity: OssSeverity.Medium, - }, - ], - displayTargetFile: '', - packageManager: '', - projectName: '', - } as OssFileResult; - - // Assert: latest result has same vulnerability count - strictEqual(ossService.getNewCriticalVulnerabilitiesCount(oldOssResult, oldOssResult), 0); - - const newOssResult = { - ..._.clone(oldOssResult), - vulnerabilities: [ - { - id: '1', - severity: OssSeverity.Critical, - }, - { - id: '2', - severity: OssSeverity.Medium, - }, - { - id: '3', - severity: OssSeverity.Critical, - }, - { - id: '4', - severity: OssSeverity.Medium, - }, - ], - } as OssFileResult; - - // Assert: latest result has more vulnerabilities - strictEqual(ossService.getNewCriticalVulnerabilitiesCount(newOssResult, oldOssResult), 1); - - // Assert: latest result has less vulnerabilities - strictEqual(ossService.getNewCriticalVulnerabilitiesCount(oldOssResult, newOssResult), 0); - }); - - test('Gets new critical vulns count correctly for multiple projects', () => { - const oldOssResult = { - vulnerabilities: [ - { - id: '1', - severity: OssSeverity.Critical, - }, - { - id: '2', - severity: OssSeverity.Medium, - }, - ], - displayTargetFile: '', - packageManager: '', - projectName: '', - } as OssFileResult; - const oldOssResults = [oldOssResult, oldOssResult]; - - // Assert: latest result has same vulnerability count - strictEqual(ossService.getNewCriticalVulnerabilitiesCount(oldOssResults, oldOssResults), 0); - - const newOssResult = { - ..._.clone(oldOssResult), - vulnerabilities: [ - { - id: '1', - severity: OssSeverity.Critical, - }, - { - id: '2', - severity: OssSeverity.Medium, - }, - { - id: '3', - severity: OssSeverity.Critical, - }, - { - id: '4', - severity: OssSeverity.Medium, - }, - ], - } as OssFileResult; - const newOssResults = [newOssResult, newOssResult]; - - // Assert: latest result has more vulnerabilities - strictEqual(ossService.getNewCriticalVulnerabilitiesCount(newOssResults, oldOssResults), 2); - - // Assert: latest result has less vulnerabilities - strictEqual(ossService.getNewCriticalVulnerabilitiesCount(oldOssResults, newOssResults), 0); - }); -}); diff --git a/src/test/unit/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.test.ts b/src/test/unit/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.test.ts index 1e8277bc5..f1ce0a5de 100644 --- a/src/test/unit/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.test.ts +++ b/src/test/unit/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService.test.ts @@ -1,23 +1,22 @@ import { strictEqual } from 'assert'; -import { EMPTY } from 'rxjs'; +import { EMPTY, Subject } from 'rxjs'; import sinon from 'sinon'; import { IAnalytics } from '../../../../../snyk/common/analytics/itly'; import { IConfiguration } from '../../../../../snyk/common/configuration/configuration'; -import { ICodeActionKindAdapter } from '../../../../../snyk/common/vscode/codeAction'; +import { ILanguageClientAdapter } from '../../../../../snyk/common/vscode/languageClient'; import { IVSCodeLanguages } from '../../../../../snyk/common/vscode/languages'; +import { ITextDocumentAdapter } from '../../../../../snyk/common/vscode/textdocument'; import { IThemeColorAdapter } from '../../../../../snyk/common/vscode/theme'; import { TextDocument, TextEditor } from '../../../../../snyk/common/vscode/types'; +import { IUriAdapter } from '../../../../../snyk/common/vscode/uri'; import { IVSCodeWindow } from '../../../../../snyk/common/vscode/window'; import { IVSCodeWorkspace } from '../../../../../snyk/common/vscode/workspace'; import { EditorDecorator } from '../../../../../snyk/snykOss/editor/editorDecorator'; -import { OssFileResult } from '../../../../../snyk/snykOss/ossResult'; -import { OssService } from '../../../../../snyk/snykOss/services/ossService'; +import { OssFileResult } from '../../../../../snyk/snykOss/interfaces'; +import { OssService } from '../../../../../snyk/snykOss/ossService'; +import { OssVulnerabilityCountProvider } from '../../../../../snyk/snykOss/providers/ossVulnerabilityCountProvider'; import { OssVulnerabilityCountService } from '../../../../../snyk/snykOss/services/vulnerabilityCount/ossVulnerabilityCountService'; -import { ModuleVulnerabilityCountProvider } from '../../../../../snyk/snykOss/services/vulnerabilityCount/vulnerabilityCountProvider'; import { LoggerMock } from '../../../mocks/logger.mock'; -import { ILanguageClientAdapter } from '../../../../../snyk/common/vscode/languageClient'; -import { IUriAdapter } from '../../../../../snyk/common/vscode/uri'; -import { ITextDocumentAdapter } from '../../../../../snyk/common/vscode/textdocument'; suite('OSS VulnerabilityCountService', () => { let workspace: IVSCodeWorkspace; @@ -25,12 +24,13 @@ suite('OSS VulnerabilityCountService', () => { let languages: IVSCodeLanguages; let ossVulnerabilityCountService: OssVulnerabilityCountService; let ossService: OssService; - let vulnerabilityCountProvider: ModuleVulnerabilityCountProvider; + let vulnerabilityCountProvider: OssVulnerabilityCountProvider; setup(() => { const logger = new LoggerMock(); ossService = { scanFinished$: EMPTY, + newResultAvailable$: new Subject(), } as unknown as OssService; workspace = {} as IVSCodeWorkspace; window = { @@ -41,7 +41,7 @@ suite('OSS VulnerabilityCountService', () => { registerCodeActionsProvider: sinon.fake(), registerHoverProvider: sinon.fake(), } as unknown as IVSCodeLanguages; - vulnerabilityCountProvider = new ModuleVulnerabilityCountProvider( + vulnerabilityCountProvider = new OssVulnerabilityCountProvider( ossService, {} as ILanguageClientAdapter, {} as IUriAdapter, @@ -49,9 +49,6 @@ suite('OSS VulnerabilityCountService', () => { ); const editorDecorator = new EditorDecorator(window, languages, {} as IThemeColorAdapter); - const codeActionProvider = { - getQuickFix: sinon.fake(), - } as ICodeActionKindAdapter; const analytics = {} as IAnalytics; const configuration = {} as IConfiguration; @@ -63,7 +60,6 @@ suite('OSS VulnerabilityCountService', () => { ossService, logger, editorDecorator, - codeActionProvider, analytics, configuration, ); @@ -97,7 +93,7 @@ suite('OSS VulnerabilityCountService', () => { strictEqual(onDidChangeActiveTextEditor.calledOnce, true); }); - test('Processes file if active editor is opened on activation', () => { + test('Processes file on receiving new scan result', () => { window.getActiveTextEditor = () => ({ document: undefined, @@ -109,9 +105,10 @@ suite('OSS VulnerabilityCountService', () => { const processFileSpy = sinon.spy(ossVulnerabilityCountService, 'processFile'); ossVulnerabilityCountService.activate(); + ossService.newResultAvailable$.next(); - strictEqual(getActiveTextEditorSpy.called, true); - strictEqual(processFileSpy.calledOnce, true); + strictEqual(getActiveTextEditorSpy.called, true, 'getActiveTextEditor should be called'); + strictEqual(processFileSpy.calledOnce, true, 'processFile should be called'); }); test("Doesn't process if file is language not supported", () => { @@ -125,7 +122,7 @@ suite('OSS VulnerabilityCountService', () => { }); test("Doesn't process if file is supported and OSS scan hasn't run", () => { - ossService.getResultArray = () => undefined; + vulnerabilityCountProvider.getResultArray = () => undefined; const tsDocument = { fileName: 'C:\\git\\project\\test.ts', languageId: 'typescript', @@ -150,7 +147,7 @@ suite('OSS VulnerabilityCountService', () => { languageId: 'typescript', getText: () => 'const x = require("react")', } as TextDocument; - ossService.getResultArray = () => [{} as OssFileResult]; + vulnerabilityCountProvider.getResultArray = () => [{} as OssFileResult]; sinon.stub(vulnerabilityCountProvider, 'isFilePartOfOssTest').returns(true); const processed = ossVulnerabilityCountService.processFile(document); diff --git a/src/test/unit/snykOss/services/watchers/dailyScanJob.test.ts b/src/test/unit/snykOss/services/watchers/dailyScanJob.test.ts deleted file mode 100644 index a9de4a5eb..000000000 --- a/src/test/unit/snykOss/services/watchers/dailyScanJob.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { strictEqual } from 'assert'; -import sinon from 'sinon'; -import { IExtension } from '../../../../../snyk/base/modules/interfaces'; -import { DailyScanJob } from '../../../../../snyk/snykOss/watchers/dailyScanJob'; - -suite('OSS DailyScanJob', () => { - let extension: IExtension; - let clock: sinon.SinonFakeTimers; - let ossScanSpy: sinon.SinonSpy; - - setup(() => { - ossScanSpy = sinon.fake(); - extension = { - runOssScan: ossScanSpy, - } as unknown as IExtension; - clock = sinon.useFakeTimers(); - }); - - teardown(() => { - sinon.restore(); - clock; - }); - - test('Runs a scan after 24 hours have passed', () => { - const job = new DailyScanJob(extension); - job.schedule(); - - clock.tick(86400000); - strictEqual(ossScanSpy.calledOnce, true); - }); - - test("Doesn't run scan before 24 hours have passed", () => { - const job = new DailyScanJob(extension); - job.schedule(); - - clock.tick(86399999); - strictEqual(ossScanSpy.calledOnce, false); - }); - - test('24h timer is reset when new schedule happens', () => { - const job = new DailyScanJob(extension); - job.schedule(); - - clock.tick(86399999); - strictEqual(ossScanSpy.called, false); - - job.schedule(); - - clock.tick(86399999); - strictEqual(ossScanSpy.called, false); - - clock.tick(1); - strictEqual(ossScanSpy.calledOnce, true); - }); -});