Skip to content

Commit

Permalink
fix: show most severe vulnerability code action [HEAD-1022] (#395)
Browse files Browse the repository at this point in the history
  • Loading branch information
j-luong committed Nov 23, 2023
1 parent 56bae82 commit d6e8156
Show file tree
Hide file tree
Showing 18 changed files with 335 additions and 33 deletions.
3 changes: 2 additions & 1 deletion src/snyk/cli/process.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 });
Expand Down
6 changes: 3 additions & 3 deletions src/snyk/common/commands/commandController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import _ from 'lodash';
import path from 'path';
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';
Expand Down Expand Up @@ -89,11 +89,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);
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/snyk/common/commands/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { completeFileSuggestionType } from '../../snykCode/interfaces';
import { CodeIssueCommandArg } from '../../snykCode/views/interfaces';
import { IacIssueCommandArg } from '../../snykIac/views/interfaces';
import { OssIssueCommandArgLanguageServer } from '../../snykOss/interfaces';
import { OssIssueCommandArg } from '../../snykOss/interfaces';
import { CodeIssueData, Issue } from '../languageServer/types';

export enum OpenCommandIssueType {
Expand All @@ -11,12 +11,12 @@ export enum OpenCommandIssueType {
}

export type OpenIssueCommandArg = {
issue: CodeIssueCommandArg | IacIssueCommandArg | OssIssueCommandArgLanguageServer;
issue: CodeIssueCommandArg | IacIssueCommandArg | OssIssueCommandArg;
issueType: OpenCommandIssueType;
};

export const isCodeIssue = (
_issue: completeFileSuggestionType | Issue<CodeIssueData> | OssIssueCommandArgLanguageServer,
_issue: completeFileSuggestionType | Issue<CodeIssueData> | OssIssueCommandArg,
issueType: OpenCommandIssueType,
): _issue is Issue<CodeIssueData> => {
return issueType === OpenCommandIssueType.CodeIssue;
Expand Down
23 changes: 17 additions & 6 deletions src/snyk/common/editor/codeActionsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> implements CodeActionProvider {
protected readonly providedCodeActionKinds = [this.codeActionKindAdapter.getQuickFix()];

constructor(
private readonly issues: ProductResult<T>,
protected readonly issues: ProductResult<T>,
private readonly codeActionKindAdapter: ICodeActionKindAdapter,
private readonly analytics: IAnalytics,
protected readonly analytics: IAnalytics,
) {}

abstract getActions(folderPath: string, document: TextDocument, issue: Issue<T>, issueRange: Range): CodeAction[];
abstract getActions(folderPath: string, document: TextDocument, issue: Issue<T>, issueRange?: Range): CodeAction[];

abstract getAnalyticsActionTypes(): [string, ...string[]] &
[SupportedQuickFixProperties, ...SupportedQuickFixProperties[]];
Expand All @@ -25,7 +32,11 @@ export abstract class CodeActionsProvider<T> 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;
}
Expand Down Expand Up @@ -57,7 +68,7 @@ export abstract class CodeActionsProvider<T> implements CodeActionProvider {
return undefined;
}

private findIssueWithRange(
protected findIssueWithRange(
result: Issue<T>[],
document: TextDocument,
clickedRange: Range,
Expand Down
4 changes: 2 additions & 2 deletions src/snyk/common/languageServer/lsExecutable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/snyk/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ class SnykExtension extends SnykLib implements IExtension {
extensionContext,
configuration,
ossSuggestionProvider,
new CodeActionAdapter(),
this.codeActionKindAdapter,
this.viewManagerService,
vsCodeWorkspace,
this.workspaceTrust,
Expand Down
1 change: 1 addition & 0 deletions src/snyk/snykCode/constants/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/snyk/snykOss/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface IOssSuggestionWebviewProvider extends IWebViewProvider<Issue<Os
openIssueId: string | undefined;
}

export type OssIssueCommandArgLanguageServer = Issue<OssIssueData> & {
export type OssIssueCommandArg = Issue<OssIssueData> & {
matchingIdVulnerabilities: Issue<OssIssueData>[];
overviewHtml: string;
};
Expand Down
8 changes: 8 additions & 0 deletions src/snyk/snykOss/ossService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ 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<OssIssueData> {
constructor(
extensionContext: ExtensionContext,
config: IConfiguration,
suggestionProvider: IOssSuggestionWebviewProvider,
codeActionAdapter: ICodeActionAdapter,
codeActionKindAdapter: ICodeActionKindAdapter,
viewManagerService: IViewManagerService,
workspace: IVSCodeWorkspace,
workspaceTrust: IWorkspaceTrust,
Expand All @@ -36,6 +40,10 @@ export class OssService extends ProductService<OssIssueData> {
languages,
logger,
);

this.registerCodeActionsProvider(
new OssCodeActionsProvider(languages, codeActionAdapter, codeActionKindAdapter, this.result, analytics),
);
}

subscribeToLsScanMessages(): Subscription {
Expand Down
160 changes: 160 additions & 0 deletions src/snyk/snykOss/providers/ossCodeActionsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
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';

export class OssCodeActionsProvider extends CodeActionsProvider<OssIssueData> {
constructor(
private readonly languages: IVSCodeLanguages,
private readonly codeActionAdapter: ICodeActionAdapter,
codeActionKindAdapter: ICodeActionKindAdapter,
issues: Readonly<ProductResult<OssIssueData>>,
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);
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<OssIssueData>,
_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<OssIssueData>): Range {
return this.languages.createRange(0, 0, 0, 0);
}

private createMostSevereVulnerabilityAction(mostSevereVulnerability: Issue<OssIssueData>): 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<OssIssueData>[] | 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<OssIssueData> objects from ossDiagnostics
const vulnerabilities: Issue<OssIssueData>[] = [];
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<OssIssueData>[]): Issue<OssIssueData> | 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<OssIssueData> | undefined;

for (const vulnerability of vulnerabilities) {
if (this.issueSeverityToRanking(vulnerability.severity) > highestSeverity) {
highestSeverity = this.issueSeverityToRanking(vulnerability.severity);
mostSevereVulnerability = vulnerability;
}
}

return mostSevereVulnerability;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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, LanguageClient, LSPTextDocument } from '../../common/vscode/types';
import { InlineValueText, LSPTextDocument } from '../../common/vscode/types';
import { IUriAdapter } from '../../common/vscode/uri';
import { convertIssue, isResultCliError, OssFileResult, OssResultBody } from '../interfaces';
import { OssService } from '../ossService';
Expand Down
7 changes: 2 additions & 5 deletions src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ProductIssueTreeProvider } from '../../common/views/issueTreeProvider';
import { TreeNode } from '../../common/views/treeNode';
import { IVSCodeLanguages } from '../../common/vscode/languages';
import { messages } from '../constants/messages';
import { OssIssueCommandArgLanguageServer } from '../interfaces';
import { OssIssueCommandArg } from '../interfaces';

export default class OssIssueTreeProvider extends ProductIssueTreeProvider<OssIssueData> {
constructor(
Expand Down Expand Up @@ -201,10 +201,7 @@ export default class OssIssueTreeProvider extends ProductIssueTreeProvider<OssIs
};
}

getOssIssueCommandArg(
vuln: Issue<OssIssueData>,
filteredVulns: Issue<OssIssueData>[],
): OssIssueCommandArgLanguageServer {
getOssIssueCommandArg(vuln: Issue<OssIssueData>, filteredVulns: Issue<OssIssueData>[]): OssIssueCommandArg {
const matchingIdVulnerabilities = filteredVulns.filter(v => v.id === vuln.id);
let overviewHtml = '';

Expand Down
Loading

0 comments on commit d6e8156

Please sign in to comment.